@zeyos/zx 2.3.0 → 3.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 (105) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/DESIGN-SYSTEM.md +136 -0
  3. package/MIGRATION.md +155 -4
  4. package/README.md +24 -13
  5. package/dist/types/compat/map/data.d.ts +3 -2
  6. package/dist/types/components/account-menu/account-menu.d.ts +144 -0
  7. package/dist/types/components/app-rail/app-rail.d.ts +250 -0
  8. package/dist/types/components/app-sidebar/app-sidebar.d.ts +169 -0
  9. package/dist/types/components/avatar/avatar.d.ts +100 -0
  10. package/dist/types/components/chart/chart.d.ts +321 -0
  11. package/dist/types/components/date-picker/month-picker.d.ts +6 -0
  12. package/dist/types/components/dialog/dialog.d.ts +9 -0
  13. package/dist/types/components/dock/dock.d.ts +495 -0
  14. package/dist/types/components/dropdown/dropdown.d.ts +2 -2
  15. package/dist/types/components/field/field.d.ts +14 -0
  16. package/dist/types/components/field-upload/field-upload.d.ts +37 -4
  17. package/dist/types/components/form/form.d.ts +25 -3
  18. package/dist/types/components/grid/grid.d.ts +172 -0
  19. package/dist/types/components/launcher/launcher.d.ts +300 -0
  20. package/dist/types/components/layout/layout.d.ts +4 -4
  21. package/dist/types/components/menu-button/menu-button.d.ts +10 -2
  22. package/dist/types/components/modal/modal.d.ts +25 -0
  23. package/dist/types/components/multi-value-editor/multi-value-editor.d.ts +7 -1
  24. package/dist/types/components/questionnaire/items.d.ts +327 -0
  25. package/dist/types/components/questionnaire/questionnaire.d.ts +449 -0
  26. package/dist/types/components/sheet/sheet-stack.d.ts +167 -0
  27. package/dist/types/components/sheet/sheet.d.ts +294 -0
  28. package/dist/types/components/split-view/split-view.d.ts +2 -21
  29. package/dist/types/components/table/table.d.ts +219 -12
  30. package/dist/types/components/tooltip/tooltip.d.ts +2 -2
  31. package/dist/types/components/value-list/value-list.d.ts +24 -3
  32. package/dist/types/core/dom.d.ts +114 -0
  33. package/dist/types/core/drag-axis.d.ts +111 -0
  34. package/dist/types/core/position.d.ts +4 -2
  35. package/dist/types/index.d.ts +12 -2
  36. package/dist/zx-compat.esm.js +641 -277
  37. package/dist/zx-compat.esm.min.js +1 -1
  38. package/dist/zx-compat.global.js +7609 -2236
  39. package/dist/zx-compat.global.min.js +3 -3
  40. package/dist/zx-zeyos.esm.js +546 -87
  41. package/dist/zx-zeyos.esm.min.js +1 -1
  42. package/dist/zx.css +1452 -101
  43. package/dist/zx.esm.js +7952 -2592
  44. package/dist/zx.esm.min.js +3 -3
  45. package/dist/zx.global.js +7952 -2592
  46. package/dist/zx.global.min.js +3 -3
  47. package/dist/zx.min.css +1 -1
  48. package/docs/api.json +2276 -337
  49. package/docs/llms.md +426 -30
  50. package/docs/llms.txt +21 -9
  51. package/package.json +4 -2
  52. package/src/compat/globals.js +3 -1
  53. package/src/compat/map/data.js +31 -9
  54. package/src/components/account-menu/account-menu.css +70 -0
  55. package/src/components/account-menu/account-menu.js +208 -0
  56. package/src/components/app-rail/app-rail.css +107 -0
  57. package/src/components/app-rail/app-rail.js +668 -0
  58. package/src/components/app-sidebar/app-sidebar.css +68 -0
  59. package/src/components/app-sidebar/app-sidebar.js +388 -0
  60. package/src/components/avatar/avatar.css +61 -0
  61. package/src/components/avatar/avatar.js +222 -0
  62. package/src/components/button/button.css +0 -1
  63. package/src/components/chart/chart.css +61 -0
  64. package/src/components/chart/chart.js +518 -0
  65. package/src/components/date-picker/month-picker.js +9 -0
  66. package/src/components/dialog/dialog.js +11 -4
  67. package/src/components/dock/dock.css +228 -0
  68. package/src/components/dock/dock.js +977 -0
  69. package/src/components/dropdown/dropdown.js +1 -1
  70. package/src/components/field/field.js +2 -0
  71. package/src/components/field-upload/field-upload.js +9 -4
  72. package/src/components/form/form.js +6 -3
  73. package/src/components/grid/grid.css +6 -0
  74. package/src/components/grid/grid.js +213 -0
  75. package/src/components/launcher/launcher.css +100 -0
  76. package/src/components/launcher/launcher.js +557 -0
  77. package/src/components/layout/layout.js +2 -2
  78. package/src/components/menu-button/menu-button.js +27 -2
  79. package/src/components/modal/modal.css +5 -1
  80. package/src/components/modal/modal.js +37 -2
  81. package/src/components/multi-value-editor/multi-value-editor.js +2 -1
  82. package/src/components/questionnaire/items.js +370 -0
  83. package/src/components/questionnaire/questionnaire.css +387 -0
  84. package/src/components/questionnaire/questionnaire.js +1033 -0
  85. package/src/components/select/select.css +0 -1
  86. package/src/components/sheet/sheet-stack.js +304 -0
  87. package/src/components/sheet/sheet.css +351 -0
  88. package/src/components/sheet/sheet.js +551 -0
  89. package/src/components/split-view/split-view.js +76 -161
  90. package/src/components/table/table.css +43 -0
  91. package/src/components/table/table.js +523 -45
  92. package/src/components/tooltip/tooltip.js +5 -2
  93. package/src/components/value-list/value-list.js +6 -3
  94. package/src/core/dom.js +302 -0
  95. package/src/core/drag-axis.js +203 -0
  96. package/src/core/position.js +68 -22
  97. package/src/index.js +19 -2
  98. package/styles/tokens/dark.css +2 -3
  99. package/styles/tokens/global.css +5 -5
  100. package/styles/tokens/semantic.css +5 -3
  101. package/styles/tokens/themes.css +1 -1
  102. package/styles/zx.css +10 -1
  103. package/dist/types/components/permission/permission.d.ts +0 -96
  104. package/src/components/permission/permission.css +0 -47
  105. 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 the dependency-free design system for ZeyOS business applications; Zx is its
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 {
@@ -211,8 +211,9 @@ reflection and ElementInternals form association: `<zx-toggle>`, `<zx-check-butt
211
211
 
212
212
  Load `zx-compat.global.js` (or import `zx-compat.esm.js`) →
213
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
214
+ translation, MooTools-style `addEvent`). `gx.compat.installGlobals()` (opt-in) installs its gx
215
+ `__()` parser only when that name is free, plus `_()`, `String.htmlSpecialChars`, and the element
216
+ `store/retrieve` shim. Full namespace listing in
216
217
  the "gx compatibility layer" component section below; class map and unsupported legacy APIs in
217
218
  `MIGRATION.md`.
218
219
 
@@ -418,6 +419,67 @@ Search input with embedded search and clear buttons (`role="search"`).
418
419
  - **Events** — `input {value}` (debounced), `submit {value}` (Enter or the button), `clear`.
419
420
  <!-- /doc -->
420
421
 
422
+ <!-- doc:launcher -->
423
+ ### Launcher — application and record search
424
+
425
+ A native-dialog launcher for applications, recent records, commands, and grouped remote results.
426
+ Zx owns ranking, focus, request cancellation, and presentation; ZeyOS owns the catalogue,
427
+ permissions, recent-history/cache policy, and the route or command ultimately invoked.
428
+
429
+ - **Constructor** — `new Launcher(null, options)` owns a body-level dialog;
430
+ `new Launcher(existingDialog, options)` enhances and later restores an existing `<dialog>`.
431
+ - **Options** — `items`, abortable `sources: [{id, label?, minQuery?, load(query, {signal})}]`,
432
+ `query`, `debounce: 250`, `minQuery: 0`, `maxResults: 100`, `placeholder`, `label`, `emptyText`,
433
+ `loadingText`, and `shortcut: 'mod+k'|false`.
434
+ - **Items** — `{id, label, description?, keywords?, group?, icon?, badge?, value?, href?, target?,
435
+ invoke?, pinned?, disabled?}`. Links stay native so modified and middle clicks work; non-link
436
+ commands may provide an application-owned `invoke` callback.
437
+ - **Methods** — `open()`, `close()`, `toggle()`, `isOpen()`, `focus()`, `setQuery()`, `getQuery()`,
438
+ `setItems()`, `setSources()`, `destroy()`.
439
+ - **Events** — cancelable `select {item, value, source, query}`, `query {query}`, `open`, `close`,
440
+ and `error {source, error}`. Preventing `select` suppresses the supplied link or callback.
441
+ - **Ranking** — local results are case/diacritic-insensitive and deterministic: exact label,
442
+ label prefix, word prefix, label substring, identifier substring, then acronym prefix. Pinned
443
+ items retain an explicit order ahead of otherwise equal results. A new async query aborts and
444
+ ignores stale source work.
445
+ - **Keyboard** — Cmd+K on macOS or Ctrl+K elsewhere opens by default; the shortcut is ignored in
446
+ editable controls, composition, and modal contexts. Arrow keys, Home/End, and Enter operate the
447
+ single listbox selection model; Escape closes and restores focus.
448
+ <!-- /doc -->
449
+
450
+ <!-- doc:avatar -->
451
+ ### Avatar
452
+
453
+ A fixed-geometry user image with deterministic initials fallback. Image loading or failure never
454
+ changes layout, and the component is decorative by default so adjacent identity text is not
455
+ announced twice.
456
+
457
+ - **Options** — `src`, `name`, `initials`, `label`, `size: 'sm'|'md'|'lg'|<pixels>`,
458
+ `shape: 'circle'|'rounded'|'square'`, `status: 'online'|'away'|'busy'|'offline'|null`, and
459
+ `statusLabel`.
460
+ - **Methods** — `set(values)`, `setStatus(status, label?)`, `hasImage()`, `destroy()`.
461
+ - **Accessibility** — set `label` only when the avatar stands alone. Presence is supplementary
462
+ status, not the account's accessible name; supply `statusLabel` when it conveys information.
463
+ <!-- /doc -->
464
+
465
+ <!-- doc:account-menu -->
466
+ ### AccountMenu
467
+
468
+ Composes `Avatar` and `MenuButton` into one account trigger and APG menu. The popup repeats the
469
+ identity before its actions; Zx never decides role visibility, changes preferences, signs a user
470
+ out, or mutates the session.
471
+
472
+ - **Options** — `account: {name, secondary, src?, initials?, status?, statusLabel?}`, `items`,
473
+ `compact: false`, `label`, and any top/bottom/left/right `placement`.
474
+ - **Methods** — `open()`, `close()`, `toggle()`, `isOpen()`, `setAccount()`, `setItems()`, `focus()`,
475
+ `getPanel()`, `destroy()`.
476
+ - **Events** — cancelable `select {value, item}`, `open`, and `close`. A canceled selection does
477
+ not run the item's callback and leaves the menu open, so an application can reject or confirm an
478
+ action without rebuilding the account surface.
479
+ - **Accessibility** — compact mode is visually avatar-only but keeps the full account-menu name on
480
+ its single native trigger; MenuButton retains focus return, typeahead, and arrow-key behavior.
481
+ <!-- /doc -->
482
+
421
483
  <!-- doc:select -->
422
484
  ### Select — APG editable combobox
423
485
 
@@ -442,7 +504,7 @@ Unifies single-select, local filtering, and async loading.
442
504
  `Select.permission(target, {groups, value})`, the record-access picker with Private and Public
443
505
  pinned above the groups. Its value is the tri-state ZeyOS stores (`false` private, `true` public,
444
506
  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`.
507
+ `msg` (`permission.private`, `permission.public`).
446
508
  - **Keyboard** (APG combobox) — ArrowDown/Alt+ArrowDown open; arrows navigate and wrap; Home/End;
447
509
  Enter selects; Esc closes; Tab closes; printable characters filter (editable) or run a typeahead
448
510
  (readonly). `aria-activedescendant` tracks the active option.
@@ -675,6 +737,102 @@ Structured modal: a header with title and close button, a body, and footer butto
675
737
  `Dialog.prompt({ title, message, value })` → string|null.
676
738
  <!-- /doc -->
677
739
 
740
+ <!-- doc:sheet -->
741
+ ### Sheet
742
+
743
+ Edge-anchored surface — a `Dialog` attached to one side of the viewport instead of floating in the
744
+ middle. One primitive covers both side sheets and mobile drawers, which differ mainly in the edge they take, so
745
+ `side` replaces what would otherwise be two components. Inherits the whole Dialog anatomy: title,
746
+ footer buttons, switchable views, and focus restoration.
747
+
748
+ - **Options** — `side: 'end'` (`'start' | 'end' | 'top' | 'bottom'`, logical), `modal: true`
749
+ (`true | 'trap-focus' | false`), `backdrop: 'dim'` (`'dim' | 'blur' | 'none'`), `size: null`
750
+ (CSS length or px number, applied to whichever axis `side` implies), `closeButton: true`,
751
+ plus everything Dialog takes — `title`, `content`, `buttons`, `closable`, `lightDismiss`
752
+ (defaults to `true` here, unlike Modal), `destroyOnClose`.
753
+ - **Methods** — `open()`, `close(result?)`, `setSide(side)`, `getSide()`, `setSize(size)`,
754
+ `isModal()`, `isDocked()`, `isOpen()`, `getSize()`, `snapTo(target)`, plus Dialog's `setTitle()`, `setContent()`,
755
+ `setButtons()`, `addView()`, `showView()`.
756
+ - **Events** — `open`, `close {result}`, `cancel` (preventable with `event.preventDefault()`),
757
+ `resize {size, ratio}`, `dockchange {docked, dock}`.
758
+ - **Docked** — a sheet has two lives, and it does not choose between them: `Dock.adopt(sheet)`
759
+ makes it a track in that dock's flow instead of an overlay, and the dock then owns its side,
760
+ size, and resizing. A docked sheet is always non-modal, `close()` collapses its track to zero
761
+ rather than sliding away, and Escape and outside-click stop dismissing it — it is part of the
762
+ layout, not something covering it. `Dock.release(sheet)` hands it back. Nothing is rebuilt
763
+ across a handoff: the element moves and reopens in place, so DOM state and listeners survive,
764
+ and no `open`/`close` is emitted for what is only a change of address.
765
+ - **Modality** — `true` delegates focus containment, page inertness, Escape, and the backdrop to
766
+ the browser through `showModal()`. `'trap-focus'` and `false` open with `show()`, so the page
767
+ keeps scrolling and stays interactive; Escape and outside-click are re-implemented, and
768
+ `'trap-focus'` adds `focusTrap()` on top. Only a modal sheet renders a `::backdrop`, so
769
+ `backdrop` is meaningful only when `modal` is `true`.
770
+ - **Resizing and detents** — `resizable` makes the inner edge draggable between `min` and `max`.
771
+ `snap` gives it detents: a number at or below 1 is a fraction of the viewport along the sheet's
772
+ axis, anything larger is pixels, and a string is any CSS length (`'320px'`, `'90%'`). With
773
+ detents a drag settles on the nearest one rather than where it was released, and `snapTo(index)`
774
+ or `snapTo(value)` moves there directly. Dragging below half the smallest allowed size dismisses
775
+ a `closable` sheet, so resize, detent, and swipe-to-dismiss are one gesture whose meaning is
776
+ decided when the pointer settles. `getSize()` reports the current pixel size. A sheet that is
777
+ neither `resizable` nor snapping still has a draggable handle where `handle` shows one — that is
778
+ what makes a drawer dismissible — but the drag may only take size away, never add it, so it
779
+ cannot be grown by dragging when nothing asked for resizing.
780
+ - **Handle** — `handle: 'auto'` shows a grab handle on `top` and `bottom` sheets, which is what
781
+ makes them read as drawers; `true` and `false` force it either way. It exists whenever the sheet
782
+ is `resizable` or has `snap`, visible or not. It is a `separator` with live
783
+ `aria-valuenow`/`aria-valuemin`/`aria-valuemax`: arrow keys along the axis step it by 16px, Shift
784
+ by 64px. A docked sheet hides its handle — the dock's own divider resizes it there.
785
+ - **Custom properties** — `--zx-sheet-size` (written by `size`; the per-side default otherwise)
786
+ and `--zx-sheet-radius` (inner corners of a top/bottom sheet, default `--zx-radius-xl`) are
787
+ internal geometry, not published styling hooks — prefer the options. `--zx-overlay-blur` is a
788
+ tier-2 semantic token and is public: it sets the `'blur'` backdrop's radius, default `8px`.
789
+ - **Motion** — entry and exit both animate, via `@starting-style` and
790
+ `transition-behavior: allow-discrete` on `display`/`overlay`; without them the sheet appears and
791
+ disappears instantly. All of it sits inside `prefers-reduced-motion: no-preference`.
792
+ - **Keyboard and screen readers** — identical to Dialog: the native `<dialog>` supplies the role
793
+ and `aria-labelledby` points at the title. Escape closes when `closable`, focus moves to the
794
+ `[autofocus]` control or the first focusable descendant on open, and returns to the opener on
795
+ close. Tab cycles within the sheet when `modal` is `true` or `'trap-focus'`.
796
+ <!-- /doc -->
797
+
798
+ <!-- doc:sheet-stack -->
799
+ ### SheetStack
800
+
801
+ An ordered group of Sheets that reads as one drill-down. Deliberately tiny, because the browser
802
+ already does the hard part: nested dialogs stack in the top layer by open order, so there is no
803
+ z-index bookkeeping, and Escape closes only the topmost, so unwinding one level at a time is
804
+ native. What is left is which sheet sits at which depth, written onto the elements as `data-depth`
805
+ and `--zx-sheet-depth` for CSS to interpret.
806
+
807
+ Owns no element, so it is a controller rather than a `Component` — constructed with options
808
+ alone, `new SheetStack({…})`.
809
+
810
+ - **Options** — `layout: 'stack'` (`'stack' | 'cascade'`), `offset: 24`, `scale: 0.04`, `max: 3`.
811
+ - **Methods** — `push(sheet)`, `pop()`, `popTo(sheet)`, `clear()`, `top()`, `size()`, `sheets()`,
812
+ `has(sheet)`, `on/off/once`, `destroy()`.
813
+ - **Events** — `push {sheet, depth}`, `pop {sheet}`. `pop` fires however a sheet left — popped,
814
+ dismissed with Escape, or closed by its own button.
815
+ - **Layouts** — `stack` slides each covered sheet back toward its own edge, scales it down, and
816
+ takes it out of the tab order; only the top one is usable, which is the drill-down feel.
817
+ `cascade` shifts each covered sheet clear of the ones in front — by their measured size, not a
818
+ guess — so they sit side by side and all stay usable. `cascade` is usually the right one for an
819
+ ERP screen, where the parent record should stay readable while a line item is edited.
820
+ - **Depth** — numbered from the top: the visible sheet is `0`. Past `max` a sheet stops being
821
+ drawn but stays open and in the stack. In a `stack` only the topmost backdrop shades the page;
822
+ one per depth would compound into black. A `cascade` shades nothing — a backdrop paints directly
823
+ beneath its own dialog in the top layer, so the top sheet's would dim exactly the sheets the
824
+ layout exists to keep readable. Pair a cascade with `modal: false` when the page behind it
825
+ should stay live as well.
826
+ - **Ownership** — the stack never owns its sheets. `destroy()` empties it and strips the depth
827
+ styling without closing anything.
828
+ - **Accessibility** — for `stack`, covered sheets are `inert`. The browser does that on its own
829
+ for stacked *modal* dialogs, but a non-modal stack gets nothing, so the attribute is set either
830
+ way. `cascade` deliberately leaves them interactive.
831
+ - **Gotcha** — `close()` on a dialog dispatches its `close` event in a queued task, not
832
+ synchronously. `pop()` therefore removes the sheet from the stack itself rather than waiting for
833
+ the event, so `popTo()` and `clear()` can loop without racing it.
834
+ <!-- /doc -->
835
+
678
836
  <!-- doc:dropdown -->
679
837
  ### Dropdown
680
838
 
@@ -682,7 +840,8 @@ Generic anchored popover in the top layer, constructed as `new Dropdown(anchor,
682
840
  light-dismisses on an outside click or Esc, and mirrors `aria-expanded` onto the anchor.
683
841
 
684
842
  - **Options** — `content`,
685
- `placement: 'bottom-start'|'bottom-end'|'top-start'|'top-end'|'bottom'|'top'`, `offset: 4`,
843
+ `placement: 'bottom-start'|'bottom-end'|'top-start'|'top-end'|'bottom'|'top'` plus the matching
844
+ `left*` and `right*` placements, `offset: 4`,
686
845
  `matchWidth: false`, `openOn: 'click'|'manual'`, `closeOnSelect: false`.
687
846
  - **Methods** — `open()`, `close()`, `toggle()`, `isOpen()`, `setContent()`, `getPanel()`.
688
847
  - **Events** — `open`, `close`.
@@ -695,8 +854,11 @@ A button that opens a `role="menu"`.
695
854
 
696
855
  - **Options** — `label`, `icon`, `kind`,
697
856
  `items: [{ label, icon?, value?, disabled?, danger?, onselect? } | '-']`, `placement`.
698
- - **Methods** — `setItems()`, `open()`, `close()`, `setLabel()`.
857
+ - **Methods** — `setItems()`, `open()`, `close()`, `setLabel()`, `getTrigger()`, `getPanel()`,
858
+ `focusFirst()`, `focusLast()`.
699
859
  - **Events** — `select {value, item}`, `open`, `close`.
860
+ - **Cancellation** — `select` is cancelable. Preventing it skips the item's `onselect` callback and
861
+ leaves the menu open, which is the composition seam used by `AccountMenu` and rail flyouts.
700
862
  - **Keyboard** — ArrowDown/Enter/Space open and focus the first item; ArrowUp opens and focuses the
701
863
  last; arrows cycle; Home/End; typeahead; Enter/Space select; Esc/Tab close.
702
864
  <!-- /doc -->
@@ -734,7 +896,11 @@ parked at the pointer, so it flips near a viewport edge like every other floatin
734
896
 
735
897
  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
898
 
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).
899
+ - **Options** — `content` (string, Node, or a function re-evaluated on every open), `placement`
900
+ (`'top'` default, plus top/bottom/left/right and their start/end variants), `offset` (6), `delay`
901
+ (400), `hideDelay` (80), `maxWidth` (260; a number is pixels, a string is any CSS length),
902
+ `trigger` (`'both'` default, or `'hover'`, `'focus'`, `'manual'` — `manual` installs no listeners
903
+ at all and leaves everything to the methods), `disabled` (false).
738
904
  - **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
905
  - **Events** — `open`, `close`. Both fire only on a real transition, so a `show()` on an open tooltip is silent.
740
906
  - **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.
@@ -759,22 +925,32 @@ Sortable, selectable, sticky-header data table — one component covering both t
759
925
  horizontally, and multi-select adds a tri-state header checkbox plus Shift+click range select.
760
926
 
761
927
  - **Options** — `columns: [{ id, label, sortable?, width ('120px'|'2fr'|'auto'), align,
928
+ type?: 'text'|'number'|'currency'|'percent'|'unit', locale?, decimals?, currency?, unit?,
762
929
  render?: (row,i)=>Node|string, sortValue?, headerTitle? }]`, `data`, `rowId: 'ID'`,
763
930
  `sort: { id, dir }`, `sortMode: 'local'|'server'`, `selectable: false|'single'|'multi'`,
764
- `stickyHeader: true`, `height`, `emptyText`, `rowClass`, `zebra: true`.
931
+ `stickyHeader: true`, `height`, `emptyText`, `rowClass`, `zebra: true`, and
932
+ `hierarchy: false|{parentId, column?, expanded?}`. Locale, currency, unit, and decimals may be
933
+ row callbacks; values remain numbers in row data while display formatting is locale-aware.
765
934
  - **Methods** — `setData()`, `addData()`, `updateRow(id,row)`, `removeRow(id)`, `getRow(id)`,
766
935
  `getData()`, `empty()`, `setSort(id,dir)`, `getSelection()`, `setSelection(ids)`,
767
936
  `clearSelection()`, `setLoading(bool)` (busy/skeleton state — dims rows, shows an indeterminate
768
937
  top bar, sets `aria-busy`, and is cleared by the next `setData`).
769
938
  - **Events** — `rowclick {row, id, index, event}`, `rowdblclick`, `sort {id, dir}`,
770
- `selectionchange {rows, ids}`, `datachange {rows}`.
939
+ `selectionchange {rows, ids}`, `datachange {rows}`, and `rowtoggle {row, id, expanded}`.
940
+ - **Hierarchy** — flat parent references are projected as an ARIA `treegrid`; `getData()` and all
941
+ editing/mutation APIs stay flat and ID-based. Orphans become roots, cycles render once, local
942
+ sort reorders siblings without mixing levels, and growing counts visible rows. Use
943
+ `toggleRow()`, `expandRow()`, `collapseRow()`, `expandAll()`, `collapseAll()`, and
944
+ `getExpanded()`. The native disclosure button works by pointer and keyboard without selecting
945
+ or clicking the row.
771
946
  - **Editing** — `editMode: false|'cell'|'row'` (default `false`, and completely inert until set).
772
947
  `'cell'` edits one cell, `'row'` opens every editable cell of the row and commits them as a unit.
773
948
  Editors are sized into the cell without changing row height, and an editing cell carries
774
949
  `data-editing="true"` (`data-invalid="true"` plus an `aria-live` message when a validator rejects).
775
950
  - **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
951
+ 'checkbox'|'textarea'|((row)=>boolean)` (`true` infers `'number'` for number/currency/percent/unit
952
+ columns and `'text'` otherwise; a function is evaluated per row, so individual rows stay
953
+ read-only), `options` for `'select'` (`[{value,label}]`, a `{value: label}` map
778
954
  whose keys are always strings, or `(row)=>items`), `editorProps` (forwarded to the underlying
779
955
  NumberField/Select/Datebox/Toggle or to the generated `<input>`/`<textarea>`),
780
956
  `editor: (row, api)=>Node` for a fully custom editor (`api` is `{value, commit(value), cancel(),
@@ -783,6 +959,10 @@ horizontally, and multi-select adds a tri-state header checkbox plus Shift+click
783
959
  seeds the text of a `'text'`/`'textarea'` editor, `parse` converts every editor's raw output back),
784
960
  and `validate: (value,row)=>true|string` (a returned string marks the cell invalid, shows the
785
961
  message, and refuses the commit).
962
+ - **Transaction editors** — currency and unit columns reuse `NumberField` and show the resolved
963
+ currency/unit as its suffix unless `editorProps.unit` overrides it. This is a generic typed-grid
964
+ contract: application code remains responsible for tax, discount, subtotal, and accounting
965
+ formula rules.
786
966
  - **Editing methods** — `startEdit(rowId, columnId)`, `commitEdit()`, `cancelEdit()`, `isEditing()`,
787
967
  `getEditing()` → `{id, columnId}|null`.
788
968
  - **Editing events** — `editstart {row, id, column, columnId, value}`;
@@ -824,6 +1004,53 @@ horizontally, and multi-select adds a tri-state header checkbox plus Shift+click
824
1004
  A function is re-called on every render, which is what to use when the placeholder holds controls.
825
1005
  <!-- /doc -->
826
1006
 
1007
+ <!-- doc:grid -->
1008
+ ### Grid and `Grid.BillingItems()`
1009
+
1010
+ `Grid` is the public extension point for repeatable data-table presets. It subclasses `Table`
1011
+ without forking its implementation, so sorting, selection, hierarchy, responsive stacking,
1012
+ editable cells/rows, loading, events, and teardown keep the Table contract.
1013
+
1014
+ - **Constructor** — `new Grid(target, tableOptions)` accepts the complete `Table` API.
1015
+ - **Preset** — `Grid.BillingItems(target, options)` supplies an editable hierarchical billing
1016
+ schema for item, quantity, unit, unit price, currency, and line total. It returns a `Grid`.
1017
+ - **Billing options** — `data`, `fields` key overrides, `units`, `currencies`, fallback `currency`,
1018
+ `locale`, `decimals`, `columnOverrides`, complete `columns` replacement, `lineTotal(row, changes)`,
1019
+ and `oneditcommit` in addition to ordinary Table options.
1020
+ - **Rows** — default field names are `id`, `parent`, `kind`, `item`, `quantity`, `unit`,
1021
+ `unitPrice`, `currency`, and `total`. `kind: 'group'|'subtotal'|'header'|'section'` is read-only;
1022
+ other rows are editable. Parent references use Table's flat-data treegrid projection.
1023
+ - **Calculation boundary** — the default line total is quantity × unit price. Applications own
1024
+ taxes, discounts, rounding, subtotal/group recalculation, optimistic persistence, and server
1025
+ validation; provide `lineTotal` or cancel `editcommit` when product policy differs.
1026
+ <!-- /doc -->
1027
+
1028
+ <!-- doc:chart -->
1029
+ ### Chart and `ChartJsAdapter`
1030
+
1031
+ An accessible, engine-neutral chart host. Zx renders state, canvas, semantic data summary, tokens,
1032
+ and lifecycle; an injected adapter owns the drawing engine. The Zx runtime imports no Chart.js
1033
+ code, so future adapters do not change the public `Chart` API.
1034
+
1035
+ - **Options** — `adapter`, `type: 'bar'`, `data`, engine-specific `chartOptions`, `label`,
1036
+ `description`, `summary: 'hidden'|'visible'`, `loading`, loading/empty/error text, and
1037
+ `aspectRatio: '16 / 9'`.
1038
+ - **Methods** — `setData()`, `setType()`, `update(chartOptions)`, `setAdapter()`, `setLoading()`,
1039
+ `setError()`, `clearError()`, `resize()`, `refreshTheme()`, `getInstance()`, `destroy()`.
1040
+ - **Events** — `ready {instance}`, `update {data, type}`, `select {datasetIndex, index, dataset,
1041
+ label, value, nativeEvent}`, and `error {error}`.
1042
+ - **Adapter contract** — `adapter.create(canvas, {type, data, options, onSelect})` returns
1043
+ `{update(spec), resize(), destroy(), instance?}`. Implementations must destroy engine resources
1044
+ and must not retain the component after teardown.
1045
+ - **`ChartJsAdapter`** — construct it with a Chart.js constructor or namespace:
1046
+ `new ChartJsAdapter(window.Chart)`. The documentation pins Chart.js 4.5.1 as a development-only
1047
+ asset; `package.json` has no runtime dependencies and application builds explicitly choose how
1048
+ the engine is loaded.
1049
+ - **Accessibility** — the canvas always has a name and optional description, while synchronized
1050
+ labels and dataset values live in a semantic table. `summary: 'hidden'` keeps it visually hidden
1051
+ for assistive technology; `'visible'` makes exact values part of the interface.
1052
+ <!-- /doc -->
1053
+
827
1054
  <!-- doc:data-filter -->
828
1055
  ### DataFilter
829
1056
 
@@ -927,6 +1154,76 @@ Beyond the built-in field types, these widget types wrap whole components, used
927
1154
  `toggle` (Toggle).
928
1155
  <!-- /doc -->
929
1156
 
1157
+ <!-- doc:questionnaire -->
1158
+ ### Questionnaire
1159
+
1160
+ A guided, one-question-at-a-time flow: onboarding intake, a service-call checklist, an audit form,
1161
+ a feedback survey. The questionnaire owns the ordered items, the active one, the answers,
1162
+ validation, progress and navigation; the page, card, dialog or drawer around it owns closing,
1163
+ persistence and transport.
1164
+
1165
+ The root is a real `<form>` and the choices are native radios and checkboxes carrying the item
1166
+ name, so `new FormData(questionnaire.toElement())` reads the answers, arrow keys inside a choice
1167
+ group are the browser's rather than ours, and the semantics do not depend on JavaScript. Three
1168
+ things separate it from a `Form` with one `Fieldset` per question: it branches, its answer control
1169
+ can be any registered `Field` type, and its validation may be asynchronous.
1170
+
1171
+ - **Options** — `items: [{name, prompt, description?, section?, required?, multiple?, skippable?,
1172
+ choices?, input?, field?, when?, next?, validate?}]`, `answers: {}` and `active: null` (the
1173
+ resume pair), `progress: true`, `review: false`, `advance: 'manual'|'auto'`, `shortcuts: true`,
1174
+ `allowBack: true`.
1175
+ - **Choices** — `[{value, label, description?, icon?, key?, disabled?, exclusive?}]`. `exclusive`
1176
+ is the "None of the above" behaviour: picking it clears the rest of a `multiple` item, and
1177
+ picking anything else clears it.
1178
+ - **Methods** — `setItems()`, `getItems()`, `goTo(name)`, `next()`, `previous()`, `skip()`,
1179
+ `submit()`, `getAnswers()`, `getAnswer(name)`, `setAnswers(values, {silent})`,
1180
+ `setAnswer(name, value, {silent})`, `getActive()`, `getPath()`, `getProgress()`, `getState()`,
1181
+ `toFormData()`, `reset()`, `focus()`, `destroy()`. `next()`, `skip()` and `submit()` return
1182
+ **promises** — `next()` because an item's `validate` may be asynchronous, and `skip()` because
1183
+ skipping the last question submits. Everything else is synchronous.
1184
+ - **Events** — `change {name, value, answers}`, `navigate {from, to, reason}` **preventable**,
1185
+ `skip {name}`, `invalid {name, message}`, `submit {answers, path}` **preventable**,
1186
+ `complete {answers, path}`.
1187
+ - **Branching** — `when(answers)` decides whether an item is asked at all, and `next` (a name, or
1188
+ `(answer, answers) => name`) decides what follows. Conditions **cascade**: each predicate sees
1189
+ only the answers of visible items declared before it, so hiding the VAT question also hides
1190
+ whatever depended on the VAT id. An answer to a conditioned-out question is kept internally —
1191
+ walk back into the branch and it is still there — but is left out of `getAnswers()` and
1192
+ `toFormData()`, so an abandoned branch never submits stale data. A `next` naming a hidden item
1193
+ walks forward from it rather than stranding the reader; naming an item that does not exist
1194
+ throws `RangeError`.
1195
+ - **Path and progress** — `getPath()` is the questionnaire's memory of the route walked, and
1196
+ `previous()` pops it: once a branch has been taken, "previous" is not `index - 1`. `getProgress()`
1197
+ returns `{index, answered, total, percent, section}` where `total` is the path plus the items
1198
+ still reachable — exact for a static flow, an estimate once a function `next` can jump somewhere
1199
+ the forward walk cannot predict.
1200
+ - **Field answers** — `field: {type: 'date'|'number'|'zxselect'|'rating'|'upload'|…}` hands the
1201
+ answer to a `Field`, so any registered field type works, including ones an application registered
1202
+ itself. The `<legend>` already asks the question, so the Field's own label is hidden from sight
1203
+ but kept as the control's accessible name unless the item gives `field.label` explicitly; the
1204
+ Field draws its own highlight, so a failing answer shows one message, not two.
1205
+ - **Validation** — `required` is checked synchronously; an item's `validate(answer, answers)` may
1206
+ return a message, `null`, or a **promise** of either, and a rejection is reported as its message.
1207
+ While the check is pending the root carries `data-state="checking"` and `aria-busy="true"` and
1208
+ the actions are disabled.
1209
+ - **Review** — `review: true` shows a summary of every answer on the path before submitting, each
1210
+ with an Edit that reopens that question; Continue from an edited question comes straight back to
1211
+ the review rather than replaying the tail.
1212
+ - **Semantics** — the item is a `<fieldset>` and the prompt its `<legend>`; the description and the
1213
+ error are associated through `aria-describedby`, the error is a `role="alert"`, and a failing
1214
+ item carries `aria-invalid="true"`. `data-state` on the root is
1215
+ `asking`, `checking`, `review`, or `complete`. Submitting does not replace the content: it sets
1216
+ `data-state="complete"`, hides the actions, and emits `complete`, leaving the thank-you screen to
1217
+ the application.
1218
+ - **Keyboard** — `1`–`9` then `a`–`z` pick the matching answer, assigned in choice order (an
1219
+ explicit `key` is reserved first, so adding one never renumbers the others) and shown as a key
1220
+ cap on each choice; the caps are `aria-hidden`, since a screen-reader user moves through the
1221
+ group with the arrow keys the radios already provide. `Enter` continues, except in a `multiline`
1222
+ input. Shortcuts are suppressed while focus is in a text control. Navigating moves focus to the
1223
+ new question's fieldset, which is what gets the prompt announced; failing validation moves it to
1224
+ the first answer control instead.
1225
+ <!-- /doc -->
1226
+
930
1227
  <!-- doc:value-list -->
931
1228
  ### ValueList
932
1229
 
@@ -991,19 +1288,6 @@ yet. Field type: `tagpicker`.
991
1288
  within the list.
992
1289
  <!-- /doc -->
993
1290
 
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
1291
  <!-- doc:groupbox -->
1008
1292
  ### Groupbox
1009
1293
 
@@ -1073,6 +1357,62 @@ markup with no JavaScript.
1073
1357
  because a CSS custom property cannot be used inside a media query.
1074
1358
  <!-- /doc -->
1075
1359
 
1360
+ <!-- doc:dock -->
1361
+ ### Dock
1362
+
1363
+ A stack of collapsible, resizable panes: the docked inspector column of a design tool, and the
1364
+ detail side of a master–detail screen. Give the root a size of its own (a grid area, `block-size:
1365
+ 100%`, a flex child) when the panes should fill a shell rather than grow with their content.
1366
+
1367
+ - **Options** — `orientation: 'vertical'`, `content: null`, `panes: []`, `resizable: true`,
1368
+ `collapsible: true`, `lazy: true`, `storageKey: null`.
1369
+ - **Pane** — `{name, title?, content?, tabs?, active?, size?, min?, grow?, collapsed?,
1370
+ collapsible?, side?}`. `content` takes a string, Node, Component, or a factory called on first
1371
+ reveal. A pane with `tabs` renders its strip in place of the title; `content` is then ignored.
1372
+ `side` (`'start' | 'end'`) only applies when the dock has a `content`.
1373
+ - **Tab** — `{name, title, content?}`, same content shapes.
1374
+ - **Methods** — `pane(name)`, `names()`, `add(pane, {index})`, `remove(name)`,
1375
+ `collapse(name)`, `expand(name)`, `toggle(name)`, `isCollapsed(name)`, `reveal(name)`,
1376
+ `setSize(name, size)`, `getSize(name)`, `activate(pane, tab)`, `getActive(pane)`,
1377
+ `state()`, `setState(state)`, `adopt(sheet, options)`, `release(sheet)`.
1378
+ - **Adopting a Sheet** — `adopt(sheet, {name?, side?, index?, size?, min?, dockAt?})` takes over a
1379
+ `Sheet`'s positioning: it becomes a track in the dock rather than an overlay. The dock owns
1380
+ where it sits, its size, and its resizing; the sheet keeps its content, header, footer, events,
1381
+ and its own `open()`/`close()`. `dockAt` names a breakpoint below which the dock hands the sheet
1382
+ back to a free overlay and reclaims the track, re-adopting it above — measured on the **dock's
1383
+ own width**, so a dock inside a split pane behaves correctly where a viewport media query would
1384
+ not. Because a dock is a flex container by construction it is always a valid host, which is why
1385
+ docking needs no arrangement from the application.
1386
+ - **Ownership** — the dock never owns an adopted sheet: `destroy()` releases them rather than
1387
+ destroying them, `remove(name)` on a sheet releases it, and a sheet that destroys itself while
1388
+ adopted drops out of the dock cleanly.
1389
+ - **Events** — `collapse {name}`, `expand {name}`, `resize {name, size}`,
1390
+ `tabchange {pane, tab, previous}`, `reveal {name}`.
1391
+ - **Sizing** — one custom property per pane and one flex rule: a sized pane is
1392
+ `flex: 0 0 var(--zx-pane-size)`, a collapsed one falls to `flex: 0 0 auto` with its body hidden,
1393
+ and exactly one element grows. With a `content` that is always the content; without one it is the
1394
+ pane declaring `grow`, or the last expanded pane. A drag therefore rewrites at most two
1395
+ properties, and collapsing a pane feeds the grower rather than redistributing across the stack.
1396
+ - **Regions** — with `content` the dock is a region rather than a plain stack: panes sit on either
1397
+ `side` of the flexible middle. Nesting one dock inside another dock's pane gives a workbench —
1398
+ a canvas with an inspector column beside it.
1399
+ - **`reveal(name)`** — accepts a pane name or a tab name. Expands whichever pane holds it,
1400
+ activates the tab when the name is a tab's, and scrolls it into view.
1401
+ - **Persistence** — `storageKey` remembers sizes, collapsed panes, and active tabs through
1402
+ `storage()`, which degrades to memory rather than throwing. `state()`/`setState()` expose the
1403
+ same shape for storing it elsewhere; unknown pane names are ignored, so a stored layout survives
1404
+ a release that added or removed panes.
1405
+ - **Keyboard and screen readers** — each pane header is a row, not one big button, so the collapse
1406
+ toggle and the tab strip can coexist; the toggle carries `aria-expanded` and `aria-controls`.
1407
+ Tabs follow the APG tab pattern with one roving tab stop, Left/Right moving between them.
1408
+ Dividers are focusable `separator`s carrying live `aria-valuenow`/`aria-valuemin`/`aria-valuemax`
1409
+ in pixels: Up/Down (or Left/Right when horizontal) resize by 16px, Shift by 64px, and Enter or
1410
+ Space collapses the pane before the divider. A divider with nothing to resize is taken out of the
1411
+ tab order.
1412
+ - **Out of scope** — drag-to-rearrange, tear-off panels, and multi-column docks. All three are
1413
+ additive without changing the API above.
1414
+ <!-- /doc -->
1415
+
1076
1416
  <!-- doc:tabbox -->
1077
1417
  ### Tabbox — APG tabs
1078
1418
 
@@ -1178,12 +1518,67 @@ menu on narrow widths.
1178
1518
  - **Events** — `change {name}`.
1179
1519
  <!-- /doc -->
1180
1520
 
1521
+ <!-- doc:app-sidebar -->
1522
+ ### AppSidebar — expanded application navigation
1523
+
1524
+ Persistent vertical application navigation with header, scrollable tree, and sticky footer slots.
1525
+ It is route-agnostic: ZeyOS supplies native `href` values or handles cancelable selections through
1526
+ `PG.go()`. This expanded presentation is the only application-nav component that reveals children
1527
+ inline.
1528
+
1529
+ - **Options** — shared application `items`, `active`, `expanded`, `collapsed`, `side: 'left'|'right'`,
1530
+ `label`, and `header`, `footer`, `railHeader`, `railFooter` content slots.
1531
+ - **Methods** — `setItems()`, `setActive()`, `setExpanded()`, `getExpanded()`, `toggleBranch()`,
1532
+ `expand()`, `collapse()`, `setCollapsed()`, `isCollapsed()`, `getRail()`, `destroy()`.
1533
+ - **Events** — cancelable `select {item, id, value, href, event}`,
1534
+ `branchchange {item, id, expanded, ids}` (`item`, `id`, and `expanded` are null for a bulk
1535
+ `setExpanded()`), and `collapsechange {collapsed}`.
1536
+ - **Minimized state** — collapse composes `AppRail` over the same normalized item tree. Expanded
1537
+ branch IDs survive the round trip but are not rendered inline while minimized.
1538
+ - **Semantics** — persistent navigation uses `<nav>` and nested lists with native links and
1539
+ disclosure buttons, not menu or menubar roles. A parent with children is disclosure-only; give
1540
+ it an explicit Overview child when the parent also needs a destination.
1541
+ <!-- /doc -->
1542
+
1543
+ <!-- doc:app-rail -->
1544
+ ### AppRail — minimized application navigation
1545
+
1546
+ The compact form of application navigation for left, right, top, or bottom placement. It can flow
1547
+ vertically or horizontally, but descendants are always anchored flyouts—never inline—and open
1548
+ toward the workspace.
1549
+
1550
+ - **Options** — shared application `items`, `active`, `orientation: 'vertical'|'horizontal'`,
1551
+ `side: 'left'|'right'|'top'|'bottom'`, `label`, `header`, `footer`, `openDelay: 80`, and
1552
+ `closeDelay: 160`.
1553
+ - **Methods** — `setItems()`, `setActive()`, `getItems()`, `openFlyout(id, {focus})`,
1554
+ `closeFlyout()`, `closeAllFlyouts()` (including pending hover/focus timers and descendant
1555
+ branches), `isFlyoutOpen()`, `focus()`, `destroy()`.
1556
+ - **Events** — cancelable `select {item, id, value, href, event}` and
1557
+ `flyoutchange {item, id, open}`.
1558
+ - **Pointer and focus** — hover or focus reveals a child collection without stealing focus. The
1559
+ close delay covers the trigger-to-panel gap; leaving both trigger and panel closes it. Click,
1560
+ Enter, Space, and the inward arrow also open it, so horizontal subnavigation is discoverable and
1561
+ usable without hover. Escape, outside activation, or selection closes it.
1562
+ - **Semantics** — every icon-only destination retains a visible Tooltip and an accessible name.
1563
+ Persistent rail destinations remain native links/buttons in a labelled navigation list; the
1564
+ transient child flyout supplies its own arrow-key focus interaction.
1565
+ <!-- /doc -->
1566
+
1181
1567
  <!-- doc:kernel -->
1182
1568
  ### Core helpers
1183
1569
 
1184
1570
  - **Component** — the base class: `on`/`off`/`once`/`emit`, `listen`, `toElement`, `msg`,
1185
1571
  `destroy`, and the static `Component.from(el)`.
1186
- - **DOM** — `h(tag, props, ...children)`, `h.raw(html)`, `htmlEscape`, `resolveElement`.
1572
+ - **DOM** — `h(tag, props, ...children)`, `h.raw(html)`, `htmlEscape`, `resolveElement`. The current
1573
+ ZeyOS builder is also first-class: `__(tag#id.class, properties, content)` is the migration alias
1574
+ for `compactElement()`. It preserves `D*` data, `S*` style, boolean/built-in property, attribute,
1575
+ and legacy `on*` callback rules while keeping content text-safe. `applyCompactProperties`,
1576
+ `appendContent`, `appendCompact` (parent return), `appendCompactChild` (child return), and
1577
+ `fragment(...children)` are the pure forms.
1578
+ - **Compact events** — `onCompactEvent`, `offCompactEvent`, and `fireCompactEvent` share one
1579
+ newest-first handler registry. Existing `DOM.on/remEvt/fireEvt` methods can delegate to this
1580
+ triad. Zx never installs globals or alters DOM prototypes; the current application owns those
1581
+ temporary seams while `UI.new*` implementations are replaced without rewriting `PG` routing.
1187
1582
  - **Icons** — `icon(name, {size, label})` and `icons`; see the Icons section — bundled inline SVG
1188
1583
  by default, Font Awesome after an opt-in.
1189
1584
  - **Positioning** — `position(anchor, floating, {placement, offset, flip, matchWidth})` →
@@ -1328,8 +1723,9 @@ never imported by `src/index.js`, and adds nothing to an application that does n
1328
1723
 
1329
1724
  - **Loading** — classic: `<script src="/assets/zx-compat.global.js">` → `window.gx`; module:
1330
1725
  `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
1726
+ `gx.compat.installGlobals()` additionally installs the gx `__()` parser only when that name is
1727
+ free, plus `_()`, `String.htmlSpecialChars`, and the element `store`/`retrieve` shim. It never
1728
+ replaces ZeyOS's compact builder. `gx.install(host)` assigns
1333
1729
  the namespace without the optional globals.
1334
1730
  - **`gx.zeyos`** — Select/SelectFilter/SelectDyn/SelectPrio, Table, Tabbox, Panel, MasterPanel,
1335
1731
  Groupbox, Search, Datebox, DatePicker, MonthPicker, TimePicker, Timebox, Toggle, Msgbox, Popup,