@zeyos/zx 4.4.0 → 5.2.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 (252) hide show
  1. package/CHANGELOG.md +447 -0
  2. package/README.md +44 -11
  3. package/dist/types/components/account-menu/account-menu.d.ts +8 -0
  4. package/dist/types/components/activity-list/activity-list.d.ts +31 -1
  5. package/dist/types/components/app-icon/app-icon.d.ts +14 -2
  6. package/dist/types/components/app-sidebar/app-sidebar.d.ts +293 -17
  7. package/dist/types/components/avatar/avatar.d.ts +46 -7
  8. package/dist/types/components/badge/badge.d.ts +21 -1
  9. package/dist/types/components/button/button.d.ts +15 -0
  10. package/dist/types/components/calendar/calendar.d.ts +67 -5
  11. package/dist/types/components/card-view/card-view.d.ts +7 -4
  12. package/dist/types/components/card-view/record-card.d.ts +11 -0
  13. package/dist/types/components/chart/chart.d.ts +10 -0
  14. package/dist/types/components/checklist/checklist.d.ts +9 -0
  15. package/dist/types/components/code-editor/code-editor.d.ts +6 -17
  16. package/dist/types/components/context-menu/context-menu.d.ts +6 -2
  17. package/dist/types/components/data-filter/data-filter.d.ts +4 -4
  18. package/dist/types/components/datebox/date-range-box.d.ts +5 -1
  19. package/dist/types/components/datebox/datebox.d.ts +7 -2
  20. package/dist/types/components/dialog/dialog.d.ts +46 -6
  21. package/dist/types/components/dropdown/dropdown.d.ts +24 -9
  22. package/dist/types/components/entity-ref/entity-ref.d.ts +2 -2
  23. package/dist/types/components/field/field.d.ts +54 -2
  24. package/dist/types/components/fieldset/external-adapter.d.ts +83 -0
  25. package/dist/types/components/fieldset/fieldset.d.ts +26 -1
  26. package/dist/types/components/file-item/file-item.d.ts +34 -6
  27. package/dist/types/components/file-list/file-list.d.ts +35 -3
  28. package/dist/types/components/filter/filter.d.ts +127 -3
  29. package/dist/types/components/filter-panel/filter-panel.d.ts +36 -7
  30. package/dist/types/components/form/form.d.ts +12 -1
  31. package/dist/types/components/kanban-view/kanban-card.d.ts +8 -0
  32. package/dist/types/components/launcher/launcher.d.ts +268 -15
  33. package/dist/types/components/list-toolbar/list-toolbar.d.ts +16 -7
  34. package/dist/types/components/menu-button/menu-button.d.ts +39 -5
  35. package/dist/types/components/message/message.d.ts +132 -17
  36. package/dist/types/components/modal/modal.d.ts +43 -2
  37. package/dist/types/components/module-bar/module-bar.d.ts +395 -0
  38. package/dist/types/components/pagination/pagination.d.ts +17 -3
  39. package/dist/types/components/select/color.d.ts +12 -0
  40. package/dist/types/components/select/glyphs.d.ts +124 -0
  41. package/dist/types/components/select/select.d.ts +17 -3
  42. package/dist/types/components/sheet/sheet.d.ts +15 -0
  43. package/dist/types/components/stat-tile/stat-tile.d.ts +16 -5
  44. package/dist/types/components/tabbox/tabbox.d.ts +9 -0
  45. package/dist/types/components/table/table.d.ts +311 -18
  46. package/dist/types/components/tag-picker/tag-picker.d.ts +21 -4
  47. package/dist/types/components/tree/tree.d.ts +13 -0
  48. package/dist/types/components/view/record-view.d.ts +59 -2
  49. package/dist/types/components/view-settings/view-settings.d.ts +879 -0
  50. package/dist/types/components/workspace-switcher/workspace-switcher.d.ts +327 -0
  51. package/dist/types/core/breakpoint.d.ts +1 -1
  52. package/dist/types/core/component.d.ts +12 -2
  53. package/dist/types/core/i18n.d.ts +27 -3
  54. package/dist/types/core/icons.d.ts +23 -25
  55. package/dist/types/core/keyboard.d.ts +98 -1
  56. package/dist/types/core/messages.d.ts +382 -0
  57. package/dist/types/core/motion.d.ts +59 -0
  58. package/dist/types/core/overlay-host.d.ts +42 -1
  59. package/dist/types/core/overlay-stack.d.ts +260 -0
  60. package/dist/types/index.d.ts +10 -5
  61. package/dist/types/internal/app-rail.d.ts +38 -0
  62. package/dist/types/internal/menu-items.d.ts +71 -3
  63. package/dist/types/zeyos/connect.d.ts +6 -0
  64. package/dist/types/zeyos/form.d.ts +50 -9
  65. package/dist/types/zeyos/index.d.ts +2 -1
  66. package/dist/types/zeyos/launcher.d.ts +9 -0
  67. package/dist/types/zeyos/module-bar.d.ts +176 -0
  68. package/dist/types/zeyos/query.d.ts +110 -5
  69. package/dist/types/zeyos/schema.d.ts +24 -1
  70. package/dist/types/zeyos/table.d.ts +345 -22
  71. package/dist/types/zeyos/view.d.ts +26 -2
  72. package/dist/zx-zeyos.esm.js +6178 -1008
  73. package/dist/zx-zeyos.esm.min.js +53 -5
  74. package/dist/zx.css +2861 -827
  75. package/dist/zx.esm.js +8909 -1480
  76. package/dist/zx.esm.min.js +55 -7
  77. package/dist/zx.global.js +8911 -1482
  78. package/dist/zx.global.min.js +55 -7
  79. package/dist/zx.min.css +1 -1
  80. package/docs/api.json +1079 -117
  81. package/docs/llms.md +956 -154
  82. package/docs/llms.txt +9 -5
  83. package/package.json +1 -1
  84. package/src/components/account-menu/account-menu.css +35 -10
  85. package/src/components/account-menu/account-menu.js +12 -0
  86. package/src/components/activity-item/activity-item.css +1 -1
  87. package/src/components/activity-item/activity-item.js +3 -0
  88. package/src/components/activity-list/activity-list.css +38 -3
  89. package/src/components/activity-list/activity-list.js +61 -1
  90. package/src/components/app-icon/app-icon.css +14 -3
  91. package/src/components/app-icon/app-icon.js +11 -1
  92. package/src/components/app-rail/app-rail.css +59 -79
  93. package/src/components/app-sidebar/app-sidebar.css +304 -168
  94. package/src/components/app-sidebar/app-sidebar.js +667 -46
  95. package/src/components/aurora/aurora.css +5 -3
  96. package/src/components/avatar/avatar.css +32 -7
  97. package/src/components/avatar/avatar.js +72 -8
  98. package/src/components/badge/badge.css +54 -2
  99. package/src/components/badge/badge.js +29 -2
  100. package/src/components/breadcrumb/breadcrumb.css +1 -1
  101. package/src/components/button/button.css +67 -9
  102. package/src/components/button/button.js +38 -0
  103. package/src/components/calendar/calendar.css +47 -47
  104. package/src/components/calendar/calendar.js +26 -52
  105. package/src/components/card/card.css +4 -4
  106. package/src/components/card-view/card-view.css +14 -12
  107. package/src/components/card-view/card-view.js +13 -9
  108. package/src/components/card-view/record-card.js +14 -3
  109. package/src/components/chart/chart.css +4 -1
  110. package/src/components/chart/chart.js +12 -0
  111. package/src/components/check-button/check-button.css +10 -2
  112. package/src/components/checklist/checklist.css +9 -2
  113. package/src/components/checklist/checklist.js +11 -5
  114. package/src/components/checklist/field-adapter.js +3 -1
  115. package/src/components/code-editor/code-editor.css +7 -6
  116. package/src/components/code-editor/code-editor.js +19 -1
  117. package/src/components/code-editor/field-adapter.js +9 -2
  118. package/src/components/context-menu/context-menu.css +9 -4
  119. package/src/components/context-menu/context-menu.js +8 -2
  120. package/src/components/copy/copy-button.css +6 -0
  121. package/src/components/copy/copy-input.css +9 -4
  122. package/src/components/data-filter/data-filter.css +5 -4
  123. package/src/components/data-filter/data-filter.js +2 -2
  124. package/src/components/date-picker/date-picker.css +6 -6
  125. package/src/components/date-picker/date-range-picker.css +3 -3
  126. package/src/components/date-picker/time-picker.css +3 -3
  127. package/src/components/datebox/date-range-box.css +79 -2
  128. package/src/components/datebox/date-range-box.js +13 -2
  129. package/src/components/datebox/datebox.css +79 -2
  130. package/src/components/datebox/datebox.js +13 -2
  131. package/src/components/dialog/dialog.css +26 -0
  132. package/src/components/dialog/dialog.js +99 -17
  133. package/src/components/dock/dock.css +5 -4
  134. package/src/components/dropdown/dropdown.css +1 -1
  135. package/src/components/dropdown/dropdown.js +159 -72
  136. package/src/components/empty-state/empty-state.css +2 -2
  137. package/src/components/entity-ref/entity-ref.css +1 -1
  138. package/src/components/entity-ref/entity-ref.js +1 -1
  139. package/src/components/field/field.css +28 -9
  140. package/src/components/field/field.js +101 -29
  141. package/src/components/field-upload/field-upload.css +1 -1
  142. package/src/components/fieldset/external-adapter.js +363 -0
  143. package/src/components/fieldset/fieldset.css +5 -3
  144. package/src/components/fieldset/fieldset.js +38 -64
  145. package/src/components/file-item/file-item.js +144 -10
  146. package/src/components/file-list/file-list.js +144 -21
  147. package/src/components/filter/filter.css +86 -2
  148. package/src/components/filter/filter.js +307 -11
  149. package/src/components/filter-panel/filter-panel.css +1 -1
  150. package/src/components/filter-panel/filter-panel.js +149 -32
  151. package/src/components/finder/finder.css +16 -14
  152. package/src/components/form/form.css +5 -4
  153. package/src/components/form/form.js +11 -2
  154. package/src/components/grid/grid.css +1 -1
  155. package/src/components/groupbox/groupbox.css +1 -1
  156. package/src/components/kanban-view/kanban-card.js +11 -3
  157. package/src/components/kanban-view/kanban-view.css +42 -18
  158. package/src/components/kanban-view/kanban-view.js +9 -6
  159. package/src/components/launcher/launcher.css +108 -26
  160. package/src/components/launcher/launcher.js +530 -42
  161. package/src/components/list-toolbar/list-toolbar.css +13 -8
  162. package/src/components/list-toolbar/list-toolbar.js +40 -14
  163. package/src/components/master-panel/master-panel.css +2 -0
  164. package/src/components/master-panel/master-panel.js +0 -1
  165. package/src/components/menu-button/menu-button.css +72 -4
  166. package/src/components/menu-button/menu-button.js +160 -18
  167. package/src/components/message/message.css +33 -3
  168. package/src/components/message/message.js +264 -22
  169. package/src/components/modal/modal.css +1 -1
  170. package/src/components/modal/modal.js +83 -15
  171. package/src/components/module-bar/module-bar.css +486 -0
  172. package/src/components/module-bar/module-bar.js +1074 -0
  173. package/src/components/multi-value-editor/multi-value-editor.css +1 -1
  174. package/src/components/navigation-bar/navigation-bar.css +18 -9
  175. package/src/components/number-field/field-adapter.js +3 -1
  176. package/src/components/number-field/number-field.css +70 -0
  177. package/src/components/number-field/number-field.js +2 -0
  178. package/src/components/pagination/pagination.css +13 -11
  179. package/src/components/pagination/pagination.js +20 -4
  180. package/src/components/panel/panel.css +4 -3
  181. package/src/components/questionnaire/questionnaire.css +4 -4
  182. package/src/components/rating/field-adapter.js +2 -1
  183. package/src/components/rating/rating.css +8 -1
  184. package/src/components/search/search.css +4 -1
  185. package/src/components/select/color.js +22 -0
  186. package/src/components/select/field-adapter.js +2 -0
  187. package/src/components/select/glyphs.js +165 -0
  188. package/src/components/select/select.css +138 -45
  189. package/src/components/select/select.js +92 -47
  190. package/src/components/sheet/sheet.css +12 -9
  191. package/src/components/sheet/sheet.js +74 -32
  192. package/src/components/slider/field-adapter.js +6 -1
  193. package/src/components/slider/slider.css +18 -4
  194. package/src/components/slider/slider.js +1 -1
  195. package/src/components/sort-control/sort-control.css +4 -1
  196. package/src/components/split-view/split-view.css +1 -1
  197. package/src/components/stat-tile/stat-tile.css +42 -47
  198. package/src/components/stat-tile/stat-tile.js +17 -6
  199. package/src/components/stepper/stepper.css +1 -1
  200. package/src/components/tabbox/tabbox.css +41 -32
  201. package/src/components/tabbox/tabbox.js +5 -1
  202. package/src/components/table/table.css +187 -37
  203. package/src/components/table/table.js +718 -73
  204. package/src/components/tag-picker/field-adapter.js +2 -1
  205. package/src/components/tag-picker/tag-picker.css +83 -5
  206. package/src/components/tag-picker/tag-picker.js +25 -8
  207. package/src/components/timebox/timebox.css +22 -2
  208. package/src/components/toggle/toggle.css +12 -1
  209. package/src/components/toolbar/toolbar.js +2 -2
  210. package/src/components/tooltip/tooltip.css +4 -0
  211. package/src/components/tooltip/tooltip.js +44 -19
  212. package/src/components/tree/tree.css +26 -17
  213. package/src/components/tree/tree.js +31 -0
  214. package/src/components/value-list/value-list.css +3 -3
  215. package/src/components/view/record-view.css +1 -1
  216. package/src/components/view/record-view.js +178 -18
  217. package/src/components/view-settings/view-settings.css +285 -0
  218. package/src/components/view-settings/view-settings.js +1830 -0
  219. package/src/components/workspace-switcher/workspace-switcher.css +188 -0
  220. package/src/components/workspace-switcher/workspace-switcher.js +463 -0
  221. package/src/core/breakpoint.js +1 -1
  222. package/src/core/component.js +88 -17
  223. package/src/core/fontawesome.js +5 -1
  224. package/src/core/i18n.js +47 -4
  225. package/src/core/icons.js +204 -61
  226. package/src/core/keyboard.js +206 -11
  227. package/src/core/messages.js +427 -0
  228. package/src/core/motion.js +147 -0
  229. package/src/core/overlay-host.js +77 -3
  230. package/src/core/overlay-stack.js +487 -0
  231. package/src/core/position.js +22 -3
  232. package/src/index.js +10 -5
  233. package/src/internal/app-rail.js +135 -55
  234. package/src/internal/menu-items.js +61 -3
  235. package/src/zeyos/connect.js +6 -2
  236. package/src/zeyos/form.js +69 -27
  237. package/src/zeyos/index.js +4 -1
  238. package/src/zeyos/launcher.js +8 -3
  239. package/src/zeyos/module-bar.js +181 -0
  240. package/src/zeyos/query.js +197 -13
  241. package/src/zeyos/schema.js +144 -27
  242. package/src/zeyos/table.js +287 -84
  243. package/src/zeyos/view.js +49 -93
  244. package/styles/base.css +72 -1
  245. package/styles/icons.css +11 -1
  246. package/styles/print.css +1 -1
  247. package/styles/tokens/dark.css +117 -37
  248. package/styles/tokens/global.css +121 -27
  249. package/styles/tokens/semantic.css +140 -43
  250. package/styles/tokens/themes.css +7 -6
  251. package/styles/zx.css +3 -1
  252. package/styles/tokens/density.css +0 -9
package/docs/llms.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > Xenon is a dependency-free design system for browser applications; Zx is its product-agnostic
4
4
  > vanilla-JavaScript implementation. ES2022 modules, WAI-ARIA-accessible native controls, semantic `--zx-*` design
5
- > tokens (light/dark + cozy/compact), and lifecycle-safe components. Zero runtime dependencies.
5
+ > tokens (light/dark, one token-driven geometry), 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
8
8
  > component's section (`website/docs.html` renders them as each component's Reference tab).
@@ -49,13 +49,26 @@ const t = new Table(target, options);
49
49
 
50
50
  ## Theming
51
51
 
52
- Load `zx.css`, then set theme/density on any ancestor (usually `<html>`):
52
+ Load `zx.css`, then set the theme on any ancestor (usually `<html>`):
53
53
 
54
54
  ```html
55
- <html data-zx-theme="dark" data-zx-density="compact">
55
+ <html data-zx-theme="dark">
56
56
  ```
57
57
 
58
- Themes: `light` | `dark` | `auto` (follows OS). Density: `cozy` | `compact`.
58
+ Themes: `light` | `dark` | `auto` (follows OS).
59
+
60
+ **One geometry.** Spacing steps are multiples of one unit
61
+ (`--zx-space-unit`, 4px: `--zx-space-1…8` = 4/8/12/16/20/24/32/40px), radius steps are fractions of
62
+ one base (`--zx-radius`, 4px: `-sm/-md/-lg/-xl` = 2/3/4/6px; `-full` only for shapes whose roundness
63
+ is the meaning — badges, avatars, toggles), and controls are `--zx-control-height` (30px) tall.
64
+ To tighten or loosen an application, override those tokens on `:root` or on any region. The
65
+ geometry and material tokens (control size, radius, spacing, blur and glass filters, durations,
66
+ easings, the type scale) are declared on `:root` only, never on a `[data-zx-theme]` block, so an
67
+ override on `:root` reaches nested light and dark regions alike; theme blocks carry colours only:
68
+
69
+ ```css
70
+ :root { --zx-space-unit: 3.5px; --zx-control-height: 28px; }
71
+ ```
59
72
 
60
73
  **Accent presets.** A second, orthogonal attribute picks the accent ramp:
