@zeyos/zx 2.3.0 → 4.0.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 (150) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/DESIGN-SYSTEM.md +96 -0
  3. package/README.md +29 -39
  4. package/dist/types/components/account-menu/account-menu.d.ts +144 -0
  5. package/dist/types/components/app-sidebar/app-sidebar.d.ts +243 -0
  6. package/dist/types/components/avatar/avatar.d.ts +100 -0
  7. package/dist/types/components/card/card.d.ts +128 -0
  8. package/dist/types/components/chart/chart.d.ts +322 -0
  9. package/dist/types/components/date-picker/month-picker.d.ts +6 -0
  10. package/dist/types/components/dialog/dialog.d.ts +9 -0
  11. package/dist/types/components/dock/dock.d.ts +495 -0
  12. package/dist/types/components/dropdown/dropdown.d.ts +2 -2
  13. package/dist/types/components/field/field.d.ts +14 -0
  14. package/dist/types/components/field-upload/field-upload.d.ts +37 -4
  15. package/dist/types/components/form/form.d.ts +25 -3
  16. package/dist/types/components/grid/grid.d.ts +172 -0
  17. package/dist/types/components/launcher/launcher.d.ts +359 -0
  18. package/dist/types/components/layout/layout.d.ts +4 -4
  19. package/dist/types/components/menu-button/menu-button.d.ts +10 -2
  20. package/dist/types/components/modal/modal.d.ts +25 -0
  21. package/dist/types/components/multi-value-editor/multi-value-editor.d.ts +7 -1
  22. package/dist/types/components/questionnaire/items.d.ts +327 -0
  23. package/dist/types/components/questionnaire/questionnaire.d.ts +449 -0
  24. package/dist/types/components/select/select.d.ts +51 -0
  25. package/dist/types/components/sheet/sheet-stack.d.ts +167 -0
  26. package/dist/types/components/sheet/sheet.d.ts +294 -0
  27. package/dist/types/components/split-view/split-view.d.ts +2 -21
  28. package/dist/types/components/table/table.d.ts +223 -15
  29. package/dist/types/components/tooltip/tooltip.d.ts +2 -2
  30. package/dist/types/components/value-list/value-list.d.ts +24 -3
  31. package/dist/types/core/dom.d.ts +122 -0
  32. package/dist/types/core/drag-axis.d.ts +111 -0
  33. package/dist/types/core/position.d.ts +4 -2
  34. package/dist/types/index.d.ts +13 -2
  35. package/dist/types/internal/app-rail.d.ts +265 -0
  36. package/dist/types/zeyos/icons.d.ts +5 -0
  37. package/dist/types/zeyos/index.d.ts +2 -1
  38. package/dist/types/zeyos/launcher.d.ts +147 -0
  39. package/dist/types/zeyos/select.d.ts +39 -0
  40. package/dist/zx-zeyos.esm.js +1870 -133
  41. package/dist/zx-zeyos.esm.min.js +1 -1
  42. package/dist/zx.css +2006 -267
  43. package/dist/zx.esm.js +8987 -2757
  44. package/dist/zx.esm.min.js +3 -3
  45. package/dist/zx.global.js +8987 -2757
  46. package/dist/zx.global.min.js +3 -3
  47. package/dist/zx.min.css +1 -1
  48. package/docs/api.json +4396 -2344
  49. package/docs/llms.md +463 -86
  50. package/docs/llms.txt +32 -19
  51. package/package.json +10 -17
  52. package/src/components/account-menu/account-menu.css +70 -0
  53. package/src/components/account-menu/account-menu.js +208 -0
  54. package/src/components/app-rail/app-rail.css +107 -0
  55. package/src/components/app-sidebar/app-sidebar.css +80 -0
  56. package/src/components/app-sidebar/app-sidebar.js +588 -0
  57. package/src/components/avatar/avatar.css +61 -0
  58. package/src/components/avatar/avatar.js +222 -0
  59. package/src/components/button/button.css +0 -1
  60. package/src/components/card/card.css +149 -0
  61. package/src/components/card/card.js +285 -0
  62. package/src/components/chart/chart.css +61 -0
  63. package/src/components/chart/chart.js +523 -0
  64. package/src/components/date-picker/month-picker.js +9 -0
  65. package/src/components/dialog/dialog.js +11 -4
  66. package/src/components/dock/dock.css +228 -0
  67. package/src/components/dock/dock.js +977 -0
  68. package/src/components/dropdown/dropdown.js +1 -1
  69. package/src/components/field/field.js +2 -0
  70. package/src/components/field-upload/field-upload.js +9 -4
  71. package/src/components/form/form.js +6 -3
  72. package/src/components/grid/grid.css +6 -0
  73. package/src/components/grid/grid.js +220 -0
  74. package/src/components/launcher/launcher.css +210 -0
  75. package/src/components/launcher/launcher.js +765 -0
  76. package/src/components/layout/layout.js +2 -2
  77. package/src/components/menu-button/menu-button.js +27 -2
  78. package/src/components/modal/modal.css +5 -1
  79. package/src/components/modal/modal.js +37 -2
  80. package/src/components/multi-value-editor/multi-value-editor.js +2 -1
  81. package/src/components/questionnaire/items.js +370 -0
  82. package/src/components/questionnaire/questionnaire.css +387 -0
  83. package/src/components/questionnaire/questionnaire.js +1033 -0
  84. package/src/components/select/field-adapter.js +9 -4
  85. package/src/components/select/select.css +144 -11
  86. package/src/components/select/select.js +259 -27
  87. package/src/components/sheet/sheet-stack.js +304 -0
  88. package/src/components/sheet/sheet.css +351 -0
  89. package/src/components/sheet/sheet.js +551 -0
  90. package/src/components/split-view/split-view.js +76 -161
  91. package/src/components/table/table.css +43 -0
  92. package/src/components/table/table.js +531 -52
  93. package/src/components/tooltip/tooltip.js +5 -2
  94. package/src/components/value-list/value-list.js +6 -3
  95. package/src/core/dom.js +322 -0
  96. package/src/core/drag-axis.js +203 -0
  97. package/src/core/position.js +68 -22
  98. package/src/index.js +20 -2
  99. package/src/internal/app-rail.js +718 -0
  100. package/src/zeyos/icons.js +8 -3
  101. package/src/zeyos/index.js +7 -1
  102. package/src/zeyos/launcher.js +338 -0
  103. package/src/zeyos/modules.js +1 -0
  104. package/src/zeyos/select.js +115 -1
  105. package/styles/tokens/dark.css +2 -3
  106. package/styles/tokens/global.css +6 -6
  107. package/styles/tokens/modules.css +1 -0
  108. package/styles/tokens/semantic.css +5 -3
  109. package/styles/tokens/themes.css +1 -1
  110. package/styles/zx.css +11 -1
  111. package/MIGRATION.md +0 -598
  112. package/dist/types/compat/base.d.ts +0 -156
  113. package/dist/types/compat/globals.d.ts +0 -20
  114. package/dist/types/compat/index.d.ts +0 -128
  115. package/dist/types/compat/map/basic.d.ts +0 -175
  116. package/dist/types/compat/map/data.d.ts +0 -102
  117. package/dist/types/compat/map/date-time.d.ts +0 -80
  118. package/dist/types/compat/map/factory.d.ts +0 -9
  119. package/dist/types/compat/map/form.d.ts +0 -98
  120. package/dist/types/compat/map/helpers.d.ts +0 -12
  121. package/dist/types/compat/map/http.d.ts +0 -36
  122. package/dist/types/compat/map/layout.d.ts +0 -106
  123. package/dist/types/compat/map/options.d.ts +0 -80
  124. package/dist/types/compat/map/select.d.ts +0 -95
  125. package/dist/types/compat/map/stubs.d.ts +0 -32
  126. package/dist/types/compat/map/utilities.d.ts +0 -37
  127. package/dist/types/compat-entry.d.ts +0 -5
  128. package/dist/types/components/permission/permission.d.ts +0 -96
  129. package/dist/zx-compat.esm.js +0 -17484
  130. package/dist/zx-compat.esm.min.js +0 -1
  131. package/dist/zx-compat.global.js +0 -23635
  132. package/dist/zx-compat.global.min.js +0 -3
  133. package/src/compat/base.js +0 -291
  134. package/src/compat/globals.js +0 -165
  135. package/src/compat/index.js +0 -84
  136. package/src/compat/map/basic.js +0 -464
  137. package/src/compat/map/data.js +0 -232
  138. package/src/compat/map/date-time.js +0 -177
  139. package/src/compat/map/factory.js +0 -45
  140. package/src/compat/map/form.js +0 -330
  141. package/src/compat/map/helpers.js +0 -46
  142. package/src/compat/map/http.js +0 -117
  143. package/src/compat/map/layout.js +0 -256
  144. package/src/compat/map/options.js +0 -267
  145. package/src/compat/map/select.js +0 -198
  146. package/src/compat/map/stubs.js +0 -26
  147. package/src/compat/map/utilities.js +0 -113
  148. package/src/compat-entry.js +0 -11
  149. package/src/components/permission/permission.css +0 -47
  150. package/src/components/permission/permission.js +0 -201
