@zeyos/zx 2.2.1 → 2.3.0

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 (39) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/README.md +28 -2
  3. package/dist/types/components/multi-value-editor/multi-value-editor.d.ts +67 -8
  4. package/dist/types/components/permission/permission.d.ts +3 -0
  5. package/dist/types/components/select/select.d.ts +36 -1
  6. package/dist/types/components/tabbox/tabbox.d.ts +12 -3
  7. package/dist/zx-compat.esm.js +302 -34
  8. package/dist/zx-compat.esm.min.js +1 -1
  9. package/dist/zx-compat.global.js +302 -34
  10. package/dist/zx-compat.global.min.js +3 -3
  11. package/dist/zx-zeyos.esm.js +280 -27
  12. package/dist/zx-zeyos.esm.min.js +1 -1
  13. package/dist/zx.css +159 -33
  14. package/dist/zx.esm.js +302 -34
  15. package/dist/zx.esm.min.js +3 -3
  16. package/dist/zx.global.js +302 -34
  17. package/dist/zx.global.min.js +3 -3
  18. package/dist/zx.min.css +1 -1
  19. package/docs/api.json +420 -17
  20. package/docs/llms.md +54 -12
  21. package/package.json +1 -1
  22. package/src/components/date-picker/date-range-picker.css +7 -0
  23. package/src/components/fieldset/fieldset.css +7 -0
  24. package/src/components/form/form.css +7 -0
  25. package/src/components/multi-value-editor/multi-value-editor.css +99 -15
  26. package/src/components/multi-value-editor/multi-value-editor.js +235 -18
  27. package/src/components/navigation-bar/navigation-bar.css +7 -0
  28. package/src/components/permission/permission.js +3 -0
  29. package/src/components/search/search.css +10 -2
  30. package/src/components/select/select.css +6 -0
  31. package/src/components/select/select.js +99 -11
  32. package/src/components/stepper/stepper.css +12 -2
  33. package/src/components/tabbox/tabbox.css +13 -3
  34. package/src/components/tabbox/tabbox.js +34 -10
  35. package/styles/tokens/dark.css +13 -10
  36. package/styles/tokens/global.css +12 -0
  37. package/styles/tokens/semantic.css +9 -5
  38. package/styles/tokens/themes.css +72 -0
  39. package/styles/zx.css +1 -0
package/CHANGELOG.md CHANGED
@@ -8,6 +8,116 @@ and friends remain the complete record.
8
8
 
9
9
  ## Unreleased
10
10
 