61
74
  `data-zx-preset="zx" | "zeyos" | "ocean" | "violet" | "rose" | "slate"` (`zx` is the default and
@@ -76,7 +89,7 @@ Keep the 600 stop at 4.5:1 or better against white and the 400 stop at 4.5:1 aga
76
89
  — that is what makes `--zx-color-on-accent` legible without being restated per theme.
77
90
 
78
91
  The theme studio treats those stable IDs as complete recipes: selecting a preset also changes its
79
- density, neutral tint, radius, control height, base text size, font stack, and material strength.
92
+ spacing unit, neutral tint, radius, control height, base text size, font stack, and material strength.
80
93
  Its **Material** control is CSS-first (`Flat`, `Glass`, or `Deep glass`). Aurora is the independent
81
94
  ambient canvas layer; material controls how surfaces respond to that light. The shared recipe
82
95
  applies to AppIcons, ordinary controls, persistent chrome, raised cards, and every transient overlay: Dropdown, ContextMenu,
@@ -92,14 +105,26 @@ Top-layer components remain DOM children of their nearest Zx theme scope, so a t
92
105
  set on an application region continues to inherit through its menus, dialogs, tooltips, and toasts.
93
106
 
94
107
  Define a product theme by overriding **semantic** tokens under a `[data-zx-theme="name"]` selector:
95
- `--zx-color-bg-page/surface/raised/control/hover/selected`, `--zx-color-glass-chrome/surface`,
108
+ `--zx-color-bg-page/surface/raised/control/muted/hover/selected`, `--zx-color-bg-current` (the
109
+ neutral "you are here" fill of the current nav item, row, tab, or pressed toggle — never the
110
+ accent), `--zx-color-bg-chrome` (navigation and inspector columns, one tone off the canvas),
111
+ `--zx-color-glass-chrome/surface`,
96
112
  `--zx-color-overlay-surface/panel/backdrop/border/hover/selected/divider/scrim`,
97
- `--zx-overlay-surface-image/toast-image/panel-image`, `--zx-color-border(-strong/-control)`,
98
- `--zx-color-text(-muted/-placeholder)`, `--zx-color-accent(-hover)/on-accent`,
99
- `--zx-color-danger/warning/success/info(+ -bg)`, `--zx-focus-ring`, `--zx-control-height/-radius`,
100
- `--zx-space-*`, `--zx-radius-*`, `--zx-text-*`. Never use tier-1 palette tokens (`--zx-gray-*`,
101
- `--zx-green-*`) or raw color literals in application/component CSS. Prefer the least-specific
102
- override that works (global token → then, if needed, a component style).
113
+ `--zx-overlay-surface-image/toast-image/panel-image`,
114
+ `--zx-color-border(-subtle/-strong/-control)` (subtle for row and grid dividers),
115
+ `--zx-color-text(-muted/-subtle/-placeholder)` (subtle for icons and non-essential meta),
116
+ `--zx-color-accent(-hover/-subtle)/on-accent`, `--zx-color-accent-solid(-hover/-active)` with
117
+ `--zx-color-on-accent-solid` (the one filled primary button per screen),
118
+ `--zx-color-danger/warning/success/info(+ -bg)`, the categorical tone palette
119
+ `--zx-color-tone-{blue,teal,green,amber,orange,red,pink,violet,slate}-bg/-fg` (tags, chips, icon
120
+ tiles, avatars — neither the accent nor a status) and `-solid` (a solid identity fill, such as a
121
+ module tile, that the white icon glyph reads on at 3:1 in both themes), the chart series
122
+ `--zx-color-chart-1…8`,
123
+ `--zx-focus-ring`, `--zx-control-height/-radius`, `--zx-space-*`, `--zx-radius-*`, `--zx-text-*`
124
+ (`-3xl` for document titles), `--zx-weight-medium/-semibold`, `--zx-tracking-tight/-title`. Never
125
+ use tier-1 palette tokens (`--zx-gray-*`, `--zx-green-*`, `--zx-emerald-*`, `--zx-tone-*`) or raw
126
+ color literals in application/component CSS. Prefer the least-specific override that works (global
127
+ token → then, if needed, a component style).
103
128
 
104
129
  **Print.** `zx.css` imports `styles/print.css` last. It releases sticky headers and scroll
105
130
  containers, drops fills, shadows, zebra striping and focus rings, hides overlays and navigation
@@ -108,6 +133,20 @@ URLs in prose but never inside a table cell or on a button. Status badges keep t
108
133
  because there it carries meaning beside the word. An application that pins `data-zx-theme="dark"`
109
134
  still prints the dark token set; `auto` resolves to light under `@media print` and is unaffected.
110
135
 
136
+ **Motion.** Motion is a setting, beside the theme: put `data-zx-motion="reduced"` on `<html>` (or
137
+ on any region) to stop movement inside it. There, every `--zx-dur-*` token is `0ms` and a base rule
138
+ collapses CSS animations and transitions to their end state — menus still open, toasts still leave,
139
+ a spinner stops turning. The operating system's reduced-motion preference does the same for the
140
+ whole page. There is no value that turns motion back on: someone who asked their system for less
141
+ motion gets less, whatever an application or region says. Apply the attribute before first paint
142
+ (the documentation site stores it with the theme). Code that animates in script asks
143
+ `prefersReducedMotion(element?)` — true under the system preference or inside a reduced region,
144
+ across shadow roots — and subscribes with `watchMotion(element, (reduced) => …, {signal, immediate})`,
145
+ which returns a disposer; Chart already does. Name motion with the curves rather than inventing
146
+ them: `--zx-ease` for state changes, `--zx-ease-spring` (a small overshoot) for things that pop in,
147
+ `--zx-ease-drawer` (a decisive ease-out) for panels that slide, as Sheet does; and lift an item
148
+ being dragged with `--zx-drag-shadow`, as KanbanView's drag preview and keyboard-grabbed card do.
149
+
111
150
  ## Talking to ZeyOS — use `@zeyos/client`
112
151
 
113
152
  For ZeyOS business data use the **dedicated client library** `@zeyos/client`
@@ -182,18 +221,34 @@ await records.saveView({ name: 'My open invoices', setDefault: true });
182
221
  operation receives a query built by `buildListQuery`.
183
222
  - `zeyosForm(client, resource, opts)` returns
184
223
  `{form, ready, load(id), save(), getForm(), destroy()}`. `fields` is an ordered allow-list;
185
- `exclude`, `labels`, `title`, `columns`, `value|id`, and `onSaved` curate behavior. `load` maps
186
- stored timestamps to `Date`; `save` validates and creates/updates, maps back to Unix seconds,
187
- calls `onSaved`, and shows a success toast.
224
+ `exclude`, `labels`, `formats`, `timeZone`, `title`, `columns`, `value|id`, and `onSaved` curate
225
+ behavior. `load` maps stored timestamps to `Date`; `save` validates and creates/updates, maps back
226
+ to Unix seconds, calls `onSaved`, and shows a success toast. Its toasts (that confirmation and a
227
+ failed request's report) appear in the theme scope of `scope` — default: the form's own element,
228
+ so a form in a scoped dialog toasts inside that scope, and the confirmation stays there when
229
+ `onSaved` closes and destroys the dialog; `notify: false` raises none, leaving the
230
+ promise's result or rejection to the caller.
188
231
  - `zeyosTable(client, resource, opts)` returns
189
- `{table, load(), setSearch(), setFilters(), loadMore(), count, page, hasMore, destroy()}`. Columns
190
- come from schema metadata, sorting uses `sortMode:'server'`, and page zero replaces rows while
191
- later pages append them using server `limit`/`offset` and `count`.
232
+ `{table, load(), setSearch(), setFilters(), loadMore(), count, page, pageSize, hasMore, destroy()}`.
233
+ Columns come from schema metadata, sorting uses `sortMode:'server'`, and paging is zero-based with
234
+ `pageSize: 50` by default: page zero replaces rows while later pages append them using server
235
+ `limit`/`offset` and `count`; `load({page, pageSize, replace: true})` replaces at any page. A
236
+ one-based `Pagination` maps onto it as `load({page: detail.page - 1, pageSize: detail.pageSize,
237
+ replace: true})` (offset `(page - 1) * pageSize`), feeding `result.count` back through
238
+ `setTotal()`. A response that resolves after `destroy()` is ignored and later loads reject; page
239
+ must be a non-negative integer and page size a positive integer (`RangeError`), filters an object
240
+ (`TypeError`), and every argument is checked before any is applied, so a rejected call changes
241
+ nothing. Table presentation options and
242
+ listeners pass through — `variant`, `zebra`, `stickyHeader`, `responsive`, `growing`, `emptyText`,
243
+ `rowClass`, `rowKind(s)`, `columnVisibility`, `columnReorder`, `hiddenColumns`, `on<event>`;
244
+ `onrowclick` runs beside `onRowClick`. The binding owns `columns`, `data`, and `sortMode`, and
245
+ leaves out editing, `rowReorder`, and `hierarchy`, which change rows it never persists.
192
246
  - `buildZeyosViewConfig(client, resource, opts)` resolves one schema to shared `ViewField[]`, a
193
247
  complete projection, and view options. `zeyosView(client, resource, CardView|TableView|KanbanView,
194
248
  opts)` returns `{view, load(), setSearch(), setFilters(), loadMore(), count, page, hasMore,
195
249
  savedViews, ready, restoreDefault(), captureView(), saveView(), applyView(), destroy()}` with
196
- paging and server sort. Title, preview, group, column, and swim-lane fields stay in the projection
250
+ paging and server sort. Paging is the same loader as `zeyosTable()` — same `pageSize: 50` default,
251
+ validation, `replace`/`pageSize` load options, and destroy guard. Title, preview, group, column, and swim-lane fields stay in the projection
197
252
  even when hidden. Without `savedViews`, the host may persist `view.getViewState()` directly.
198
253
  - `createSavedViewRegistry(transport, {userId,workspaceId,resource})` coordinates private named
199
254
  Table/Card/Kanban configurations as one versioned, atomically replaced document. Entries capture
@@ -206,7 +261,11 @@ await records.saveView({ name: 'My open invoices', setDefault: true });
206
261
  scope's `userId` is intentionally not sent to the endpoint; the server session remains authority.
207
262
 
208
263
  Field mapping is schema-driven: entity/FK → async `zxselect`; enum/list → `optionlist` or native
209
- `select`; indexed `date` bigint → `date`; other date/time bigints → `datetime`; money/price/numeric
264
+ `select` with readable labels (a schema `{value, label}` as given, a stored constant in sentence
265
+ case — `INVOICED` → "Invoiced", `PARTLYPAID` → "Partly paid"); date-only bigints (indexed `date`,
266
+ `duedate`, `birthdate`, `datefrom`/`dateto` outside appointments and events, `datecancel`,
267
+ `lastbilling`) → `date`; other date/time bigints and `lastmodified` → `datetime`;
268
+ `formats: { field: 'date' }` overrides the guess; money/price/numeric
210
269
  → right-aligned `float`/number columns (amount columns use the row currency); boolean/checked →
211
270
  `toggle` and a check column; percent/progress → numeric percent; email/tel/url → typed text; arrays
212
271
  → `valuelist`; long text/JSON → `textarea`; short scalar text → `text`.
@@ -214,8 +273,15 @@ Field mapping is schema-driven: entity/FK → async `zxselect`; enum/list → `o
214
273
  ZeyOS queries always use `filters` (plural), not `filter`. Full-text input becomes `query`; sort is
215
274
  sent as signed server fields (`+field`/`-field`); projection supports aliases and dot joins; and
216
275
  search/filter/sort/pagination remain server-side. ZeyOS timestamps are integer Unix seconds, so
217
- forms convert them to/from `Date`. Use `dataFilterStateToFilters(state, defs)` before
218
- `tableBinding.setFilters(...)` when wiring a `DataFilter`.
276
+ forms convert them to/from `Date`. A date-only field stores midnight of a calendar day in the
277
+ instance's zone — a day, not an instant: tables show it with no time, and forms edit it as that day,
278
+ so neither the viewer's zone nor daylight saving moves it. Pass the instance's IANA zone as
279
+ `timeZone` (table, form, view; default: the runtime's own); `unixSecondsToCalendarDay(seconds, tz)`
280
+ and `calendarDayToUnixSeconds(date, tz)` do the same conversion for application code. Use
281
+ `dataFilterStateToFilters(state, defs, {timeZone})` before `tableBinding.setFilters(...)` when wiring
282
+ a `DataFilter`: with `timeZone`, a `format: 'date'` value (`Date` or `YYYY-MM-DD`) becomes that
283
+ day's midnight in the zone, as forms store it, while `datetime` values stay instants; without it,
284
+ dates convert as exact instants. `FilterPanel` takes the same `timeZone` option.
219
285
 
220
286
  ### Module icons and colours
221
287
 
@@ -253,6 +319,41 @@ registerModules({ tickets: '#f04639', 'my-fork': { label: 'My Fork', icon: 'zeyo
253
319
  throw.
254
320
  - `registerModules(map)` adds forks and weblets or overrides shipped entries (a bare string sets
255
321
  the colour only). Runtime ZeyOS menu data is authoritative: feed its colours in at startup.
322
+
323
+ ### Module bar from the ZeyOS menu
324
+
325
+ The module bar and the module-layout sidebar read the same shell state the launcher does, plus
326
+ each module's menu and its counts:
327
+
328
+ ```js
329
+ import { buildZeyosAppSidebarConfig, buildZeyosModuleBarConfig } from '/assets/zx-zeyos.esm.js';
330
+ import { AppSidebar, ModuleBar } from '/assets/zx.esm.js';
331
+
332
+ const state = {
333
+ modules: ['messages', 'tasks', 'billing', 'contacts', 'calendar', 'tickets'],
334
+ pinned: ['messages', 'tasks', 'billing', 'tickets'], // the rest are under "All modules"
335
+ activeIdentifier: 'tickets',
336
+ active: 'tickets-mine', // a menu entry, when more precise
337
+ canUseModule: (module) => permissions.has(module),
338
+ menus: { tickets: [{ id: 'tickets-mine', label: 'My tickets' }, ['tickets-all', 'All tickets', '#tickets']] },
339
+ badges: { messages: 7, 'tickets-mine': 3 },
340
+ resolveApplication: (app) => ({ href: routeFor(app) })
341
+ };
342
+ new AppSidebar(nav, buildZeyosAppSidebarConfig(state, { onselect: route }));
343
+ new ModuleBar(header, buildZeyosModuleBarConfig(state, { orientation: 'horizontal', side: 'top' }));
344
+ ```
345
+
346
+ - `zeyosModuleItems(state, {standardIcons})` → `ModuleItem[]`: every permitted module, fork, and
347
+ weblet, with its kit icon (or `renderApplicationIcon(app, normalized)`), its colour — the
348
+ payload's, else the registry's, so `registerModules()` reaches the bar — its menu from
349
+ `menus[identifier]` (objects or `[id, label, href]` arrays, sections, nesting; unsafe hrefs are
350
+ disabled), its badge from `badges`, `pinned` from the pinned order, and forks and weblets in
351
+ their own catalogue group (`labels.forks`, `labels.weblets`). A module with a menu is a panel
352
+ module; one without is a destination.
353
+ - `buildZeyosModuleBarConfig(state, options)` → ModuleBar options; `buildZeyosAppSidebarConfig()`
354
+ → AppSidebar options in `layout: 'modules'`. Both draw the classic ZeyOS identity tiles
355
+ (`appearance: 'solid'`) unless told otherwise. `zeyosModuleBar(target, state, options)` creates
356
+ the bar directly.
256
357
  <!-- /doc -->
257
358
 
258
359
  ## Custom elements
@@ -282,19 +383,36 @@ accept.
282
383
  `aria-keyshortcuts`. The host binds the action and owns editing/modality/permission guards;
283
384
  a hint never installs a global handler. Dialog and Sheet footer descriptors accept it too.
284
385
  - **`buttonGroup([button(…), …])`** → a container that joins the buttons into one control.
386
+ - **Appearance** — `primary` is the screen's one solid accent and reads the
387
+ `--zx-color-accent-solid*` roles, so in dark it keeps the deep stop with white ink rather than a
388
+ bright slab. A `ghost` button's icon is drawn in `--zx-color-text-subtle` until hovered. A button
389
+ with `aria-pressed="true"` (a Toolbar item with `active`, a view toggle) is a mode, not a pick:
390
+ it takes the neutral `--zx-color-bg-current` fill and the text colour, never the accent.
285
391
  <!-- /doc -->
286
392
 
287
393
  <!-- doc:badge -->
288
394
  ### Badge — `badge(options)` / `badgeGroup(badges)`
289
395
 
290
396
  Factories (not classes) returning a status pill: a small, non-interactive `<span>` carrying a
291
- semantic colour, used for stages, counts, tags, and record-header metadata. A badge with no label
292
- but a `title` is named for assistive technology through `role="img"`; everything else is plain text
293
- and needs no ARIA.
397
+ semantic colour — or, with `tone`, a categorical one — used for stages, counts, tags, and
398
+ record-header metadata. A badge with no label but a `title` is named for assistive technology
399
+ through `role="img"`; everything else is plain text and needs no ARIA.
294
400
 
295
401
  - **Options** — `label`, `icon` (an icon name, rendered before the label), `kind:
296
- 'neutral'|'accent'|'success'|'warning'|'danger'|'info'`, `variant: 'soft'|'solid'|'outline'`,
297
- `size: 'md'|'sm'`, `dot` (a leading status dot instead of an icon), `title`.
402
+ 'neutral'|'accent'|'success'|'warning'|'danger'|'info'`, `tone:
403
+ 'blue'|'teal'|'green'|'amber'|'orange'|'red'|'pink'|'violet'|'slate'|null`, `variant:
404
+ 'soft'|'solid'|'outline'`, `size: 'md'|'sm'`, `dot` (a leading status dot instead of an icon),
405
+ `title`.
406
+ - **`kind` vs `tone`** — `kind` says how something is doing (status); `tone` says what it is (a
407
+ project, a label, a module) and draws from the `--zx-color-tone-*` palette, which is neither the
408
+ accent nor the status set, so a project tagged red never reads as an error. When `tone` is set it
409
+ decides the colour and `kind` is ignored for it; every `variant` still applies (soft: tinted,
410
+ borderless; solid: filled with the tone's ink; outline). The badge carries `data-tone`. Badges
411
+ are medium weight and sentence case.
412
+ - **Neutral and small** — the neutral soft fill is a wash of the text colour, not an opaque surface
413
+ step, so it stays visible on raised surfaces in the dark theme. `size: 'sm'` fits table cells
414
+ and board cards; a small badge with `dot` keeps the 12px text and a full spacing step between
415
+ dot and label.
298
416
  - **Methods** — none. `badge(options)` → `HTMLSpanElement`, `badgeGroup([badge(…), …])` → a wrapping
299
417
  inline row with its own spacing; you own both elements.
300
418
  - **Events** — none; a badge is not interactive.
@@ -317,8 +435,12 @@ a `MenuButton`, where choosing one activates the original control.
317
435
  `setActive(name, active?)`, `destroy()`.
318
436
  - **Events** — `action {name, item}`, emitted for descriptor-built items in addition to their own
319
437
  `onclick`, from the row and from the overflow menu alike.
320
- - **Keyboard** — Tab reaches the toolbar once; ArrowLeft/ArrowRight move between controls and wrap;
321
- Home/End jump to the first and last control.
438
+ - **Keyboard** — Tab reaches the toolbar once; ArrowLeft/ArrowRight move between controls and wrap,
439
+ in visual order under `dir="rtl"`; Home/End jump to the first and last control. A text field, select,
440
+ or contenteditable item keeps its own keys: Home/End and the arrows move its caret, and an arrow
441
+ moves on to the neighbouring control only once the caret is at that edge of the text.
442
+ - **Pressed items** — an item with `active` renders `aria-pressed="true"` and takes the neutral
443
+ `--zx-color-bg-current` fill, never the accent.
322
444
  <!-- /doc -->
323
445
 
324
446
  <!-- doc:empty-state -->
@@ -413,7 +535,8 @@ shape is genuinely known. All three are factories returning elements.
413
535
 
414
536
  Two-state pressed button on a native `<button aria-pressed>`. The check indicator has a glyph in
415
537
  both states — an empty box when off, a check when on — so an unpressed or disabled button still
416
- reads as a two-state control.
538
+ reads as a two-state control. Pressed, the button goes neutral (the `--zx-color-bg-current` fill,
539
+ a stronger border, the text colour); only the check glyph, the mark, keeps the accent.
417
540
 
418
541
  - **Options** — `label: '' | [onLabel, offLabel]`, `checked: false`, `icon: true`,
419
542
  `disabled: false`.
@@ -428,6 +551,8 @@ reads as a two-state control.
428
551
  Switch (`role="switch"`, `aria-checked`).
429
552
 
430
553
  - **Options** — `checked: false`, `value: true`, `label: null`, `disabled: false`.
554
+ - **Label** — start-aligned and wrapping like text within the available width, the track level
555
+ with its first line.
431
556
  - **Methods** — `get()`, `set(checked, {silent})`, `toggle()`, `getValue()` (returns
432
557
  `options.value` when on, `false` when off), `enable()`/`disable()`.
433
558
  - **Events** — `change {checked, value}`.
@@ -452,21 +577,40 @@ Zx owns ranking, focus, request cancellation, and presentation; ZeyOS owns the c
452
577
  permissions, recent-history/cache policy, and the route or command ultimately invoked.
453
578
 
454
579
  - **Constructor** — `new Launcher(null, options)` owns a top-layer dialog in the opener's nearest
455
- Zx theme scope;
580
+ Zx theme scope — never inside another open dialog, which it stacks above in the top layer. Open,
581
+ it is a modal on the shared overlay stack: Esc and presses inside it reach no popup left open
582
+ behind it, and toasts move into it while it is open;
456
583
  `new Launcher(existingDialog, options)` enhances and later restores an existing `<dialog>`.
457
584
  - **Options** — `items`, abortable `sources: [{id, label?, minQuery?, when?, order?, load(query, {signal})}]`,
458
585
  `query`, `debounce: 250`, `minQuery: 0`, `maxResults: 100`, `placeholder`, `label`, `emptyText`,
459
- `loadingText`, `shortcut: 'mod+k'|false`, visible `hints` labels (or `false`), and `scope` to
460
- explicitly select an element whose nearest Zx theme scope owns the launcher.
461
- - **Items** — `{id, label, description?, keywords?, group?, icon?, badge?, value?, href?, target?,
462
- invoke?, pinned?, disabled?, kind?, current?, when?, groupOrder?, itemOrder?}`. `icon` accepts a
463
- bundled icon name, Node, or factory. Links stay native so modified and middle clicks work;
586
+ `loadingText`, `shortcut: 'mod+k'|false`, visible `hints` labels `{move, open, back, close}` (or
587
+ `false`), and `scope` to explicitly select an element whose nearest Zx theme scope owns the launcher.
588
+ - **Items** — `{id, label, description?, keywords?, group?, icon?, badge?, hint?, shortcut?, keys?,
589
+ value?, href?, target?, invoke?, pinned?, disabled?, kind?, current?, when?, groupOrder?,
590
+ itemOrder?, items?, placeholder?, order?}`. `icon` accepts a bundled icon name, Node, or
591
+ factory. Links stay native so modified and middle clicks work;
464
592
  non-link commands may provide an application-owned `invoke` callback. Malformed and executable
465
- URL schemes are stripped; a descriptor with no safe link or callback is disabled.
466
- - **Methods** — `open()`, `close()`, `toggle()`, `isOpen()`, `focus()`, `setQuery()`, `getQuery()`,
467
- `setItems()`, `setCurrent()`, `setSources()`, `destroy()`.
468
- - **Events** — cancelable `select {item, value, source, query}`, `query {query}`, `open`, `close`,
469
- and `error {source, error}`. Preventing `select` suppresses the supplied link or callback.
593
+ URL schemes are stripped; a descriptor with no safe link or callback is disabled. `hint` is muted
594
+ text at the end of the row (a current value, a project); `shortcut: ['⌘', 'K']` (or Button's
595
+ `{label, keys}`) draws key caps for display only, and `keys: 'Meta+K'` adds `aria-keyshortcuts`.
596
+ - **Pages** — an item with `items` opens a page instead of being selected: an array, or a loader
597
+ `(query, {signal}) => items | Promise<items>` that runs when the page opens and, debounced, as
598
+ its query changes (the loading and empty states show meanwhile; `order: 'source'` keeps the
599
+ loader's order instead of ranking it). Opening a page clears the query, shows a scope chip with
600
+ the item's label before the input — part of the combobox's name, `Search <label>` — and adds
601
+ "⌫ Back" to the footer; `placeholder` sets the page's placeholder. Page rows show a chevron.
602
+ Backspace on an empty query, ArrowLeft on an empty query, or the chip's × goes back one page,
603
+ restoring the query and highlighted row beneath; Escape closes from any page, and the next
604
+ opening starts on the root. Pages nest. `open({page})` opens straight into a page — a local page
605
+ id (at any depth of array pages, the path becoming the back stack) or a page item. A polite
606
+ status line announces each change ("Agents, 7 results").
607
+ - **Methods** — `open({page}?)`, `close()`, `toggle()`, `isOpen()`, `focus()`, `setQuery()`,
608
+ `getQuery()`, `back()`, `getPage()`, `setItems()` (the root's items), `setCurrent()`,
609
+ `setSources()`, `destroy()`.
610
+ - **Events** — cancelable `select {item, value, source, query, page}`, `query {query}`,
611
+ `page {page, path}` (after every page change; `page` is null on the root), `open`, `close`, and
612
+ `error {source, error, page}` (a failing page loader reports as source `page:<id>`). Preventing
613
+ `select` suppresses the supplied link or callback.
470
614
  - **Ranking** — local results are case/diacritic-insensitive and deterministic: exact label,
471
615
  label prefix, word prefix, label substring, identifier substring, then acronym prefix. Pinned
472
616
  items retain an explicit order ahead of otherwise equal results. A new async query aborts and
@@ -474,13 +618,18 @@ permissions, recent-history/cache policy, and the route or command ultimately in
474
618
  - **Keyboard** — Cmd+K on macOS or Ctrl+K elsewhere opens by default; the shortcut is ignored in
475
619
  editable controls, composition, and modal contexts. Application tiles use spatial arrow-key
476
620
  movement; Up/Down may continue into record groups. Home/End, Page Up/Down, and Enter operate the
477
- single listbox selection model; Escape closes and restores focus. `aria-current` marks the
478
- active destination independently from the keyboard's `aria-selected` option.
621
+ single listbox selection model; Escape closes and restores focus. Enter, or ArrowRight with the
622
+ caret at the end of the query, opens a page row; Backspace or ArrowLeft on an empty query goes
623
+ back. `aria-current` marks the active destination independently from the keyboard's
624
+ `aria-selected` option.
479
625
  - **Optional ZeyOS adapter** — import `zeyosLauncher()` or `buildZeyosLauncherConfig()` from
480
626
  `@zeyos/zx/zeyos`. The adapter receives the permitted module/menu catalogue, pins, current
481
627
  identifier, recent loader or cache, abortable search, destination resolvers, and application/
482
628
  record icon renderers explicitly. It maps the complete shell payload without reading globals or
483
629
  taking over application routing.
630
+ - **Appearance** — group labels are sentence-case captions. The application you are in
631
+ (`aria-current="page"`) takes the neutral `--zx-color-bg-current` fill and a semibold label; the
632
+ highlighted row is the lighter hover step. Neither uses the accent.
484
633
  <!-- /doc -->
485
634
 
486
635
  <!-- doc:app-icon -->
@@ -490,7 +639,9 @@ A fixed-size, product-agnostic application identity. It keeps glyph geometry sta
490
639
  sidebars, entity results, and recent-item rows while identity colour and material remain explicit
491
640
  options rather than application globals.
492
641
 
493
- - **Options** — `icon` (bundled name, Node, or lazy renderer), `color`, `size: 36`,
642
+ - **Options** — `icon` (bundled name, Node, or lazy renderer), `color` (any CSS colour) or `tone`
643
+ (one of the nine categorical tones, drawn with its `--zx-color-tone-<tone>-solid` fill; `color`
644
+ wins when both are given), `size: 36`,
494
645
  `iconSize: '52%'`, `label`, `badge`, `selected: false`, `shape: 'tile'|'circle'`,
495
646
  `glass: false|'subtle'|'strong'`, and
496
647
  additional `class` names.
@@ -519,13 +670,20 @@ options rather than application globals.
519
670
  <!-- doc:avatar -->
520
671
  ### Avatar
521
672
 
522
- A fixed-geometry user image with deterministic initials fallback. Image loading or failure never
523
- changes layout, and the component is decorative by default so adjacent identity text is not
524
- announced twice.
525
-
526
- - **Options** — `src`, `name`, `initials`, `label`, `size: 'sm'|'md'|'lg'|<pixels>`,
527
- `shape: 'circle'|'rounded'|'square'`, `status: 'online'|'away'|'busy'|'offline'|null`, and
528
- `statusLabel`.
673
+ A fixed-geometry user image with a deterministic fallback: initials on a tint from the categorical
674
+ tone palette, picked by a stable hash of the name. Image loading or failure never changes layout,
675
+ and the component is decorative by default so adjacent identity text is not announced twice.
676
+
677
+ - **Options** — `src`, `name`, `initials`, `label`, `size: 'sm'|'md'|'lg'|<pixels>` (24 / 32 /
678
+ 40px at the default spacing unit), `shape: 'circle'|'rounded'|'square'`, `tone:
679
+ 'blue'|'teal'|'green'|'amber'|'orange'|'red'|'pink'|'violet'|'slate'|null`, `status:
680
+ 'online'|'away'|'busy'|'offline'|null`, and `statusLabel`.
681
+ - **Fallback** — no ring; the `--zx-color-tone-*-bg` fill with its `-fg` ink, initials at 0.4× the
682
+ size (never under 10px), weight 600, one initial below 24px. `tone: null` derives the tone with
683
+ `avatarTone(name)` (exported from the module): FNV-1a over the trimmed, NFC-normalised,
684
+ lower-cased name, mapped onto the eight chromatic tones, so a person keeps their colour on every
685
+ screen. `slate`, the neutral, is reserved for an avatar with no name and can be set explicitly.
686
+ The root carries `data-tone`. `avatarInitials(name, limit = 2)` returns the initials.
529
687
  - **Methods** — `set(values)`, `setStatus(status, label?)`, `hasImage()`, `destroy()`.
530
688
  - **Accessibility** — set `label` only when the avatar stands alone. Presence is supplementary
531
689
  status, not the account's accessible name; supply `statusLabel` when it conveys information.
@@ -563,10 +721,11 @@ A durable or transient file row composed from `EntityRef`, MIME/byte-size metada
563
721
  status, and accessible progress. It never starts a transfer, decides authorization, resolves a
564
722
  preview, or persists a file.
565
723
 
566
- - **Options** — `id`, `name`, `size` (bytes), `mime`, `subtitle`, `icon: 'file'`, additional
724
+ - **Options** — `id`, `name`, `size` (bytes), `mime`, `typeLabel: null`, `subtitle`,
725
+ `icon: 'file'`, additional
567
726
  `metadata`, safe native `link`, `actions`, status (`ready` | `temporary` | `waiting` |
568
727
  `uploading` | `processing` | `success` | `error`), `statusLabel`, `progress` (0–100), `indeterminate`,
569
- `density: 'sm'|'md'|'lg'`, `wrap`, `actionsLabel`, and `locale`.
728
+ `identitySize: 'sm'|'md'|'lg'`, `wrap`, `actionsLabel`, and `locale`.
570
729
  - **Methods** — `set(values)`, `setStatus(status,label?)`,
571
730
  `setProgress(percent|null,{indeterminate?})`, `getFile()`, `focus()`, and `destroy()`.
572
731
  - **Events** — cancelable `activate {id,file,href,event}` and
@@ -574,6 +733,11 @@ preview, or persists a file.
574
733
  suppresses callbacks and navigation rather than merely observing them.
575
734
  - **Progress** — determinate work exposes min/max/now and percentage text. Indeterminate work
576
735
  omits `aria-valuenow`; status changes are announced separately.
736
+ - **Type** — the MIME type is shown as a readable type ("PDF", "CSV", "Image", "Spreadsheet"),
737
+ falling back to the name's extension ("EXE") when it is unfamiliar and never to the raw string;
738
+ `typeLabel` overrides it and `''` hides it. The default `icon: 'file'` gives way to a glyph for
739
+ the MIME type (`file-pdf`, `file-image`, …) when the active icon set has one — Font Awesome, or
740
+ a glyph added with `registerIcons()`; any other `icon` is used as given.
577
741
  <!-- /doc -->
578
742
 
579
743
  <!-- doc:file-list -->
@@ -590,6 +754,11 @@ Explicit duplicate file IDs are rejected; source order is preserved.
590
754
  - **Events** — cancelable `activate {id,index,file,href,event}` and
591
755
  `action {id,fileId,index,action,file,event}`, plus `datachange {items}` after a collection
592
756
  mutation. Returned items are defensive snapshots.
757
+ - **Updates in place** — mutations are reconciled by file `id`: unchanged rows are left alone and a
758
+ changed one is updated through `FileItem.set()`, so upload progress can tick through
759
+ `updateItem(id, {progress})` while focus (say, on that file's Cancel action) and the scroll
760
+ position stay put. Removing the focused file moves focus to the file that takes its place. Files
761
+ without an `id` are rebuilt on every mutation.
593
762
  <!-- /doc -->
594
763
 
595
764
  <!-- doc:activity-item -->
@@ -620,13 +789,20 @@ incremental host-controlled mutation. It deliberately preserves source order ins
620
789
  whether a feed is newest- or oldest-first.
621
790
 
622
791
  - **Options** — `items`, `groupBy` (property, resolver, or null), `groupLabel`,
623
- `label: 'Activity'`, `loading`, `loadingCount: 3`, `loadingText`, `emptyText`,
624
- `groupHeadingLevel`, and `itemHeadingLevel`.
792
+ `label: 'Activity'`, `variant: 'default'|'compact'`, `loading`, `loadingCount: 3`,
793
+ `loadingText`, `emptyText`, `groupHeadingLevel`, and `itemHeadingLevel`.
794
+ - **Compact** — `variant: 'compact'` (reflected as `data-variant`) keeps every entry on one line at
795
+ any width, for a summary card narrower than 30rem: actor and title read as one sentence that
796
+ wraps as text, the time keeps the trailing end instead of stacking under the title, an avatar
797
+ descriptor without a `size` is small, and metadata follows as a muted second line.
625
798
  - **Methods** — `setItems(items,{silent?})`, `getItems()`, `getItem(id)`,
626
799
  `appendItems(items,{silent?})`, `prependItems(items,{silent?})`,
627
800
  `updateItem(id,values,{silent?})`, `removeItem(id,{silent?})`, `setLoading()`, and `destroy()`.
628
801
  - **Events** — `datachange {items}` after mutation. Each child's cancelable `zx-action` bubbles
629
802
  naturally through the list for delegated application handling.
803
+ - **Focus** — entries are regrouped on every mutation; focus on an entry's control returns to the
804
+ same control of the same activity (matched by `id`, else position), so a feed prepending live
805
+ entries does not drop the reader to `<body>`.
630
806
  - **Grouping** — groups keep first-seen group order and item source order. The exported pure
631
807
  `groupActivityItems()` and `normalizeActivityItems()` helpers do not mutate caller arrays.
632
808
  <!-- /doc -->
@@ -640,13 +816,59 @@ out, or mutates the session.
640
816
 
641
817
  - **Options** — `account: {name, secondary, src?, initials?, status?, statusLabel?}`, `items`,
642
818
  `compact: false`, `label`, and any top/bottom/left/right `placement`.
643
- - **Methods** — `open()`, `close()`, `toggle()`, `isOpen()`, `setAccount()`, `setItems()`, `focus()`,
819
+ - **Methods** — `open()`, `close()`, `toggle()`, `isOpen()`, `setAccount()`, `setItems()`,
820
+ `setCompact(compact)` (the avatar alone, or with the name and secondary line — for a navigation
821
+ column that narrows and widens), `focus()`,
644
822
  `getPanel()`, `destroy()`.
645
823
  - **Events** — cancelable `select {value, item}`, `open`, and `close`. A canceled selection does
646
824
  not run the item's callback and leaves the menu open, so an application can reject or confirm an
647
825
  action without rebuilding the account surface.
648
826
  - **Accessibility** — compact mode is visually avatar-only but keeps the full account-menu name on
649
827
  its single native trigger; MenuButton retains focus return, typeahead, and arrow-key behavior.
828
+ - **Appearance** — the trigger is a flat row (avatar, name, secondary line, chevron) with the hover
829
+ fill, not a button: it takes back the border, glass fill, blur and shadow MenuButton's `.zx-btn`
830
+ would give it, whatever order the stylesheets load in.
831
+ <!-- /doc -->
832
+
833
+ <!-- doc:workspace-switcher -->
834
+ ### WorkspaceSwitcher
835
+
836
+ Composes `AppIcon` and `MenuButton` into the workspace switcher at the head of a navigation column:
837
+ a trigger with the current workspace's tile, name and secondary line, opening an APG menu of the
838
+ workspaces with the current one checked, then the application's own actions. Zx never decides
839
+ which workspaces a user may see, loads another workspace, or changes the session — switching is a
840
+ cancelable event the application acts on.
841
+
842
+ - **Options** — `workspaces: [{id, label, secondary?, icon?, tone?, color?, src?, initials?, badge?,
843
+ disabled?}]` (the tile is the image, else the icon, else the initial, on the tone — derived from
844
+ the label when neither `tone` nor `color` is given), `active` (defaults to the first enabled
845
+ workspace, which also replaces an id that is not listed or is disabled), `items` (actions after
846
+ the workspaces; the separator between them is added for you), `compact: false`, `heading` (null
847
+ resolves message `workspaceSwitcher.heading`, "Workspaces"; `''` leaves it out), `label` (`%1` is
848
+ the active workspace's name, filled in again after every switch; null resolves
849
+ `workspaceSwitcher.label`, "Switch workspace: %1", or `workspaceSwitcher.labelEmpty`, "Switch
850
+ workspace", while no workspace is active), and any `placement` (default `bottom-start`).
851
+ - **Messages** — `workspaceSwitcher.heading`, `workspaceSwitcher.label` (`%1` the workspace name),
852
+ `workspaceSwitcher.labelEmpty`; override them per instance through `msg`, or for every instance
853
+ through `setTranslator()`.
854
+ - **Methods** — `getActive()`, `setActive(id, {silent = true})` (`silent: false` emits `select`
855
+ first), `setWorkspaces(list)` (keeps the active workspace while it is still listed and enabled,
856
+ else the first enabled one becomes active), `setItems()`,
857
+ `setCompact(compact)`, `open()`, `close()`, `toggle()`, `isOpen()`, `focus()`, `getPanel()`,
858
+ `destroy()`.
859
+ - **Events** — cancelable `select {workspace, id, previous}`: a canceled switch keeps the previous
860
+ workspace active and the menu open. Cancelable `action {value, item}` for the extra items: a
861
+ canceled action does not run the item's callback. Both honour `preventDefault()` on the component
862
+ event and on the bubbling `zx-select` / `zx-action` DOM event alike. `open` and `close`. Choosing the current
863
+ workspace again only closes the menu.
864
+ - **Semantics** — one native `<button aria-haspopup="menu" aria-expanded>` trigger, named "Switch
865
+ workspace: <name>" (or the custom `label`) in both states; compact mode shows the tile alone and adds the name as its
866
+ `title`. Workspaces are `menuitemradio` rows with `aria-checked` under a presentational heading;
867
+ MenuButton keeps focus return, typeahead and the arrow-key map.
868
+ - **Appearance** — the trigger is a flat full-width row (tile, name, secondary line, chevron) with
869
+ the hover fill, at least a control and a space-2 tall; its tile is the module bar's tile size, so
870
+ as an `AppSidebar` `railHeader` it lines up with the module tiles under it. Keep it in step with a
871
+ modules sidebar by calling `setCompact(presentation !== 'expanded')` on `presentationchange`.
650
872
  <!-- /doc -->
651
873
 
652
874
  <!-- doc:select -->
@@ -661,8 +883,9 @@ Unifies single-select, local filtering, and async loading.
661
883
  `clearable: false`, `filter: false | 'local' | async (query)=>items`, `searchKeys`,
662
884
  `minQuery: 0`, `debounce: 200`, `listHeight: 280`, `groupKey`.
663
885
  - **Getters** — `.value`, `.selected`.
664
- - **Methods** — `set(id, {silent})`, `setItems()`, `setFixedItems()`, `setActions()`, `reset()`,
665
- `open()`/`close()`, `enable()`/`disable()`, `focus()`.
886
+ - **Methods** — `get()` (the same as `.value`), `set(id, {silent})`, `setItems()`,
887
+ `setFixedItems()`, `setActions()`, `reset()`, `open()`/`close()`, `enable()`/`disable()`,
888
+ `focus()`.
666
889
  - **Events** — `change {value, item}` (`item` is null on clear), preventable
667
890
  `action {id, action, query, select}`, `open`, `close`, `query {query}`, `loaded {items}`.
668
891
  - **Fixed choices** — `fixedItems` pins entries to the top of the list, separated by a rule (the
@@ -671,13 +894,33 @@ Unifies single-select, local filtering, and async loading.
671
894
  entries are narrowed locally against the same query and stay resolvable by `set()`. They survive
672
895
  `setItems()` and are shown below `minQuery`. Use them for choices with no row in the source —
673
896
  "Unassigned", "Everyone", "Private".
674
- - **Presets** — `Select.priority(target, opts)` renders the five-level, five-square ZeyOS priority
675
- scale; `Select.status(target, opts)` supplies shape- and semantic-color-distinct workflow states
676
- and accepts replacement `items`; `Select.permission(target, {groups, value})` pins Private and
897
+ - **Presets** — `Select.priority(target, opts)` renders the five-level ZeyOS priority scale (values
898
+ 0–4) as grey signal bars filled to the level; only the highest is coloured, a danger square with
899
+ "!". A "No priority" row (value null, message `priority.none`) heads the list and is the empty
900
+ control's placeholder; `clearable: false` removes it, as does a `required` `zxselect` field with
901
+ `preset: 'priority'`. `Select.status(target, opts)` supplies shape- and semantic-color-distinct workflow states
902
+ and accepts replacement `items` (each `{ID, name, indicator, tone, description?, shortcut?}`);
903
+ `Select.permission(target, {groups, value})` pins Private and
677
904
  Public above record-access groups. Its value is the tri-state ZeyOS stores (`false` private,
678
905
  `true` public, or a group ID), and `groups` takes an array or loader `(query)=>items`.
906
+ - **Glyphs** — `statusIcon(status, {tone?, indicator?, label?})` and `priorityIcon(level, {label?})`
907
+ return the exact glyphs the two presets draw (`.zx-status-icon`, `.zx-priority-icon`; inside a
908
+ Select they also carry `.zx-select__status-indicator` / `.zx-select__priority-icon`), for list
909
+ rows, board columns, filter chips, and headings. Status ids: `backlog` (dotted ring), `todo`
910
+ (empty ring), `in-progress` (ring with a centre dot), `in-review` or `review` (half filled),
911
+ `ready` (a third filled), `waiting` (two thirds filled), `done` (filled, check), `canceled` or
912
+ `cancelled` (filled, cross), `duplicate` (ring with a slash); a `Select.status` item object is
913
+ accepted too, and an unknown id draws a muted empty ring. Indicators: `dotted`, `ring`,
914
+ `progress`, `review`, `ready`, `waiting`, `done`, `canceled`, `duplicate`; tones: `muted`,
915
+ `neutral`, `warning`, `info`, `accent`, `success`, `danger`. Priority levels: `null` (no
916
+ priority, dashes), `0` lowest … `3` high (signal bars filled to the level), `4` highest (danger
917
+ square with "!"). Both are decorative (`aria-hidden`) — always pair them with the status or
918
+ priority as text, or a tooltip on the control holding them; `label` instead makes a standalone
919
+ glyph `role="img"` with that name.
679
920
  - **Entity preset** — `Select.entity(target, options)` renders icon/color, label, and secondary
680
- text using `iconKey`, `colorKey`, and `subtitleKey` (or `renderIcon`). `recent` pins a deduplicated
921
+ text using `iconKey`, `colorKey`, and `subtitleKey` (or `renderIcon`). `colorKey` takes a CSS
922
+ colour or a token reference (`var(--zx-color-tone-blue-fg)`); without one the glyph is a neutral
923
+ tile in muted ink. `recent` pins a deduplicated
681
924
  **Recent** section above ordinary/local/async results; `create: true|actionDescriptor` appends an
682
925
  application-owned command at the bottom without changing the selected ID. `recentLabel` and the
683
926
  ordinary Select options remain overridable. For schema-driven ZeyOS loading,
@@ -686,6 +929,13 @@ Unifies single-select, local filtering, and async loading.
686
929
  - **Keyboard** (APG combobox) — ArrowDown/Alt+ArrowDown open; arrows navigate and wrap; Home/End;
687
930
  Enter selects; Esc closes; Tab closes; printable characters filter (editable) or run a typeahead
688
931
  (readonly). `aria-activedescendant` tracks the active option.
932
+ - **Appearance** — the highlighted option is a neutral step; a chosen option is a pick and keeps
933
+ the accent tint and its check mark. Group labels are sentence-case captions. The focus ring
934
+ follows keyboard focus: a click on a read-only input (no `filter`) does not draw it
935
+ (`data-focus-origin="pointer"` on the root until the next key). Disabled, the whole control fades
936
+ as a disabled Field does, and not a second time inside one.
937
+ - **Quiet fields** — inside `[data-zx-fields="quiet"]` the value reads as text: no border, fill,
938
+ chevron or clear button until hover; full field chrome while focused or open.
689
939
  <!-- /doc -->
690
940
 
691
941
  <!-- doc:sort-control -->
@@ -713,7 +963,10 @@ from `Select`, with its themed popover and keyboard behavior.
713
963
  Searchable multi-check list with optional async loading.
714
964
 
715
965
  - **Options** — `items`, `valueKey: 'ID'`, `labelKey: 'name'`, `checkedKey: 'on'`, `search: true`,
716
- `height: 280`, `defaultChecked: false`, `load: async ()=>items`.
966
+ `height: 280`, `defaultChecked: false`, `hideLabel: false` (leaves out the heading; the group
967
+ keeps its text as `aria-label`), `load: async ()=>items`.
968
+ - **In a Field** — field type `checklist` draws no heading of its own: the field's label names the
969
+ group (`aria-labelledby`).
717
970
  - **Methods** — `setItems()`, `getValues()`, `setValues(ids)`, `checkAll()`/`uncheckAll()`,
718
971
  `search(query)`, `reload()`.
719
972
  - **Events** — `change {values}`, `loaded`.
@@ -739,6 +992,11 @@ Field type: `number`.
739
992
  set (otherwise they keep their caret meaning), Enter commits, wheel steps while focused.
740
993
  - **Also exported** — `parseNumber(raw)` (accepts `.` and `,` as the decimal separator, ignores
741
994
  grouping, returns null for junk) and `snapNumber(value, {min, max, step, precision})`.
995
+ - **In a Field** — field type `number` fills the control column like a text input and takes the
996
+ field key as its `name`.
997
+ - **Quiet fields** — inside `[data-zx-fields="quiet"]` the number reads as text: no border, no
998
+ fill, the value start-aligned with the quiet values around it, both steppers at the trailing end
999
+ and hidden (kept in the layout so nothing moves) until hover or focus; full chrome on focus.
742
1000
  <!-- /doc -->
743
1001
 
744
1002
  <!-- doc:rating -->
@@ -770,7 +1028,9 @@ from; everything Zx adds is chrome around it. Field type: `slider`.
770
1028
  `showValue: true`, `showBounds: false` (draws `min` and `max` either side of the track),
771
1029
  `showInput: false` (adds a number box for entering a value precisely instead of aiming at it),
772
1030
  `unit: ''`, `marks: null` (numbers, or `{value, label}` objects), `disabled: false`,
773
- `readonly: false`, `size: 'md'|'sm'`, `formatValue: (value) => string`.
1031
+ `readonly: false`, `size: 'md'|'sm'`, `formatValue: (value) => string`. Each mark sits under
1032
+ the thumb's centre at its value; the outermost labels end flush with the track instead of
1033
+ spilling out of the component.
774
1034
  - **Methods** — `get()`, `set(value, {silent})`, `setRange(min, max, step?)`, `setLabel()`,
775
1035
  `setReadonly()`, `focus()`, `enable()`/`disable()`. `value` is also a getter/setter.
776
1036
  - **Events** — `input {value}` continuously while dragging, `change {value}` once the value is
@@ -805,7 +1065,8 @@ to a hidden textarea when the async Clipboard API is unavailable or denied.
805
1065
  that did not happen.
806
1066
  - **Accessibility** — on success the glyph becomes a tick and a visually hidden `role="status"`
807
1067
  speaks the confirmation, then both revert. The icon-only form carries the `title` as its
808
- accessible name.
1068
+ accessible name. The status line is positioned inside the button, so it never adds scrollable
1069
+ overflow to the page or to a scrolling pane.
809
1070
 
810
1071
  #### CopyInput
811
1072
 
@@ -856,10 +1117,16 @@ Text date input with a calendar popover. `DateTimeBox(target, opts)` is a Datebo
856
1117
  tokens are the date-utility ones: `%d %m %Y %H %M`.
857
1118
 
858
1119
  - **Options** — `value: Date | number(unix) | string`, `format: '%d.%m.%Y'`, `time: false`, `min`,
859
- `max`, `placeholder`, `clearable: true`, `disabled`.
1120
+ `max`, `placeholder`, `clearable: true` (the clear button shows while there is a value and the
1121
+ box is enabled; without `clearable` the input keeps no room for it), `disabled`.
1122
+ - **In a Field** — field types `date`/`datetime` (and `daterange`) fill the control column like a
1123
+ text input and give way in a narrow two-column row; standalone, the box keeps its own width.
860
1124
  - **Methods** — `get(unit='date')` (`'seconds'` → unix), `set(value, {silent})` (accepts a Date;
861
1125
  pass Date objects, not display strings), `open()`/`close()`, `enable()`/`disable()`, `focus()`.
862
1126
  - **Events** — `change {date}`, `invalid {text}`, `open`, `close`.
1127
+ - **Quiet fields** — inside `[data-zx-fields="quiet"]` the date reads as text: no border, no fill,
1128
+ the calendar button hidden until hover; full chrome on focus. An invalid value keeps its danger
1129
+ border. DateRangeBox behaves the same way.
863
1130
  <!-- /doc -->
864
1131
 
865
1132
  <!-- doc:date-range -->
@@ -877,7 +1144,7 @@ An inline two-month calendar for picking a start and an end day, plus the form-u
877
1144
 
878
1145
  #### DateRangeBox
879
1146
 
880
- - **Options** — everything DateRangePicker takes, plus `format: '%d.%m.%Y'`, `separator: ' – '`, `placeholder: null` (derived from the format when null), `clearable: true`. Typed text is parsed as `"<start><separator><end>"`; a lone date reads as a one-day range and a trailing separator keeps the range half open.
1147
+ - **Options** — everything DateRangePicker takes, plus `format: '%d.%m.%Y'`, `separator: ' – '`, `placeholder: null` (derived from the format when null), `clearable: true` (no clear button while disabled, and no room kept for one without it). Typed text is parsed as `"<start><separator><end>"`; a lone date reads as a one-day range and a trailing separator keeps the range half open.
881
1148
  - **Methods** — `get(unit = 'date')` (`'seconds'` returns `{start, end}` as Unix seconds), `set(range, {silent})` accepting `{start, end}` of Dates, Unix-seconds numbers, or formatted strings, `clear({silent})`, `open()`, `close()`, `enable()`, `disable()`, `focus()`, `destroy()`.
882
1149
  - **Events** — `change {start, end}`, `invalid {text}` when the typed value cannot be parsed (the text stays editable and the field takes `data-state="invalid"` plus `aria-invalid`), `open`, `close`.
883
1150
  <!-- /doc -->
@@ -893,6 +1160,8 @@ use `TimePicker` or `DateTimeBox`.
893
1160
  - **Methods** — `get(unit)`, `set(value, unit?, {silent})`, `enable()`/`disable()`.
894
1161
  - **Events** — `change {value}`, in `options.unit`.
895
1162
  - **Also exported** — the pure helpers `splitTime` and `joinTime`.
1163
+ - **Quiet fields** — inside `[data-zx-fields="quiet"]` the segments (native inputs) and the sign
1164
+ button drop their border and fill until hover or focus; the sign keeps its colour.
896
1165
  <!-- /doc -->
897
1166
 
898
1167
  <!-- doc:message -->
@@ -908,8 +1177,27 @@ messages rather than the page, and uses the translucent glass surface tokens wit
908
1177
  `Message.progress(text, {scope?})` → `{ update(pct, text?), done(), fail(text?) }`. `scope` is
909
1178
  an element or selector whose nearest Zx theme scope owns that floating region; without it the
910
1179
  focused opener supplies the scope.
1180
+ - **Region lifecycle** — a floating region is created on the first toast for its scope and removed
1181
+ when its last toast closes. One whose host was emptied or left the document is replaced on the
1182
+ next toast rather than reused, and a scope that is itself detached falls back to the focused
1183
+ element's scope. `Message.disposeRegion(scope)` → boolean removes a scope's region at once,
1184
+ toasts and all, for a host torn down while toasts are still showing. While a modal Dialog or
1185
+ Sheet is open the region moves into the topmost one, so toasts and their actions stay visible
1186
+ and clickable above it, and moves back when it closes. A press on a toast dismisses no open menu
1187
+ or drawer.
911
1188
  - **Per-message options** — `kind: 'info'`, `timeout`, `closable: true`, `maxVisible: 5` (extras
912
1189
  queue, and hovering pauses dismissal).
1190
+ - **Action** — `action: { label, onclick?, kind? }` renders one small button (`.zx-btn`,
1191
+ `data-size="sm"`, secondary unless `kind` is `primary`/`danger`/`ghost`) in a toast or an inline
1192
+ message; activating it runs `onclick(event)` and closes the message. A message with an action
1193
+ stays at least 8000ms (`timeout: 0` still persists). Every message holds its timer while the
1194
+ pointer is over it or focus is inside it (WCAG 2.2.1) and resumes once neither is; the region is
1195
+ not modal, so Tab moves between the action and the close button, and closing a message that holds
1196
+ focus returns focus to where it came from. The live region still announces the text.
1197
+ - **Leading** — `leading: Node | component` (an Avatar, say) replaces the kind icon. It is
1198
+ decorative; the text must name what it shows.
1199
+ - **Handle** — every `show()` (static or inline) and `info/success/warning/error()` return
1200
+ `{ close() }`; closing twice, or a queued message, is safe.
913
1201
  <!-- /doc -->
914
1202
 
915
1203
  <!-- doc:modal -->
@@ -919,7 +1207,9 @@ Thin overlay on the native `<dialog>` element. The surface uses the shared, read
919
1207
  panel material; the backdrop uses `--zx-color-overlay-backdrop`, and focus returns to the opener on close.
920
1208
 
921
1209
  - **Options** — `content`, `width: 'auto'`, `closable: true`, `lightDismiss: false`,
922
- `destroyOnClose: false`, `scope: null` (the opener's nearest Zx theme scope by default).
1210
+ `destroyOnClose: false`, `scope: null` (the opener's nearest Zx theme scope by default). The
1211
+ scope is resolved again on every `open()`; a `scope` element not yet in the document leaves the
1212
+ dialog detached until then, rather than parking it on `<body>` outside the theme.
923
1213
  - **Methods** — `open()`, `close(result?)`, `setContent()`, `isOpen()`.
924
1214
  - **Events** — `open`, `close {result}`, `cancel` (Esc; preventable with `event.preventDefault()`).
925
1215
  <!-- /doc -->
@@ -933,9 +1223,17 @@ Structured modal: a header with title and close button, a body, and footer butto
933
1223
  `buttons: [{ label, kind, action: 'close'|'cancel'|fn(dialog), autofocus, shortcut }]`, `closable: true`.
934
1224
  - **Methods** — `setTitle()`, `setContent()`, `setButtons()`,
935
1225
  `addView(key, { content, buttons? })`, `showView(key)`.
936
- - **Statics returning Promises** — `Dialog.alert({ title, message })`,
937
- `Dialog.confirm({ title, message, danger })` → boolean,
938
- `Dialog.prompt({ title, message, value })` → string|null.
1226
+ - **Statics returning Promises** — `Dialog.alert({ title, message, okLabel })`,
1227
+ `Dialog.confirm({ title, message, okLabel, cancelLabel, danger })` → boolean,
1228
+ `Dialog.prompt({ title, message, value, placeholder, okLabel, cancelLabel })` → string|null.
1229
+ The labels default to the translated "OK" and "Cancel"; name the action instead where you can
1230
+ ("Delete", "Rename").
1231
+ - **Type to confirm** — `Dialog.confirm({ ..., confirmText: 'Website Redesign' })` adds an input
1232
+ labelled "Type **Website Redesign** to confirm" (a real `<label>`; message
1233
+ `dialog.typeToConfirm`, `'Type %1 to confirm'`). Focus starts in the input; the confirm button
1234
+ stays disabled, and Enter does nothing, until the value matches exactly (case-sensitive, ignoring
1235
+ surrounding spaces). Escape, the close button, and Cancel resolve `false` as before. Use it for
1236
+ deletes that cannot be undone, with `danger: true` and an `okLabel` that names the action.
939
1237
  <!-- /doc -->
940
1238
 
941
1239
  <!-- doc:sheet -->
@@ -968,7 +1266,16 @@ When a Dock adopts it, the Sheet becomes an opaque layout surface instead.
968
1266
  the browser through `showModal()`. Floating `'trap-focus'` and `false` sheets use a manual
969
1267
  Popover top layer, so they stay viewport-anchored even inside a transformed or clipped theme
970
1268
  scope while the page keeps scrolling and stays interactive. Escape and outside-click are
971
- re-implemented, and `'trap-focus'` adds `focusTrap()` on top. Their top-layer backdrop is
1269
+ re-implemented through the overlay stack, and `'trap-focus'` adds `focusTrap()` on top: Esc
1270
+ closes the sheet when focus is in it (or in a popup it opened) and no popup above it takes the
1271
+ key first, an Esc already handled inside (`defaultPrevented`) is ignored, and a press inside a
1272
+ menu or dropdown anchored in the sheet counts as inside the sheet. Closing the sheet closes the
1273
+ popups it opened. Light dismiss stops at the first layer that contains the press: a press inside
1274
+ the upper of two light-dismiss sheets (or inside any overlay above this one) closes neither the
1275
+ lower sheet nor anything beneath it; a press outside every layer closes each one that
1276
+ light-dismisses. A floating non-modal sheet opened from inside a modal Dialog is hosted by that
1277
+ dialog while open, so it stays usable above it, closes when the dialog closes, and goes back to
1278
+ its theme scope when it closes. Their top-layer backdrop is
972
1279
  explicitly transparent and non-filtering; `backdrop` is meaningful only when `modal` is `true`.
973
1280
  A docked Sheet remains a native non-modal dialog in its Dock's flow.
974
1281
  - **Resizing and detents** — `resizable` makes the inner edge draggable between `min` and `max`.
@@ -987,12 +1294,14 @@ When a Dock adopts it, the Sheet becomes an opaque layout surface instead.
987
1294
  `aria-valuenow`/`aria-valuemin`/`aria-valuemax`: arrow keys along the axis step it by 16px, Shift
988
1295
  by 64px. A docked sheet hides its handle — the dock's own divider resizes it there.
989
1296
  - **Custom properties** — `--zx-sheet-size` (written by `size`; the per-side default otherwise)
990
- and `--zx-sheet-radius` (inner corners of a top/bottom sheet, default `--zx-radius-xl`) are
1297
+ and `--zx-sheet-radius` (inner corners of a top/bottom sheet, default `--zx-radius-lg`) are
991
1298
  internal geometry, not published styling hooks — prefer the options. `--zx-overlay-blur` is a
992
1299
  tier-2 semantic token and is public: it sets the `'blur'` backdrop's radius, default `8px`.
993
1300
  - **Motion** — entry and exit both animate, via `@starting-style` and
994
1301
  `transition-behavior: allow-discrete` on `display`/`overlay`; without them the sheet appears and
995
- disappears instantly. All of it sits inside `prefers-reduced-motion: no-preference`.
1302
+ disappears instantly. The slide (and a docked track's collapse, and a stack's shift) runs on
1303
+ `--zx-ease-drawer`; the fade keeps `--zx-ease`. All of it sits inside
1304
+ `prefers-reduced-motion: no-preference`, and stops inside a `data-zx-motion="reduced"` region.
996
1305
  - **Keyboard and screen readers** — identical to Dialog: the native `<dialog>` supplies the role
997
1306
  and `aria-labelledby` points at the title. Escape closes when `closable`, focus moves to the
998
1307
  `[autofocus]` control or the first focusable descendant on open, and returns to the opener on
@@ -1049,6 +1358,28 @@ DOM child of the anchor's nearest Zx theme scope while the browser lifts it into
1049
1358
  `left*` and `right*` placements, `offset: 4`,
1050
1359
  `matchWidth: false`, `openOn: 'click'|'manual'`, `closeOnSelect: false`, `scope: null` (the
1051
1360
  anchor's nearest Zx theme scope by default).
1361
+ - **Hosting** — the host is resolved again on every `open()`. A dropdown created before its anchor
1362
+ is in the document keeps its panel detached until the first open instead of parking it on
1363
+ `<body>`, so an anchor inserted into a `[data-zx-theme]` region or a `.zx-scope` afterwards still
1364
+ gets its theme. Tooltip, MenuButton, and ContextMenu panels follow the same rule. While open, a
1365
+ panel whose anchor sits in an open `<dialog>` (a modal Dialog or Sheet, a focus-trapping or
1366
+ non-modal Sheet) is hosted by that dialog instead, so `showModal()` does not make it inert and a
1367
+ focus trap still reaches it; it returns to the theme scope when it closes.
1368
+ - **Overlay stack** — an open panel is one entry on the shared overlay stack
1369
+ (`src/core/overlay-stack.js`). Esc closes only the topmost layer that takes it, and an Esc that
1370
+ something inside already handled (`defaultPrevented`) closes nothing. A press outside the panel
1371
+ and every overlay opened from it closes it; a press on the anchor does not. Light dismiss stops
1372
+ at the first layer that contains the press, so a press inside an overlay above the panel (one
1373
+ not opened from it) leaves the panel open; nothing below an open modal reacts at all. A panel
1374
+ opened from inside another overlay closes with that overlay.
1375
+ - **Keyboard** — the panel follows its trigger in the Tab order: Tab on the anchor of an open panel
1376
+ moves to the panel's first Tab stop, or onto the panel itself when it has a `tabindex`; Tab past
1377
+ the last stop continues with whatever follows the anchor; Shift+Tab from the first
1378
+ stop returns to the anchor. A Tab stop is what the browser itself stops at: a `tabindex="-1"` part
1379
+ of a composite (a Select's toggle), a roving toolbar's inactive items, disabled, hidden and inert
1380
+ controls, and all but one radio of a group are not. A panel with no Tab stop is skipped. Esc and `close()` with
1381
+ focus inside return focus to the anchor. Opening moves no focus — give the panel `role` and
1382
+ `aria-*` semantics yourself if it is more than a disclosure.
1052
1383
  - **Methods** — `open()`, `close()`, `toggle()`, `isOpen()`, `setContent()`, `getPanel()`.
1053
1384
  - **Events** — `open`, `close`.
1054
1385
  <!-- /doc -->
@@ -1068,13 +1399,32 @@ A button that opens a `role="menu"`.
1068
1399
  default.
1069
1400
  - `role: 'menuitemcheckbox'|'menuitemradio'` with `checked` exposes an APG checked action. The
1070
1401
  application owns state changes and calls `setItems()` with the new model.
1071
- - **Methods** — `setItems()`, `open()`, `close()`, `setLabel()`, `getTrigger()`, `getPanel()`,
1072
- `focusFirst()`, `focusLast()`.
1402
+ - `{type: 'choice', label, value, options: [{value, label, icon|glyph}], onselect(value, entry,
1403
+ menu)}` draws a setting chosen in place — a theme, a layout: the label, then a small segmented
1404
+ group of `menuitemradio` options named by it. Choosing an option checks it and calls
1405
+ `onselect`; the menu stays open so the change shows, and `select` does not fire. Left and Right
1406
+ move within the row; Up and Down leave it, landing on another row's checked option.
1407
+ `AccountMenu` takes the same entries; `ContextMenu`, which closes on every activation, leaves
1408
+ them out.
1409
+ - **Methods** — `setItems()`, `open({returnFocus})`, `close()`, `toggle()` (opens when closed,
1410
+ closes when open, moves no focus), `isOpen()`, `setLabel()`, `getTrigger()`, `getPanel()`,
1411
+ `focusFirst({returnFocus})`, `focusLast({returnFocus})`.
1073
1412
  - **Events** — `select {value, item}`, `open`, `close`.
1413
+ - **Focus return** — choosing an item closes the menu and returns focus to the trigger before the
1414
+ `select` event and the item's `onselect` run, so a dialog they open returns to the trigger when
1415
+ it closes, and focus they move themselves stays where they put it. Escape returns focus the same
1416
+ way. `returnFocus` sets the destination for one opening: `true` (default) is the trigger, an
1417
+ element is that element — the list row a keyboard shortcut opened the menu from — and `false`
1418
+ leaves focus alone.
1074
1419
  - **Cancellation** — `select` is cancelable. Preventing it skips the item's `onselect` callback and
1075
- leaves the menu open, which is the composition seam used by `AccountMenu` and rail flyouts.
1420
+ leaves the menu open with focus back on the item, which is the composition seam used by
1421
+ `AccountMenu` and rail flyouts.
1076
1422
  - **Keyboard** — ArrowDown/Enter/Space open and focus the first item; ArrowUp opens and focuses the
1077
- last; arrows cycle; Home/End; typeahead; Enter/Space select; Esc/Tab close.
1423
+ last; arrows cycle; Home/End; typeahead; Enter/Space select; Esc/Tab close. Esc reaches the menu
1424
+ through the overlay stack, so inside a Sheet or Dialog it closes the menu and leaves the sheet
1425
+ open; a menu opened inside a modal Dialog is hosted by it and stays usable.
1426
+ - **Appearance** — the highlighted item is a neutral step, a checked item keeps its accent check
1427
+ mark, and section headings are sentence-case captions. ContextMenu shares the same rules.
1078
1428
  <!-- /doc -->
1079
1429
 
1080
1430
  <!-- doc:context-menu -->
@@ -1120,7 +1470,7 @@ A description bubble anchored to another element. The panel is a manual popover
1120
1470
  at all and leaves everything to the methods), `disabled` (false).
1121
1471
  - **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.
1122
1472
  - **Events** — `open`, `close`. Both fire only on a real transition, so a `show()` on an open tooltip is silent.
1123
- - **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.
1473
+ - **Keyboard** — Focusing the anchor opens the tooltip at once; moving focus away closes it. `Escape` closes it without moving focus (WCAG 1.4.13 "dismissible") — and only it, when it is the topmost open overlay, so one press never closes a tooltip and the menu beneath it. A `pointerdown` anywhere closes it, and focus that arrives with a click does not re-open it. A closed tooltip listens to nothing outside its anchor; an open one is an entry on the shared overlay stack.
1124
1474
  <!-- /doc -->
1125
1475
 
1126
1476
  <!-- doc:split-view -->
@@ -1150,19 +1500,72 @@ Sortable, selectable, sticky-header data table. `fr` widths fill the container w
1150
1500
  widths scroll horizontally, and multi-select adds a tri-state header checkbox plus Shift+click
1151
1501
  range select.
1152
1502
 
1153
- - **Options** — `columns: [{ id, label, sortable?, width ('120px'|'2fr'|'auto'), align,
1503
+ - **Options** — `columns: [{ id, label, ariaLabel?, sortable?, width ('120px'|'2fr'|'auto'), align,
1154
1504
  type?: 'text'|'number'|'currency'|'percent'|'unit', locale?, decimals?, currency?, unit?,
1155
1505
  render?: (row,i)=>Node|string, sortValue?, headerTitle? }]`, `data`, `rowId: 'ID'`,
1156
1506
  `sort: { id, dir }`, `sortMode: 'local'|'server'`, `selectable: false|'single'|'multi'`,
1157
- `stickyHeader: true`, `height`, `emptyText`, `rowClass`, `zebra: true`, and
1158
- `hierarchy: false|{parentId, column?, expanded?}`. Locale, currency, unit, and decimals may be
1159
- row callbacks; values remain numbers in row data while display formatting is locale-aware.
1507
+ `stickyHeader: true`, `height`, `emptyText`, `rowClass`, `zebra: true`, `variant:
1508
+ 'default'|'plain'`, `hierarchy: false|{parentId, column?, expanded?}`, `interactive: false`,
1509
+ and `label`/`labelledBy`. Locale, currency, unit, and decimals may be row callbacks; values
1510
+ remain numbers in row data while display formatting is locale-aware.
1511
+ - **Appearance** — the header sits on the table's surface (no band) in the muted text colour at a
1512
+ medium weight, sentence case; the sorted column's label takes the text colour beside its arrow.
1513
+ Rows are divided by `--zx-color-border-subtle`, the header by the ordinary border, and every cell
1514
+ uses tabular figures. Checked rows of a multi-selection keep the accent tint (`--zx-color-bg-selected`);
1515
+ the selected row of a `selectable: 'single'` table is the current record of a master–detail list
1516
+ and takes the neutral `--zx-color-bg-current` fill. Sortable and plain headers set the same type,
1517
+ and the scroll region reserves no scrollbar gutter until the table actually scrolls.
1518
+ - **Columns in the DOM** — every header and body cell carries `data-column="<id>"` in every mode,
1519
+ read-only included, so a table can be styled per column. A column with `label: ''` (icons,
1520
+ actions) keeps a blank header named by `ariaLabel`, or by its id; `ariaLabel` also names the
1521
+ column in the chooser and its cell editors.
1522
+ - **`variant: 'plain'`** — for a table inside a card or panel that already draws the box: no outer
1523
+ frame, no header band, no zebra, hairline row dividers. Reflected as `data-variant="plain"` on the
1524
+ root; TableView passes it through `table: { variant: 'plain' }`.
1160
1525
  - **Methods** — `setData()`, `addData()`, `updateRow(id,row)`, `removeRow(id)`, `getRow(id)`,
1161
1526
  `getData()`, `empty()`, `setSort(id,dir)`, `getSort()`, `clearSort()`, `getSelection()`, `setSelection(ids)`,
1162
1527
  `clearSelection()`, `setLoading(bool)` (busy/skeleton state — dims rows, shows an indeterminate
1163
1528
  top bar, sets `aria-busy`, and is cleared by the next `setData`).
1164
1529
  - **Events** — `rowclick {row, id, index, event}`, `rowdblclick`, `sort {id, dir}`,
1165
1530
  `selectionchange {rows, ids}`, `datachange {rows}`, and `rowtoggle {row, id, expanded}`.
1531
+ `rowclick` fires for Enter on a focused row as well as for a click; `event` is then the keydown.
1532
+ - **Keyboard** — an *interactive* table implements the WAI-ARIA APG **Grid** pattern
1533
+ (`role="grid"`, with `aria-multiselectable` for `selectable: 'multi'`), or the **Treegrid**
1534
+ pattern with `hierarchy`. A table is interactive when it is `selectable`, has `editMode` or
1535
+ `hierarchy`, sets `interactive: true`, or has a `rowclick`/`rowdblclick` listener attached through
1536
+ `on()` or an `onrowclick`/`onrowdblclick` option (attaching one later turns the grid on). A host
1537
+ that listens only for the bubbling `zx-rowclick` DOM event passes `interactive: true`. TableView
1538
+ always bridges `rowclick` to `recordclick`, so its table is always a grid. Any other table keeps
1539
+ plain `<table>` semantics, no grid role, no name unless `label` is given, and no row Tab stop.
1540
+ In a grid the rows are one Tab stop with a roving `tabindex`; the row checkboxes and disclosure
1541
+ buttons leave the Tab order because the row keys cover them (links and buttons a host renders
1542
+ in cells keep theirs). Focus is on rows by default and on cells when `editMode` is set —
1543
+ reflected as `data-focus="row"|"cell"` on the root. ArrowUp/ArrowDown move a row,
1544
+ ArrowLeft/ArrowRight a cell (cell focus), Home/End go to the first/last row (cell focus:
1545
+ first/last cell of the row) and Ctrl+Home/End to the first/last row's corner cell,
1546
+ PageUp/PageDown move by the rows visible in the scroll region (10 when the table does not scroll
1547
+ itself). Enter activates exactly like a click (`rowclick`; selects in `single`), Enter/F2 on an
1548
+ editable cell opens its editor, Space toggles the row's selection when `selectable` (activates
1549
+ otherwise), Shift+ArrowUp/Down extends a `multi` selection from its anchor, and Ctrl/Cmd+A
1550
+ selects the rows in the DOM — like the header checkbox, not the rows `growing` holds back nor the
1551
+ children of collapsed branches. A sort, new data, or a change in expansion starts the anchor
1552
+ over. Treegrid: ArrowRight expands a collapsed row, then moves into its first child; ArrowLeft
1553
+ collapses an expanded row, then moves to its parent. With cell focus, ArrowLeft from the first
1554
+ cell lands on the row and ArrowRight from the row goes back to its first cell. The focused row
1555
+ (and column) is kept by id across `setData`, sorting, expansion, growing, and `updateRow`; when
1556
+ a collapsed ancestor hides it, the ancestor takes focus, and when it is gone, its neighbour does.
1557
+ Focus is only restored when the body had it, and without scrolling, so a polling refresh never
1558
+ pulls the page back to the table. An emptied table keeps focus on its frame until rows return.
1559
+ Column headers keep their sort buttons in the Tab order rather than joining arrow navigation —
1560
+ the one deliberate departure from the APG grid, which puts the header row inside it. Rows and
1561
+ cells show an inset `--zx-focus-ring` with a transparent outline that forced colours paint.
1562
+ - **Naming** — `label` or `labelledBy` (an element id) names the table; a grid without either is
1563
+ named by the `table.label` message (English "Table"), so pass a real name. Row controls are named
1564
+ after the row's first cell with text through `table.selectRow` (`Select %1`), `table.expandRow`,
1565
+ `table.collapseRow`, and `table.moveRow`; `table.row` (`row %1`) stands in for a row without
1566
+ text, and `table.selectAll` names the header checkbox. Sort buttons and selection checkboxes are
1567
+ at least 24×24 px targets; the checkbox is drawn so the glyph stays `--zx-space-4` while a
1568
+ transparent border makes up the target.
1166
1569
  - **Hierarchy** — flat parent references are projected as an ARIA `treegrid`; `getData()` and all
1167
1570
  editing/mutation APIs stay flat and ID-based. Orphans become roots, cycles render once, local
1168
1571
  sort reorders siblings without mixing levels, and growing counts visible rows. Use
@@ -1174,6 +1577,8 @@ range select.
1174
1577
  `'cell'` edits one cell, `'row'` opens every editable cell of the row and commits them as a unit.
1175
1578
  Editors are sized into the cell without changing row height, and an editing cell carries
1176
1579
  `data-editing="true"` (`data-invalid="true"` plus an `aria-live` message when a validator rejects).
1580
+ Text, number, and date editors select their content on entry, so typing replaces the value
1581
+ ("6" over "1.00" gives 6); a textarea keeps its caret.
1177
1582
  - **Editable columns** — extra column keys: `editable: false|true|'text'|'number'|'select'|'date'|
1178
1583
  'checkbox'|'textarea'|((row)=>boolean)` (`true` infers `'number'` for number/currency/percent/unit
1179
1584
  columns and `'text'` otherwise; a function is evaluated per row, so individual rows stay
@@ -1252,7 +1657,16 @@ range select.
1252
1657
  `{label, target}` object names the chooser's trigger and mounts the disclosure into a host
1253
1658
  element — a list toolbar, say — instead of the view's own. `label` omitted resolves
1254
1659
  `recordView.fields`. This is what lets a translated application use the chooser at all: the
1255
- trigger used to be the literal `Fields`, with no way to change it or move it.
1660
+ trigger used to be the literal `Fields`, with no way to change it or move it. `TableView`,
1661
+ `CardView`, and `KanbanView` all honour `target`.
1662
+
1663
+ **The chooser is public.** `getFieldControls()` returns the view's field chooser — the shared
1664
+ disclosure, or for a `TableView` with `fieldControls: true` its Table's column chooser — and
1665
+ `mountFieldControls(target)` moves it into a host element (`null` hands it back). The view keeps
1666
+ owning it wherever it sits and removes it on `destroy()`; `ListToolbar.bind()` uses exactly this.
1667
+ In the shared chooser the move buttons keep focus on the field they moved (or its other button once
1668
+ one end disables), a polite live region announces "Name moved to position 2 of 5"
1669
+ (`recordView.fieldMoved`), and Escape closes the disclosure and returns focus to its trigger.
1256
1670
 
1257
1671
  `TableView` is the record-oriented entry point above the lower-level `Table`. It extends
1258
1672
  `RecordView` and composes one complete `Table`, so editing, hierarchy, growing, row movement,
@@ -1266,7 +1680,8 @@ responsive stacking, loading, and advanced table events remain available through
1266
1680
  `fieldControls: true`, and `emptyText`. At least one field remains visible.
1267
1681
  - **Shared methods** — `setData()`, `addData()`, `updateRecord()`, `removeRecord()`, `getRecord()`,
1268
1682
  `getData()`, field getters/order/visibility methods, `setSort()`/`getSort()`, selection methods,
1269
- `setLoading()`, `getViewState()`, and `setViewState()`.
1683
+ `setLoading()`, `getViewState()`, `setViewState()`, `getFieldControls()`, and
1684
+ `mountFieldControls(target)`.
1270
1685
  - **Portable state** — `{version:1,fieldOrder,hiddenFields,sort}` is JSON-safe configuration only.
1271
1686
  Records, selection, DOM nodes, rendered values, and callbacks are never serialized. A state from
1272
1687
  one record view may seed another; unknown fields are ignored and new fields append.
@@ -1285,7 +1700,15 @@ portable state as TableView and KanbanView.
1285
1700
 
1286
1701
  - **Options** — the shared RecordView options plus `titleField`, `subtitleField`, `preview`,
1287
1702
  `previewAlt`, `link`, `actions`, `groupBy`, `groupOrder`, `minCardWidth`, `maxColumns`,
1288
- `variant: 'outlined'|'raised'|'filled'`, `loadingCount`, `headingLevel`, and `label`.
1703
+ `variant: 'outlined'|'raised'|'filled'`, `loadingCount`, `headingLevel`, and `label` (null
1704
+ resolves message `cardView.label`, "Card view").
1705
+ - **Messages** — `cardView.label`, `cardView.sort`, `cardView.sortCards`, `cardView.unsorted`,
1706
+ `cardView.sortAscending`/`cardView.sortDescending` (`%1` the field), `cardView.ungrouped`
1707
+ ("Ungrouped"), `cardView.groupEmpty` ("No records in this group."), `table.empty` for the empty
1708
+ collection ("No records", shared with Table), and the card's own `recordCard.select`,
1709
+ `recordCard.actions` (`%1` the title), `recordCard.untitled`, `recordCard.selected`,
1710
+ `recordCard.notSelected`. English comes from the default catalogue; override per instance
1711
+ through `msg`, or for every instance through `setTranslator()`.
1289
1712
  - **Selection trigger** — `selectionTrigger:'checkbox'` preserves existing selection behavior.
1290
1713
  With `'card'`, selectable backgrounds toggle on click, Enter or Space, Shift+click selects a range,
1291
1714
  and the checkbox is omitted. Links, actions and text selection keep their native behavior. Selection
@@ -1309,6 +1732,8 @@ portable state as TableView and KanbanView.
1309
1732
  `selectionTrigger:'card'`, Enter also toggles selection. Links, actions,
1310
1733
  selection controls, and selected text do not also activate the card. `recordaction` adds the
1311
1734
  resolved action to the shared event set.
1735
+ - **Appearance** — a selected card is a pick and keeps the accent: the `--zx-color-bg-selected`
1736
+ tint inside an accent outline. Metadata pairs are borderless tinted chips, not boxes in a box.
1312
1737
  <!-- /doc -->
1313
1738
 
1314
1739
  <!-- doc:kanban-view -->
@@ -1334,6 +1759,9 @@ cards, advisory work-in-progress limits, and equivalent pointer/programmatic/key
1334
1759
  `eyebrow-start`, `eyebrow-end`, and `title-prefix`. Unslotted metadata stays as quiet inline
1335
1760
  facts. Titles clamp visually to two lines while retaining their complete accessible text. Move
1336
1761
  and action controls reveal on hover/focus/selection/grab, and remain visible on touch devices.
1762
+ - **Assignees** — `assignees` faces (a name, names, or `{name, src?, initials?, tone?}`) are drawn
1763
+ like a small Avatar: initials on the tint `avatarTone(name)` picks, so a person has the same
1764
+ colour on the board as in every Avatar; a descriptor's own `tone` wins.
1337
1765
  - **Movement boundary** — `recordmove {record,id,from,to,column,swimlane,limitExceeded}` is
1338
1766
  cancelable and fires before a commit/request; either the component event or bubbling
1339
1767
  `zx-recordmove` mirror may veto it. `accept` is a hard eligibility gate; WIP is visual advice.
@@ -1344,7 +1772,8 @@ cards, advisory work-in-progress limits, and equivalent pointer/programmatic/key
1344
1772
  chooses a column, Up/Down chooses a bucket-relative position, and Alt+Up/Down chooses a swim
1345
1773
  lane. A polite live region announces every target, result, and reached/exceeded WIP limit.
1346
1774
  Collapse keeps focus on its control, including when a programmatic collapse hides the focused
1347
- card. Native drag is an enhancement.
1775
+ card. Native drag is an enhancement. The pointer-drag preview and a keyboard-grabbed card lift
1776
+ on `--zx-drag-shadow`.
1348
1777
  <!-- /doc -->
1349
1778
 
1350
1779
  <!-- doc:grid -->
@@ -1384,13 +1813,16 @@ code, so future adapters do not change the public `Chart` API.
1384
1813
  `setError()`, `clearError()`, `resize()`, `refreshTheme()`, `getInstance()`, `destroy()`.
1385
1814
  - **Events** — `ready {instance}`, `update {data, type}`, `select {datasetIndex, index, dataset,
1386
1815
  label, value, nativeEvent}`, and `error {error}`.
1387
- - **Adapter contract** — `adapter.create(canvas, {type, data, options, onSelect})` returns
1388
- `{update(spec), resize(), destroy(), instance?}`. Implementations must destroy engine resources
1389
- and must not retain the component after teardown.
1816
+ - **Adapter contract** — `adapter.create(canvas, {type, data, options, reducedMotion, onSelect})`
1817
+ returns `{update(spec), resize(), destroy(), instance?}`. Implementations must destroy engine
1818
+ resources and must not retain the component after teardown. `reducedMotion` is true under the
1819
+ system preference or inside a `data-zx-motion="reduced"` region, and an adapter must then turn its
1820
+ engine's animation off; Chart watches for the answer changing and updates the mounted engine.
1390
1821
  - **`ChartJsAdapter`** — construct it with a Chart.js constructor or namespace:
1391
1822
  `new ChartJsAdapter(window.Chart)`. The documentation pins Chart.js 4.5.1 as a development-only
1392
1823
  asset; `package.json` has no runtime dependencies and application builds explicitly choose how
1393
- the engine is loaded.
1824
+ the engine is loaded. Under reduced motion it sets `animation: false`, over any animation the
1825
+ application configured.
1394
1826
  - **Accessibility** — the canvas always has a name and optional description, while synchronized
1395
1827
  labels and dataset values live in a semantic table. `summary: 'hidden'` keeps it visually hidden
1396
1828
  for assistive technology; `'visible'` makes exact values part of the interface.
@@ -1409,7 +1841,18 @@ windowed loader.
1409
1841
  `locale`, `weekStart: 1`, `weekNumbers: true`, `workweek: false`, `agendaDays: 14`,
1410
1842
  `slotDuration: 30`, `slotMinTime: 0`, `slotMaxTime: 1440`, `scrollTime: 480`, `eventLimit: 3`,
1411
1843
  `selectable: false`, `editable: false`, `eventStartEditable`, `eventDurationEditable`,
1412
- `optimistic: true`, `nowIndicator: true`, `now`, `disabled`, and `renderEvent(event)`.
1844
+ `optimistic: true`, `nowIndicator: true`, `now`, `disabled`, `msg`, and `renderEvent(event)`.
1845
+ - **Messages** — every key is namespaced `calendar.*`: `calendar.today`, `calendar.previous`,
1846
+ `calendar.next`, `calendar.new`, `calendar.agenda`/`day`/`week`/`month`/`year` (the view
1847
+ switch), `calendar.viewSwitch`, `calendar.label` (the root's name), `calendar.loading`,
1848
+ `calendar.noEvents`, `calendar.allDay`, `calendar.more` and `calendar.weekNumber` (`%1`),
1849
+ `calendar.dayEvent`/`calendar.dayEvents`, `calendar.resize`, `calendar.moveStarted`,
1850
+ `calendar.resizeStarted`, `calendar.editPreview` (`%1` title, `%2`–`%3` the times),
1851
+ `calendar.editCancelled`, `calendar.editCommitted`. Until 6.0 the bare keys of earlier versions
1852
+ (`today`, `next`, `loading`, …, and `calendar` for `calendar.label`) are still honoured, read
1853
+ right after the namespaced key in the `msg` option and in the host translator; rename them.
1854
+ The built-in English now lives in the default catalogue rather than in `Calendar.defaults.msg`,
1855
+ so a host translator reaches every calendar string.
1413
1856
  - **Event input** — generic records default to `{id,title,start,end,allDay,color,location,editable,
1414
1857
  durationEditable}`. Every field has a string-or-function reader (`eventId`, `eventTitle`,
1415
1858
  `eventStart`, …); numeric dates use `dateUnit: 'milliseconds'|'seconds'`. Inputs are normalized
@@ -1431,6 +1874,9 @@ windowed loader.
1431
1874
  - **View layout** — agenda days keep every event in one vertically stacked content column. Month
1432
1875
  uses a continuous seven-day grid with full-height day boundaries and event lanes overlaid below
1433
1876
  the date labels; today is indicated with accent text instead of a decorative edge highlight.
1877
+ - **Appearance** — the view switch is one framed control with the current view on the neutral
1878
+ `--zx-color-bg-current` fill; weekday headings are sentence case; inner grid lines use
1879
+ `--zx-color-border-subtle`; the New button is the solid primary (`--zx-color-accent-solid`).
1434
1880
  - **Pointer editing** — a timed move preserves the point where the event was grabbed and snaps the
1435
1881
  resulting start to `slotDuration`. The lower grip stretches the event in its original day column,
1436
1882
  while pointer capture keeps either interaction active if the pointer leaves the event surface.
@@ -1453,6 +1899,8 @@ Declarative client-side filter bar producing a filtered array, commonly wired to
1453
1899
 
1454
1900
  - **Options** — `filters: [{ type: 'select'|'text'|'custom', id, label, field(s)|get, options?,
1455
1901
  predicate?, emptyLabel?, placeholder? }]`, `data`, `autoApply: true`, `clearLabel`.
1902
+ - **Messages** — `dataFilter.clear` ("Clear", unless `clearLabel` is given) and `dataFilter.all`
1903
+ ("All", a select filter's empty option unless it sets `emptyLabel`).
1456
1904
  - **Methods** — `setData()`, `apply()` → rows, `clear()`, `getState()`, `setState()`,
1457
1905
  `addFilter()`.
1458
1906
  - **Events** — `filter {rows, state}`.
@@ -1470,7 +1918,7 @@ form renders, with no per-application translation layer in between.
1470
1918
  - **Options** — `fields: Record<string, {type, label?, options?, min?, max?, step?, search?}>`,
1471
1919
  `order` (display order; omitted uses object order), `value` (the applied filters, drafted
1472
1920
  from), `mode: 'draft'|'live'`, `serialize: 'seconds'|'ms'|'iso'|(date, edge) => unknown`,
1473
- `emptyText`, `msg`.
1921
+ `timeZone` (IANA zone the days are counted in; null = the runtime's own), `emptyText`, `msg`.
1474
1922
  - **Types** — `text`, `select`, `date`, `date:range`, `int`, `float`, `number`, and
1475
1923
  `int:range` / `float:range` / `number:range`. An unrecognised type renders a visible,
1476
1924
  labelled placeholder naming it and is excluded from the value — never dropped silently.
@@ -1495,7 +1943,10 @@ Five behaviours are deliberate rather than incidental:
1495
1943
 
1496
1944
  `serialize` decides how a date leaves the component, because the answer is a wire-format
1497
1945
  decision rather than an application one. A range's upper bound takes the **end** of its day, or
1498
- a one-day range matches nothing.
1946
+ a one-day range matches nothing. `timeZone` decides *which* day: with the ZeyOS instance's zone
1947
+ (`'Europe/Vienna'`), 15 January serializes from 23:00 UTC on the 14th wherever the browser is, and
1948
+ an incoming Unix timestamp shows as the day it falls on there — the rule `zeyosForm()` and
1949
+ `zeyosTable()` apply to date-only fields. An unknown zone throws a `RangeError`.
1499
1950
  <!-- /doc -->
1500
1951
 
1501
1952
  <!-- doc:filter -->
@@ -1512,6 +1963,11 @@ adapter to compile into its own backend query language.
1512
1963
  debounce?}`. Built-in types cover text, number/money, date/datetime, boolean, enum/status/
1513
1964
  priority/country/currency/unit/entity, and tags. Compatible operators and their arity come from
1514
1965
  the exported `filterOperators` matrix; applications may explicitly override/add operator IDs.
1966
+ - **Relative dates** — `withinLast` and `withinNext` take a span, not two dates: the editor is a
1967
+ whole-number amount and a unit select, and the value is the pair `[amount, unit]` with `unit` one
1968
+ of `RELATIVE_DATE_UNITS` (`'days'|'weeks'|'months'`), e.g. `[7, 'days']`. `validate()` requires a
1969
+ positive whole amount and a known unit; `relativeDateValueError(value)` is the same check for an
1970
+ adapter. A value that is not such a pair (two dates) shows as an empty amount.
1515
1971
  - **Options** — `fields`, `value`, `operators`, `allowGroups: true`, `maxDepth: 3`,
1516
1972
  `maxConditions: 50`, `autoApply: false`, `readonly`, `disabled`, `applyLabel`, `clearLabel`,
1517
1973
  `searchable: false`, `layout: 'standard'|'compact'`, `rootLogic: null|'and'|'or'`,
@@ -1559,7 +2015,12 @@ mutate the caller's data.
1559
2015
  double-click), `check {ids, node}`.
1560
2016
  - **Keyboard** — ↑/↓ move, → expands then descends, ← collapses then ascends, Home/End, Enter
1561
2017
  activates, Space selects or toggles the checkbox, `*` expands every sibling, typing jumps to the
1562
- next match.
2018
+ next match. A typed character's default action is prevented, so find-as-you-type and page
2019
+ shortcuts do not act on it too; typing into a control inside a row is left to that control.
2020
+ - **Appearance** — the selected node of a single-selection tree is where you are: the neutral
2021
+ `--zx-color-bg-current` fill, the text colour, a semibold label. A `selection: 'multi'` tree is a
2022
+ set of picks and keeps the accent tint. Icons, folders included, are drawn in
2023
+ `--zx-color-text-subtle`; badges are plain muted counts.
1563
2024
  <!-- /doc -->
1564
2025
 
1565
2026
  <!-- doc:finder -->
@@ -1580,6 +2041,9 @@ breadcrumb.
1580
2041
  - **Keyboard** — ↑/↓ move within the active column, → steps into the selected branch and lands on
1581
2042
  its first row, ← returns to the parent column and truncates the path, Home/End, Enter opens a
1582
2043
  leaf, typing jumps to the next match.
2044
+ - **Appearance** — the selected row of each column is the path you are on: the neutral
2045
+ `--zx-color-bg-current` fill, the text colour and a semibold label, with subtle icons and
2046
+ column dividers.
1583
2047
  <!-- /doc -->
1584
2048
 
1585
2049
  <!-- doc:form -->
@@ -1587,7 +2051,8 @@ breadcrumb.
1587
2051
 
1588
2052
  #### Form
1589
2053
 
1590
- - **Options** — `fieldsets: []`, `actions: [button descriptors]`, `novalidate: true`.
2054
+ - **Options** — `fieldsets: []`, `actions: [button descriptors]`, `novalidate: true`,
2055
+ `idPrefix: null` (handed to the fieldsets it builds; a fieldset's own wins).
1591
2056
  - **Methods** (proxied across the fieldsets) — `getValues()`, `setValues()`, `getField()`,
1592
2057
  `setValue()`, `getValue()`, `reset()`, `setHighlights()`, `clearHighlights()`, `addFieldset()`,
1593
2058
  `setActions()`, `submit()`.
@@ -1596,21 +2061,68 @@ breadcrumb.
1596
2061
 
1597
2062
  #### Fieldset
1598
2063
 
1599
- - **Options** — `title`, `columns: 1|2|3`, `fields: { id: Field-options }`.
1600
- - **Methods** — `addField(id, opts)`, `getField`/`hasField`/`getFields`,
2064
+ - **Options** — `title` (the legend: a quiet sentence-case caption, small, medium weight, muted —
2065
+ the fields are the content), `columns: 1|2|3`, `fields: { id: Field-options }`,
2066
+ `idPrefix: null`.
2067
+ - **Keys and ids** — a field key names the value, not an element: it is the field's `name` (and
2068
+ a native control's `name`) and its `data-field-id`, never a DOM id by itself. Control ids are
2069
+ `<idPrefix>-<key>`, with a prefix generated per instance unless `idPrefix` is given, so a form
2070
+ mounted twice repeats no id and a field `stage` never collides with a `#stage` elsewhere; the
2071
+ label, description, and message ids derive from it (`<id>-label`). Radio groups are named after
2072
+ the control id. An `id` in the field options that differs from the key is used as given.
2073
+ - **Methods** — `addField(id, opts|Field|element|component)`, `getField`/`hasField`/`getFields`,
1601
2074
  `getValues`/`setValues`/`getValue`/`setValue`, `reset`, `clear`, `focus(id)`,
1602
2075
  `setHighlights`/`clearHighlights`.
2076
+ - **Adopting a component** — `addField(id, component)` wraps any component (or element) that is
2077
+ not a registered field type and drives it through its own API. Values: `getValue`/`setValue`,
2078
+ `get`/`set`, `getValues`/`setValues`, `isChecked`/`setChecked`, or a `value` property, tried in
2079
+ that order (a Toggle reads its on-value, as the `toggle` type does); an element is read through
2080
+ `value`, or `checked` for a checkbox or radio. Disabled: `setDisabled(bool)`, then
2081
+ `enable()`/`disable()`, then a `disabled` property; a plain wrapper gets `aria-disabled` and its
2082
+ form controls disabled. Read-only: `setReadonly`/`setReadOnly` or the element's `readOnly`. The
2083
+ component's own `change` becomes the field's `change`, once per edit. Writes keep the caller's
2084
+ `silent`: `setValues(values)` lets the component emit its own `change` to its own listeners and
2085
+ the field emit one `change`; `setValues(values, {silent: true})` and `reset()` stay quiet.
2086
+ Removing the field puts the component back, unsubscribes from it, and undoes the disabled and
2087
+ read-only state the field set.
1603
2088
 
1604
2089
  #### Field
1605
2090
 
1606
- - **Options** — `id`, `type: 'text'`, `label`, `description`, `value`, `placeholder`, `required`,
1607
- `disabled`, `options`, `layout: 'stack'|'inline'`, `props: {}`.
2091
+ - **Options** — `id` (the control's DOM id; generated when omitted), `name` (the field name and
2092
+ native control `name`; defaults to `id` on a standalone field), `type: 'text'`, `label`,
2093
+ `description`, `value`, `placeholder`, `required`, `disabled`, `readonly` (shown and focusable,
2094
+ not editable; ignored by types whose control has no read-only state, such as `checkbox`,
2095
+ `select`, `zxselect`, `date`, `time`), `options`, `layout: 'stack'|'inline'`, `props: {}`.
2096
+ - **Properties** — `controlId` (the control's DOM id), `name`.
1608
2097
  - **Built-in types** — `text`, `password`, `textarea`, `checkbox`, `int`, `float`, `select`,
1609
- `optionlist`, `hidden`, `html`.
1610
- - **Methods** — `getValue`/`setValue`, `focus`, `reset`, `setDisabled`,
2098
+ `optionlist`, `hidden`, `html` (display content as text: no input frame, a control's height,
2099
+ inset like the values around it in a quiet form).
2100
+ - **Methods** — `getValue`/`setValue`, `focus`, `reset`, `setDisabled`, `setReadonly`,
1611
2101
  `setHighlight(msg, kind)`, `clearHighlight`, `getInput`, `own(child)`, plus the statics
1612
2102
  `Field.register(type, adapter)` and `Field.has(type)`.
1613
- - **Events** — `change {value}`, `invalid {message}`.
2103
+ - **Events** — `change {value}`, once per committed edit whatever the type (a typed value on
2104
+ leaving the input, a slider on release, `code` when the editor loses focus after an edit);
2105
+ `input {value}` for every keystroke or drag step from the types that tell the two apart
2106
+ (`code`, `slider`); `invalid {message}`.
2107
+ - **Adapters** — `Field.register(type, (field, options) => ({el, get, set, focus, setDisabled,
2108
+ setReadonly?, destroy?, labelTarget?}))`. An adapter whose widget reports its own commits
2109
+ forwards them with `field.emit('change', {value})` and stops the native `change` its inner
2110
+ control bubbles (`field.listen(widget.el, 'change', (e) => e.stopImmediatePropagation())`), so
2111
+ the Field does not report the same edit twice.
2112
+ - **Labels** — small, medium weight, in the muted text colour: quieter than the values they name.
2113
+ With `layout: 'inline'` the label lines up with the control's first line.
2114
+
2115
+ #### Quiet fields
2116
+
2117
+ Inside any element with `data-zx-fields="quiet"` — an inspector, a property list, an inline record
2118
+ — fields read as values rather than a wall of boxes. Native inputs, selects and textareas, and the
2119
+ composite controls (`Select`, `TagPicker`, `NumberField`, `Datebox`, `DateRangeBox`, `Timebox`)
2120
+ show no border, fill, chevron, stepper or calendar button until hovered (the hover fill and the
2121
+ affordances come back) and their full field chrome and focus ring while focused or open. A
2122
+ NumberField's value starts where a quiet text input's text starts, and `html` display content is
2123
+ inset to match. Disabled
2124
+ and read-only controls never light up; an invalid Datebox keeps its danger border. It is a context
2125
+ attribute, not an option: set it on the container.
1614
2126
  <!-- /doc -->
1615
2127
 
1616
2128
  <!-- doc:form-widgets -->
@@ -1643,8 +2155,10 @@ stays a separate policy decision.
1643
2155
  - **Events** — `input {value}`, `change {value}`.
1644
2156
 
1645
2157
  Tab indents rather than moving focus, which is what a code surface has to do and what makes it a
1646
- keyboard trap if nothing else is arranged — Escape then Tab leaves the field. A `code` Form field
1647
- adapter is registered by `registerFieldAdapters()`.
2158
+ keyboard trap if nothing else is arranged — so Escape releases the capture until the next
2159
+ keystroke in the editor: Escape then Tab (or Shift+Tab) leaves the field, and any other key,
2160
+ or leaving the editor, restores indentation. A `code` Form field adapter is registered by
2161
+ `registerFieldAdapters()`.
1648
2162
  <!-- /doc -->
1649
2163
 
1650
2164
  <!-- doc:questionnaire -->
@@ -1768,12 +2282,18 @@ shows a fixed set, and `ValueList` takes free text with no catalogue. Unknown ID
1768
2282
  yet. Field type: `tagpicker`.
1769
2283
 
1770
2284
  - **Options** — `items`, `values: []`, `valueKey: 'ID'`, `labelKey: 'name'`, `iconKey: 'icon'`,
1771
- `colorKey: 'color'`, `searchKeys`,
2285
+ `colorKey: 'color'`, `toneKey: 'tone'`, `searchKeys`,
1772
2286
  `filter: 'local'|(query)=>Promise<items>`, `minQuery: 0`, `debounce: 200`, `allowCreate: false`,
1773
2287
  `max` (maximum tags; further options become `aria-disabled`), `closeOnSelect: false`,
1774
2288
  `placeholder`, `listHeight: 260`, `disabled`, `readonly`, `renderItem`, `renderTag`.
1775
- - **Tag identity** — icon values accept a bundled icon name or Node; colors are scoped to the tag
1776
- and its option through `--zx-tag-color`. Custom `renderItem`/`renderTag` remain authoritative.
2289
+ - **Tag identity** — icon values accept a bundled icon name or Node; colors (a CSS colour or a token
2290
+ reference such as `var(--zx-color-tone-blue-fg)`) are scoped to the tag and its option through
2291
+ `--zx-tag-color`. `toneKey` names a categorical tone from the Badge palette (`blue`, `teal`,
2292
+ `green`, `amber`, `orange`, `red`, `pink`, `violet`, `slate`): the tag takes its fill and ink as a
2293
+ toned Badge does (`data-tone`), and a tone wins over a colour. Custom `renderItem`/`renderTag`
2294
+ remain authoritative.
2295
+ - **Read-only** — `readonly` (or `setReadonly(true)`) shows only the tags: no input, chevron, or
2296
+ remove buttons, so a full row of tags never wraps onto an empty line.
1777
2297
  - **Methods** — `getValues()`, `getItems()`, `setValues(values, {silent})`, `addValue()`,
1778
2298
  `removeValue()`, `clear()`, `reset()`, `setItems()`, `isFull()`, `open()`/`close()`, `focus()`,
1779
2299
  `setReadonly()`, `enable()`/`disable()`.
@@ -1782,6 +2302,8 @@ yet. Field type: `tagpicker`.
1782
2302
  - **Keyboard** — ↓ opens and moves, ↑ moves, Enter toggles the active option (or creates one from
1783
2303
  the query), Escape closes, Backspace on an empty input removes the last tag, Home/End jump
1784
2304
  within the list.
2305
+ - **Quiet fields** — inside `[data-zx-fields="quiet"]` the tags read as the value: no border, fill
2306
+ or chevron until hover; full field chrome while focused or open.
1785
2307
  <!-- /doc -->
1786
2308
 
1787
2309
  <!-- doc:groupbox -->
@@ -1827,15 +2349,22 @@ component count. `statTile()` is the convenience form of `new StatTile(null, opt
1827
2349
  returns the component, the way `tooltip()` does; call `.toElement()` for the node to place.
1828
2350
 
1829
2351
  - **Options** — `label`, `value`, `format: 'number'|'currency'|'percent'|'fileSize'|null|fn`,
1830
- `currency`, `locale`, `delta`, `deltaKind: 'positive'|'negative'|'neutral'`, `deltaFormat`,
1831
- `deltaLabel`, `trend: number[]`, `icon`, `kind`, `href`, `clickable`, `loading`, `msg`.
2352
+ `currency`, `locale`, `caption`, `delta`, `deltaKind: 'positive'|'negative'|'neutral'`,
2353
+ `deltaFormat`, `deltaLabel`, `trend: number[]`, `icon`, `kind`, `href`, `clickable`, `loading`,
2354
+ `msg`.
2355
+ - **Appearance** — the label is a sentence-case muted line (13px, medium); the value is the loud
2356
+ part (24px, weight 650, tabular figures); `caption` is one muted context line under it ("of
2357
+ 13,184"). `icon` is a muted glyph beside the label, never a coloured chip; `kind` tints only the
2358
+ sparkline. The tile is a column with the sparkline at its foot, so tiles in a row are the same
2359
+ height and line up whether or not each has a caption, a delta or a trend.
1832
2360
  - **Methods** — `update(options)`, `getAccessibleName()`.
1833
2361
  - **Events** — `click {event}`.
1834
2362
 
1835
2363
  - **A delta is never colour alone.** It carries its sign and a direction glyph, and `deltaKind`
1836
2364
  decides the colour rather than the sign — rising ticket counts are not a success, and a
1837
2365
  component that assumes "up is good" is wrong for half the metrics in any business.
1838
- - **The accessible name is label + value + spoken delta** ("up 2 vs. last week"). The visible
2366
+ - **The accessible name is label + value (with its caption) + spoken delta** ("Active items,
2367
+ 2,184 of 13,184, up 2 vs. last week"). The visible
1839
2368
  `+2` row and the sparkline are `aria-hidden`, because a sparkline nobody can read is
1840
2369
  decoration and saying the delta twice is worse than saying it once.
1841
2370
  - **The element follows the behaviour** — `href` renders an `<a>`, `onclick` without `href` a
@@ -1990,10 +2519,12 @@ detail side of a master–detail screen. Give the root a size of its own (a grid
1990
2519
 
1991
2520
  Four appearances, all square-cornered. `divided` (the default) sets flat blocks on a muted track,
1992
2521
  split by hairline dividers, above a bordered panel, with the active block lifted onto the panel
1993
- surface under an accent rule. `bracket` outlines folder tabs that fuse into that same panel, the
1994
- active tab bridging the seam. `line` underlines the active tab across a full-width rule, for page-
1995
- and section-level navigation. `segmented` renders a compact group on a muted fill with the active
1996
- tab raised onto a control surface, so the row reads as one control inside a toolbar or card header.
2522
+ surface. `bracket` outlines folder tabs that fuse into that same panel, the active tab bridging
2523
+ the seam. `line` underlines the active tab in the text colour across a full-width rule, for page-
2524
+ and section-level navigation. `segmented` renders a compact framed group with the active tab on
2525
+ the neutral `--zx-color-bg-current` fill, so the row reads as one control inside a toolbar or card
2526
+ header. The active tab is where you are, not a pick: every variant gives it the text colour and a
2527
+ semibold label, and none uses the accent. Tab badges are neutral counts, not accent bubbles.
1997
2528
 
1998
2529
  `divided` and `bracket` draw the panel box themselves; `line` and `segmented` leave the panel bare
1999
2530
  for the surrounding layout to frame. The boxed variants read `--zx-tabbox-radius` (default `0px`)
@@ -2001,7 +2532,8 @@ from the component root, so `el.style.setProperty('--zx-tabbox-radius', 'var(--z
2001
2532
  restores rounded corners without forking the stylesheet.
2002
2533
 
2003
2534
  - **Options** — `tabs: [{ name, title, content, icon?, closable?, disabled? }]`, `active`,
2004
- `variant: 'divided'|'bracket'|'line'|'segmented'`, `keepAlive: true`.
2535
+ `variant: 'divided'|'bracket'|'line'|'segmented'`, `keepAlive: true`, `label` (the tab list's
2536
+ accessible name — give one whenever a page has more than one tab list).
2005
2537
  - **Methods** — `addTab()`, `removeTab()`, `openTab(name)`, `getActive()`, `setTitle()`,
2006
2538
  `setBadge(name, text|null)`, `enableTab()`/`disableTab()`.
2007
2539
  - **Events** — `change {name, previous}` (preventable), `close {name}`.
@@ -2065,7 +2597,7 @@ columns, in what shape. The parts have always existed (`Search`, `SortControl`,
2065
2597
  forgetting the same thing.
2066
2598
 
2067
2599
  - **Options** — `search: {placeholder?, value?, debounce?, clearable?}|false`, `count`,
2068
- `countText`, `views: Array<string|{id, label?, icon?}>` (string presets `auto|cards|table|list`),
2600
+ `countText`, `views: Array<string|{id, label?, icon?}>` (string presets `auto|table|list|cards|gallery|board`),
2069
2601
  `view`, `tools: [{id, label?, icon?, badge?, badgeKind?, pressed?, disabled?, onclick?}]`,
2070
2602
  `label`, `msg`.
2071
2603
  - **Methods** — `setCount()`, `setBadge()`, `setPressed()`, `setToolDisabled()`, `setTools()`,
@@ -2080,20 +2612,95 @@ Three properties are the reason to use this rather than assemble your own:
2080
2612
  rows out of twenty with nothing on screen saying why, and the badge is what prevents that.
2081
2613
  - **The count line is a live region**, and `setCount(null)` clears it. The previous number
2082
2614
  describes the previous filters; a stale count during a load is worse than none.
2083
- - **Tool buttons meet the 44 px target** in both densities.
2084
-
2085
- `bind(view)` is optional sugar for a `TableView` or `CardView`: it feature-detects rather than
2086
- importing them, adopts the view's shape, and mounts a field chooser built with
2087
- `fieldControls: {target}` into the toolbar. The toolbar is fully functional without it.
2615
+ - **Tool buttons meet the 44 px target** at any spacing.
2616
+
2617
+ The current view and a pressed tool take the neutral `--zx-color-bg-current` fill — a mode the
2618
+ list is in, not a pick — never the accent tint.
2619
+
2620
+ `bind(view)` is optional sugar for a `TableView`, `CardView`, or `KanbanView`: it feature-detects
2621
+ rather than importing them, adopts the view's shape, and mounts the view's field chooser into the
2622
+ tools slot through the view's public `mountFieldControls()` — for a default `TableView` that is its
2623
+ Table's column chooser. `unbind()` hands it back to the view, and a view destroyed first takes it
2624
+ with it. A chooser already mounted here with `fieldControls: {target: toolbar.getToolsElement()}`
2625
+ is left where it is. The toolbar is fully functional without `bind()`.
2626
+ <!-- /doc -->
2627
+
2628
+ <!-- doc:view-settings -->
2629
+ ### ViewSettings — the content of a view-settings drawer
2630
+
2631
+ Which fields a record view shows and in what order, which hidden field to bring back, what the
2632
+ records are grouped by, and which fields a board uses for columns and swim lanes. It is content,
2633
+ not a container: put it in a `Sheet`, a `Dropdown`, or a settings page.
2634
+
2635
+ - **Options** — `fields: ViewSettingsField[]` (a `ViewField` plus `type`, `groupable`, `locked`),
2636
+ `fieldOrder`, `hiddenFields`, `groupBy`, `grouping: boolean|{fields?}`,
2637
+ `board: {columnBy, swimlaneBy, showEmptyColumns?, fields?}|false` (`fields` restricts the
2638
+ Columns and Swim lanes choices; default: the Group by fields), `sections` (order and presence of
2639
+ `'fields'|'grouping'|'board'`), `types: Record<type, {icon?, label?}>`, `searchThreshold` (8),
2640
+ `headingLevel` (3), `label`, `msg`.
2641
+ - **Methods** — `getState()`, `setState(partial, {silent})`, `setFields()`, `getFields()`,
2642
+ `showField()`, `hideField()`, `moveField(id, index)`, `bind(view, {board?})`, `unbind()`,
2643
+ `focus()`, `destroy()`.
2644
+ - **Events** — `change {state, reason: 'order'|'visibility'|'group'|'board'}`,
2645
+ `fieldorderchange {order}`, `fieldvisibilitychange {visible, hidden}`, `groupchange {field}`,
2646
+ `boardchange {board}`.
2647
+ - **State** — `{fieldOrder, hiddenFields, groupBy, board}`: JSON-safe, ready to persist.
2648
+
2649
+ The shown fields are listed in order, each with a drag handle, its type glyph, and a hide button;
2650
+ the hidden ones follow with an explicit **Add** button, which puts the field at the end of the
2651
+ shown list. Above `searchThreshold` fields a search narrows the hidden list (label, id, and type
2652
+ name, ignoring case and accents) and Enter adds the first match. A `locked` field — the record
2653
+ title — has neither handle nor hide button and keeps its place while the others move around it.
2654
+ `locked` governs only what the panel offers: a bound view has no such notion, so a locked field the
2655
+ view hides itself appears under Hidden with Add like any other. The last shown field cannot be
2656
+ hidden.
2657
+
2658
+ Rows reorder by pointer (mouse, touch, and pen share one path: drag the handle, an insertion line
2659
+ shows where it lands, Escape cancels; near the top or bottom edge of the nearest scroll container,
2660
+ the container scrolls) and by keyboard on the focused handle: ArrowUp/ArrowDown
2661
+ move one step, Home/End to the ends, Space or Enter picks the row up and drops it, Escape returns
2662
+ it. Every move is announced in a polite live region ("Owner moved to position 3 of 6"). A redraw
2663
+ from outside — `setState()`, `setFields()`, a bound view's change — cancels a pick-up or drag in
2664
+ progress (announced) and keeps focus on the same field's control.
2665
+
2666
+ The type glyph comes from a built-in map that `types` extends: text, number (int, float,
2667
+ percent), money, date, datetime, time, boolean, select (enum), status, priority, entity, tags,
2668
+ url, email, image, file, rating. The type name is the glyph's `title` and visually hidden text,
2669
+ so it is seen and heard. Grouping and the board offer fields with `groupable !== false` and a
2670
+ discrete type (text, select, status, priority, boolean, entity), or `groupable: true`;
2671
+ `grouping.fields` restricts the Group by choices.
2672
+
2673
+ `bind(view)` connects a `TableView`, `CardView`, or `KanbanView`: it reads the view's fields and
2674
+ state, pushes order and visibility through `setFieldOrder()`/`setFieldVisible()`, and follows the
2675
+ view's own `statechange`; `unbind()` releases the view. A panel given its own `fields` lists
2676
+ those and no others (the title a card draws is not a column to offer); given none, it lists every
2677
+ field the view has. The view's labels and types win over the panel's descriptors, which add only
2678
+ `locked`, `groupable`, and a type where the view has just the `'text'` default. While bound the view owns the field list: call the view's `setFields()` — the
2679
+ panel's own `setFields()` is not pushed and lasts until the view's next change. Grouping and board
2680
+ changes are only emitted — the host applies them.
2088
2681
  <!-- /doc -->
2089
2682
 
2090
2683
  <!-- doc:pagination -->
2091
2684
  ### Pagination
2092
2685
 
2093
- The pager for a server-backed list, shaped to drop straight onto `Table` with
2094
- `sortMode: 'server'` or onto the `zeyosTable` binding — `getState()` hands back the `offset` and
2095
- `pageSize` a query needs. `page` is 1-based and always clamped into `[1, pages]`; a `total` of 0 is
2096
- treated as one empty page rather than zero pages.
2686
+ The pager for a server-backed list. It owns no data: `getState()` and every `change` hand back
2687
+ the `page`, `pageSize`, and zero-based `offset = (page - 1) * pageSize` a query needs. `page` is
2688
+ 1-based and always clamped into `[1, pages]`; a `total` of 0 is treated as one empty page rather
2689
+ than zero pages.
2690
+
2691
+ The ZeyOS bindings page differently — zero-based, `pageSize: 50`, appending pages above zero — so
2692
+ driving `zeyosTable()` (or `zeyosView()`) takes one mapping in the `change` listener:
2693
+
2694
+ ```js
2695
+ const binding = zeyosTable(client, 'contacts', { pageSize: 25 });
2696
+ const pager = new Pagination(host, { pageSize: binding.pageSize });
2697
+ pager.on('change', async ({ detail }) => {
2698
+ const result = await binding.load({ page: detail.page - 1, pageSize: detail.pageSize, replace: true });
2699
+ pager.setTotal(result.count ?? 0); // silent, so it cannot loop
2700
+ });
2701
+ ```
2702
+
2703
+ In `mode: 'loadmore'` each `change` is one `binding.loadMore()`, which appends.
2097
2704
 
2098
2705
  - **Options** — `page: 1`, `pageSize: 25`, `total: 0`, `pageSizes: [25, 50, 100]`, `siblings: 1`,
2099
2706
  `boundaries: 1`, `showPageSize: true`, `showSummary: true` (renders "26–50 of 312"),
@@ -2108,6 +2715,9 @@ treated as one empty page rather than zero pages.
2108
2715
  - **Semantics** — `<nav aria-label="Pagination">`; the current page carries `aria-current="page"`;
2109
2716
  previous/next are labelled icon buttons that go `disabled` at the ends; the ellipses are
2110
2717
  `aria-hidden` spans, not buttons.
2718
+ - **Appearance** — the summary and the page-size select sit together at the start of the bar,
2719
+ the pager alone at the end; the current page is the neutral `--zx-color-bg-current` fill with a
2720
+ semibold figure: where you are, not a call to action.
2111
2721
  <!-- /doc -->
2112
2722
 
2113
2723
  <!-- doc:navigation-bar -->
@@ -2126,6 +2736,9 @@ bar and wrong for a phone bottom bar, where the navigation is the point. `overfl
2126
2736
  every item at every width; `overflowBelow` moves the threshold; `minVisible: n` pins the first `n`
2127
2737
  items and overflows the rest. At the default the collapse is a container query with no observer
2128
2738
  installed; any other threshold installs a `ResizeObserver`, which is the cost of asking for one.
2739
+
2740
+ The current item is the neutral `--zx-color-bg-current` fill with the text colour and a semibold
2741
+ label; item badges are neutral counts. Only the brand mark keeps the accent.
2129
2742
  <!-- /doc -->
2130
2743
 
2131
2744
  <!-- doc:app-sidebar -->
@@ -2136,36 +2749,149 @@ vertical rail, and horizontal rail layouts. It mounts exactly one presentation a
2136
2749
  preserving active and expanded branch state. Only the expanded vertical state reveals descendants
2137
2750
  inline; every rail state opens descendants toward the workspace in anchored flyouts.
2138
2751
 
2139
- - **Options** — `items`, `active`, `expanded`, `collapsed`, `collapsible`,
2140
- `orientation: 'vertical'|'horizontal'`, `side: 'left'|'right'|'top'|'bottom'`, `label`,
2752
+ - **Options** — `items`, `active`, `expanded`, `collapsed`, `collapsible` (`false` removes the
2753
+ minimise and expand buttons), `orientation: 'vertical'|'horizontal'`,
2754
+ `side: 'left'|'right'|'top'|'bottom'`, `layout: 'tree'|'modules'`, `module`, `moduleBar`
2755
+ (`{appearance, labels: 'auto'|'beside'|'below'|'hidden', allModules, label}` for the module
2756
+ strip), `label`,
2141
2757
  `header`, `footer`, `railHeader`, `railFooter`, `openDelay: 80`, `closeDelay: 160`, and
2142
2758
  `renderIcon(item, context)`. Icon values may also be names, nodes, components, or factories.
2143
- - **Methods** — `setItems()`, `getItems()`, `setActive()`, `setExpanded()`, `getExpanded()`,
2144
- `toggleBranch()`, `expand()`, `collapse()`, `setCollapsed()`, `isCollapsed()`,
2759
+ - **Sections** — an item with `section: true` is a quiet heading over its `children`, which stay
2760
+ at its own level: never indented, collapsed, focused, or selected, and it needs no `id`.
2761
+ `actions` (a node) adds trailing controls such as an add button. Sections replace stacking
2762
+ several sidebars: one `<nav>`, one keyboard sequence, one rail. On a rail a section becomes a
2763
+ rule; in a flyout, a caption.
2764
+ - **Modules layout** — `layout: 'modules'` reads the top level as modules (`ModuleItem`s: `tone`
2765
+ or `color`, `badge`, `pinned`, `group`) and draws them as a `ModuleBar` strip beside a panel
2766
+ with one module's menu. A module with `children`, `header` (a node above its menu, such as its
2767
+ primary action), or `content` (a node below it, such as a tree or a history) is a panel module:
2768
+ choosing its tile shows its panel and fires a cancelable `modulechange`; any other module is a
2769
+ destination and fires `select`. The panel starts on `module`, else the module holding `active`,
2770
+ and follows `setActive()` into another module. A `section` at the top level heads its modules on
2771
+ the strip.
2772
+ - **Presentations** — the modules layout shows `'expanded'` (both menus: the strip names its
2773
+ modules beside their tiles, and the side menu shows), `'compact'` (the module menu collapsed to
2774
+ icons beside the side menu), or `'collapsed'` (the icon strip alone; every module's menu opens as
2775
+ a flyout beside its tile). A small three-way switch at the foot of the strip, above
2776
+ `railFooter`, chooses between them: an APG radio group ("Navigation layout") of one pictogram per
2777
+ presentation, in a row while the modules are named and a column on the icon strip; arrow keys
2778
+ move and choose, Home and End jump. `presentationSwitch: false` leaves it out when the choice
2779
+ lives in a menu instead: `presentationChoice({label})` returns the same choice as a menu
2780
+ `choice` entry (`account.setItems([sidebar.presentationChoice(), …])`; rebuild it after
2781
+ `presentationchange`), and `presentationGlyph(name)` is its pictogram. `presentations` (default all three) decides which it offers;
2782
+ `presentation` sets the first one; `getPresentation()`, `setPresentation()`, and
2783
+ `togglePresentation()` (the next one, wrapping — Mod+B in an application) drive it, and
2784
+ `presentationchange {presentation, previous}` reports it. `moduleBar.labels: 'hidden'` never
2785
+ names the modules. `railHeader`/`railFooter` are the strip's head and foot in every
2786
+ presentation. The arrow toward the panel moves from a tile into its menu, and Left Arrow on a
2787
+ top-level row of the menu returns to the tile.
2788
+ - **Methods** — `setItems()`, `getItems()`, `setActive()`, `setBadge(id, badge)`, `setExpanded()`,
2789
+ `getExpanded()`, `toggleBranch()`, `expand()`, `collapse()`, `setCollapsed()`, `isCollapsed()`,
2790
+ `setModule(id, {silent})`, `getModule()`, `setModuleLabels(labels)`, `getPresentation()`,
2791
+ `setPresentation(presentation, {silent})`, `togglePresentation()`, `presentationChoice({label})`,
2145
2792
  `setLayout({orientation, side})`, `openFlyout(id, {focus})`, `closeFlyout()`,
2146
- `closeAllFlyouts()`, `isFlyoutOpen()`, `focus()`, `destroy()`.
2793
+ `closeAllFlyouts()`, `isFlyoutOpen()`, `focus()`, `destroy()`. `getItems()` returns copies of
2794
+ the normalized tree; a leaf carries no `children` property.
2147
2795
  - **Events** — cancelable `select {item, id, value, href, event}`,
2148
- `branchchange {item, id, expanded, ids}`, `collapsechange {collapsed}`, and
2149
- `flyoutchange {item, id, open}`.
2796
+ `branchchange {item, id, expanded, ids}`, `collapsechange {collapsed}`,
2797
+ `flyoutchange {item, id, open}`, cancelable `modulechange {item, id, previous}`, and
2798
+ `presentationchange {presentation, previous}`.
2150
2799
  - **Presentation state** — horizontal navigation is always effectively minimized and never exposes
2151
2800
  an expand control. Switching layouts does not erase the saved vertical collapsed preference or
2152
- expanded branch IDs. `AppSidebar` is the single public application-navigation component; use
2153
- `collapsed: true` or `orientation: 'horizontal'` for its rail presentations.
2801
+ expanded branch IDs. `AppSidebar` is the one component that owns an application's navigation
2802
+ tree; use `collapsed: true` or `orientation: 'horizontal'` for its rail presentations, and
2803
+ `layout: 'modules'` for the module strip it composes from `ModuleBar`.
2804
+ - **Current state** — the current page is neutral: the `--zx-color-bg-current` fill, the full
2805
+ text colour and a semibold label, in the tree, the rail and every flyout; hover is a lighter
2806
+ step of the same overlay. A closed branch (or a rail item) over the active route takes the
2807
+ semibold label or the text colour, never the accent. Nothing is docked to an edge.
2154
2808
  - **Pointer and focus** — hover or focus reveals rail descendants without stealing focus. Click,
2155
2809
  Enter, Space, and the arrow toward the workspace also open a flyout; Escape, outside activation,
2156
2810
  or selection closes it. Expanded navigation supports arrows plus Home/End across visible rows.
2157
2811
  - **Semantics** — persistent navigation uses `<nav>` and nested lists with native links and
2158
- disclosure buttons, not menu or menubar roles. Icon-only destinations retain an accessible name
2812
+ disclosure buttons, not menu or menubar roles. The `<nav>` is the only landmark: a root the
2813
+ sidebar creates is a plain `<div>`, and a caller's own root keeps its element and attributes.
2814
+ Icon-only destinations retain an accessible name
2159
2815
  and Tooltip. A parent with children is disclosure-only; add an Overview child when it also needs
2160
2816
  a destination. Malformed and executable URL schemes are stripped; an item with no remaining
2161
2817
  link or callback is disabled in every presentation.
2162
2818
  <!-- /doc -->
2163
2819
 
2820
+ <!-- doc:module-bar -->
2821
+ ### ModuleBar — the module level of an application
2822
+
2823
+ One colour-coded tile per module, in a vertical strip or a horizontal bar: the level above an
2824
+ application's menus. Use it on its own — the classic module bar in an application header, beside
2825
+ a tree-layout `AppSidebar` — or through `AppSidebar`'s `layout: 'modules'`, which composes it.
2826
+
2827
+ ```js
2828
+ import { ModuleBar } from '/assets/zx.esm.js';
2829
+
2830
+ const bar = new ModuleBar(null, {
2831
+ items: [
2832
+ { id: 'mail', label: 'Mail', icon: 'envelope', tone: 'blue', badge: 7, children: [
2833
+ { id: 'inbox', label: 'Inbox', href: '#inbox' }, { id: 'sent', label: 'Sent', href: '#sent' }
2834
+ ] },
2835
+ { id: 'billing', label: 'Billing', icon: 'file-invoice', color: '#535494', href: '#billing' }
2836
+ ],
2837
+ active: 'inbox',
2838
+ onselect: (event) => { event.preventDefault(); route(event.detail.id); }
2839
+ });
2840
+ bar.setBadge('mail', 8);
2841
+ ```
2842
+
2843
+ - **Items** (`ModuleItem`) — the `AppNavItem` shape plus `tone` (a categorical tone that follows
2844
+ the theme) or `color` (any CSS colour; it wins), `pinned: true|number` (when any module is
2845
+ pinned, unpinned ones are under "All modules" only), `group` (the catalogue heading), and
2846
+ `description`. Children are the module's menu. A `section` at the top level heads its modules:
2847
+ a caption over them while names are printed beside the tiles, a short rule between groups on an
2848
+ icon strip or bar, and their group in the catalogue.
2849
+ - **Options** — `items`, `active` (a module or anything in a module's menu),
2850
+ `orientation: 'vertical'|'horizontal'`, `side`, `appearance: 'tint'|'solid'|'plain'` (the glyph
2851
+ on a tint of the module colour; the solid identity tile; monochrome glyphs),
2852
+ `labels: 'hidden'|'below'|'beside'` (names under the tiles of a vertical strip, or beside them —
2853
+ a navigation column of its own, a row per module with its count at the end),
2854
+ `menus: 'flyout'|'none'`, `expanded`, `controls`, `allModules: 'auto'|'always'|'never'`,
2855
+ `header`, `footer`, `label` (default "Modules"), `openDelay`, `closeDelay`, `renderIcon`.
2856
+ - **Menus** — with `menus: 'flyout'` a module's children open beside its tile on hover, focus,
2857
+ click, or the arrow toward the workspace, exactly like a rail. With `menus: 'none'` an adjacent
2858
+ panel the application owns shows them: every module then fires `select`, `setExpanded(id)`
2859
+ names the module the panel shows, and each tile with a menu reports `aria-expanded` and
2860
+ `aria-controls` the `controls` id.
2861
+ - **Overflow** — modules that do not fit move behind the "All modules" button; the current module
2862
+ (and the expanded one) always stays. The catalogue lists every module as a labelled tile,
2863
+ grouped by `group`, with a filter field past twelve modules. `partitionModules(items, capacity,
2864
+ keep, reserve)` is the pure function deciding what shows; `reserve` keeps the button's slot for
2865
+ `allModules: 'always'`.
2866
+ - **Methods** — `setItems()`, `getItems()`, `setActive()`, `setBadge(id, badge)` (in place, on the
2867
+ tile, in its menu, and in the catalogue), `setExpanded(id)`, `setLabels(labels)` (how a column
2868
+ expands to names and collapses to icons), `setLayout({orientation, side})`,
2869
+ `openFlyout()`, `closeFlyout()`, `closeAllFlyouts()`, `isFlyoutOpen()`, `openAllModules()`,
2870
+ `closeAllModules()`, `isAllModulesOpen()`, `focus()`, `destroy()`.
2871
+ - **Events** — cancelable `select {item, id, value, href, event}`, `flyoutchange {item, id, open}`,
2872
+ `overflowchange {visible, overflow}`, and `allmodules {open}`.
2873
+ - **Keyboard** — the bar is one Tab stop, on the current module. Arrow keys along the bar move
2874
+ between tiles and wrap; Home and End go to the first and last control; typing jumps to the next
2875
+ module whose name starts with the letters typed; the arrow toward the workspace opens a menu and
2876
+ moves into it; Escape, or the arrow back toward the strip, closes a menu onto its tile; Escape
2877
+ closes the catalogue onto its button, and so does tabbing out of it. In the catalogue, arrows
2878
+ move across the grid, typing goes to the filter field (past twelve modules), and Enter chooses.
2879
+ - **Semantics** — a `<nav>` of native links and buttons, never a menu or menubar. The module that
2880
+ is or holds the current page carries `aria-current` (`page` or `true`); an icon-only tile's
2881
+ accessible name is its label, with its count ("Mail (7)"), and it has a tooltip. Current is
2882
+ the neutral `--zx-color-bg-current` fill behind the tile, never the accent or an edge bar.
2883
+ - **Geometry** — a target is one control plus a space step, a tile one control less one, so
2884
+ `--zx-control-height` resizes the bar; a coarse pointer keeps a 40px target.
2885
+ <!-- /doc -->
2886
+
2164
2887
  <!-- doc:kernel -->
2165
2888
  ### Core helpers
2166
2889
 
2167
2890
  - **Component** — the base class: `on`/`off`/`once`/`emit`, `listen`, `toElement`, `msg`,
2168
- `destroy`, and the static `Component.from(el)`.
2891
+ `destroy`, and the static `Component.from(el)`. `msg(key, ...args)` always returns text: the
2892
+ instance's `msg` option (a `{[lang]: {…}}` table for the active language, then flat keys), else
2893
+ the host translator, else the class's built-in `msg` defaults, else the default English
2894
+ catalogue, and only then the key itself; `%1`, `%2`, … are filled from `args`.
2169
2895
  - **DOM** — `h(tag, props, ...children)`, `h.raw(html)`, `htmlEscape`, `resolveElement`.
2170
2896
  `__(tag#id.class, properties, content)` is Zx's compact DOM builder and public alias for
2171
2897
  `compactElement()`. It preserves `D*` data, `S*` style, boolean/built-in property, attribute,
@@ -2182,11 +2908,16 @@ inline; every rail state opens descendants toward the workspace in anchored flyo
2182
2908
  `{update, destroy}`. It anchors a manual popover with CSS anchor positioning where the browser
2183
2909
  supports it and a scroll/resize-tracked fallback where it does not. Options are resolved once,
2184
2910
  so changing a placement means `destroy()` and a fresh `position()` call.
2185
- - **i18n** — `setTranslator`, `setLanguage`, `getLanguage`, `translate`, `printf`.
2911
+ - **i18n** — `setTranslator`, `setLanguage`, `getLanguage`, `translate`, `printf`, and
2912
+ `defaultMessages`, the frozen English catalogue of every component key (`table.empty` → "No
2913
+ records") that a host copies as the starting point for a locale.
2186
2914
  - **Dates** — `formatDate(d, fmt)`, `parseDate(s, fmt)` (tokens
2187
2915
  `%d %m %Y %y %H %M %S %a %A %b %B %s`), `clampDate`, `isSameDay`, `addDays`, `addMonths`,
2188
2916
  `getWeekStart`.
2189
2917
  - **Keyboard** — `focusTrap`, `rovingTabindex`, `typeahead`.
2918
+ - **Motion** — `prefersReducedMotion(element?)` → boolean, and
2919
+ `watchMotion(element, fn, {signal?, immediate?})` → disposer, for code that animates in script;
2920
+ see Theming.
2190
2921
  - **Utils** — `debounce`, `uid`, `deepMerge`, `isElement`, `clamp`, `toArray`.
2191
2922
  <!-- /doc -->
2192
2923
 
@@ -2203,7 +2934,8 @@ import { loadFontAwesome, useFontAwesome, registerIcons } from '/assets/zx.esm.j
2203
2934
 
2204
2935
  await loadFontAwesome({ kit: 'ae8320b210', style: 'duotone' }); // injects the kit, switches over
2205
2936
  useFontAwesome(); // page already carries Font Awesome; no network
2206
- registerIcons({ rocket: ['0 0 512 512', 'M…'] }); // extend the bundled inline set
2937
+ registerIcons({ rocket: ['0 0 512 512', 'M…'] }); // a filled glyph (Font Awesome paths)
2938
+ registerIcons({ anchor: ['0 0 24 24', 'M12 6v16…', 'stroke'] }); // an outline glyph (Lucide paths)
2207
2939
  ```
2208
2940
 
2209
2941
  - `loadFontAwesome(kit | {kit, style, family, fixedWidth, activate})` — injects the kit script once
@@ -2212,7 +2944,20 @@ registerIcons({ rocket: ['0 0 512 512', 'M…'] }); // extend the bundled inli
2212
2944
  domains configured in the Font Awesome account.
2213
2945
  - `useFontAwesome(opts)` / `useBuiltinIcons()` / `configureIcons(opts)` / `getIconConfig()` —
2214
2946
  switch or read the renderer without touching the network.
2215
- - `iconNames()` lists every inline glyph; `registerIcons(map)` adds `[viewBox, path]` entries.
2947
+ - `iconNames()` lists every inline glyph, bundled and registered; `icons` is the bundled table.
2948
+ - `registerIcons(map)` adds or replaces inline glyphs. `[viewBox, path]` is a filled shape — the
2949
+ form Font Awesome path data comes in. `[viewBox, path, 'stroke']` is an outline drawn with the
2950
+ bundled pen; its stroke scales with the view box, so a 24-unit outline set such as Lucide renders
2951
+ exactly like the bundled glyphs. Anything else throws `TypeError`.
2952
+
2953
+ **The bundled set is outline.** The inline glyphs are Lucide icons (https://lucide.dev, ISC licence;
2954
+ the notice is in `src/core/icons.js`) on a 24-unit grid: `fill="none"`, `stroke="currentColor"`,
2955
+ round caps and joins, a stroke of 1.75 units — about 1.2px at the default 16px, the weight of
2956
+ 13–14px interface text — and never thinner than one pixel, so 11–13px glyphs stay legible. The
2957
+ names are Zx's own (`gear`, `dots`, `warning`, `success`, `drag`, …), not Lucide's. Colour follows
2958
+ `currentColor`; muting an icon is the caller's choice. The paint sits on the `<svg>` as attributes,
2959
+ so CSS on `.zx-icon` overrides it — `fill: currentColor` turns an outline into its solid form, which
2960
+ is how a state such as a set star or a liked heart is shown without a second glyph.
2216
2961
 
2217
2962
  **Names decide their own renderer.** A bare name (`'check'`) follows the active provider, and the
2218
2963
  built-in names are translated to their Font Awesome counterparts (`x` → `fa-xmark`, `search` →
@@ -2229,7 +2974,8 @@ itself once per distinct name through `console.warn`. Pass `icon(name, { strict:
2229
2974
  old `RangeError`, at a call site that can afford to fail: a test, or a build-time check.
2230
2975
 
2231
2976
  Font Awesome elements are sized with `font-size` and occupy 1em; bundled SVGs use width/height
2232
- attributes. Both honour `label` (`null` ⇒ `aria-hidden`, otherwise `role="img"` + `aria-label`).
2977
+ attributes (default 16). Both honour `label` (`null` ⇒ `aria-hidden`, otherwise `role="img"` +
2978
+ `aria-label`).
2233
2979
  <!-- /doc -->
2234
2980
 
2235
2981
  <!-- doc:helpers -->
@@ -2287,12 +3033,20 @@ entry point as the components. See also the Kernel section for `Component`, `h`,
2287
3033
  `copyToClipboard(text)` → `Promise<boolean>` uses the async Clipboard API and falls back to a
2288
3034
  hidden textarea, never rejecting.
2289
3035
  - **Translation** — `setTranslator(fn)`, `setLanguage(lang)`, `getLanguage()`,
2290
- `translate(key, args)`, `printf(str, args)` (`%s` placeholders). Every component routes its
2291
- built-in strings through `translate()`, so one translator covers the library, and
2292
- `setLanguage()` also drives locale-dependent behaviour such as the first day of the week.
3036
+ `translate(key, args)`, `printf(str, args)` (`%1`, `%2`, … placeholders), `defaultMessages`.
3037
+ Every component resolves its built-in strings through `msg()`, so one translator covers the
3038
+ library: per-instance `msg` option → `setTranslator()` function → the component's built-in
3039
+ defaults → the default English catalogue → the key. The translator answers one key at a time;
3040
+ null, undefined, or the key itself means "not translated" and English shows, so an application
3041
+ without a translator never sees a raw key. `defaultMessages` is that catalogue — copy it to
3042
+ start a locale (`setTranslator((key) => de[key])`). `setLanguage()` also selects the language
3043
+ table of a `msg` option and drives locale-dependent behaviour such as the first day of the week.
2293
3044
  - **Keyboard** — `focusTrap(container)` → `{activate, release}` confines Tab to a container;
2294
- `rovingTabindex(container, itemSelector, {orientation, wrap})` → `{focusFirst, focusLast,
2295
- destroy}` keeps one tab stop in a group; `typeahead(getItems, onMatch)` →
3045
+ `rovingTabindex(container, itemSelector, {orientation, wrap, rtl: 'auto'})` → `{focusFirst,
3046
+ focusLast, destroy}` keeps one tab stop in a group — horizontal arrows follow `dir="rtl"`, and a
3047
+ text field, select, or contenteditable item keeps Home/End and its arrows (an arrow leaves a text
3048
+ field only from the edge of its text); toggling a `<fieldset disabled>` around items inside the
3049
+ container moves the tab stop off them; `typeahead(getItems, onMatch)` →
2296
3050
  `(event|string) => void` resolves buffered printable keystrokes to an item (500 ms buffer,
2297
3051
  repeated-letter cycling).
2298
3052
  <!-- /doc -->
@@ -2333,15 +3087,54 @@ Registered elements: `<zx-toggle>`, `<zx-check-button>`, `<zx-select items='[…
2333
3087
  <!-- doc:tokens -->
2334
3088
  ### Design tokens
2335
3089
 
2336
- Two tiers of CSS custom properties. Dark mode and density are pure token/attribute swaps
2337
- (`[data-zx-theme="dark"]`, `[data-zx-density="compact"]`), and `tests/lint-tokens.js` forbids raw
2338
- colour literals and tier-1 references in component CSS.
2339
-
2340
- - **Tier 1** — the global palette in `styles/tokens/global.css`, never referenced by components:
2341
- `--zx-gray-0…950`, `--zx-green-*`, `--zx-red/amber/blue-*`, `--zx-space-1…8`,
2342
- `--zx-radius-sm/md/lg/full`, the type scale, shadows, and motion.
3090
+ Two tiers of CSS custom properties. Dark mode is a pure token/attribute swap
3091
+ (`[data-zx-theme="dark"]`), there is one geometry derived from `--zx-space-unit` and
3092
+ `--zx-radius`, and `tests/lint-tokens.js` forbids raw colour literals and tier-1 colour references
3093
+ in component CSS.
3094
+
3095
+ - **Tier 1** — the global palette and primitives in `styles/tokens/global.css`: `--zx-gray-0…950`,
3096
+ `--zx-emerald-*` (the brand ramp the default accent reads), the status ramps
3097
+ `--zx-green/red/amber/blue-*` (green is a yellow-green, deliberately apart from the brand), the
3098
+ tone ramps `--zx-tone-<hue>-300/500/700` — none of them referenced by components — plus the
3099
+ spacing scale (`--zx-space-unit`, `--zx-space-1…8`), the radius scale (`--zx-radius`,
3100
+ `--zx-radius-sm/md/lg/xl/full`), the type scale (`--zx-text-xs…3xl`, 12–30px), weights
3101
+ (`--zx-weight-medium` 500, `--zx-weight-semibold` 600), tracking (`--zx-tracking-tight`
3102
+ -0.01em, set on `.zx-scope`; `--zx-tracking-title` -0.015em), shadows, and motion
3103
+ (`--zx-dur-fast` 120ms, `--zx-dur-base` 180ms — both 0ms in a reduced-motion region — and the
3104
+ curves `--zx-ease`, `--zx-ease-spring`, `--zx-ease-drawer`).
2343
3105
  - **Tier 2** — the semantic set, the only tokens components may use: `--zx-color-*`,
2344
- `--zx-control-*`, `--zx-focus-ring`, `--zx-overlay-shadow`.
3106
+ `--zx-control-*`, `--zx-focus-ring`, `--zx-surface-shadow` (the contact shadow of a resting
3107
+ surface: a panel, an outlined card in a lane, a slider thumb), `--zx-overlay-shadow`,
3108
+ `--zx-drag-shadow` (an item being dragged; its own value in dark, with a faint light rim). The
3109
+ lint also rejects the accent ramp `--zx-accent-*`, the module palette `--zx-module-*`, and the
3110
+ raw `--zx-shadow-*` steps in component and layout CSS, and any `outline: 0`/`outline: none`:
3111
+ forced-colors mode drops the box-shadow focus ring, so components make the outline transparent
3112
+ and `styles/base.css` repaints it in a system colour there. The roles that carry the visual
3113
+ language:
3114
+ - **Columns.** `--zx-color-bg-surface` is the working canvas; `--zx-color-bg-chrome` is the
3115
+ navigation and inspector tone beside it (one step below the canvas in light, between page and
3116
+ canvas in dark), so tone rather than a line separates them. Dividers inside a column use
3117
+ `--zx-color-border-subtle`; `--zx-color-border` is for frames.
3118
+ - **Current vs selected.** Where you *are* — the active navigation item, the current row, the
3119
+ open node, the active view — is neutral: `--zx-color-bg-current` (translucent text, about 8%,
3120
+ so it works on any column), text colour, weight 600. Hover is `--zx-color-bg-hover`, about half
3121
+ as strong. What you have *picked* keeps the accent tint `--zx-color-bg-selected`.
3122
+ - **Text.** `--zx-color-text`, `--zx-color-text-muted` (secondary text, 4.5:1 on every column and
3123
+ on a current row), `--zx-color-text-subtle` (icons and non-essential meta, 3:1),
3124
+ `--zx-color-text-placeholder`.
3125
+ - **Solid accent.** `--zx-color-accent-solid` (`-hover`, `-active`) with
3126
+ `--zx-color-on-accent-solid` is the one primary action per screen: the 600 stop with white ink in
3127
+ both themes. `--zx-color-accent` is the accent as text, marks, and focus (600 in light, 400 in
3128
+ dark) and pairs with `--zx-color-on-accent` when something is filled with it.
3129
+ - **Status.** `--zx-color-success/warning/danger/info` and their `-bg` fills mean state and
3130
+ nothing else; they ship with an icon or a word.
3131
+ - **Tones.** `--zx-color-tone-{blue,teal,green,amber,orange,red,pink,violet,slate}-bg` and `-fg`:
3132
+ categorical colour for tags, chips, icon tiles, and avatars — neither the accent nor a status. A
3133
+ soft fill (13% of the tone over the surface in light, 20% in dark) with ink at 4.5:1 or better.
3134
+ - **Chart series.** `--zx-color-chart-1…8`, assigned in order, never cycled: blue, orange, teal,
3135
+ amber, pink, green, violet, red. Neighbours stay apart under red-green colour-vision deficiency
3136
+ and every series clears 3:1 as a mark on the light and the dark surface; the list does not open
3137
+ on the accent.
2345
3138
  - **Component styling hooks** — a short, deliberately small set of component-level custom
2346
3139
  properties for restyling one component without touching its internals. Each falls back to the
2347
3140
  semantic token it replaces, so setting none of them changes nothing:
@@ -2350,4 +3143,13 @@ colour literals and tier-1 references in component CSS.
2350
3143
  Reach for the least specific override that works: a global semantic token first, one of these
2351
3144
  second, an internal selector never. Component CSS declares many other `--zx-<component>-*`
2352
3145
  properties — those are internal geometry, not API, and will change without notice.
3146
+
3147
+ **Base typography.** `.zx-scope` sets Inter with `--zx-tracking-tight` and the `cv11` feature (the
3148
+ single-storey a, chosen because it reads rounder and calmer at 13–14px; fonts without it ignore it).
3149
+
3150
+ **Quiet fields.** Put `data-zx-fields="quiet"` on a property panel, an inspector, or an inline
3151
+ record: native inputs, selects, and textareas inside it drop their border and fill until hover
3152
+ (`--zx-color-bg-hover`) or focus (the normal border, the control fill, and the focus ring), so values
3153
+ read as text rather than as a wall of boxes. Read-only and disabled fields stay quiet. The rules have
3154
+ zero specificity, so a component's own input styling still wins.
2353
3155
  <!-- /doc -->