@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.
@@ -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-bpMP0b0W.js"></script>
8
- <link rel="stylesheet" crossorigin href="/dev-client/assets/index-DfEfrp6P.css">
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>
@@ -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
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.8.0";
2
+ var version = "0.8.1-dev-20260718224903";
3
3
 
4
4
  //#endregion
5
5
  export { version };
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-16
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 # 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)
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=&section=` 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. A mode toggle switches between this canvas
118
- view and a published view the clean full-template render, the same page with
119
- the overlay off. A `template-kit dev` wordmark in the toolbar links back to the
120
- home launcher (`/`) the canvas mirror is otherwise chromeless.
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, mode, and selection are encoded in the URL as
123
- `/canvas/<template>?mode=&sel=`, where `sel` names a section instance
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 (not the section instance as a whole) docks an **edit panel**
129
- beside the iframe. One JSON editor handles every slot type you edit the
130
- slot's value in its own JSON shape rather than switching between per-type
131
- widgets alongside a **fill-state badge** (filled / empty / missing) for the
132
- selected slot and a per-instance **fixture picker**. The picker lists that
133
- section's whole fixture corpus, including its synthetic empty-content
134
- fixtures, so you can drive a slot into an empty or missing state without
135
- touching `fixtures.ts`. Applying a JSON edit, switching the fixture, or
136
- resetting the instance re-renders that section instance alone, the same way
137
- it renders in the stack, and swaps its HTML into the iframe in place — no
138
- page reload, no other instance disturbedthen re-anchors the selection ring
139
- to the refreshed markup. The kit dev server is the harness's only backend, so
140
- this loop never touches fixture files, a database, or another service. Edit
141
- state lives only in the tab: nothing is written to disk, and reloading the
142
- page loses it. A re-rendered instance's `"use client"` islands stay static
143
- they do not re-hydrate until you rebuild; the panel notes this so it isn't
144
- mistaken for a bug.
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-16
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 errors
157
- never touch another's. Hydration is idempotent, so calling `hydrateIslands` again after
158
- injecting new markup is safe.
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.0",
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}