11
+ ## 2.3.0 — 2026-08-23
12
+
13
+ ### Added
14
+
15
+ - **Six standard themes, as an attribute.** `data-zx-preset="zx" | "zeyos" | "ocean" | "violet" |
16
+ "rose" | "slate"` picks the accent ramp, orthogonally to `data-zx-theme` picking the polarity.
17
+ A preset is five tier-1 declarations — `--zx-accent-300` … `--zx-accent-700` — because the accent
18
+ roles now resolve through that ramp rather than through the green palette directly, so one block
19
+ rethemes light, dark, and auto together with no second list to keep in sync. Every shipped
20
+ preset keeps its 600 stop above 4.5:1 against white and its 400 stop above 4.5:1 against the dark
21
+ page, which is what lets `--zx-color-on-accent` stay a single value per theme. Status colours are
22
+ deliberately not part of a preset.
23
+ - **A theme studio at `/theme.html`.** Every component on one page under a live theme: cycle the
24
+ standard presets (or press `[` and `]`), set a custom accent that is derived into a full ramp in
25
+ OKLab with the contrast ratio reported as you go, tint the neutral ramp, and change radius,
26
+ control height, base text size, type stack, and density — then copy the result out as CSS.
27
+ What you pick applies to the whole documentation site until you reset it.
28
+
29
+ - **`Select` takes fixed choices.** `fixedItems` pins entries to the top of the list, above a rule,
30
+ and treats them as part of the control rather than of its data: a `filter` function replaces the
31
+ item list on every query, so a pinned entry is narrowed locally against the same query instead,
32
+ survives `setItems()`, shows below `minQuery`, and stays resolvable by `set()` once the first
33
+ results have landed. That is what a choice with no row in the source needs — "Unassigned",
34
+ "Everyone", "Private". `setFixedItems()` replaces them later.
35
+ - **`Select.permission()`**, the record-access picker: Private and Public pinned above the groups a
36
+ record may be shared with, carrying the tri-state ZeyOS stores — `false`, `true`, or a group ID —
37
+ so it binds straight to the record field. `groups` takes an array, or a loader `(query) => items`
38
+ for an installation whose group list is too long to ship with the form.
39
+
40
+ - **`MultiValueEditor` rows reorder by dragging.** Each row grows a handle that drags it to a new
41
+ position, with a line showing where it will land. `reorder` picks what a row offers — `'both'`
42
+ (the default), `'drag'` for the handle alone, `'buttons'` for the arrows alone, or `'none'`.
43
+ Dragging goes through the handle rather than the whole row, so selecting text inside a row's
44
+ input still works, and the handle is a keyboard control too (arrow keys, Home, End) — which is
45
+ what keeps `reorder: 'drag'` operable without a pointer.
46
+
47
+ ### Changed
48
+
49
+ - **The documentation shows the demo data behind an example.** `items: catalogue()` documented
50
+ nothing on its own — the shape the component wants was in a helper the reader could only find by
51
+ opening the whole module. Every example's code block now carries a tab per module-level
52
+ declaration it uses, transitively, extracted from the demo module's own text. Nothing about
53
+ writing a demo changes: declare the data at the top of the module and it appears.
54
+
55
+ ### Deprecated
56
+
57
+ - **`Permission`.** Its three radios and a disabled group picker spend a form row on a choice
58
+ `Select.permission()` makes in one control, and the group list could only ever be an array held
59
+ in memory. The class stays for `gx.zeyos.Permission` and goes away in 3.0; the value and the
60
+ `groups` option are identical, so replacing it is a one-line change.
61
+
62
+ ### Fixed
63
+
64
+ - **`MultiValueEditor` no longer collapses in a host that sizes to its contents.** It is no longer
65
+ a size container at all. `inline-size: 100%` does not rescue one: the percentage resolves against
66
+ a containing block the host measured from the contribution containment had already zeroed, so the
67
+ editor still came out 26px wide in an inline-block, a table cell, or a shrink-to-fit flex row,
68
+ with the row spilling out to the left of it. The rows reflow on their own now — the controls wrap
69
+ under the value when the row runs out of room — which also reads the width that decides the
70
+ layout, the row's, rather than the editor's. Verified from a 1000px grid down to a 100px pane
71
+ with nothing overflowing. `DateRangePicker`, `Fieldset`, and `NavigationBar` still use
72
+ containment and are only partly rescued — see below.
73
+ - **`MultiValueEditor` keeps focus on the move button.** Up and down disabled themselves at the
74
+ ends of the list, so a row that reached the top took focus down to the document with it, and a
75
+ move sent focus to the value input rather than leaving it on the button that had just moved the
76
+ row — pressing up twice was impossible without going back for it. The ends now mark themselves
77
+ `aria-disabled` and stay focusable.
78
+ - **`MultiValueEditor` keeps a stored value its option list has lost.** A value with no matching
79
+ option displayed as the first option while `getValues()` went on returning the stored one, so a
80
+ form that was only opened and saved rewrote the field. The unknown value is now an option in its
81
+ own right.
82
+
83
+ - **The × on a closable `Tabbox` tab closes it.** It was drawn as a decorative glyph with no
84
+ handler, so clicking it merely activated the tab and closing was reachable only through the
85
+ Delete key. It is now a real hit target that emits `close` and hands focus to the neighbouring
86
+ tab, with Delete unchanged. It stays a `<span>` rather than a nested `<button>`, which buttons
87
+ cannot contain.
88
+ - **Size containers fill their host instead of collapsing inside it.** `DateRangePicker`,
89
+ `Fieldset`, and `NavigationBar` declare `container-type: inline-size` to pick their own layout,
90
+ which means they contribute nothing to their own intrinsic width — so in a flex row, an
91
+ inline-block, or a grid `auto` track they were squeezed to a few pixels and their contents
92
+ spilled out on top of each other. They now take `inline-size: 100%`, and `Form` does too, because
93
+ containment travels upward: a form shrink-wrapped to its action buttons while the fieldset inside
94
+ it overflowed. Eight examples on the documentation site were rendering this way.
95
+
96
+ This fixes every host that has a width of its own, which is the case that was actually biting.
97
+ It cannot fix a host that shrink-wraps its contents — an `inline-block` or a `fit-content` track
98
+ with no width of its own — because there 100% resolves against a containing block the collapse
99
+ has already zeroed. Only dropping containment does, as `MultiValueEditor` now has;
100
+ `DateRangePicker`, `Fieldset`, and `NavigationBar` still need a definite width from their host.
101
+ `tests/unit/size-containers.test.js` keeps every `container-type` rule declaring a width.
102
+ - **A focused `Search` field is ringed as one control.** The input carried its own focus ring on
103
+ top of the ring the field already draws on `:focus-within`, so a smaller rounded box appeared
104
+ inside the field and cut the submit button out of it — the field read as an input with a
105
+ detached button beside it. The input now rides the field's ring, and the clear and submit
106
+ buttons mark themselves with an inset ring so tabbing still shows which one has focus without
107
+ breaking the outline.
108
+
109
+ - **The generated API reference stopped losing methods to inline type casts.** `/** @type {X} */
110
+ (expr)` is a doc comment with no method behind it, and the scanner in `tools/build-api.js` was
111
+ allowed to run past it to the next one — swallowing that method's own comment, and with it the
112
+ method. Sixty-three public methods across forty-four components had no row on the documentation
113
+ site, `Dialog.open()` and `Table.commitEdit()` among them, and a few of the rows that did appear
114
+ had absorbed a neighbour's parameters into signatures like
115
+ `destroy(options, column, index, node)`.
116
+ - **A horizontal `Stepper` spans its container.** The connectors were already written to absorb
117
+ whatever space was left over, but the steps themselves could not grow, so there was never any
118
+ slack to absorb and the rail bunched up on the left of a wide card. The labels still size to
119
+ themselves; the rules between them now stretch.
120
+
11
121
  ## 2.2.1 — 2026-08-21
