@kubex/zinc 1.1.92 → 1.1.95

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 (53) hide show
  1. package/dist/custom-elements.json +2880 -453
  2. package/dist/vscode.html-custom-data.json +209 -22
  3. package/dist/web-types.json +452 -39
  4. package/dist/zn.d.ts +633 -61
  5. package/dist/zn.min.css +1 -1
  6. package/dist/zn.min.js +865 -560
  7. package/docs/pages/components/page-builder.md +143 -19
  8. package/docs/pages/components/schedule-builder.md +345 -0
  9. package/docs/pages/components/slash-menu.md +132 -6
  10. package/docs/pages/components/textarea.md +16 -0
  11. package/package.json +1 -1
  12. package/scss/_root.scss +7 -1
  13. package/src/components/alert/alert.scss +9 -13
  14. package/src/components/button/button.scss +5 -2
  15. package/src/components/chip/chip.scss +1 -1
  16. package/src/components/icon-picker/icon-picker.component.ts +1 -1
  17. package/src/components/inline-edit/inline-edit.component.ts +6 -1
  18. package/src/components/input/input.component.ts +12 -2
  19. package/src/components/linked-select/linked-select.component.ts +22 -5
  20. package/src/components/page/page.scss +20 -9
  21. package/src/components/page-builder/modules/page-section-card/page-section-card.component.ts +16 -9
  22. package/src/components/page-builder/modules/page-section-card/page-section-card.scss +13 -0
  23. package/src/components/page-builder/modules/page-section-card/page-section-card.test.ts +9 -0
  24. package/src/components/page-builder/page-builder.component.ts +535 -232
  25. package/src/components/page-builder/page-builder.scss +230 -19
  26. package/src/components/page-builder/page-builder.test.ts +790 -110
  27. package/src/components/page-builder/page-tree.test.ts +483 -0
  28. package/src/components/page-builder/page-tree.ts +329 -0
  29. package/src/components/page-builder/page.types.ts +98 -10
  30. package/src/components/page-nav/page-nav.scss +9 -1
  31. package/src/components/panel/panel.component.ts +5 -1
  32. package/src/components/priority-list/priority-list.component.ts +1 -0
  33. package/src/components/priority-list/priority-list.scss +2 -1
  34. package/src/components/remarkd-editor/remarkd-editor.component.ts +198 -9
  35. package/src/components/remarkd-editor/remarkd-editor.scss +81 -0
  36. package/src/components/remarkd-editor/remarkd-editor.test.ts +179 -0
  37. package/src/components/schedule-builder/index.ts +12 -0
  38. package/src/components/schedule-builder/schedule-builder.component.ts +1543 -0
  39. package/src/components/schedule-builder/schedule-builder.scss +448 -0
  40. package/src/components/schedule-builder/schedule-builder.test.ts +344 -0
  41. package/src/components/settings-container/settings-container.scss +2 -1
  42. package/src/components/slash-item/slash-item.component.ts +1 -1
  43. package/src/components/slash-menu/slash-menu-items.ts +48 -0
  44. package/src/components/slash-menu/slash-menu.component.ts +134 -27
  45. package/src/components/slash-menu/slash-menu.scss +90 -12
  46. package/src/components/slash-menu/slash-menu.test.ts +107 -0
  47. package/src/components/textarea/textarea.component.ts +12 -2
  48. package/src/components/textarea/textarea.test.ts +2 -2
  49. package/src/components/toggle/toggle.component.ts +2 -1
  50. package/src/components/translations/translations.component.ts +5 -1
  51. package/src/zinc.ts +1 -0
  52. package/docs/superpowers/plans/2026-08-03-theme-editor.md +0 -1536
  53. 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`.