package/docs/llms.md CHANGED
@@ -1,7 +1,7 @@
1
- # Zx — agent reference
1
+ # Xenon Design System (Zx) — agent reference
2
2
 
3
- > Zx is the dependency-free, vanilla-JavaScript UI component library for ZeyOS business
4
- > applications. ES2022 modules, WAI-ARIA-accessible native controls, semantic `--zx-*` design
3
+ > Xenon is a dependency-free design system for browser applications; Zx is its product-agnostic
4
+ > vanilla-JavaScript implementation. ES2022 modules, WAI-ARIA-accessible native controls, semantic `--zx-*` design
5
5
  > tokens (light/dark + cozy/compact), and lifecycle-safe components. Zero runtime dependencies.
6
6
  > This file is the single, complete reference a coding agent needs to build UI with Zx; each
7
7
  > component below is wrapped in `<!-- doc:<name> -->` markers so tools can extract a single
@@ -61,7 +61,7 @@ need not be set). A preset repoints five tier-1 tokens — `--zx-accent-300` …
61
61
  and the semantic tier takes its own stop per polarity (600/700 on light surfaces, 300/400/500 on
62
62
  dark), so one five-line block rethemes light, dark, and auto together. Status colours are not part
63
63
  of a preset. Define your own the same way, or build one at `/theme.html` (the theme studio, which
64
- renders every component live and exports the CSS):
64
+ renders every component family live and exports the CSS):
65
65
 
66
66
  ```css
67
67
  :root {
@@ -85,8 +85,8 @@ override that works (global token → then, if needed, a component style).
85
85
 
86
86
  For ZeyOS business data use the **dedicated client library** `@zeyos/client`
87
87
  (`npm install @zeyos/client`) — a zero-dependency, typed OpenAPI client (accounts,
88
- transactions/invoices, tickets, 50+ resources), OAuth2/session auth, retries, schema
89
- introspection. It replaces the legacy `gx.zeyos.Client` / `gx.zeyos.Request`.
88
+ transactions/invoices, tickets, 50+ resources), OAuth2/session auth, retries, and schema
89
+ introspection. Zx accepts the client by injection and does not bundle it.
90
90
 
91
91
  ```js
92
92
  import { createZeyosClient, MemoryTokenStore, normalizeListResult } from '@zeyos/client';
@@ -207,15 +207,6 @@ reflection and ElementInternals form association: `<zx-toggle>`, `<zx-check-butt
207
207
  `<zx-checklist>`, `<zx-datebox>`, `<zx-timebox>`, `<zx-search>`, `<zx-groupbox>`, `<zx-tabbox>`,
208
208
  `<zx-table>`, `<zx-dialog>`.
209
209
 
210
- ## gx compatibility
211
-
212
- Load `zx-compat.global.js` (or import `zx-compat.esm.js`) →
213
- `window.gx.{core,ui,util,zeyos,bootstrap}` wrapping Zx components (constructor/option/event-name
214
- translation, MooTools-style `addEvent`). `gx.compat.installGlobals()` (opt-in) installs `__()`,
215
- `_()`, `String.htmlSpecialChars`, and the element `store/retrieve` shim. Full namespace listing in
216
- the "gx compatibility layer" component section below; class map and unsupported legacy APIs in
217
- `MIGRATION.md`.
218
-
219
210
  ## Conventions (editing component source)
220
211
 
221
212
  Follow `AGENTS.md`. Key rules: never declare instance class fields for state used in `render()`
@@ -228,14 +219,13 @@ pattern with `:focus-visible` rings and `prefers-reduced-motion` guards.
228
219
 
229
220
  ```
230
221
  src/components/<name>/ component + CSS src/core/ kernel (component, dom, http, i18n, date…)
231
- src/compat/ gx compat layer styles/ tokens + base.css
232
222
  website/ marketing page + docs.html (docs.js) specs/ per-component specs
233
223
  website/demos/ per-component demo modules website/layouts/ application layout examples
234
224
  tests/ node unit + smoke dist/ built bundles
235
225
 
236
226
  npm run serve # http://127.0.0.1:8321/website/docs.html (no build)
237
227
  npm test # node --test tests/unit/*.test.js + node tests/lint-tokens.js
238
- npm run build # dist/: zx.esm.js, zx.global.js (window.zx), zx-compat.global.js (window.gx), zx.css
228
+ npm run build # dist/: ESM, global, ZeyOS binding, and CSS assets
239
229
  node tools/build-module-tokens.js # regenerate styles/tokens/modules.css from src/zeyos/modules.js