12
122
 
13
123
  ### Fixed
package/README.md CHANGED
@@ -217,8 +217,34 @@ the document. Select a built-in theme and density on any ancestor (normally `<ht
217
217
  ```
218
218
 
219
219
  Theme values are `light`, `dark`, and `auto`; density values are `cozy` and `compact`. `auto`
220
- follows the operating-system color preference. A product theme can override semantic tokens
221
- without reaching into component CSS:
220
+ follows the operating-system color preference.
221
+
222
+ A third attribute picks the **accent**, orthogonally to the other two:
223
+
224
+ ```html
225
+ <html data-zx-theme="dark" data-zx-preset="ocean">
226
+ ```
227
+
228
+ Six presets ship — `zx` (the ZeyOS green, and the default), `zeyos`, `ocean`, `violet`, `rose`,
229
+ `slate`. Each is five declarations of the tier-1 accent ramp that the semantic tier resolves
230
+ through, so a preset rethemes light, dark, and auto at once:
231
+
232
+ ```css
233
+ [data-zx-preset="acme"] {
234
+ --zx-accent-300: #c4b4ff;
235
+ --zx-accent-400: #a684ff;
236
+ --zx-accent-500: #8e51ff;
237
+ --zx-accent-600: #7f22fe;
238
+ --zx-accent-700: #7008e7;
239
+ }
240
+ ```
241
+
242
+ Light surfaces take the 600 and 700 stops, dark ones take 300, 400, and 500 — so keep 600 legible
243
+ against white and 400 against near-black, and `--zx-color-on-accent` stays correct in both. The
244
+ [theme studio](https://zx.zeyos.com/theme.html) renders every component under a live theme and
245
+ exports the result.
246
+
247
+ Anything beyond the accent is a semantic token override, without reaching into component CSS:
222
248
 
223
249
  ```css
224
250
  :root[data-zx-theme="acme"] {
@@ -12,21 +12,28 @@ export type MultiValueEditorOptions = {
12
12
  * Add-row button text.
13
13
  */
14
14
  addLabel?: string;
15
+ /**
16
+ * Reordering affordances each row gets:
17
+ * a drag handle, the up/down buttons, both, or neither.
18
+ */
19
+ reorder?: 'both' | 'buttons' | 'drag' | 'none';
15
20
  };
16
- /**
17
- * @typedef {Object} MultiValueEditorOptions
18
- * @property {unknown[]} [values=[]] Initial ordered values.
19
- * @property {unknown[]|Record<string, string>|null} [options=null] Allowed values; null uses text inputs.
20
- * @property {string} [addLabel='Add value'] Add-row button text.
21
- */
22
21
  /**
23
22
  * Ordered value editor with explicit editable rows and controls.
23
+ *
24
+ * Rows reorder by dragging their handle or by pressing the up/down buttons — `reorder` picks which
25
+ * of the two a row offers. The handle is also a keyboard control (arrows, Home, End), so drag-only
26
+ * mode still reorders without a pointer.
24
27
  * @fires MultiValueEditor#change
25
28
  * @extends {Component<MultiValueEditorOptions>}
26
29
  */
27
30
  export declare class MultiValueEditor extends Component<MultiValueEditorOptions> {
28
31
  _values: string[];
29
32
  _ownedNodes: any[];
33
+ _initialState: string;
34
+ _dragIndex: number;
35
+ /** @type {HTMLElement|null} Row made draggable by a pointer press on its handle. */
36
+ _armed: HTMLElement | null;
30
37
  _rows: HTMLElement;
31
38
  _add: HTMLElement;
32
39
  static cssName: string;
@@ -48,12 +55,64 @@ export declare class MultiValueEditor extends Component<MultiValueEditorOptions>
48
55
  setValues(values: unknown[], { silent }?: {
49
56
  silent?: boolean;
50
57
  }): this;
51
- /** @param {number} [focusIndex=-1] @returns {void} */
52
- _renderRows(focusIndex?: number): void;
58
+ /** @returns {{drag: boolean, buttons: boolean}} */
59
+ _reorder(): {
60
+ drag: boolean;
61
+ buttons: boolean;
62
+ };
63
+ /**
64
+ * @param {number} [focusIndex=-1] Row to focus after the render, or -1 for none.
65
+ * @param {keyof PARTS} [focusPart='value'] Which control of that row takes focus.
66
+ * @returns {void}
67
+ */
68
+ _renderRows(focusIndex?: number, focusPart?: keyof PARTS): void;
53
69
  /** @param {MouseEvent} event @returns {void} */
54
70
  _handleClick(event: MouseEvent): void;
55
71
  /** @param {Event} event @returns {void} */
56
72
  _handleChange(event: Event): void;
73
+ /**
74
+ * The handle is the keyboard equivalent of dragging it, so `reorder: 'drag'` is still operable
75
+ * without a pointer.
76
+ * @param {KeyboardEvent} event
77
+ * @returns {void}
78
+ */
79
+ _handleKeydown(event: KeyboardEvent): void;
80
+ /**
81
+ * Arms the row for dragging. Only the handle does this, so text selection inside a row's input
82
+ * keeps working and a stray drag cannot reorder anything.
83
+ * @param {PointerEvent} event
84
+ * @returns {void}
85
+ */
86
+ _handlePointerDown(event: PointerEvent): void;
87
+ /** @returns {void} */
88
+ _disarm(): void;
89
+ /** @param {DragEvent} event @returns {void} */
90
+ _handleDragStart(event: DragEvent): void;
91
+ /** @param {DragEvent} event @returns {void} */
92
+ _handleDragOver(event: DragEvent): void;
93
+ /** @param {DragEvent} event @returns {void} */
94
+ _handleDragLeave(event: DragEvent): void;
95
+ /** @param {DragEvent} event @returns {void} */
96
+ _handleDrop(event: DragEvent): void;
97
+ /** @returns {void} */
98
+ _finishDrag(): void;
99
+ /**
100
+ * Insertion slot (0…length) for a pointer at `clientY`.
101
+ * @param {number} clientY Pointer position.
102
+ * @returns {number}
103
+ */
104
+ _dropTarget(clientY: number): number;
105
+ /** @param {number} slot Insertion slot to mark. @returns {void} */
106
+ _showDrop(slot: number): void;
107
+ /** @returns {void} */
108
+ _clearDrop(): void;
109
+ /**
110
+ * @param {number} from Index to move.
111
+ * @param {number} to Destination index.
112
+ * @param {keyof PARTS} part Control of the moved row that takes focus.
113
+ * @returns {boolean} Whether anything moved.
114
+ */
115
+ _move(from: number, to: number, part: keyof PARTS): boolean;
57
116
  /** @returns {void} */
58
117
  _emitChange(): void;
59
118
  /** @returns {void} */
@@ -44,6 +44,9 @@ export type PermissionChangeDetail = {
44
44
  /**
45
45
  * Record-permission choice for private, public, and group access.
46
46
  *
47
+ * @deprecated Use `Select.permission(target, {groups})`, which carries the same tri-state value in
48
+ * one control and accepts a loader function for installations whose group list is too long to ship
49
+ * with the form. This class stays for `gx.zeyos.Permission` and will be removed in 3.0.
47
50
  * @fires Permission#change
48
51
  * @extends {Component<PermissionOptions>}
49
52
  */
@@ -8,6 +8,10 @@ export type SelectOptions = {
8
8
  * Available values.
9
9
  */
10
10
  items?: SelectItem[];
11
+ /**
12
+ * Choices pinned above the list, kept through filtering and async loads.
13
+ */
14
+ fixedItems?: SelectItem[];
11
15
  /**
12
16
  * Item ID property or reader.
13
17
  */
@@ -120,7 +124,9 @@ export type SelectSetOptions = {
120
124
  export declare class Select extends Component<SelectOptions> {
121
125
  _items: any[] | SelectItem[];
122
126
  _visibleItems: any[];
123
- _selected: any;
127
+ _fixedItems: any[] | SelectItem[];
128
+ _visibleFixed: any[] | SelectItem[];
129
+ _selected: SelectItem;
124
130
  _optionEntries: any[];
125
131
  _activeIndex: number;
126
132
  _open: boolean;
@@ -159,6 +165,12 @@ export declare class Select extends Component<SelectOptions> {
159
165
  * @returns {this}
160
166
  */
161
167
  setItems(items: SelectItem[]): this;
168
+ /**
169
+ * Replaces the pinned choices and preserves selection by ID when possible.
170
+ * @param {SelectItem[]} items New fixed items.
171
+ * @returns {this}
172
+ */
173
+ setFixedItems(items: SelectItem[]): this;
162
174
  /**
163
175
  * Restores the configured initial value.
164
176
  * @param {{silent?: boolean}} [options={}] Reset behavior.
@@ -184,6 +196,20 @@ export declare class Select extends Component<SelectOptions> {
184
196
  * @returns {Select}
185
197
  */
186
198
  static priority(target: Element | string | null, options?: SelectOptions): Select;
199
+ /**
200
+ * Creates the record-permission picker: the fixed Private and Public choices above the groups a
201
+ * record can be shared with. The value is the tri-state ZeyOS stores — `false` for private,
202
+ * `true` for public, or a group ID — so the control binds straight to the record field.
203
+ *
204
+ * @param {Element|string|null} target Component target.
205
+ * @param {SelectOptions & {groups?: SelectItem[]|((query: string) => Promise<SelectItem[]>|SelectItem[])}} [options={}]
206
+ * Select overrides other than the fixed choices; `groups` holds the shareable groups, either as
207
+ * an array (filtered locally) or as a loader function (queried remotely).
208
+ * @returns {Select}
209
+ */
210
+ static permission(target: Element | string | null, options?: SelectOptions & {
211
+ groups?: SelectItem[] | ((query: string) => Promise<SelectItem[]> | SelectItem[]);
212
+ }): Select;
187
213
  /** Cleans up the panel, pending async work, and enhanced target contents. @returns {void} */
188
214
  destroy(): void;
189
215
  /** @param {KeyboardEvent} event @returns {void} */
@@ -224,6 +250,15 @@ export declare class Select extends Component<SelectOptions> {
224
250
  _itemLabel(item: SelectItem | null): string;
225
251
  /** @param {SelectItem|null} item @param {SelectValueReader|SelectLabelReader} reader @returns {unknown} */
226
252
  _read(item: SelectItem | null, reader: SelectValueReader | SelectLabelReader): unknown;
253
+ /** @returns {Array<SelectValueReader|SelectLabelReader>} */
254
+ _searchKeys(): Array<SelectValueReader | SelectLabelReader>;
255
+ /**
256
+ * Narrows the pinned choices. They are always matched here and never handed to a filter
257
+ * function: a fixed choice belongs to the control, not to whatever the remote source returns.
258
+ * @param {string} query Search text.
259
+ * @returns {SelectItem[]}
260
+ */
261
+ _matchFixed(query: string): SelectItem[];
227
262
  /** @returns {number} */
228
263
  _selectedEntryIndex(): number;
229
264
  /** @param {number} itemIndex @returns {number} */
@@ -18,7 +18,7 @@ export type TabDefinition = {
18
18
  */
19
19
  icon?: string;
20
20
  /**
21
- * Whether Delete can close the tab.
21
+ * Whether the tab shows a close control and Delete closes it.
22
22
  */
23
23
  closable?: boolean;
24
24
  /**
@@ -61,7 +61,8 @@ export type TabboxOptions = {
61
61
  previous: string | null;
62
62
  }>) => void;
63
63
  /**
64
- * Close callback.
64
+ * Close callback, fired when the
65
+ * user dismisses a closable tab.
65
66
  */
66
67
  onclose?: (event: CustomEvent<{
67
68
  name: string;
@@ -107,7 +108,8 @@ export type TabRecord = {
107
108
  * control, for a toolbar or card header.
108
109
  * @property {boolean} [keepAlive=true] Keep inactive panel elements mounted.
109
110
  * @property {(event: CustomEvent<{name: string, previous: string|null}>) => void} [onchange] Change callback.
110
- * @property {(event: CustomEvent<{name: string}>) => void} [onclose] Close callback.
111
+ * @property {(event: CustomEvent<{name: string}>) => void} [onclose] Close callback, fired when the
112
+ * user dismisses a closable tab.
111
113
  */
112
114
  /**
113
115
  * @typedef {Object} TabRecord
@@ -211,6 +213,13 @@ export declare class Tabbox extends Component<TabboxOptions> {
211
213
  destroy(): void;
212
214
  /** @param {KeyboardEvent} event @returns {void} */
213
215
  _onKeydown(event: KeyboardEvent): void;
216
+ /**
217
+ * Closes a tab on a user gesture, keeping roving focus inside the tablist.
218
+ * @param {TabRecord} record Tab to close.
219
+ * @returns {void}
220
+ * @fires Tabbox#close
221
+ */
222
+ _closeTab(record: TabRecord): void;
214
223
  /** @param {string} name @param {boolean} emitClose @returns {boolean} */
215
224
  _removeTab(name: string, emitClose: boolean): boolean;
216
225
  /** @param {TabRecord} record @returns {void} */