@kubex/zinc 1.1.67 → 1.1.69

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.
Files changed (37) hide show
  1. package/dist/custom-elements.json +1288 -20
  2. package/dist/vscode.html-custom-data.json +167 -11
  3. package/dist/web-types.json +360 -17
  4. package/dist/zn.d.ts +368 -20
  5. package/dist/zn.min.js +764 -645
  6. package/docs/pages/components/collapsible.md +6 -2
  7. package/docs/pages/components/preview-frame-demo.njk +33 -4
  8. package/docs/pages/components/preview-frame.md +20 -0
  9. package/docs/pages/components/theme-editor.md +269 -0
  10. package/docs/superpowers/plans/2026-08-03-theme-editor.md +1536 -0
  11. package/docs/superpowers/specs/2026-08-03-theme-editor-design.md +327 -0
  12. package/package.json +1 -1
  13. package/src/components/collapsible/collapsible.component.ts +21 -23
  14. package/src/components/collapsible/collapsible.scss +10 -0
  15. package/src/components/data-table/data-table.component.ts +49 -44
  16. package/src/components/data-table/data-table.scss +4 -0
  17. package/src/components/data-table/data-table.test.ts +42 -0
  18. package/src/components/defined-label/defined-label.component.ts +118 -95
  19. package/src/components/defined-label/defined-label.scss +40 -28
  20. package/src/components/dropdown/dropdown.component.ts +9 -0
  21. package/src/components/editor/editor.component.ts +20 -21
  22. package/src/components/file/file.component.ts +70 -0
  23. package/src/components/page-builder/page-builder.scss +1 -1
  24. package/src/components/preview-frame/preview-frame.component.ts +126 -16
  25. package/src/components/preview-frame/preview-frame.scss +27 -0
  26. package/src/components/preview-frame/preview-frame.test.ts +253 -0
  27. package/src/components/theme-editor/index.ts +12 -0
  28. package/src/components/theme-editor/theme-editor.component.ts +761 -0
  29. package/src/components/theme-editor/theme-editor.scss +309 -0
  30. package/src/components/theme-editor/theme-editor.test.ts +1584 -0
  31. package/src/events/events.ts +2 -0
  32. package/src/events/zn-theme-change.ts +13 -0
  33. package/src/events/zn-theme-submit.ts +9 -0
  34. package/src/types/web-test-runner-commands.d.ts +6 -0
  35. package/src/zinc.ts +1 -0
  36. package/tsconfig.json +7 -1
  37. package/web-test-runner.config.js +3 -1
@@ -109,11 +109,15 @@ Use the `count-element` attribute to count specific elements instead of all chil
109
109
 
110
110
  ### Flush Content
111
111
 
112
- Use the `flush` attribute to remove the left padding/margin from the content area, making it align with the edges.
112
+ Use the `flush` attribute to remove the horizontal padding from both the content
113
+ area and the header, so the caption and the content align with the container's
114
+ edges. This matches `flush` on [`zn-pane`](/components/pane/),
115
+ [`zn-panel`](/components/panel/) and [`zn-tabs`](/components/tabs/), and suits a
116
+ narrow column where an indent wastes width.
113
117
 