240
230
  ```
241
231
 
@@ -418,31 +408,107 @@ Search input with embedded search and clear buttons (`role="search"`).
418
408
  - **Events** — `input {value}` (debounced), `submit {value}` (Enter or the button), `clear`.
419
409
  <!-- /doc -->
420
410
 
411
+ <!-- doc:launcher -->
412
+ ### Launcher — application and record search
413
+
414
+ A native-dialog launcher for applications, recent records, commands, and grouped remote results.
415
+ Zx owns ranking, focus, request cancellation, and presentation; ZeyOS owns the catalogue,
416
+ permissions, recent-history/cache policy, and the route or command ultimately invoked.
417
+
418
+ - **Constructor** — `new Launcher(null, options)` owns a body-level dialog;
419
+ `new Launcher(existingDialog, options)` enhances and later restores an existing `<dialog>`.
420
+ - **Options** — `items`, abortable `sources: [{id, label?, minQuery?, when?, order?, load(query, {signal})}]`,
421
+ `query`, `debounce: 250`, `minQuery: 0`, `maxResults: 100`, `placeholder`, `label`, `emptyText`,
422
+ `loadingText`, `shortcut: 'mod+k'|false`, and visible `hints` labels (or `false`).
423
+ - **Items** — `{id, label, description?, keywords?, group?, icon?, badge?, value?, href?, target?,
424
+ invoke?, pinned?, disabled?, kind?, current?, when?, groupOrder?, itemOrder?}`. `icon` accepts a
425
+ bundled icon name, Node, or factory. Links stay native so modified and middle clicks work;
426
+ non-link commands may provide an application-owned `invoke` callback. Malformed and executable
427
+ URL schemes are stripped; a descriptor with no safe link or callback is disabled.
428
+ - **Methods** — `open()`, `close()`, `toggle()`, `isOpen()`, `focus()`, `setQuery()`, `getQuery()`,
429
+ `setItems()`, `setCurrent()`, `setSources()`, `destroy()`.
430
+ - **Events** — cancelable `select {item, value, source, query}`, `query {query}`, `open`, `close`,
431
+ and `error {source, error}`. Preventing `select` suppresses the supplied link or callback.
432
+ - **Ranking** — local results are case/diacritic-insensitive and deterministic: exact label,
433
+ label prefix, word prefix, label substring, identifier substring, then acronym prefix. Pinned
434
+ items retain an explicit order ahead of otherwise equal results. A new async query aborts and
435
+ ignores stale source work.
436
+ - **Keyboard** — Cmd+K on macOS or Ctrl+K elsewhere opens by default; the shortcut is ignored in
437
+ editable controls, composition, and modal contexts. Application tiles use spatial arrow-key
438
+ movement; Up/Down may continue into record groups. Home/End, Page Up/Down, and Enter operate the
439
+ single listbox selection model; Escape closes and restores focus. `aria-current` marks the
440
+ active destination independently from the keyboard's `aria-selected` option.
441
+ - **Optional ZeyOS adapter** — import `zeyosLauncher()` or `buildZeyosLauncherConfig()` from
442
+ `@zeyos/zx/zeyos`. The adapter receives the permitted module/menu catalogue, pins, current
443
+ identifier, recent loader or cache, abortable search, destination resolvers, and application/
444
+ record icon renderers explicitly. It maps the complete shell payload without reading globals or
445
+ taking over application routing.
446
+ <!-- /doc -->
447
+
448
+ <!-- doc:avatar -->
449
+ ### Avatar
450
+
451
+ A fixed-geometry user image with deterministic initials fallback. Image loading or failure never
452
+ changes layout, and the component is decorative by default so adjacent identity text is not
453
+ announced twice.
454
+
455
+ - **Options** — `src`, `name`, `initials`, `label`, `size: 'sm'|'md'|'lg'|<pixels>`,
456
+ `shape: 'circle'|'rounded'|'square'`, `status: 'online'|'away'|'busy'|'offline'|null`, and
457
+ `statusLabel`.
458
+ - **Methods** — `set(values)`, `setStatus(status, label?)`, `hasImage()`, `destroy()`.
459
+ - **Accessibility** — set `label` only when the avatar stands alone. Presence is supplementary
460
+ status, not the account's accessible name; supply `statusLabel` when it conveys information.
461
+ <!-- /doc -->
462
+
463
+ <!-- doc:account-menu -->
464
+ ### AccountMenu
465
+
466
+ Composes `Avatar` and `MenuButton` into one account trigger and APG menu. The popup repeats the
467
+ identity before its actions; Zx never decides role visibility, changes preferences, signs a user
468
+ out, or mutates the session.
469
+
470
+ - **Options** — `account: {name, secondary, src?, initials?, status?, statusLabel?}`, `items`,
471
+ `compact: false`, `label`, and any top/bottom/left/right `placement`.
472
+ - **Methods** — `open()`, `close()`, `toggle()`, `isOpen()`, `setAccount()`, `setItems()`, `focus()`,
473
+ `getPanel()`, `destroy()`.
474
+ - **Events** — cancelable `select {value, item}`, `open`, and `close`. A canceled selection does
475
+ not run the item's callback and leaves the menu open, so an application can reject or confirm an
476
+ action without rebuilding the account surface.
477
+ - **Accessibility** — compact mode is visually avatar-only but keeps the full account-menu name on
478
+ its single native trigger; MenuButton retains focus return, typeahead, and arrow-key behavior.
479
+ <!-- /doc -->
480
+
421
481
  <!-- doc:select -->
422
482
  ### Select — APG editable combobox
423
483
 
424
484
  Unifies single-select, local filtering, and async loading.
425
485
 
426
486
  - **Options** — `items: []`, `fixedItems: []`, `valueKey: 'ID'` (string key or `(item)=>id`),
427
- `labelKey: 'name'` (string or `(item)=>string`), `renderItem`, `renderValue`, `value`,
428
- `disabled`, `placeholder`, `clearable: false`, `filter: false | 'local' | async (query)=>items`,
429
- `searchKeys`, `minQuery: 0`, `debounce: 200`, `listHeight: 280`, `groupKey`.
487
+ `labelKey: 'name'` (string or `(item)=>string`), `renderItem`, `renderValue`,
488
+ `renderValueAdornment`, `renderNone`, `noneLabel`, `actions`, `value`, `disabled`, `placeholder`,
489
+ `clearable: false`, `filter: false | 'local' | async (query)=>items`, `searchKeys`,
490
+ `minQuery: 0`, `debounce: 200`, `listHeight: 280`, `groupKey`.
430
491
  - **Getters** — `.value`, `.selected`.
431
- - **Methods** — `set(id, {silent})`, `setItems()`, `setFixedItems()`, `reset()`, `open()`/`close()`,
432
- `enable()`/`disable()`, `focus()`.
433
- - **Events** — `change {value, item}` (`item` is null on clear), `open`, `close`, `query {query}`,
434
- `loaded {items}`.
492
+ - **Methods** — `set(id, {silent})`, `setItems()`, `setFixedItems()`, `setActions()`, `reset()`,
493
+ `open()`/`close()`, `enable()`/`disable()`, `focus()`.
494
+ - **Events** — `change {value, item}` (`item` is null on clear), preventable
495
+ `action {id, action, query, select}`, `open`, `close`, `query {query}`, `loaded {items}`.
435
496
  - **Fixed choices** — `fixedItems` pins entries to the top of the list, separated by a rule (the
436
497
  rule is dropped when `groupKey` is on, since a heading already divides them). They belong to the
437
498
  control, not to its data: a `filter` function replaces `items` on every query, while the pinned
438
499
  entries are narrowed locally against the same query and stay resolvable by `set()`. They survive
439
500
  `setItems()` and are shown below `minQuery`. Use them for choices with no row in the source —
440
501
  "Unassigned", "Everyone", "Private".
441
- - **Presets** — `Select.priority(target, opts)`, a 5-level priority picker;
442
- `Select.permission(target, {groups, value})`, the record-access picker with Private and Public
443
- pinned above the groups. Its value is the tri-state ZeyOS stores (`false` private, `true` public,
444
- or a group ID), `groups` takes an array or a loader `(query)=>items`, and the labels come from
445
- `msg` (`permission.private`, `permission.public`). Replaces the deprecated `Permission`.
502
+ - **Presets** — `Select.priority(target, opts)` renders the five-level, five-square ZeyOS priority
503
+ scale; `Select.status(target, opts)` supplies shape- and semantic-color-distinct workflow states
504
+ and accepts replacement `items`; `Select.permission(target, {groups, value})` pins Private and
505
+ Public above record-access groups. Its value is the tri-state ZeyOS stores (`false` private,
506
+ `true` public, or a group ID), and `groups` takes an array or loader `(query)=>items`.
507
+ - **ZeyOS entity preset** — import `zeyosEntitySelect(client, resource, options)` or the DOM-free
508
+ `buildZeyosEntitySelectConfig()` from `@zeyos/zx/zeyos`. It composes the async client binding
509
+ with module-colored icons, subtitle/group readers, a deliberate none row, and an optional
510
+ application-owned create command. The selected value remains the record ID and exact product
511
+ glyphs stay injectable through `renderIcon`.
446
512
  - **Keyboard** (APG combobox) — ArrowDown/Alt+ArrowDown open; arrows navigate and wrap; Home/End;
447
513
  Enter selects; Esc closes; Tab closes; printable characters filter (editable) or run a typeahead
448
514
  (readonly). `aria-activedescendant` tracks the active option.
@@ -675,6 +741,102 @@ Structured modal: a header with title and close button, a body, and footer butto
675
741
  `Dialog.prompt({ title, message, value })` → string|null.
676
742
  <!-- /doc -->
677
743
 
744
+ <!-- doc:sheet -->
745
+ ### Sheet
746
+
747
+ Edge-anchored surface — a `Dialog` attached to one side of the viewport instead of floating in the
748
+ middle. One primitive covers both side sheets and mobile drawers, which differ mainly in the edge they take, so
749
+ `side` replaces what would otherwise be two components. Inherits the whole Dialog anatomy: title,
750
+ footer buttons, switchable views, and focus restoration.
751
+
752
+ - **Options** — `side: 'end'` (`'start' | 'end' | 'top' | 'bottom'`, logical), `modal: true`
753
+ (`true | 'trap-focus' | false`), `backdrop: 'dim'` (`'dim' | 'blur' | 'none'`), `size: null`
754
+ (CSS length or px number, applied to whichever axis `side` implies), `closeButton: true`,
755
+ plus everything Dialog takes — `title`, `content`, `buttons`, `closable`, `lightDismiss`
756
+ (defaults to `true` here, unlike Modal), `destroyOnClose`.
757
+ - **Methods** — `open()`, `close(result?)`, `setSide(side)`, `getSide()`, `setSize(size)`,
758
+ `isModal()`, `isDocked()`, `isOpen()`, `getSize()`, `snapTo(target)`, plus Dialog's `setTitle()`, `setContent()`,
759
+ `setButtons()`, `addView()`, `showView()`.
760
+ - **Events** — `open`, `close {result}`, `cancel` (preventable with `event.preventDefault()`),
761
+ `resize {size, ratio}`, `dockchange {docked, dock}`.
762
+ - **Docked** — a sheet has two lives, and it does not choose between them: `Dock.adopt(sheet)`
763
+ makes it a track in that dock's flow instead of an overlay, and the dock then owns its side,
764
+ size, and resizing. A docked sheet is always non-modal, `close()` collapses its track to zero
765
+ rather than sliding away, and Escape and outside-click stop dismissing it — it is part of the
766
+ layout, not something covering it. `Dock.release(sheet)` hands it back. Nothing is rebuilt
767
+ across a handoff: the element moves and reopens in place, so DOM state and listeners survive,
768
+ and no `open`/`close` is emitted for what is only a change of address.
769
+ - **Modality** — `true` delegates focus containment, page inertness, Escape, and the backdrop to
770
+ the browser through `showModal()`. `'trap-focus'` and `false` open with `show()`, so the page
771
+ keeps scrolling and stays interactive; Escape and outside-click are re-implemented, and
772
+ `'trap-focus'` adds `focusTrap()` on top. Only a modal sheet renders a `::backdrop`, so
773
+ `backdrop` is meaningful only when `modal` is `true`.
774
+ - **Resizing and detents** — `resizable` makes the inner edge draggable between `min` and `max`.
775
+ `snap` gives it detents: a number at or below 1 is a fraction of the viewport along the sheet's
776
+ axis, anything larger is pixels, and a string is any CSS length (`'320px'`, `'90%'`). With
777
+ detents a drag settles on the nearest one rather than where it was released, and `snapTo(index)`
778
+ or `snapTo(value)` moves there directly. Dragging below half the smallest allowed size dismisses
779
+ a `closable` sheet, so resize, detent, and swipe-to-dismiss are one gesture whose meaning is
780
+ decided when the pointer settles. `getSize()` reports the current pixel size. A sheet that is
781
+ neither `resizable` nor snapping still has a draggable handle where `handle` shows one — that is
782
+ what makes a drawer dismissible — but the drag may only take size away, never add it, so it
783
+ cannot be grown by dragging when nothing asked for resizing.
784
+ - **Handle** — `handle: 'auto'` shows a grab handle on `top` and `bottom` sheets, which is what
785
+ makes them read as drawers; `true` and `false` force it either way. It exists whenever the sheet
786
+ is `resizable` or has `snap`, visible or not. It is a `separator` with live
787
+ `aria-valuenow`/`aria-valuemin`/`aria-valuemax`: arrow keys along the axis step it by 16px, Shift
788
+ by 64px. A docked sheet hides its handle — the dock's own divider resizes it there.
789
+ - **Custom properties** — `--zx-sheet-size` (written by `size`; the per-side default otherwise)
790
+ and `--zx-sheet-radius` (inner corners of a top/bottom sheet, default `--zx-radius-xl`) are
791
+ internal geometry, not published styling hooks — prefer the options. `--zx-overlay-blur` is a
792
+ tier-2 semantic token and is public: it sets the `'blur'` backdrop's radius, default `8px`.
793
+ - **Motion** — entry and exit both animate, via `@starting-style` and
794
+ `transition-behavior: allow-discrete` on `display`/`overlay`; without them the sheet appears and
795
+ disappears instantly. All of it sits inside `prefers-reduced-motion: no-preference`.
796
+ - **Keyboard and screen readers** — identical to Dialog: the native `<dialog>` supplies the role
797
+ and `aria-labelledby` points at the title. Escape closes when `closable`, focus moves to the
798
+ `[autofocus]` control or the first focusable descendant on open, and returns to the opener on
799
+ close. Tab cycles within the sheet when `modal` is `true` or `'trap-focus'`.
800
+ <!-- /doc -->
801
+
802
+ <!-- doc:sheet-stack -->
803
+ ### SheetStack
804
+
805
+ An ordered group of Sheets that reads as one drill-down. Deliberately tiny, because the browser
806
+ already does the hard part: nested dialogs stack in the top layer by open order, so there is no
807
+ z-index bookkeeping, and Escape closes only the topmost, so unwinding one level at a time is
808
+ native. What is left is which sheet sits at which depth, written onto the elements as `data-depth`
809
+ and `--zx-sheet-depth` for CSS to interpret.
810
+
811
+ Owns no element, so it is a controller rather than a `Component` — constructed with options
812
+ alone, `new SheetStack({…})`.
813
+
814
+ - **Options** — `layout: 'stack'` (`'stack' | 'cascade'`), `offset: 24`, `scale: 0.04`, `max: 3`.
815
+ - **Methods** — `push(sheet)`, `pop()`, `popTo(sheet)`, `clear()`, `top()`, `size()`, `sheets()`,
816
+ `has(sheet)`, `on/off/once`, `destroy()`.
817
+ - **Events** — `push {sheet, depth}`, `pop {sheet}`. `pop` fires however a sheet left — popped,
818
+ dismissed with Escape, or closed by its own button.
819
+ - **Layouts** — `stack` slides each covered sheet back toward its own edge, scales it down, and
820
+ takes it out of the tab order; only the top one is usable, which is the drill-down feel.
821
+ `cascade` shifts each covered sheet clear of the ones in front — by their measured size, not a
822
+ guess — so they sit side by side and all stay usable. `cascade` is usually the right one for an
823
+ ERP screen, where the parent record should stay readable while a line item is edited.
824
+ - **Depth** — numbered from the top: the visible sheet is `0`. Past `max` a sheet stops being
825
+ drawn but stays open and in the stack. In a `stack` only the topmost backdrop shades the page;
826
+ one per depth would compound into black. A `cascade` shades nothing — a backdrop paints directly
827
+ beneath its own dialog in the top layer, so the top sheet's would dim exactly the sheets the
828
+ layout exists to keep readable. Pair a cascade with `modal: false` when the page behind it
829
+ should stay live as well.
830
+ - **Ownership** — the stack never owns its sheets. `destroy()` empties it and strips the depth
831
+ styling without closing anything.
832
+ - **Accessibility** — for `stack`, covered sheets are `inert`. The browser does that on its own
833
+ for stacked *modal* dialogs, but a non-modal stack gets nothing, so the attribute is set either
834
+ way. `cascade` deliberately leaves them interactive.
835
+ - **Gotcha** — `close()` on a dialog dispatches its `close` event in a queued task, not
836
+ synchronously. `pop()` therefore removes the sheet from the stack itself rather than waiting for
837
+ the event, so `popTo()` and `clear()` can loop without racing it.
838
+ <!-- /doc -->
839
+
678
840
  <!-- doc:dropdown -->
679
841
  ### Dropdown
680
842
 
@@ -682,7 +844,8 @@ Generic anchored popover in the top layer, constructed as `new Dropdown(anchor,
682
844
  light-dismisses on an outside click or Esc, and mirrors `aria-expanded` onto the anchor.
683
845
 
684
846
  - **Options** — `content`,
685
- `placement: 'bottom-start'|'bottom-end'|'top-start'|'top-end'|'bottom'|'top'`, `offset: 4`,
847
+ `placement: 'bottom-start'|'bottom-end'|'top-start'|'top-end'|'bottom'|'top'` plus the matching
848
+ `left*` and `right*` placements, `offset: 4`,
686
849
  `matchWidth: false`, `openOn: 'click'|'manual'`, `closeOnSelect: false`.
687
850
  - **Methods** — `open()`, `close()`, `toggle()`, `isOpen()`, `setContent()`, `getPanel()`.
688
851
  - **Events** — `open`, `close`.
@@ -695,8 +858,11 @@ A button that opens a `role="menu"`.
695
858
 
696
859
  - **Options** — `label`, `icon`, `kind`,
697
860
  `items: [{ label, icon?, value?, disabled?, danger?, onselect? } | '-']`, `placement`.
698
- - **Methods** — `setItems()`, `open()`, `close()`, `setLabel()`.
861
+ - **Methods** — `setItems()`, `open()`, `close()`, `setLabel()`, `getTrigger()`, `getPanel()`,
862
+ `focusFirst()`, `focusLast()`.
699
863
  - **Events** — `select {value, item}`, `open`, `close`.
864
+ - **Cancellation** — `select` is cancelable. Preventing it skips the item's `onselect` callback and
865
+ leaves the menu open, which is the composition seam used by `AccountMenu` and rail flyouts.
700
866
  - **Keyboard** — ArrowDown/Enter/Space open and focus the first item; ArrowUp opens and focuses the
701
867
  last; arrows cycle; Home/End; typeahead; Enter/Space select; Esc/Tab close.
702
868
  <!-- /doc -->
@@ -734,7 +900,11 @@ parked at the pointer, so it flips near a viewport edge like every other floatin
734
900
 
735
901
  A description bubble anchored to another element. The panel is a manual popover positioned by the kernel's `position()`, so it escapes clipping and stacking contexts and flips near a viewport edge; it carries `role="tooltip"` and a generated id, and the anchor gets `aria-describedby` pointing at it only while it is open — closing puts the anchor's original value back, including no value at all. The bubble is never interactive: `pointer-events: none` keeps it from stealing the hover it depends on, and nothing inside it can take focus. Hover opens it after `delay`; keyboard focus opens it immediately, because a keyboard user cannot hover a moment longer; a touch pointer never opens it, since a bubble with no "move away" gesture would simply stick. Only one is ever visible at a time, which falls out of the dismissal rules rather than a registry.
736
902
 
737
- - **Options** — `content` (string, Node, or a function re-evaluated on every open), `placement` (`'top'` default, plus `bottom`, `top-start`, `top-end`, `bottom-start`, `bottom-end`), `offset` (6), `delay` (400), `hideDelay` (80), `maxWidth` (260; a number is pixels, a string is any CSS length), `trigger` (`'both'` default, or `'hover'`, `'focus'`, `'manual'` — `manual` installs no listeners at all and leaves everything to the methods), `disabled` (false).
903
+ - **Options** — `content` (string, Node, or a function re-evaluated on every open), `placement`
904
+ (`'top'` default, plus top/bottom/left/right and their start/end variants), `offset` (6), `delay`
905
+ (400), `hideDelay` (80), `maxWidth` (260; a number is pixels, a string is any CSS length),
906
+ `trigger` (`'both'` default, or `'hover'`, `'focus'`, `'manual'` — `manual` installs no listeners
907
+ at all and leaves everything to the methods), `disabled` (false).
738
908
  - **Methods** — `show()`, `hide()`, `toggle()`, `isOpen()`, `setContent(content)`, `enable()`, `disable()`, `isDisabled()`, `getAnchor()`, `destroy()`. Convenience factories: `tooltip(anchor, contentOrOptions)` takes either a content value or a full option object, and `describe(anchor, text)` is the one-liner replacement for a `title` attribute. Both return the instance.
739
909
  - **Events** — `open`, `close`. Both fire only on a real transition, so a `show()` on an open tooltip is silent.
740
910
  - **Keyboard** — Focusing the anchor opens the tooltip at once; moving focus away closes it. `Escape` closes it without moving focus, satisfying WCAG 1.4.13. A `pointerdown` anywhere closes it, and focus that arrives with a click does not re-open it.
@@ -754,27 +924,37 @@ Two panes with a divider the user owns. The layout is a three-track CSS grid dri
754
924
  <!-- doc:table -->
755
925
  ### Table
756
926
 
757
- Sortable, selectable, sticky-header data table — one component covering both the legacy
758
- `gx.ui.Table` and `SimpleTable`. `fr` widths fill the container while px/auto widths scroll
759
- horizontally, and multi-select adds a tri-state header checkbox plus Shift+click range select.
927
+ Sortable, selectable, sticky-header data table. `fr` widths fill the container while px/auto
928
+ widths scroll horizontally, and multi-select adds a tri-state header checkbox plus Shift+click
929
+ range select.
760
930
 
761
931
  - **Options** — `columns: [{ id, label, sortable?, width ('120px'|'2fr'|'auto'), align,
932
+ type?: 'text'|'number'|'currency'|'percent'|'unit', locale?, decimals?, currency?, unit?,
762
933
  render?: (row,i)=>Node|string, sortValue?, headerTitle? }]`, `data`, `rowId: 'ID'`,
763
934
  `sort: { id, dir }`, `sortMode: 'local'|'server'`, `selectable: false|'single'|'multi'`,
764
- `stickyHeader: true`, `height`, `emptyText`, `rowClass`, `zebra: true`.
935
+ `stickyHeader: true`, `height`, `emptyText`, `rowClass`, `zebra: true`, and
936
+ `hierarchy: false|{parentId, column?, expanded?}`. Locale, currency, unit, and decimals may be
937
+ row callbacks; values remain numbers in row data while display formatting is locale-aware.
765
938
  - **Methods** — `setData()`, `addData()`, `updateRow(id,row)`, `removeRow(id)`, `getRow(id)`,
766
939
  `getData()`, `empty()`, `setSort(id,dir)`, `getSelection()`, `setSelection(ids)`,
767
940
  `clearSelection()`, `setLoading(bool)` (busy/skeleton state — dims rows, shows an indeterminate
768
941
  top bar, sets `aria-busy`, and is cleared by the next `setData`).
769
942
  - **Events** — `rowclick {row, id, index, event}`, `rowdblclick`, `sort {id, dir}`,
770
- `selectionchange {rows, ids}`, `datachange {rows}`.
943
+ `selectionchange {rows, ids}`, `datachange {rows}`, and `rowtoggle {row, id, expanded}`.
944
+ - **Hierarchy** — flat parent references are projected as an ARIA `treegrid`; `getData()` and all
945
+ editing/mutation APIs stay flat and ID-based. Orphans become roots, cycles render once, local
946
+ sort reorders siblings without mixing levels, and growing counts visible rows. Use
947
+ `toggleRow()`, `expandRow()`, `collapseRow()`, `expandAll()`, `collapseAll()`, and
948
+ `getExpanded()`. The native disclosure button works by pointer and keyboard without selecting
949
+ or clicking the row.
771
950
  - **Editing** — `editMode: false|'cell'|'row'` (default `false`, and completely inert until set).
772
951
  `'cell'` edits one cell, `'row'` opens every editable cell of the row and commits them as a unit.
773
952
  Editors are sized into the cell without changing row height, and an editing cell carries
774
953
  `data-editing="true"` (`data-invalid="true"` plus an `aria-live` message when a validator rejects).
775
954
  - **Editable columns** — extra column keys: `editable: false|true|'text'|'number'|'select'|'date'|
776
- 'checkbox'|'textarea'|((row)=>boolean)` (`true` means `'text'`; a function is evaluated per row, so
777
- individual rows stay read-only), `options` for `'select'` (`[{value,label}]`, a `{value: label}` map
955
+ 'checkbox'|'textarea'|((row)=>boolean)` (`true` infers `'number'` for number/currency/percent/unit
956
+ columns and `'text'` otherwise; a function is evaluated per row, so individual rows stay
957
+ read-only), `options` for `'select'` (`[{value,label}]`, a `{value: label}` map
778
958
  whose keys are always strings, or `(row)=>items`), `editorProps` (forwarded to the underlying
779
959
  NumberField/Select/Datebox/Toggle or to the generated `<input>`/`<textarea>`),
780
960
  `editor: (row, api)=>Node` for a fully custom editor (`api` is `{value, commit(value), cancel(),
@@ -783,6 +963,10 @@ horizontally, and multi-select adds a tri-state header checkbox plus Shift+click
783
963
  seeds the text of a `'text'`/`'textarea'` editor, `parse` converts every editor's raw output back),
784
964
  and `validate: (value,row)=>true|string` (a returned string marks the cell invalid, shows the
785
965
  message, and refuses the commit).
966
+ - **Transaction editors** — currency and unit columns reuse `NumberField` and show the resolved
967
+ currency/unit as its suffix unless `editorProps.unit` overrides it. This is a generic typed-grid
968
+ contract: application code remains responsible for tax, discount, subtotal, and accounting
969
+ formula rules.
786
970
  - **Editing methods** — `startEdit(rowId, columnId)`, `commitEdit()`, `cancelEdit()`, `isEditing()`,
787
971
  `getEditing()` → `{id, columnId}|null`.
788
972
  - **Editing events** — `editstart {row, id, column, columnId, value}`;
@@ -824,6 +1008,53 @@ horizontally, and multi-select adds a tri-state header checkbox plus Shift+click
824
1008
  A function is re-called on every render, which is what to use when the placeholder holds controls.
825
1009
  <!-- /doc -->
826
1010
 
1011
+ <!-- doc:grid -->
1012
+ ### Grid and `Grid.BillingItems()`
1013
+
1014
+ `Grid` is the public extension point for repeatable data-table presets. It subclasses `Table`
1015
+ without forking its implementation, so sorting, selection, hierarchy, responsive stacking,
1016
+ editable cells/rows, loading, events, and teardown keep the Table contract.
1017
+
1018
+ - **Constructor** — `new Grid(target, tableOptions)` accepts the complete `Table` API.
1019
+ - **Preset** — `Grid.BillingItems(target, options)` supplies an editable hierarchical billing
1020
+ schema for item, quantity, unit, unit price, currency, and line total. It returns a `Grid`.
1021
+ - **Billing options** — `data`, `fields` key overrides, `units`, `currencies`, fallback `currency`,
1022
+ `locale`, `decimals`, `columnOverrides`, complete `columns` replacement, `lineTotal(row, changes)`,
1023
+ and `oneditcommit` in addition to ordinary Table options.
1024
+ - **Rows** — default field names are `id`, `parent`, `kind`, `item`, `quantity`, `unit`,
1025
+ `unitPrice`, `currency`, and `total`. `kind: 'group'|'subtotal'|'header'|'section'` is read-only;
1026
+ other rows are editable. Parent references use Table's flat-data treegrid projection.
1027
+ - **Calculation boundary** — the default line total is quantity × unit price. Applications own
1028
+ taxes, discounts, rounding, subtotal/group recalculation, optimistic persistence, and server
1029
+ validation; provide `lineTotal` or cancel `editcommit` when product policy differs.
1030
+ <!-- /doc -->
1031
+
1032
+ <!-- doc:chart -->
1033
+ ### Chart and `ChartJsAdapter`
1034
+
1035
+ An accessible, engine-neutral chart host. Zx renders state, canvas, semantic data summary, tokens,
1036
+ and lifecycle; an injected adapter owns the drawing engine. The Zx runtime imports no Chart.js
1037
+ code, so future adapters do not change the public `Chart` API.
1038
+
1039
+ - **Options** — `adapter`, `type: 'bar'`, `data`, engine-specific `chartOptions`, `label`,
1040
+ `description`, `summary: 'hidden'|'visible'`, `loading`, loading/empty/error text, and
1041
+ `aspectRatio: '16 / 9'`.
1042
+ - **Methods** — `setData()`, `setType()`, `update(chartOptions)`, `setAdapter()`, `setLoading()`,
1043
+ `setError()`, `clearError()`, `resize()`, `refreshTheme()`, `getInstance()`, `destroy()`.
1044
+ - **Events** — `ready {instance}`, `update {data, type}`, `select {datasetIndex, index, dataset,
1045
+ label, value, nativeEvent}`, and `error {error}`.
1046
+ - **Adapter contract** — `adapter.create(canvas, {type, data, options, onSelect})` returns
1047
+ `{update(spec), resize(), destroy(), instance?}`. Implementations must destroy engine resources
1048
+ and must not retain the component after teardown.
1049
+ - **`ChartJsAdapter`** — construct it with a Chart.js constructor or namespace:
1050
+ `new ChartJsAdapter(window.Chart)`. The documentation pins Chart.js 4.5.1 as a development-only
1051
+ asset; `package.json` has no runtime dependencies and application builds explicitly choose how
1052
+ the engine is loaded.
1053
+ - **Accessibility** — the canvas always has a name and optional description, while synchronized
1054
+ labels and dataset values live in a semantic table. `summary: 'hidden'` keeps it visually hidden
1055
+ for assistive technology; `'visible'` makes exact values part of the interface.
1056
+ <!-- /doc -->
1057
+
827
1058
  <!-- doc:data-filter -->
828
1059
  ### DataFilter
829
1060
 
@@ -927,6 +1158,76 @@ Beyond the built-in field types, these widget types wrap whole components, used
927
1158
  `toggle` (Toggle).
928
1159
  <!-- /doc -->
929
1160
 
1161
+ <!-- doc:questionnaire -->
1162
+ ### Questionnaire
1163
+
1164
+ A guided, one-question-at-a-time flow: onboarding intake, a service-call checklist, an audit form,
1165
+ a feedback survey. The questionnaire owns the ordered items, the active one, the answers,
1166
+ validation, progress and navigation; the page, card, dialog or drawer around it owns closing,
1167
+ persistence and transport.
1168
+
1169
+ The root is a real `<form>` and the choices are native radios and checkboxes carrying the item
1170
+ name, so `new FormData(questionnaire.toElement())` reads the answers, arrow keys inside a choice
1171
+ group are the browser's rather than ours, and the semantics do not depend on JavaScript. Three
1172
+ things separate it from a `Form` with one `Fieldset` per question: it branches, its answer control
1173
+ can be any registered `Field` type, and its validation may be asynchronous.
1174
+
1175
+ - **Options** — `items: [{name, prompt, description?, section?, required?, multiple?, skippable?,
1176
+ choices?, input?, field?, when?, next?, validate?}]`, `answers: {}` and `active: null` (the
1177
+ resume pair), `progress: true`, `review: false`, `advance: 'manual'|'auto'`, `shortcuts: true`,
1178
+ `allowBack: true`.
1179
+ - **Choices** — `[{value, label, description?, icon?, key?, disabled?, exclusive?}]`. `exclusive`
1180
+ is the "None of the above" behaviour: picking it clears the rest of a `multiple` item, and
1181
+ picking anything else clears it.
1182
+ - **Methods** — `setItems()`, `getItems()`, `goTo(name)`, `next()`, `previous()`, `skip()`,
1183
+ `submit()`, `getAnswers()`, `getAnswer(name)`, `setAnswers(values, {silent})`,
1184
+ `setAnswer(name, value, {silent})`, `getActive()`, `getPath()`, `getProgress()`, `getState()`,
1185
+ `toFormData()`, `reset()`, `focus()`, `destroy()`. `next()`, `skip()` and `submit()` return
1186
+ **promises** — `next()` because an item's `validate` may be asynchronous, and `skip()` because
1187
+ skipping the last question submits. Everything else is synchronous.
1188
+ - **Events** — `change {name, value, answers}`, `navigate {from, to, reason}` **preventable**,
1189
+ `skip {name}`, `invalid {name, message}`, `submit {answers, path}` **preventable**,
1190
+ `complete {answers, path}`.
1191
+ - **Branching** — `when(answers)` decides whether an item is asked at all, and `next` (a name, or
1192
+ `(answer, answers) => name`) decides what follows. Conditions **cascade**: each predicate sees
1193
+ only the answers of visible items declared before it, so hiding the VAT question also hides
1194
+ whatever depended on the VAT id. An answer to a conditioned-out question is kept internally —
1195
+ walk back into the branch and it is still there — but is left out of `getAnswers()` and
1196
+ `toFormData()`, so an abandoned branch never submits stale data. A `next` naming a hidden item
1197
+ walks forward from it rather than stranding the reader; naming an item that does not exist
1198
+ throws `RangeError`.
1199
+ - **Path and progress** — `getPath()` is the questionnaire's memory of the route walked, and
1200
+ `previous()` pops it: once a branch has been taken, "previous" is not `index - 1`. `getProgress()`
1201
+ returns `{index, answered, total, percent, section}` where `total` is the path plus the items
1202
+ still reachable — exact for a static flow, an estimate once a function `next` can jump somewhere
1203
+ the forward walk cannot predict.
1204
+ - **Field answers** — `field: {type: 'date'|'number'|'zxselect'|'rating'|'upload'|…}` hands the
1205
+ answer to a `Field`, so any registered field type works, including ones an application registered
1206
+ itself. The `<legend>` already asks the question, so the Field's own label is hidden from sight
1207
+ but kept as the control's accessible name unless the item gives `field.label` explicitly; the
1208
+ Field draws its own highlight, so a failing answer shows one message, not two.
1209
+ - **Validation** — `required` is checked synchronously; an item's `validate(answer, answers)` may
1210
+ return a message, `null`, or a **promise** of either, and a rejection is reported as its message.
1211
+ While the check is pending the root carries `data-state="checking"` and `aria-busy="true"` and
1212
+ the actions are disabled.
1213
+ - **Review** — `review: true` shows a summary of every answer on the path before submitting, each
1214
+ with an Edit that reopens that question; Continue from an edited question comes straight back to
1215
+ the review rather than replaying the tail.
1216
+ - **Semantics** — the item is a `<fieldset>` and the prompt its `<legend>`; the description and the
1217
+ error are associated through `aria-describedby`, the error is a `role="alert"`, and a failing
1218
+ item carries `aria-invalid="true"`. `data-state` on the root is
1219
+ `asking`, `checking`, `review`, or `complete`. Submitting does not replace the content: it sets
1220
+ `data-state="complete"`, hides the actions, and emits `complete`, leaving the thank-you screen to
1221
+ the application.
1222
+ - **Keyboard** — `1`–`9` then `a`–`z` pick the matching answer, assigned in choice order (an
1223
+ explicit `key` is reserved first, so adding one never renumbers the others) and shown as a key
1224
+ cap on each choice; the caps are `aria-hidden`, since a screen-reader user moves through the
1225
+ group with the arrow keys the radios already provide. `Enter` continues, except in a `multiline`
1226
+ input. Shortcuts are suppressed while focus is in a text control. Navigating moves focus to the
1227
+ new question's fieldset, which is what gets the prompt announced; failing validation moves it to
1228
+ the first answer control instead.
1229
+ <!-- /doc -->
1230
+
930
1231
  <!-- doc:value-list -->
931
1232
  ### ValueList
932
1233
 
@@ -991,19 +1292,6 @@ yet. Field type: `tagpicker`.
991
1292
  within the list.
992
1293
  <!-- /doc -->
993
1294
 
994
- <!-- doc:permission -->
995
- ### Permission
996
-
997
- Private / public / group record-permission selector. **Deprecated** — use
998
- `Select.permission(target, {groups})`, which carries the same tri-state value in one control and
999
- can query the groups remotely. This class stays for `gx.zeyos.Permission` and is removed in 3.0.
1000
-
1001
- - **Options** — `value: true|false|groupId`, `groups: []`, `groupsValueKey: 'ID'`,
1002
- `groupsLabelKey: 'name'`.
1003
- - **Methods** — `get()` → `'private'|'public'|groupId`, `set(value)`.
1004
- - **Events** — `change {value}`.
1005
- <!-- /doc -->
1006
-
1007
1295
  <!-- doc:groupbox -->
1008
1296
  ### Groupbox
1009
1297
 
@@ -1014,6 +1302,30 @@ Collapsible titled section built on the native `<details>` element.
1014
1302
  - **Events** — `open`, `close`.
1015
1303
  <!-- /doc -->
1016
1304
 
1305
+ <!-- doc:card -->
1306
+ ### Card
1307
+
1308
+ A semantic content or record surface. Card is intentionally not a Panel preset: it has no
1309
+ disclosure state or application chrome, and composes as one repeated item in a grid or list. A
1310
+ linked Card keeps the root non-interactive and renders a real title anchor as its primary action,
1311
+ so buttons and links in its action, content, and footer regions remain valid sibling controls.
1312
+
1313
+ - **Options** — `title: ''`, `media: null`, `content: null`, `actions: []`, `footer: null`,
1314
+ `link: null|string|{href, target?, rel?, onclick?}`, `variant: 'outlined'|'raised'|'filled'`,
1315
+ `orientation: 'vertical'|'horizontal'`, `headingLevel: 3`.
1316
+ - **Methods** — `setTitle()`, `setMedia()`, `setContent()`, `setActions()`, `setFooter()`,
1317
+ `setLink()`.
1318
+ - **Content** — media, body, and footer accept text, a Node, a Component, or null. Enhanced target
1319
+ children become body content when no explicit content is supplied, and `destroy()` restores the
1320
+ target exactly.
1321
+ - **Interaction** — `link` stretches the native title anchor across the visual surface; secondary
1322
+ controls stay above it and are never nested inside it. A linked card requires a non-empty title.
1323
+ Executable/data URL schemes are rejected. Commands belong in `actions`, and selectable cards
1324
+ should compose a native checkbox or radio.
1325
+ - **Layout** — horizontal media uses the public `--zx-card-media-size` hook and stacks through a
1326
+ container query when the card itself becomes narrow.
1327
+ <!-- /doc -->
1328
+
1017
1329
  <!-- doc:panel -->
1018
1330
  ### Panel / MasterPanel
1019
1331
 
@@ -1073,6 +1385,62 @@ markup with no JavaScript.
1073
1385
  because a CSS custom property cannot be used inside a media query.
1074
1386
  <!-- /doc -->
1075
1387
 
1388
+ <!-- doc:dock -->
1389
+ ### Dock
1390
+
1391
+ A stack of collapsible, resizable panes: the docked inspector column of a design tool, and the
1392
+ detail side of a master–detail screen. Give the root a size of its own (a grid area, `block-size:
1393
+ 100%`, a flex child) when the panes should fill a shell rather than grow with their content.
1394
+
1395
+ - **Options** — `orientation: 'vertical'`, `content: null`, `panes: []`, `resizable: true`,
1396
+ `collapsible: true`, `lazy: true`, `storageKey: null`.
1397
+ - **Pane** — `{name, title?, content?, tabs?, active?, size?, min?, grow?, collapsed?,
1398
+ collapsible?, side?}`. `content` takes a string, Node, Component, or a factory called on first
1399
+ reveal. A pane with `tabs` renders its strip in place of the title; `content` is then ignored.
1400
+ `side` (`'start' | 'end'`) only applies when the dock has a `content`.
1401
+ - **Tab** — `{name, title, content?}`, same content shapes.
1402
+ - **Methods** — `pane(name)`, `names()`, `add(pane, {index})`, `remove(name)`,
1403
+ `collapse(name)`, `expand(name)`, `toggle(name)`, `isCollapsed(name)`, `reveal(name)`,
1404
+ `setSize(name, size)`, `getSize(name)`, `activate(pane, tab)`, `getActive(pane)`,
1405
+ `state()`, `setState(state)`, `adopt(sheet, options)`, `release(sheet)`.
1406
+ - **Adopting a Sheet** — `adopt(sheet, {name?, side?, index?, size?, min?, dockAt?})` takes over a
1407
+ `Sheet`'s positioning: it becomes a track in the dock rather than an overlay. The dock owns
1408
+ where it sits, its size, and its resizing; the sheet keeps its content, header, footer, events,
1409
+ and its own `open()`/`close()`. `dockAt` names a breakpoint below which the dock hands the sheet
1410
+ back to a free overlay and reclaims the track, re-adopting it above — measured on the **dock's
1411
+ own width**, so a dock inside a split pane behaves correctly where a viewport media query would
1412
+ not. Because a dock is a flex container by construction it is always a valid host, which is why
1413
+ docking needs no arrangement from the application.
1414
+ - **Ownership** — the dock never owns an adopted sheet: `destroy()` releases them rather than
1415
+ destroying them, `remove(name)` on a sheet releases it, and a sheet that destroys itself while
1416
+ adopted drops out of the dock cleanly.
1417
+ - **Events** — `collapse {name}`, `expand {name}`, `resize {name, size}`,
1418
+ `tabchange {pane, tab, previous}`, `reveal {name}`.
1419
+ - **Sizing** — one custom property per pane and one flex rule: a sized pane is
1420
+ `flex: 0 0 var(--zx-pane-size)`, a collapsed one falls to `flex: 0 0 auto` with its body hidden,
1421
+ and exactly one element grows. With a `content` that is always the content; without one it is the
1422
+ pane declaring `grow`, or the last expanded pane. A drag therefore rewrites at most two
1423
+ properties, and collapsing a pane feeds the grower rather than redistributing across the stack.
1424
+ - **Regions** — with `content` the dock is a region rather than a plain stack: panes sit on either
1425
+ `side` of the flexible middle. Nesting one dock inside another dock's pane gives a workbench —
1426
+ a canvas with an inspector column beside it.
1427
+ - **`reveal(name)`** — accepts a pane name or a tab name. Expands whichever pane holds it,
1428
+ activates the tab when the name is a tab's, and scrolls it into view.
1429
+ - **Persistence** — `storageKey` remembers sizes, collapsed panes, and active tabs through
1430
+ `storage()`, which degrades to memory rather than throwing. `state()`/`setState()` expose the
1431
+ same shape for storing it elsewhere; unknown pane names are ignored, so a stored layout survives
1432
+ a release that added or removed panes.
1433
+ - **Keyboard and screen readers** — each pane header is a row, not one big button, so the collapse
1434
+ toggle and the tab strip can coexist; the toggle carries `aria-expanded` and `aria-controls`.
1435
+ Tabs follow the APG tab pattern with one roving tab stop, Left/Right moving between them.
1436
+ Dividers are focusable `separator`s carrying live `aria-valuenow`/`aria-valuemin`/`aria-valuemax`
1437
+ in pixels: Up/Down (or Left/Right when horizontal) resize by 16px, Shift by 64px, and Enter or
1438
+ Space collapses the pane before the divider. A divider with nothing to resize is taken out of the
1439
+ tab order.
1440
+ - **Out of scope** — drag-to-rearrange, tear-off panels, and multi-column docks. All three are
1441
+ additive without changing the API above.
1442
+ <!-- /doc -->
1443
+
1076
1444
  <!-- doc:tabbox -->
1077
1445
  ### Tabbox — APG tabs
1078
1446
 
@@ -1178,12 +1546,54 @@ menu on narrow widths.
1178
1546
  - **Events** — `change {name}`.
1179
1547
  <!-- /doc -->
1180
1548
 
1549
+ <!-- doc:app-sidebar -->
1550
+ ### AppSidebar — application navigation in every presentation
1551
+
1552
+ One route-agnostic application-navigation owner with an expanded vertical tree, a minimized
1553
+ vertical rail, and horizontal rail layouts. It mounts exactly one presentation at a time while
1554
+ preserving active and expanded branch state. Only the expanded vertical state reveals descendants
1555
+ inline; every rail state opens descendants toward the workspace in anchored flyouts.
1556
+
1557
+ - **Options** — `items`, `active`, `expanded`, `collapsed`, `collapsible`,
1558
+ `orientation: 'vertical'|'horizontal'`, `side: 'left'|'right'|'top'|'bottom'`, `label`,
1559
+ `header`, `footer`, `railHeader`, `railFooter`, `openDelay: 80`, `closeDelay: 160`, and
1560
+ `renderIcon(item, context)`. Icon values may also be names, nodes, components, or factories.
1561
+ - **Methods** — `setItems()`, `getItems()`, `setActive()`, `setExpanded()`, `getExpanded()`,
1562
+ `toggleBranch()`, `expand()`, `collapse()`, `setCollapsed()`, `isCollapsed()`,
1563
+ `setLayout({orientation, side})`, `openFlyout(id, {focus})`, `closeFlyout()`,
1564
+ `closeAllFlyouts()`, `isFlyoutOpen()`, `focus()`, `destroy()`.
1565
+ - **Events** — cancelable `select {item, id, value, href, event}`,
1566
+ `branchchange {item, id, expanded, ids}`, `collapsechange {collapsed}`, and
1567
+ `flyoutchange {item, id, open}`.
1568
+ - **Presentation state** — horizontal navigation is always effectively minimized and never exposes
1569
+ an expand control. Switching layouts does not erase the saved vertical collapsed preference or
1570
+ expanded branch IDs. `AppSidebar` is the single public application-navigation component; use
1571
+ `collapsed: true` or `orientation: 'horizontal'` for its rail presentations.
1572
+ - **Pointer and focus** — hover or focus reveals rail descendants without stealing focus. Click,
1573
+ Enter, Space, and the arrow toward the workspace also open a flyout; Escape, outside activation,
1574
+ or selection closes it. Expanded navigation supports arrows plus Home/End across visible rows.
1575
+ - **Semantics** — persistent navigation uses `<nav>` and nested lists with native links and
1576
+ disclosure buttons, not menu or menubar roles. Icon-only destinations retain an accessible name
1577
+ and Tooltip. A parent with children is disclosure-only; add an Overview child when it also needs
1578
+ a destination. Malformed and executable URL schemes are stripped; an item with no remaining
1579
+ link or callback is disabled in every presentation.
1580
+ <!-- /doc -->
1581
+
1181
1582
  <!-- doc:kernel -->
1182
1583
  ### Core helpers
1183
1584
 
1184
1585
  - **Component** — the base class: `on`/`off`/`once`/`emit`, `listen`, `toElement`, `msg`,
1185
1586
  `destroy`, and the static `Component.from(el)`.
1186
- - **DOM** — `h(tag, props, ...children)`, `h.raw(html)`, `htmlEscape`, `resolveElement`.
1587
+ - **DOM** — `h(tag, props, ...children)`, `h.raw(html)`, `htmlEscape`, `resolveElement`. The current
1588
+ ZeyOS builder is also first-class: `__(tag#id.class, properties, content)` is the migration alias
1589
+ for `compactElement()`. It preserves `D*` data, `S*` style, boolean/built-in property, attribute,
1590
+ and legacy `on*` callback rules while keeping content text-safe. `applyCompactProperties`,
1591
+ `appendContent`, `appendCompact` (parent return), `appendCompactChild` (child return), and
1592
+ `fragment(...children)` are the pure forms.
1593
+ - **Compact events** — `onCompactEvent`, `offCompactEvent`, and `fireCompactEvent` share one
1594
+ newest-first handler registry. Existing `DOM.on/remEvt/fireEvt` methods can delegate to this
1595
+ triad. Zx never installs globals or alters DOM prototypes; the current application owns those
1596
+ temporary seams while `UI.new*` implementations are replaced without rewriting `PG` routing.
1187
1597
  - **Icons** — `icon(name, {size, label})` and `icons`; see the Icons section — bundled inline SVG
1188
1598
  by default, Font Awesome after an opt-in.
1189
1599
  - **Positioning** — `position(anchor, floating, {placement, offset, flip, matchWidth})` →
@@ -1319,39 +1729,6 @@ bug wearing a layout costume, while a tooltip that is always there is noise.
1319
1729
  `data-lines` plus `--zx-truncate-lines` select the multi-line form.
1320
1730
  <!-- /doc -->
1321
1731
 
1322
- <!-- doc:gx-compat -->
1323
- ### gx compatibility layer
1324
-
1325
- Opt-in re-implementation of the legacy MooTools-era `gx` API on top of Zx, so existing ZeyOS
1326
- screens keep running while their code is modernised file by file. It lives in its own bundle, is
1327
- never imported by `src/index.js`, and adds nothing to an application that does not load it.
1328
-
1329
- - **Loading** — classic: `<script src="/assets/zx-compat.global.js">` → `window.gx`; module:
1330
- `import { gx } from '/assets/zx-compat.esm.js'` (or `src/compat/index.js` in-repo).
1331
- `gx.compat.installGlobals()` additionally installs the legacy free functions `__()`, `_()`,
1332
- `String.htmlSpecialChars`, and the element `store`/`retrieve` shim. `gx.install(host)` assigns
1333
- the namespace without the optional globals.
1334
- - **`gx.zeyos`** — Select/SelectFilter/SelectDyn/SelectPrio, Table, Tabbox, Panel, MasterPanel,
1335
- Groupbox, Search, Datebox, DatePicker, MonthPicker, TimePicker, Timebox, Toggle, Msgbox, Popup,
1336
- Dialog, Dropdown, Checklist, Permission, Client, Request, Factory.
1337
- - **`gx.bootstrap`** — Form, Fieldset, Field, the Select variants, Checklist, Table, Tabbox,
1338
- NavigationBar, MenuButton, Message, Popup/PopupAlert/PopupConfirm, CheckButton, DataFilter,
1339
- ValueList, MultiValueEditor, FieldUpload, Timebox.
1340
- - **`gx.util`** — `formatNum`, `formatTime`, `getMinutes`, `getNumber`, `printf`, `parseResult`,
1341
- `Parse`, `isArray`/`isObject`/`isFunction`/`isString`/`isNumber`/`isElement`/`isNode`,
1342
- `Console`.
1343
- - **`gx.ui`** — Container, SimpleTable, Timebox, and the inert visual-effect stubs
1344
- Blend/Collapse/Hud/Toggling/HGroup/Templates. **`gx.core`** — Settings. **`gx.compat`** —
1345
- installGlobals, installElementStorage, parse, GxWrapper.
1346
- - **Migrating off it** — legacy classes collapse onto fewer Zx components: all four Select
1347
- variants → one `Select` with a `filter` option; `gx.ui.Table` + `SimpleTable` → one `Table`;
1348
- `Msgbox`/`bootstrap.Message` → `Message`; `Popup`/`PopupAlert`/`PopupConfirm` → `Modal`/`Dialog`
1349
- with Promise-returning statics; `gx.zeyos.Client`/`Request` → `@zeyos/client`. Anything that
1350
- depended on MooTools prototype extensions is gone. `MIGRATION.md` has the full map and the
1351
- deliberate behaviour changes; `website/compat.html` runs legacy snippets against Zx as a smoke
1352
- test.
1353
- <!-- /doc -->
1354
-
1355
1732
  <!-- doc:elements -->
1356
1733
  ### Custom elements — `defineElements()`
1357
1734