@kubex/zinc 1.1.91 → 1.1.94
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/dist/custom-elements.json +1738 -110
- package/dist/vscode.html-custom-data.json +136 -4
- package/dist/web-types.json +307 -4
- package/dist/zn.d.ts +298 -2
- package/dist/zn.min.js +708 -516
- package/docs/pages/components/page-builder.md +22 -0
- package/docs/pages/components/schedule-builder.md +345 -0
- package/package.json +1 -1
- package/src/components/alert/alert.scss +9 -13
- package/src/components/chip/chip.scss +1 -1
- package/src/components/content-block/content-block.component.ts +3 -2
- package/src/components/icon-picker/icon-picker.component.ts +1 -1
- package/src/components/linked-select/linked-select.component.ts +22 -5
- package/src/components/page/page.scss +13 -7
- package/src/components/page-builder/page-builder.component.ts +52 -7
- package/src/components/page-builder/page-builder.scss +166 -9
- package/src/components/page-builder/page-builder.test.ts +134 -0
- package/src/components/page-builder/page.types.ts +23 -3
- package/src/components/page-nav/page-nav.scss +9 -1
- package/src/components/panel/panel.component.ts +5 -1
- package/src/components/priority-list/priority-list.component.ts +1 -0
- package/src/components/priority-list/priority-list.scss +2 -1
- package/src/components/schedule-builder/index.ts +12 -0
- package/src/components/schedule-builder/schedule-builder.component.ts +1543 -0
- package/src/components/schedule-builder/schedule-builder.scss +448 -0
- package/src/components/schedule-builder/schedule-builder.test.ts +344 -0
- package/src/components/toggle/toggle.component.ts +2 -1
- package/src/zinc.ts +1 -0
- package/docs/superpowers/plans/2026-08-03-theme-editor.md +0 -1536
- package/docs/superpowers/specs/2026-08-03-theme-editor-design.md +0 -327
|
@@ -1,327 +0,0 @@
|
|
|
1
|
-
# zn-theme-editor — Design
|
|
2
|
-
|
|
3
|
-
Date: 2026-08-03
|
|
4
|
-
Status: approved, ready for implementation planning
|
|
5
|
-
|
|
6
|
-
## Purpose
|
|
7
|
-
|
|
8
|
-
A theme editor surface: form controls on the left, a live `zn-preview-frame` on
|
|
9
|
-
the right. Changing a control pushes the new values into the preview iframe over
|
|
10
|
-
postMessage, so the previewed page re-themes instantly with no network
|
|
11
|
-
round-trip. A toolbar above the preview switches the preview's light/dark mode
|
|
12
|
-
and its viewport width (desktop / tablet / mobile).
|
|
13
|
-
|
|
14
|
-
## Scope
|
|
15
|
-
|
|
16
|
-
Two components are touched:
|
|
17
|
-
|
|
18
|
-
1. `zn-preview-frame` — gains a `device` property, a `setTheme()` method, and a
|
|
19
|
-
guard for the empty-`data-uri` case.
|
|
20
|
-
2. `zn-theme-editor` — new component: layout, toolbar, value harvesting, and
|
|
21
|
-
optional persistence.
|
|
22
|
-
|
|
23
|
-
## Decisions
|
|
24
|
-
|
|
25
|
-
| Decision | Choice | Rationale |
|
|
26
|
-
|---|---|---|
|
|
27
|
-
| Control definition | Slotted markup — the author places real Zinc form controls in the default slot | No new schema to maintain; reuses every existing control's labelling and validation |
|
|
28
|
-
| Dark/light | Per-mode value sets; the toolbar toggle switches which set the controls are bound to | **Revised — see Revision 2 below.** Originally a single shared set; the owner reversed this because light and dark genuinely need different values |
|
|
29
|
-
| Sync | Live debounced postMessage push, plus optional debounced POST auto-save | Instant feedback; persistence is opt-in via `action` |
|
|
30
|
-
| Composition | The editor renders its own `zn-preview-frame` internally | One tag to use, layout and toolbar wiring guaranteed correct |
|
|
31
|
-
| Viewport | `device` property on `zn-preview-frame` | Sizing belongs to the frame and is reusable outside the editor; narrowing the *iframe* makes the embed's own media queries fire |
|
|
32
|
-
|
|
33
|
-
Explicitly out of scope: a `preview` slot escape hatch (the editor always
|
|
34
|
-
renders its own frame — one code path, no missing-frame handling); saving
|
|
35
|
-
`device` as part of the theme.
|
|
36
|
-
|
|
37
|
-
## Revision 2 — per-mode values (2026-08-03)
|
|
38
|
-
|
|
39
|
-
The original design had one shared value set, with the mode toggle only telling
|
|
40
|
-
the preview which way to render. The owner revised this after the first six
|
|
41
|
-
tasks shipped: **every control holds a light value and a dark value**, and the
|
|
42
|
-
toggle switches which set the controls are bound to.
|
|
43
|
-
|
|
44
|
-
Three decisions fix the shape:
|
|
45
|
-
|
|
46
|
-
1. **Every named control is per-mode.** No opt-in attribute — a uniform rule with
|
|
47
|
-
nothing for an author to remember. The cost is that genuinely
|
|
48
|
-
mode-independent values (radius, spacing) are duplicated and can drift; that
|
|
49
|
-
was accepted knowingly.
|
|
50
|
-
2. **Dark defaults are authored per control** via a `dark-value` attribute
|
|
51
|
-
alongside the existing `value`. A control with no `dark-value` initialises its
|
|
52
|
-
dark entry from `value`, so adding dark support to an existing editor is
|
|
53
|
-
additive and never breaks it.
|
|
54
|
-
3. **Both sets are persisted as bracketed FormData keys** — `light[background]`,
|
|
55
|
-
`dark[background]`. Because *every* control is per-mode there is no shared
|
|
56
|
-
bucket, so every key is bracketed. Bracket notation keeps the body as
|
|
57
|
-
`FormData`, which PHP and Go both decode natively.
|
|
58
|
-
|
|
59
|
-
Consequences that ripple through the earlier sections below — where the two
|
|
60
|
-
disagree, this revision wins:
|
|
61
|
-
|
|
62
|
-
- `values` becomes `{light: {...}, dark: {...}}` rather than a flat map.
|
|
63
|
-
- `zn-theme-change` detail becomes `{values: {light, dark}, mode, device}`.
|
|
64
|
-
- The **frame payload is unchanged**: still `{mode, values}` with `values` flat,
|
|
65
|
-
carrying only the active mode's set. The embed never needs the inactive half.
|
|
66
|
-
- Toggling mode now **writes the target set back into the slotted controls** and
|
|
67
|
-
then pushes. Write-back must be suppressed from re-entering the change handler,
|
|
68
|
-
or the toggle would queue a spurious save.
|
|
69
|
-
- Booleans (`zn-checkbox`, `zn-toggle`) take their dark state from `dark-value`
|
|
70
|
-
parsed as truthy (`"1"`/`"true"`), falling back to the `checked` attribute.
|
|
71
|
-
|
|
72
|
-
## Revision 3 — grouping controls into sections (2026-08-03)
|
|
73
|
-
|
|
74
|
-
The owner asked to group form inputs into sections. **No new component**: authors
|
|
75
|
-
wrap each group in the existing `zn-collapsible`, which already provides a
|
|
76
|
-
caption, an optional description, item counts, `default="open|closed"`, and
|
|
77
|
-
optional localStorage persistence of the open state.
|
|
78
|
-
|
|
79
|
-
This works for value collection today without any change, because harvesting
|
|
80
|
-
walks `[name]` descendants of each assigned element rather than only direct
|
|
81
|
-
children. What the editor owes is layout (`::slotted` rules so sections sit
|
|
82
|
-
correctly in the controls column) and documentation.
|
|
83
|
-
|
|
84
|
-
### The gap sections expose, now fixed
|
|
85
|
-
|
|
86
|
-
`slotchange` fires only when a slot's **direct** assignment changes. With
|
|
87
|
-
sections, controls sit one level deeper, so a control added inside an existing
|
|
88
|
-
section after mount was invisible to the editor: no light/dark entry seeded, no
|
|
89
|
-
push. This was a knowingly-deferred minor while wrappers were the exception;
|
|
90
|
-
sections make them the normal structure, so it moves onto the main path.
|
|
91
|
-
|
|
92
|
-
Fix: a `MutationController` over the editor's light-DOM subtree, and the
|
|
93
|
-
mount-race guard's comparison basis generalised from *assigned elements* to *the
|
|
94
|
-
deep set of named controls*. That subsumes `slotchange` and preserves all five of
|
|
95
|
-
the guard's correctness properties — it still compares element identity, never
|
|
96
|
-
values, so no push can be suppressed on the basis of what a control contains.
|
|
97
|
-
|
|
98
|
-
**Observe `childList` and `subtree` only — never `attributes`.** Some Zinc
|
|
99
|
-
controls reflect state to attributes (`zn-checkbox` reflects `checked`), so
|
|
100
|
-
observing attributes would make mode write-back trigger the observer and feed
|
|
101
|
-
back into the change path.
|
|
102
|
-
|
|
103
|
-
### Known constraint on styling inside a section
|
|
104
|
-
|
|
105
|
-
Sections and their controls all live in the editor's **light DOM**, so the
|
|
106
|
-
editor's stylesheet can reach the sections via `::slotted(...)` but cannot style
|
|
107
|
-
controls nested inside them. Stacking of controls within a section is
|
|
108
|
-
`zn-collapsible`'s own concern or the page's; the docs must show a pattern that
|
|
109
|
-
looks right rather than assuming the editor can fix it from outside.
|
|
110
|
-
|
|
111
|
-
## `zn-preview-frame` changes
|
|
112
|
-
|
|
113
|
-
### `device`
|
|
114
|
-
|
|
115
|
-
```ts
|
|
116
|
-
@property({reflect: true}) device: 'desktop' | 'tablet' | 'mobile' = 'desktop';
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
Constrains and centres the iframe: `desktop` = 100%, `tablet` = 768px,
|
|
120
|
-
`mobile` = 390px.
|
|
121
|
-
|
|
122
|
-
Implemented as a `.preview__stage` wrapper between `.preview` and the iframe,
|
|
123
|
-
carrying `width: <device width>; max-width: 100%; margin: 0 auto`. The iframe
|
|
124
|
-
keeps its existing percentage width, now relative to the stage, so the zoom
|
|
125
|
-
transform and all existing zoom assertions are untouched. The `max-width`
|
|
126
|
-
prevents a tablet width from overflowing a narrower panel.
|
|
127
|
-
|
|
128
|
-
Composes with `zoom`: at `zoom=1` (the editor's case) a `tablet` iframe is
|
|
129
|
-
exactly 768 CSS px. At `zoom<1` the iframe lays out at `768/zoom` px and scales
|
|
130
|
-
back down, so the embed sees proportionally more CSS pixels — which is the
|
|
131
|
-
correct browser-zoom-out semantics.
|
|
132
|
-
|
|
133
|
-
Because the iframe element itself is narrowed, the embedded document reports the
|
|
134
|
-
narrow viewport width and its own media queries fire. A wrapper-based approach
|
|
135
|
-
would not achieve this.
|
|
136
|
-
|
|
137
|
-
### `setTheme(values)`
|
|
138
|
-
|
|
139
|
-
```ts
|
|
140
|
-
setTheme(theme: Record<string, unknown>): void
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
Stores the payload and posts `{type: 'hp-preview:theme', ...theme}` to the
|
|
144
|
-
frame, checked against `frameOrigin` as with all other traffic. The stored
|
|
145
|
-
payload is **re-posted after every `hp-preview:ready` handshake**, so a frame
|
|
146
|
-
reload does not drop the in-progress theme.
|
|
147
|
-
|
|
148
|
-
The frame remains the sole postMessage gatekeeper — it is the only thing that
|
|
149
|
-
knows the iframe and its trusted origin. The editor never touches
|
|
150
|
-
`contentWindow`.
|
|
151
|
-
|
|
152
|
-
### Empty-`data-uri` guard
|
|
153
|
-
|
|
154
|
-
`_sendConfig()` currently runs on every `hp-preview:ready`, unconditionally. A
|
|
155
|
-
theme-editor-only setup has no `data-uri`, so this becomes `fetch('')`, which
|
|
156
|
-
returns the current page's HTML, fails to parse as JSON, and paints the error
|
|
157
|
-
overlay over an otherwise working preview. Fix: skip the config fetch when
|
|
158
|
-
`dataUri` is empty.
|
|
159
|
-
|
|
160
|
-
## `zn-theme-editor`
|
|
161
|
-
|
|
162
|
-
### Markup
|
|
163
|
-
|
|
164
|
-
```html
|
|
165
|
-
<zn-theme-editor
|
|
166
|
-
src="/embed?t=..."
|
|
167
|
-
frame-origin="https://pay.example"
|
|
168
|
-
data-uri="/theme/config"
|
|
169
|
-
action="/theme/save"
|
|
170
|
-
mode="light"
|
|
171
|
-
device="desktop">
|
|
172
|
-
<zn-color-select name="background" label="Background"></zn-color-select>
|
|
173
|
-
<zn-input name="radius" type="number" label="Corner radius"></zn-input>
|
|
174
|
-
<zn-button slot="footer">Save</zn-button>
|
|
175
|
-
</zn-theme-editor>
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
### Properties
|
|
179
|
-
|
|
180
|
-
| Property | Attribute | Type | Default | Notes |
|
|
181
|
-
|---|---|---|---|---|
|
|
182
|
-
| `src` | `src` | string | `''` | Forwarded to the frame |
|
|
183
|
-
| `frameOrigin` | `frame-origin` | string | `''` | Forwarded; fail-closed as today |
|
|
184
|
-
| `dataUri` | `data-uri` | string | `''` | Forwarded; optional base config |
|
|
185
|
-
| `action` | `action` | string | `''` | POST target; empty = no persistence |
|
|
186
|
-
| `mode` | `mode` | `light \| dark` | `light` | Reflected |
|
|
187
|
-
| `device` | `device` | `desktop \| tablet \| mobile` | `desktop` | Reflected; forwarded to the frame |
|
|
188
|
-
| `minHeight` | `min-height` | number | `480` | Forwarded to the frame |
|
|
189
|
-
| `debounce` | `debounce` | number | `150` | Debounce before the postMessage push |
|
|
190
|
-
| `saveDebounce` | `save-debounce` | number | `1000` | Debounce before the POST |
|
|
191
|
-
|
|
192
|
-
`values` is a read-only getter returning the current harvested map.
|
|
193
|
-
|
|
194
|
-
### Slots
|
|
195
|
-
|
|
196
|
-
- default — the controls, rendered in the left column.
|
|
197
|
-
- `footer` — actions pinned beneath the controls (rendered only when present,
|
|
198
|
-
via `HasSlotController`).
|
|
199
|
-
|
|
200
|
-
### Events
|
|
201
|
-
|
|
202
|
-
- `zn-theme-change` — `{values, mode, device}`, emitted whenever any of the three
|
|
203
|
-
changes (debounced for value changes, immediate for the toolbar), so a host
|
|
204
|
-
can drive its own save button when `action` is unset. A new event, not
|
|
205
|
-
`zn-change`: `src/events/zn-change.ts` types that event's detail as
|
|
206
|
-
`Record<PropertyKey, never>` repo-wide, so it cannot carry a payload. Follows
|
|
207
|
-
the `zn-flow-change` precedent.
|
|
208
|
-
- `zn-error` — `{message}` for failed saves. Frame errors already bubble and
|
|
209
|
-
compose out through the editor, so they are captured for display but not
|
|
210
|
-
re-emitted.
|
|
211
|
-
|
|
212
|
-
### Layout
|
|
213
|
-
|
|
214
|
-
Two columns: controls at `--zn-theme-editor-controls-width` (default `280px`),
|
|
215
|
-
preview filling the rest, stacking on narrow viewports. Above the preview sits a
|
|
216
|
-
toolbar with a three-way device segmented control (lucide `monitor`, `tablet`,
|
|
217
|
-
`smartphone`) and a `sun`/`moon` mode toggle. An inline `part="error"` strip
|
|
218
|
-
renders above the toolbar when there is an error.
|
|
219
|
-
|
|
220
|
-
The toolbar uses **native `<button>` elements** with `zn-icon` glyphs, not
|
|
221
|
-
`zn-button`. Two reasons: `zn-button` does not forward an accessible name to its
|
|
222
|
-
internal `<button>`, so icon-only Zinc buttons fail the axe check; and native
|
|
223
|
-
buttons carry proper `aria-pressed` toggle semantics. Styling them natively is
|
|
224
|
-
safe because `zn-.min.css`'s native-button rules do not cross the shadow
|
|
225
|
-
boundary, so the component's own SCSS is the only stylesheet in play. Lucide
|
|
226
|
-
glyphs render `aria-hidden`, so `aria-label` on each button is the accessible
|
|
227
|
-
name.
|
|
228
|
-
|
|
229
|
-
CSS parts: `base`, `controls`, `footer`, `toolbar`, `preview`, `error`.
|
|
230
|
-
|
|
231
|
-
## Value harvesting
|
|
232
|
-
|
|
233
|
-
Slotted controls are light-DOM children, so their `zn-change`, `zn-input`,
|
|
234
|
-
`change` and `input` events bubble to the editor host. One listener set covers
|
|
235
|
-
every control, including ones added to the slot later.
|
|
236
|
-
|
|
237
|
-
On each such event — and on `slotchange`, and once on first render — the editor
|
|
238
|
-
walks the default slot's assigned elements and collects every `[name]`
|
|
239
|
-
descendant (and any assigned element that is itself named) into a map. The
|
|
240
|
-
`[name]` **attribute** is the selector, so controls must carry `name` in markup
|
|
241
|
-
— which the slotted-markup design makes the norm anyway. Setting only the `.name`
|
|
242
|
-
property in JS will not register the control.
|
|
243
|
-
|
|
244
|
-
- boolean controls contribute `.checked`; everything else contributes `.value`.
|
|
245
|
-
"Boolean" means specifically `zn-checkbox`, `zn-toggle`, and
|
|
246
|
-
`input[type=checkbox]`. Radios are not special-cased — `zn-radio-group`
|
|
247
|
-
carries the selected value on `.value` like any other control.
|
|
248
|
-
- disabled and unnamed controls are skipped, matching native form semantics
|
|
249
|
-
- on duplicate names, the last one wins
|
|
250
|
-
|
|
251
|
-
The initial harvest matters: it pushes the authored attribute defaults into the
|
|
252
|
-
preview immediately, so the frame never renders un-themed and then snaps to the
|
|
253
|
-
real values.
|
|
254
|
-
|
|
255
|
-
## Theme message
|
|
256
|
-
|
|
257
|
-
```js
|
|
258
|
-
{ type: 'hp-preview:theme', mode: 'light', values: { background: '#ffffff', radius: '8' } }
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
`mode` travels in the payload because the embed cannot infer it. `device`
|
|
262
|
-
deliberately does not — the embed learns its width from the actual iframe box.
|
|
263
|
-
|
|
264
|
-
Consequently: **a mode change re-pushes the payload; a device change only
|
|
265
|
-
resizes the frame.** `zn-change` reports both regardless.
|
|
266
|
-
|
|
267
|
-
## Persistence
|
|
268
|
-
|
|
269
|
-
When `action` is set, changes also POST a `FormData` of the harvested values
|
|
270
|
-
(`credentials: 'same-origin'`) on the `saveDebounce` timer. `mode` and `device`
|
|
271
|
-
are view state and are not included.
|
|
272
|
-
|
|
273
|
-
Saves serialize through a single-slot queue: if changes land while a POST is in
|
|
274
|
-
flight, exactly one further save runs afterwards with the latest values. Without
|
|
275
|
-
this, overlapping POSTs can complete out of order and leave the server holding a
|
|
276
|
-
stale value.
|
|
277
|
-
|
|
278
|
-
## Error handling
|
|
279
|
-
|
|
280
|
-
Save failures emit `zn-error` and render into the inline `part="error"` strip.
|
|
281
|
-
Frame errors are captured into the same strip; they already bubble and compose
|
|
282
|
-
out to the host on their own, so the editor does not re-emit them.
|
|
283
|
-
|
|
284
|
-
No generation counter is needed here — the single-slot save queue guarantees at
|
|
285
|
-
most one POST in flight, so there is no stale response to discard. (This differs
|
|
286
|
-
from `zn-preview-frame`, where concurrent `refresh()` calls genuinely can race.)
|
|
287
|
-
|
|
288
|
-
## Testing
|
|
289
|
-
|
|
290
|
-
`zn-theme-editor` (`npx web-test-runner --group theme-editor`):
|
|
291
|
-
|
|
292
|
-
1. renders and is accessible
|
|
293
|
-
2. pushes the authored control defaults to the frame on first render
|
|
294
|
-
3. harvests and pushes updated values when a slotted control changes
|
|
295
|
-
4. toggling mode reflects the attribute and re-pushes with the new `mode`
|
|
296
|
-
5. the device buttons set the frame's `device` property
|
|
297
|
-
6. no POST is issued when `action` is unset; one is issued when it is set
|
|
298
|
-
(stubbed `fetch`)
|
|
299
|
-
|
|
300
|
-
`zn-preview-frame` (`npx web-test-runner --group preview-frame`):
|
|
301
|
-
|
|
302
|
-
1. `device` constrains the iframe width
|
|
303
|
-
2. `setTheme()` is replayed after a `hp-preview:ready` handshake
|
|
304
|
-
3. no config fetch when `dataUri` is empty
|
|
305
|
-
|
|
306
|
-
Notes: `npm run test:component` is watch-mode only and hangs non-interactive
|
|
307
|
-
shells. `zn-button` overrides `click()` and dispatches nothing — tests must
|
|
308
|
-
dispatch a composed, bubbling `MouseEvent` instead. Gate lint on touched files
|
|
309
|
-
with `npx eslint <paths>`; the repo-wide run has many pre-existing problems.
|
|
310
|
-
|
|
311
|
-
## Docs
|
|
312
|
-
|
|
313
|
-
- New `docs/pages/components/theme-editor.md` following the existing component
|
|
314
|
-
page conventions.
|
|
315
|
-
- Extend `docs/pages/components/preview-frame-demo.njk` to handle
|
|
316
|
-
`hp-preview:theme` (apply the background value and a mode class) so the
|
|
317
|
-
theme-editor docs example is genuinely live. The existing preview-frame
|
|
318
|
-
examples must keep working.
|
|
319
|
-
- Docs example scripts run before `zn.min.js` registers elements: method calls
|
|
320
|
-
need `customElements.whenDefined(...)`; property assignment is safe.
|
|
321
|
-
- Any literal `{{ }}` in docs markdown needs `{% raw %}…{% endraw %}`.
|
|
322
|
-
|
|
323
|
-
## Build
|
|
324
|
-
|
|
325
|
-
Do not run `npm run build` — it kills the running `npm run watch`, which
|
|
326
|
-
rebuilds `dist` incrementally. New component must be exported from
|
|
327
|
-
`src/zinc.ts`.
|