114
118
  ```html:preview
115
119
  <zn-collapsible caption="Flush Content"
116
- description="No left padding on content"
120
+ description="No horizontal padding on the header or content"
117
121
  flush>
118
122
  <zn-button>This button aligns with the left edge</zn-button>
119
123
  <p>Content starts at the container edge</p>
@@ -9,12 +9,12 @@ permalink: /components/preview-frame-demo/index.html
9
9
  ---
10
10
  {% raw %}
11
11
  <div class="shell">
12
- <p id="waiting">Waiting for hp-preview:config…</p>
12
+ <p id="waiting">Waiting for the host…</p>
13
13
 
14
14
  <div class="card" id="card" hidden>
15
15
  <div class="card__accent"></div>
16
- <h2 id="merchant"></h2>
17
- <p class="card__amount" id="amount"></p>
16
+ <h2 id="merchant">Merchant</h2>
17
+ <p class="card__amount" id="amount">£24.99</p>
18
18
  <label>Card number
19
19
  <input type="text" placeholder="4242 4242 4242 4242" disabled>
20
20
  </label>
@@ -26,7 +26,7 @@ permalink: /components/preview-frame-demo/index.html
26
26
  <input type="text" placeholder="123" disabled>
27
27
  </label>
28
28
  </div>
29
- <button id="pay" type="button"></button>
29
+ <button id="pay" type="button">Pay now</button>
30
30
  </div>
31
31
  </div>
32
32
 
@@ -106,6 +106,16 @@ permalink: /components/preview-frame-demo/index.html
106
106
  font-weight: 600;
107
107
  cursor: pointer;
108
108
  }
109
+
110
+ body.is-dark {
111
+ background: #101014;
112
+ color: #f4f4f5;
113
+ }
114
+
115
+ body.is-dark .card {
116
+ background: #18181b;
117
+ border-color: #33333a;
118
+ }
109
119
  </style>
110
120
 
111
121
  <script>
@@ -133,6 +143,25 @@ permalink: /components/preview-frame-demo/index.html
133
143
  post({type: 'hp-preview:rendered'});
134
144
  });
135
145
 
146
+ // The theme half of the protocol: applied independently of the config, so
147
+ // the editor can push values before any config exists.
148
+ window.addEventListener('message', e => {
149
+ const data = e.data;
150
+ if (data?.type !== 'hp-preview:theme') return;
151
+
152
+ const values = data.values || {};
153
+ document.body.classList.toggle('is-dark', data.mode === 'dark');
154
+ const card = document.getElementById('card');
155
+ card.hidden = false;
156
+ if (values.background) card.style.background = values.background;
157
+ if (values.accent) card.style.setProperty('--accent', values.accent);
158
+ if (values.radius !== undefined && values.radius !== '') {
159
+ card.style.borderRadius = values.radius + 'px';
160
+ }
161
+ document.getElementById('waiting').hidden = true;
162
+ post({type: 'hp-preview:rendered'});
163
+ });
164
+
136
165
  post({type: 'hp-preview:ready'});
137
166
  </script>
138
167
  {% endraw %}
@@ -27,6 +27,26 @@ The example below embeds the [demo embed page](/components/preview-frame-demo/),
27
27
 
28
28
  The frame always fills the panel; `zoom` (0–1, default `1`) zooms the previewed page out browser-style — e.g. `zoom="0.4"` renders the content at 40% size with correspondingly more of the page visible. `min-height` (default `480`) sets the visible panel height in pixels.
29
29
 
30
+ Set `fill` to make the panel fill its container's height instead — `min-height` then becomes a floor rather than the height, for hosts (like [`zn-theme-editor`](/components/theme-editor/)) whose layout already stretches the panel to match a taller sibling. `zoom` is ignored when `fill` is set, since its oversize maths needs a known pixel height to scale against, which `fill` deliberately doesn't have.
31
+
32
+ `device` constrains and centres the preview to `desktop` (full width), `tablet`
33
+ (768px) or `mobile` (390px), resizing the iframe itself so the embedded page's
34
+ media queries fire. `setTheme({mode, values})` posts an `hp-preview:theme` message and
35
+ replays it after each ready handshake, which is how
36
+ [`zn-theme-editor`](/components/theme-editor/) drives a live preview.
37
+
38
+ The panel behind the preview is a dot grid, so the frame's bounds stay visible
39
+ instead of blending into the page — at `tablet` or `mobile` the dots fill the
40
+ gutters either side of the narrowed iframe. Tune it with
41
+ `--zn-preview-frame-dot-spacing` (default `20px`) and
42
+ `--zn-preview-frame-dot-opacity` (default `0.08`). The iframe itself is given an
43
+ opaque background, so the dots never show through the previewed page.
44
+
45
+ Set `backdrop="panel"` to swap the dot grid for a plain `rgb(var(--zn-panel))`
46
+ fill — used by [`zn-theme-editor`](/components/theme-editor/)'s `standalone`
47
+ mode, where the frame is already inside its own bordered panel. `backdrop="dots"`
48
+ is the default.
49
+
30
50
  ## Live Form Updates
31
51
 
32
52
  In a real deployment, editing a watched form auto-saves it and the preview refreshes with the newly saved config. This docs site is static, so the example simulates the save: form changes are encoded into a `data:` payload URI and `refresh()` re-runs the fetch → `hp-preview:config` cycle — the same path a real save triggers.
@@ -0,0 +1,269 @@
1
+ ---
2
+ meta:
3
+ title: Theme Editor
4
+ description: Theme controls on the left, a live preview frame on the right, with light/dark and device switching.
5
+ layout: component
6
+ fullWidth: true
7
+ ---
8
+
9
+ Put form controls in the default slot and give each a `name`. Every control is
10
+ per-mode: it holds a light value and a dark value, and the sidebar's mode
11
+ toggle swaps which one the control displays. Changing a control harvests every
12
+ named control's *currently displayed* value and pushes the active mode's set
13
+ into the embedded [preview frame](/components/preview-frame/) as an
14
+ `hp-preview:theme` message — no save, no fetch, no page reload.
15
+
16
+ The controls column runs the full height of the component, with its own
17
+ header row on top holding the light/dark mode toggle beside its caption. The
18
+ toolbar sits opposite it, above the preview only, and switches the width the
19
+ preview renders at: desktop (full width), tablet (768px) or mobile (390px).
20
+ Because the iframe itself is resized, the embedded page's own media queries
21
+ fire.
22
+
23
+ Set `controls-caption` and `preview-caption` to label each column's header
24
+ row — both are empty by default, rendering no text (the controls column's
25
+ header row still renders either way, so the two columns stay aligned).
26
+
27
+ The preview always fills its column, leaving no dead space beneath it.
28
+ `min-height` (default `480`) is a floor for that column, not a fixed height —
29
+ it's still forwarded to the [preview frame](/components/preview-frame/), which
30
+ uses it the same way.
31
+
32
+ Controls can be organized into tabs with collapsible groups inside each —
33
+ `sections` with `groups` below is the canonical arrangement:
34
+
35
+ ```html:preview
36
+ <zn-theme-editor
37
+ id="theme-editor-demo"
38
+ src="/components/preview-frame-demo/"
39
+ min-height="420"
40
+ controls-caption="Theme Builder"
41
+ preview-caption="Live Preview"
42
+ sections='[
43
+ {"name":"colors","caption":"Colors","groups":[
44
+ {"name":"brand","caption":"Brand","open":true},
45
+ {"name":"background","caption":"Background"}
46
+ ]},
47
+ {"name":"shapes","caption":"Shapes","groups":[
48
+ {"name":"radius","caption":"Radius"}
49
+ ]}
50
+ ]'>
51
+ <zn-input slot="brand" name="accent" label="Accent" value="#6936f5" dark-value="#f5c542" type="color"></zn-input>
52
+ <zn-input slot="background" name="background" label="Background" value="#ffffff" dark-value="#18181b" type="color"></zn-input>
53
+ <zn-input slot="radius" name="radius" label="Corner radius" type="number" value="4"></zn-input>
54
+ </zn-theme-editor>
55
+
56
+ <script>
57
+ document.getElementById('theme-editor-demo').frameOrigin = location.origin;
58
+ </script>
59
+ ```
60
+
61
+ :::tip
62
+ `frame-origin` must match the embed's origin exactly — messages from any other
63
+ origin are ignored. The example sets it at runtime because the docs site is
64
+ same-origin.
65
+ :::
66
+
67
+ ## Dark values
68
+
69
+ Give a control a `dark-value` attribute alongside `value` to author its dark
70
+ variant, as `accent` and `background` do above. A control with no
71
+ `dark-value` falls back to its `value` in dark mode, so adding dark support to
72
+ an existing editor is additive:
73
+
74
+ ```html
75
+ <zn-color-select name="accent" label="Accent" value="#6936f5" dark-value="#f5c542"></zn-color-select>
76
+ ```
77
+
78
+ For boolean controls (`zn-checkbox`, `zn-toggle`, `input[type=checkbox]`),
79
+ two separate rules apply depending on whether `dark-value` is present:
80
+
81
+ - **Present** — parsed as truthy: `dark-value="1"` or `dark-value="true"`
82
+ seeds the dark state checked, any other value seeds it unchecked.
83
+ - **Absent** — the dark state mirrors the control's own light `checked`
84
+ state, whatever that is (including `true`), the same fallback rule
85
+ non-boolean controls get.
86
+
87
+ ## Reading and Persisting Values
88
+
89
+ Every change emits `zn-theme-change` with `{values, mode, device}`, where
90
+ `values` is `{light, dark}` — both full sets, regardless of which mode is
91
+ active:
92
+
93
+ ```js
94
+ editor.addEventListener('zn-theme-change', event => {
95
+ console.log(event.detail.values.light, event.detail.values.dark);
96
+ });
97
+ ```
98
+
99
+ Set `action` to persist automatically instead — both sets are POSTed as
100
+ `FormData` on a longer debounce (`save-debounce`, default `1000`ms), with every
101
+ key bracketed by mode: `light[accent]`, `dark[accent]`, `light[radius]`,
102
+ `dark[radius]`. `mode` and `device` are view state and are never saved.
103
+
104
+ ```html
105
+ <zn-theme-editor src="/embed?t=..." frame-origin="https://pay.example" action="/theme/save">
106
+ <zn-color-select name="accent" label="Accent" value="#6936f5" dark-value="#f5c542"></zn-color-select>
107
+ </zn-theme-editor>
108
+ ```
109
+
110
+ Saves are serialized: if changes land while a POST is in flight, exactly one
111
+ further save runs afterwards with the latest values.
112
+
113
+ ### Manual saving with a submit button
114
+
115
+ Set `submit-label` to render a built-in save button at the right of the toolbar,
116
+ opposite the device controls —
117
+ empty (the default) renders no button. Add `manual` to disable the debounced
118
+ auto-save entirely, so persistence only happens when the button is clicked;
119
+ the live preview keeps updating on every change either way, only saving
120
+ becomes explicit:
121
+
122
+ ```html
123
+ <zn-theme-editor
124
+ src="/embed?t=..." frame-origin="https://pay.example"
125
+ manual
126
+ submit-label="Save theme">
127
+ <zn-color-select name="accent" label="Accent" value="#6936f5" dark-value="#f5c542"></zn-color-select>
128
+ </zn-theme-editor>
129
+ ```
130
+
131
+ Clicking the button flushes any pending edit, then saves immediately through
132
+ the same single-slot save queue used for auto-save — it never opens a second
133
+ concurrent request. With `action` set, a successful save emits `zn-theme-submit`
134
+ carrying `{values}` (both `light` and `dark` sets); a failed save surfaces
135
+ through the same error strip and `zn-error` as auto-save. With no `action`,
136
+ nothing is POSTed but `zn-theme-submit` still fires, so a host can persist the
137
+ values itself:
138
+
139
+ ```js
140
+ editor.addEventListener('zn-theme-submit', event => {
141
+ console.log(event.detail.values.light, event.detail.values.dark);
142
+ });
143
+ ```
144
+
145
+ The button shows a loading state while its save is in flight and ignores
146
+ further clicks until it resolves.
147
+
148
+ ## Controls
149
+
150
+ Any Zinc form control works. Controls must carry `name` as an **attribute** —
151
+ `zn-checkbox` and `zn-toggle` contribute their `checked` state as a boolean,
152
+ everything else contributes `value`. Disabled and unnamed controls are skipped.
153
+ Every control is per-mode with no opt-in attribute, so mode-independent values
154
+ (like `radius` above) end up duplicated across `light` and `dark` — accepted
155
+ knowingly to keep the rule uniform.
156
+
157
+ The value store is the theme, not a mirror of the currently visible controls:
158
+ removing a control from the markup keeps its key in both value sets, and it
159
+ keeps being pushed to the preview and included in saves. Clear it server-side
160
+ or re-add the control under the same name to edit it again.
161
+
162
+ The `footer` slot holds actions beneath the controls:
163
+
164
+ ```html
165
+ <zn-theme-editor src="/embed?t=..." frame-origin="https://pay.example">
166
+ <zn-color-select name="accent" label="Accent"></zn-color-select>
167
+ <zn-button slot="footer">Save</zn-button>
168
+ </zn-theme-editor>
169
+ ```
170
+
171
+ Set the controls column width with `--zn-theme-editor-controls-width`
172
+ (default `343px`, matching page-builder's palette). Below 768px the columns
173
+ stack.
174
+
175
+ ## Grouping controls into tabs and collapsibles
176
+
177
+ Set `sections` to a JSON array of `{name, caption, groups}`. Each section
178
+ becomes a `zn-tabs` tab; each entry in its `groups` — `{name, caption,
179
+ description?, open?}` — becomes a collapsible inside that tab, and a control
180
+ is assigned to a group with `slot="<group-name>"`:
181
+
182
+ ```html
183
+ <zn-theme-editor
184
+ src="/embed?t=..." frame-origin="https://pay.example"
185
+ sections='[
186
+ {"name":"colors","caption":"Colors","groups":[
187
+ {"name":"brand","caption":"Brand","open":true},
188
+ {"name":"semantic","caption":"Semantic"}
189
+ ]},
190
+ {"name":"shapes","caption":"Shapes","groups":[{"name":"radius","caption":"Radius"}]}
191
+ ]'>
192
+ <zn-color-select slot="brand" name="accent" label="Accent" value="#6936f5"></zn-color-select>
193
+ <zn-input slot="radius" name="radius" label="Corner radius" type="number" value="4"></zn-input>
194
+ </zn-theme-editor>
195
+ ```
196
+
197
+ A group with no assigned controls renders no collapsible, and a section none
198
+ of whose groups are populated renders no tab — the same "no chrome for empty
199
+ config" rule flat sections already followed. Every tab's panel stays mounted
200
+ while hidden (`zn-tabs` toggles visibility, never removes a panel), so
201
+ switching tabs never drops a value out of the theme, a preview push or a save.
202
+
203
+ ### Flat sections (no groups)
204
+
205
+ A section can omit `groups` and just take controls directly via
206
+ `slot="<section-name>"`, exactly as before nesting existed. `section-layout`
207
+ then decides the presentation — stacked `zn-collapsible`s (`"collapsible"`,
208
+ the default) or a `zn-tabs` strip (`"tabs"`) — and is otherwise ignored: once
209
+ *any* section has a populated `groups`, every section renders as a nested tab
210
+ regardless of `section-layout`.
211
+
212
+ ```html
213
+ <zn-theme-editor
214
+ src="/embed?t=..." frame-origin="https://pay.example"
215
+ section-layout="tabs"
216
+ sections='[{"name":"colors","caption":"Colors"},{"name":"layout","caption":"Layout"}]'>
217
+ <zn-color-select slot="colors" name="accent" label="Accent" value="#6936f5"></zn-color-select>
218
+ <zn-input slot="layout" name="radius" label="Corner radius" type="number" value="4"></zn-input>
219
+ </zn-theme-editor>
220
+ ```
221
+
222
+ An author can also slot their own `zn-collapsible` into any named slot (or the
223
+ default slot) instead of relying on `groups` — its presentation is then
224
+ entirely its own; add `flush` yourself if you want it to run the full width of
225
+ the column.
226
+
227
+ ## Preview sources
228
+
229
+ Set `sources` to a JSON array of `{label, src}` to render a dropdown in the
230
+ toolbar, beside the device buttons, for switching which page the preview
231
+ loads:
232
+
233
+ ```html
234
+ <zn-theme-editor
235
+ src="/embed?t=..." frame-origin="https://pay.example"
236
+ sources='[{"label":"Checkout","src":"/embed/checkout"},{"label":"Storefront","src":"/embed/storefront"}]'>
237
+ <zn-color-select name="accent" label="Accent" value="#6936f5"></zn-color-select>
238
+ </zn-theme-editor>
239
+ ```
240
+
241
+ The first entry is the initial selection — it wins over an explicit `src` when
242
+ `sources` is non-empty. Selecting a different entry reloads the iframe; nothing
243
+ further is needed to keep the theme, since the frame retains the last pushed
244
+ payload and replays it once the reloaded page re-announces itself ready. Leave
245
+ `sources` unset (the default) and `src` behaves exactly as it always has, with
246
+ no dropdown rendered.
247
+
248
+ ## Collapsing the controls column
249
+
250
+ Set `controls-collapsed` to hide the controls column, or click the chevron
251
+ toggle that sits on the seam between the columns. Collapsing is purely a
252
+ layout change — it never affects harvested values or pushes a new theme to
253
+ the preview. Below the 768px stacked breakpoint the toggle is hidden, since
254
+ there's no side-by-side seam to tuck into — the editor also un-collapses
255
+ itself if it's already showing `controls-collapsed` when the layout narrows
256
+ that far, so the controls are never stuck unreachable.
257
+
258
+ ## Standalone panel
259
+
260
+ Set `standalone` to present the editor as its own bordered, rounded panel —
261
+ useful when it isn't already embedded in a page shell that provides that
262
+ chrome. It also switches the preview's [backdrop](/components/preview-frame/)
263
+ from the dot grid to a plain panel to match.
264
+
265
+ ```html
266
+ <zn-theme-editor src="/embed?t=..." frame-origin="https://pay.example" standalone>
267
+ <zn-color-select name="accent" label="Accent" value="#6936f5" dark-value="#f5c542"></zn-color-select>
268
+ </zn-theme-editor>
269
+ ```