@homepages/template-kit 0.8.0 → 0.8.1-dev-20260718224903
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -0
- package/dist/cli/check/loader.js +9 -6
- package/dist/cli/dev/build-css.js +11 -5
- package/dist/cli/dev/compose-template.js +3 -2
- package/dist/cli/dev/diagnostics.js +68 -0
- package/dist/cli/dev/index.js +12 -2
- package/dist/cli/dev/island-canvas-bootstrap.js +172 -0
- package/dist/cli/dev/section-page.js +38 -1
- package/dist/cli/dev/server.js +26 -2
- package/dist/cli/dev/tsconfig-paths-plugin.js +90 -0
- package/dist/cli/dev/vite-server.js +7 -1
- package/dist/dev-client/assets/index-3Gn34X_P.js +50 -0
- package/dist/dev-client/assets/index-DxQeiHfu.css +1 -0
- package/dist/dev-client/index.html +2 -2
- package/dist/island-runtime.js +1 -1
- package/dist/package.js +1 -1
- package/guide/dev.md +70 -27
- package/guide/islands.md +23 -4
- package/package.json +1 -1
- package/dist/dev-client/assets/index-DfEfrp6P.css +0 -1
- package/dist/dev-client/assets/index-bpMP0b0W.js +0 -50
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*{box-sizing:border-box}html,body,#root{margin:0;padding:0;height:100%;background:#0f1115;color:#e6e7eb;font:14px/1.5 ui-sans-serif,system-ui,-apple-system,Segoe UI,Helvetica,Arial,sans-serif}a{color:inherit;text-decoration:none}button,select,input,textarea{font:inherit;color:inherit;background:#1a1d24;border:1px solid #2c313a;border-radius:4px;padding:6px 10px}button{cursor:pointer}button:hover{background:#232831}button:disabled{opacity:.4;cursor:default}.empty{padding:40px;color:#9ca0a8;text-align:center}.boot-error{padding:24px;color:#ff8b8b;font:13px/1.5 ui-monospace,Menlo,monospace;white-space:pre-wrap}.canvas-mirror{display:flex;flex-direction:column;height:100%;min-height:0}.canvas-iframe{flex:1 1 auto;width:100%;border:0;background:#fff;min-height:0}.home{max-width:880px;margin:0 auto;padding:40px 24px}.home-header{margin-bottom:28px}.home-title{font-size:15px;font-weight:600}.home-tagline{font-size:13px;color:#9ca0a8;margin-top:4px}.home-template{display:inline-flex;align-items:center;gap:8px;font-size:12px;color:#9ca0a8;margin-bottom:24px}.home-template select{min-width:160px}.home-tools{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}.tool-card{display:flex;flex-direction:column;gap:12px;background:#14171c;border:1px solid #21252d;border-radius:8px;padding:20px}.tool-card h2{font-size:14px;font-weight:600;margin:0}.tool-card p{font-size:13px;color:#9ca0a8;margin:0;line-height:1.5}.tool-card .controls{display:flex;flex-direction:column;gap:8px}.tool-card label{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:12px;color:#9ca0a8}.tool-card label select{min-width:140px}.open-btn{margin-top:auto;text-align:center;padding:8px 12px;border:1px solid #2c313a;border-radius:6px;background:#1a1d24;color:inherit;cursor:pointer}.open-btn:hover{background:#232831}.open-btn:disabled{opacity:.4;cursor:default}.home-hint{font-size:12px;color:#6b7280}.canvas-mirror__stage{display:flex;min-height:0;flex:1}.canvas-mirror__stage .canvas-iframe{flex:1}.instance-panel{width:340px;flex:none;overflow-y:auto;border-left:1px solid #e5e7eb;padding:12px;background:#fff;font:13px system-ui,sans-serif;display:flex;flex-direction:column;gap:10px}.instance-panel__head{display:flex;flex-direction:column;gap:8px}.instance-panel__slot{display:flex;align-items:center;gap:8px}.instance-panel__type{color:#6b7280}.instance-panel__fill{margin-left:auto;padding:1px 7px;border-radius:999px;font-size:11px}.instance-panel__fill--filled{background:#dcfce7;color:#166534}.instance-panel__fill--empty{background:#f3f4f6;color:#6b7280}.instance-panel__fill--missing{background:#fee2e2;color:#991b1b}.instance-panel__fixture{display:flex;flex-direction:column;gap:3px;color:#6b7280}.instance-panel__json{width:100%;min-height:180px;font:12px ui-monospace,monospace;resize:vertical}.instance-panel__error{color:#b91c1c;white-space:pre-wrap}.instance-panel__actions{display:flex;gap:8px}.instance-panel__note{color:#9ca3af;font-size:11px}
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<title>template-kit dev</title>
|
|
7
|
-
<script type="module" crossorigin src="/dev-client/assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/dev-client/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/dev-client/assets/index-3Gn34X_P.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/dev-client/assets/index-DxQeiHfu.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
package/dist/island-runtime.js
CHANGED
|
@@ -17,7 +17,7 @@ function createEditorPropsBridge(react) {
|
|
|
17
17
|
}
|
|
18
18
|
async function hydrateIslands(options) {
|
|
19
19
|
const root = options.root ?? document;
|
|
20
|
-
const markers = Array.from(root.querySelectorAll(`[${ATTR_ISLAND_KEY}]`)).filter((element) => !roots.has(element));
|
|
20
|
+
const markers = Array.from(root.querySelectorAll(`[${ATTR_ISLAND_KEY}]`)).filter((element) => !roots.has(element) && element.parentElement?.closest(`[${"data-tr-island"}]`) == null);
|
|
21
21
|
if (markers.length === 0) return [];
|
|
22
22
|
const payloads = /* @__PURE__ */ new Map();
|
|
23
23
|
const duplicateIds = /* @__PURE__ */ new Set();
|
package/dist/package.js
CHANGED
package/guide/dev.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
purpose: The `template-kit dev` command — the local preview server, what it serves, its URLs, and the agent-facing render endpoints.
|
|
3
3
|
status: living
|
|
4
4
|
related: [INDEX.md, theme-and-css.md, schema-system.md, check.md, islands.md]
|
|
5
|
-
updated: 2026-07-
|
|
5
|
+
updated: 2026-07-18
|
|
6
6
|
---
|
|
7
7
|
# template-kit dev
|
|
8
8
|
|
|
@@ -14,9 +14,10 @@ see real output while you author, and reload as you edit.
|
|
|
14
14
|
## Usage
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
template-kit dev
|
|
18
|
-
template-kit dev <template>
|
|
19
|
-
template-kit dev --port 4000
|
|
17
|
+
template-kit dev # serve every template in the workspace
|
|
18
|
+
template-kit dev <template> # preselect one template in the UI
|
|
19
|
+
template-kit dev --port 4000 # choose the port (default 5180)
|
|
20
|
+
template-kit dev --diagnostics-cmd "<cmd>" # show lint/validate violations in the preview
|
|
20
21
|
```
|
|
21
22
|
|
|
22
23
|
Run it from the workspace root or from inside a template's own folder — running
|
|
@@ -90,6 +91,32 @@ omits keeps the fixture's value; a key set explicitly to `null` replaces it with
|
|
|
90
91
|
`null`, so you can probe an empty/missing state a fixture doesn't otherwise cover.
|
|
91
92
|
The encoded payload is capped at 16 KB.
|
|
92
93
|
|
|
94
|
+
## Diagnostics
|
|
95
|
+
|
|
96
|
+
`--diagnostics-cmd "<cmd>"` wires an external command into the section preview
|
|
97
|
+
as a live violations banner. It's unset by default — nothing changes unless you
|
|
98
|
+
configure it.
|
|
99
|
+
|
|
100
|
+
When set, opening `/sections/<template>/<section>/<fixture>` runs
|
|
101
|
+
`<cmd> --template <t> --section <s>` (the command string is split on
|
|
102
|
+
whitespace — no shell, no quoting — and `--template`/`--section` are always
|
|
103
|
+
appended as separate arguments). The command must print JSON to stdout:
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
{ "violations": [{ "rule": "some-rule", "message": "what's wrong", "file": "schema.ts", "line": 12 }] }
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`file` and `line` are optional. A non-zero exit or unparseable stdout surfaces
|
|
110
|
+
as an error in the banner rather than crashing the server. If any violations
|
|
111
|
+
come back, the preview shows a dismissible banner at the top of the page; it
|
|
112
|
+
re-fetches (and reappears, if still violated) on every file-triggered reload,
|
|
113
|
+
the same way the rest of the preview already goes live on a change.
|
|
114
|
+
|
|
115
|
+
`GET /api/diagnostics?template=§ion=` is the underlying endpoint, if you
|
|
116
|
+
want to query it directly: `{available: false}` when no command is configured,
|
|
117
|
+
`{available: true, violations: [...]}` on success, `{available: true, error}`
|
|
118
|
+
when the command failed or its output didn't parse.
|
|
119
|
+
|
|
93
120
|
## Views: the full template, and the section render behind the canvas
|
|
94
121
|
|
|
95
122
|
The whole template renders at `/template/<template>` — every section in
|
|
@@ -114,34 +141,50 @@ the same slot name in two different sections selects independently. Click a slot
|
|
|
114
141
|
to select it (a gold ring); hover to preview one (a blue ring). Only slots that
|
|
115
142
|
instance's schema marks `editable_by_user: true` select individually — clicking
|
|
116
143
|
anywhere else in a section selects that section instance as a whole. The overlay
|
|
117
|
-
never changes the rendered markup.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
144
|
+
never changes the rendered markup. The canvas mirror is fully chromeless — no
|
|
145
|
+
toolbar, no back-link to the home launcher; navigate back with the browser's
|
|
146
|
+
own back button or a bookmarked `/` URL. To see the clean published render
|
|
147
|
+
instead, open the **Full template viewer** card from the home launcher.
|
|
121
148
|
|
|
122
|
-
The current template
|
|
123
|
-
`/canvas/<template>?
|
|
149
|
+
The current template and selection are encoded in the URL as
|
|
150
|
+
`/canvas/<template>?sel=`, where `sel` names a section instance
|
|
124
151
|
(`<instanceId>`), a slot within it (`<instanceId>~<slotId>`), or one item of
|
|
125
152
|
a repeating slot (`<instanceId>~<slotId>~<itemIndex>`) — so a canvas link
|
|
126
153
|
reloads and shares exactly the view you're looking at.
|
|
127
154
|
|
|
128
|
-
Selecting a slot
|
|
129
|
-
|
|
130
|
-
slot's value in its own JSON
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
155
|
+
Selecting a slot or a section instance docks an **edit panel** beside the
|
|
156
|
+
iframe. A slot selection's panel additionally shows a slot editor: one JSON
|
|
157
|
+
editor handles every slot type — you edit the slot's value in its own JSON
|
|
158
|
+
shape rather than switching between per-type widgets — alongside a
|
|
159
|
+
**fill-state badge** (filled / empty / missing) for the selected slot and a
|
|
160
|
+
per-instance **fixture picker**. The picker lists that section's whole
|
|
161
|
+
fixture corpus, including its synthetic empty-content fixtures, so you can
|
|
162
|
+
drive a slot into an empty or missing state without touching `fixtures.ts`.
|
|
163
|
+
Applying a JSON edit, switching the fixture, or resetting the instance
|
|
164
|
+
re-renders that section instance alone, the same way it renders in the
|
|
165
|
+
stack, and swaps its HTML into the iframe in place — no page reload, no
|
|
166
|
+
other instance disturbed — then re-anchors the selection ring to the
|
|
167
|
+
refreshed markup, and re-hydrates that instance's islands. The kit dev
|
|
168
|
+
server is the harness's only backend, so this loop never touches fixture
|
|
169
|
+
files, a database, or another service. Edit state lives only in the tab:
|
|
170
|
+
nothing is written to disk, and reloading the page loses it. A section
|
|
171
|
+
instance selection has no slot editor to dock, since there's no single slot
|
|
172
|
+
to edit.
|
|
173
|
+
|
|
174
|
+
An island whose component declares `editor.live: true` (see [Islands](islands.md))
|
|
175
|
+
hydrates in the canvas the same way it does on a published page, but behind a
|
|
176
|
+
click-through shield until it's selected: the first click on a live island
|
|
177
|
+
selects its owning slot or section instance instead of reaching the
|
|
178
|
+
component, dropping the shield; a second click reaches the component itself.
|
|
179
|
+
Pressing Escape clears the selection and re-arms every shield. An island with
|
|
180
|
+
no `editor` export, or `editor.live: false`, stays static markup in the
|
|
181
|
+
canvas — same as a published page's non-live islands. Some islands own no
|
|
182
|
+
slot of their own (e.g. a slider or map sitting beside a `<Slot>` rather than
|
|
183
|
+
wrapped by one); their shield only ever drops under a section instance
|
|
184
|
+
selection. Selecting a slot or a section instance docks a read-only
|
|
185
|
+
**Islands** list in the edit panel showing every top-level island in that
|
|
186
|
+
section instance, whether it's live or static, its shield placement, and
|
|
187
|
+
whether it's currently interactive or shielded.
|
|
145
188
|
|
|
146
189
|
## Agent-facing endpoints
|
|
147
190
|
|
package/guide/islands.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
purpose: How interactivity works in a template — client components ("islands"), their props contract, and their editor options.
|
|
3
3
|
status: living
|
|
4
4
|
related: [INDEX.md, eslint.md, dev.md]
|
|
5
|
-
updated: 2026-07-
|
|
5
|
+
updated: 2026-07-18
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Islands: interactivity in a template
|
|
@@ -141,6 +141,24 @@ hydrates:
|
|
|
141
141
|
The marker is layout-invisible (`display:contents`), so an island can sit directly
|
|
142
142
|
inside a grid or flex container without adding a box.
|
|
143
143
|
|
|
144
|
+
## Nesting one island inside another
|
|
145
|
+
|
|
146
|
+
An island can render another island — for example, an accordion island whose expanded
|
|
147
|
+
content is itself an existing expandable-text island. Because the build wraps every
|
|
148
|
+
`"use client"` module in a marker, the nested island's marker ends up inside the parent
|
|
149
|
+
island's server-rendered output.
|
|
150
|
+
|
|
151
|
+
A nested marker is not hydrated as its own root. It is left to the marker that contains
|
|
152
|
+
it, which already renders — and hydrates — it as part of its own React tree. The nested
|
|
153
|
+
component still runs and stays interactive, but it loses its own root and its own editor
|
|
154
|
+
treatment:
|
|
155
|
+
|
|
156
|
+
- Its `editor.live`/`shieldMode`/`liveSurface`/`props` declaration has no independent
|
|
157
|
+
effect in the canvas — the parent island's declaration governs the whole subtree, since
|
|
158
|
+
there is no separate root for the editor to select or shield.
|
|
159
|
+
- Its props `<script>` still renders in the DOM (harmless) but goes unread; props reach
|
|
160
|
+
the nested component through the parent's own render instead.
|
|
161
|
+
|
|
144
162
|
## Hosting islands yourself
|
|
145
163
|
|
|
146
164
|
`@homepages/template-kit/island-runtime` is the loader. It is not something a template
|
|
@@ -153,9 +171,10 @@ await hydrateIslands({ load: (key) => ISLANDS[key]() }); // ISLANDS: the build'
|
|
|
153
171
|
unmountIslands(sectionEl); // before replacing sectionEl's HTML
|
|
154
172
|
```
|
|
155
173
|
|
|
156
|
-
Each island is an independent React root: one island's state, re-renders, and
|
|
157
|
-
never touch another's.
|
|
158
|
-
|
|
174
|
+
Each top-level island is an independent React root: one island's state, re-renders, and
|
|
175
|
+
errors never touch another's. An island nested inside another (see above) hydrates as
|
|
176
|
+
part of its parent's root instead. Hydration is idempotent, so calling `hydrateIslands`
|
|
177
|
+
again after injecting new markup is safe.
|
|
159
178
|
|
|
160
179
|
## Talking to HomePages from an island
|
|
161
180
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homepages/template-kit",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.1-dev-20260718224903",
|
|
4
4
|
"description": "Authoring kit for HomePages marketing-section templates: schema system, contract primitives, theme tokens, and the template-kit CLI.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
*{box-sizing:border-box}html,body,#root{margin:0;padding:0;height:100%;background:#0f1115;color:#e6e7eb;font:14px/1.5 ui-sans-serif,system-ui,-apple-system,Segoe UI,Helvetica,Arial,sans-serif}a{color:inherit;text-decoration:none}button,select,input,textarea{font:inherit;color:inherit;background:#1a1d24;border:1px solid #2c313a;border-radius:4px;padding:6px 10px}button{cursor:pointer}button:hover{background:#232831}button:disabled{opacity:.4;cursor:default}.empty{padding:40px;color:#9ca0a8;text-align:center}.boot-error{padding:24px;color:#ff8b8b;font:13px/1.5 ui-monospace,Menlo,monospace;white-space:pre-wrap}.canvas-mirror{display:flex;flex-direction:column;height:100%;min-height:0}.canvas-toolbar{display:flex;gap:8px;padding:6px 10px;border-bottom:1px solid #e2e5ea;background:#fafbfc}.canvas-toolbar button{font:12px system-ui,sans-serif;padding:3px 8px;border:1px solid #cbd2dc;border-radius:5px;background:#fff;cursor:pointer}.canvas-iframe{flex:1 1 auto;width:100%;border:0;background:#fff;min-height:0}.home{max-width:880px;margin:0 auto;padding:40px 24px}.home-header{margin-bottom:28px}.home-title{font-size:15px;font-weight:600}.home-tagline{font-size:13px;color:#9ca0a8;margin-top:4px}.home-template{display:inline-flex;align-items:center;gap:8px;font-size:12px;color:#9ca0a8;margin-bottom:24px}.home-template select{min-width:160px}.home-tools{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}.tool-card{display:flex;flex-direction:column;gap:12px;background:#14171c;border:1px solid #21252d;border-radius:8px;padding:20px}.tool-card h2{font-size:14px;font-weight:600;margin:0}.tool-card p{font-size:13px;color:#9ca0a8;margin:0;line-height:1.5}.tool-card .controls{display:flex;flex-direction:column;gap:8px}.tool-card label{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:12px;color:#9ca0a8}.tool-card label select{min-width:140px}.open-btn{margin-top:auto;text-align:center;padding:8px 12px;border:1px solid #2c313a;border-radius:6px;background:#1a1d24;color:inherit;cursor:pointer}.open-btn:hover{background:#232831}.open-btn:disabled{opacity:.4;cursor:default}.home-hint{font-size:12px;color:#6b7280}.home-link{font-weight:600;color:#1a1d24}.canvas-toolbar .spacer{flex:1}.canvas-mirror__stage{display:flex;min-height:0;flex:1}.canvas-mirror__stage .canvas-iframe{flex:1}.instance-panel{width:340px;flex:none;overflow-y:auto;border-left:1px solid #e5e7eb;padding:12px;background:#fff;font:13px system-ui,sans-serif;display:flex;flex-direction:column;gap:10px}.instance-panel__head{display:flex;flex-direction:column;gap:8px}.instance-panel__slot{display:flex;align-items:center;gap:8px}.instance-panel__type{color:#6b7280}.instance-panel__fill{margin-left:auto;padding:1px 7px;border-radius:999px;font-size:11px}.instance-panel__fill--filled{background:#dcfce7;color:#166534}.instance-panel__fill--empty{background:#f3f4f6;color:#6b7280}.instance-panel__fill--missing{background:#fee2e2;color:#991b1b}.instance-panel__fixture{display:flex;flex-direction:column;gap:3px;color:#6b7280}.instance-panel__json{width:100%;min-height:180px;font:12px ui-monospace,monospace;resize:vertical}.instance-panel__error{color:#b91c1c;white-space:pre-wrap}.instance-panel__actions{display:flex;gap:8px}.instance-panel__note{color:#9ca3af;font-size:11px}
|