@zeyos/zx 5.0.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 (154) hide show
  1. package/CHANGELOG.md +164 -0
  2. package/README.md +2 -1
  3. package/dist/types/components/account-menu/account-menu.d.ts +8 -0
  4. package/dist/types/components/activity-list/activity-list.d.ts +9 -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 +288 -14
  7. package/dist/types/components/calendar/calendar.d.ts +67 -5
  8. package/dist/types/components/card-view/card-view.d.ts +7 -4
  9. package/dist/types/components/card-view/record-card.d.ts +11 -0
  10. package/dist/types/components/context-menu/context-menu.d.ts +6 -2
  11. package/dist/types/components/data-filter/data-filter.d.ts +4 -4
  12. package/dist/types/components/dropdown/dropdown.d.ts +22 -7
  13. package/dist/types/components/field/field.d.ts +30 -0
  14. package/dist/types/components/fieldset/external-adapter.d.ts +83 -0
  15. package/dist/types/components/fieldset/fieldset.d.ts +7 -0
  16. package/dist/types/components/file-list/file-list.d.ts +35 -3
  17. package/dist/types/components/filter/filter.d.ts +127 -3
  18. package/dist/types/components/filter-panel/filter-panel.d.ts +36 -7
  19. package/dist/types/components/launcher/launcher.d.ts +8 -0
  20. package/dist/types/components/list-toolbar/list-toolbar.d.ts +15 -6
  21. package/dist/types/components/menu-button/menu-button.d.ts +6 -0
  22. package/dist/types/components/message/message.d.ts +14 -1
  23. package/dist/types/components/modal/modal.d.ts +30 -0
  24. package/dist/types/components/module-bar/module-bar.d.ts +395 -0
  25. package/dist/types/components/pagination/pagination.d.ts +17 -3
  26. package/dist/types/components/select/select.d.ts +6 -0
  27. package/dist/types/components/sheet/sheet.d.ts +15 -0
  28. package/dist/types/components/table/table.d.ts +265 -17
  29. package/dist/types/components/view/record-view.d.ts +59 -2
  30. package/dist/types/components/view-settings/view-settings.d.ts +879 -0
  31. package/dist/types/components/workspace-switcher/workspace-switcher.d.ts +327 -0
  32. package/dist/types/core/component.d.ts +12 -2
  33. package/dist/types/core/i18n.d.ts +27 -3
  34. package/dist/types/core/keyboard.d.ts +98 -1
  35. package/dist/types/core/messages.d.ts +382 -0
  36. package/dist/types/core/overlay-host.d.ts +28 -1
  37. package/dist/types/core/overlay-stack.d.ts +260 -0
  38. package/dist/types/index.d.ts +5 -2
  39. package/dist/types/internal/app-rail.d.ts +38 -0
  40. package/dist/types/internal/menu-items.d.ts +71 -3
  41. package/dist/types/zeyos/index.d.ts +1 -0
  42. package/dist/types/zeyos/launcher.d.ts +9 -0
  43. package/dist/types/zeyos/module-bar.d.ts +176 -0
  44. package/dist/types/zeyos/query.d.ts +31 -5
  45. package/dist/types/zeyos/table.d.ts +245 -8
  46. package/dist/types/zeyos/view.d.ts +16 -2
  47. package/dist/zx-zeyos.esm.js +4422 -430
  48. package/dist/zx-zeyos.esm.min.js +5 -5
  49. package/dist/zx.css +1417 -134
  50. package/dist/zx.esm.js +6986 -830
  51. package/dist/zx.esm.min.js +8 -8
  52. package/dist/zx.global.js +6986 -830
  53. package/dist/zx.global.min.js +8 -8
  54. package/dist/zx.min.css +1 -1
  55. package/docs/api.json +794 -17
  56. package/docs/llms.md +503 -63
  57. package/docs/llms.txt +6 -2
  58. package/package.json +1 -1
  59. package/src/components/account-menu/account-menu.js +12 -0
  60. package/src/components/activity-list/activity-list.js +27 -1
  61. package/src/components/app-icon/app-icon.css +13 -2
  62. package/src/components/app-icon/app-icon.js +11 -1
  63. package/src/components/app-rail/app-rail.css +19 -0
  64. package/src/components/app-sidebar/app-sidebar.css +204 -3
  65. package/src/components/app-sidebar/app-sidebar.js +646 -40
  66. package/src/components/breadcrumb/breadcrumb.css +1 -1
  67. package/src/components/calendar/calendar.css +1 -1
  68. package/src/components/calendar/calendar.js +26 -52
  69. package/src/components/card/card.css +2 -2
  70. package/src/components/card-view/card-view.css +5 -5
  71. package/src/components/card-view/card-view.js +13 -9
  72. package/src/components/card-view/record-card.js +14 -3
  73. package/src/components/code-editor/code-editor.css +1 -1
  74. package/src/components/code-editor/field-adapter.js +9 -2
  75. package/src/components/context-menu/context-menu.js +8 -2
  76. package/src/components/copy/copy-input.css +1 -1
  77. package/src/components/data-filter/data-filter.js +2 -2
  78. package/src/components/date-picker/time-picker.css +2 -2
  79. package/src/components/dock/dock.css +5 -4
  80. package/src/components/dropdown/dropdown.js +157 -72
  81. package/src/components/field/field.js +33 -3
  82. package/src/components/fieldset/external-adapter.js +363 -0
  83. package/src/components/fieldset/fieldset.js +10 -59
  84. package/src/components/file-list/file-list.js +144 -21
  85. package/src/components/filter/filter.css +86 -2
  86. package/src/components/filter/filter.js +300 -8
  87. package/src/components/filter-panel/filter-panel.js +149 -32
  88. package/src/components/finder/finder.css +1 -1
  89. package/src/components/kanban-view/kanban-view.css +3 -3
  90. package/src/components/kanban-view/kanban-view.js +5 -2
  91. package/src/components/launcher/launcher.css +1 -1
  92. package/src/components/launcher/launcher.js +29 -3
  93. package/src/components/list-toolbar/list-toolbar.js +39 -13
  94. package/src/components/master-panel/master-panel.js +0 -1
  95. package/src/components/menu-button/menu-button.css +63 -0
  96. package/src/components/menu-button/menu-button.js +97 -11
  97. package/src/components/message/message.js +60 -20
  98. package/src/components/modal/modal.css +1 -1
  99. package/src/components/modal/modal.js +66 -13
  100. package/src/components/module-bar/module-bar.css +486 -0
  101. package/src/components/module-bar/module-bar.js +1074 -0
  102. package/src/components/multi-value-editor/multi-value-editor.css +1 -1
  103. package/src/components/number-field/field-adapter.js +2 -1
  104. package/src/components/pagination/pagination.css +5 -5
  105. package/src/components/pagination/pagination.js +20 -4
  106. package/src/components/panel/panel.css +3 -2
  107. package/src/components/questionnaire/questionnaire.css +4 -4
  108. package/src/components/rating/field-adapter.js +2 -1
  109. package/src/components/rating/rating.css +1 -1
  110. package/src/components/search/search.css +4 -1
  111. package/src/components/select/select.js +9 -0
  112. package/src/components/sheet/sheet.css +1 -1
  113. package/src/components/sheet/sheet.js +74 -32
  114. package/src/components/slider/field-adapter.js +6 -1
  115. package/src/components/slider/slider.css +2 -2
  116. package/src/components/split-view/split-view.css +1 -1
  117. package/src/components/stat-tile/stat-tile.css +1 -1
  118. package/src/components/stepper/stepper.css +1 -1
  119. package/src/components/tabbox/tabbox.css +9 -5
  120. package/src/components/table/table.css +86 -11
  121. package/src/components/table/table.js +628 -57
  122. package/src/components/tag-picker/field-adapter.js +2 -1
  123. package/src/components/tooltip/tooltip.js +35 -17
  124. package/src/components/tree/tree.css +1 -1
  125. package/src/components/view/record-view.css +1 -1
  126. package/src/components/view/record-view.js +178 -18
  127. package/src/components/view-settings/view-settings.css +285 -0
  128. package/src/components/view-settings/view-settings.js +1830 -0
  129. package/src/components/workspace-switcher/workspace-switcher.css +188 -0
  130. package/src/components/workspace-switcher/workspace-switcher.js +463 -0
  131. package/src/core/component.js +88 -17
  132. package/src/core/fontawesome.js +5 -1
  133. package/src/core/i18n.js +47 -4
  134. package/src/core/icons.js +12 -2
  135. package/src/core/keyboard.js +206 -11
  136. package/src/core/messages.js +427 -0
  137. package/src/core/overlay-host.js +64 -9
  138. package/src/core/overlay-stack.js +487 -0
  139. package/src/core/position.js +22 -3
  140. package/src/index.js +5 -2
  141. package/src/internal/app-rail.js +134 -53
  142. package/src/internal/menu-items.js +61 -3
  143. package/src/zeyos/form.js +5 -3
  144. package/src/zeyos/index.js +3 -0
  145. package/src/zeyos/launcher.js +8 -3
  146. package/src/zeyos/module-bar.js +181 -0
  147. package/src/zeyos/query.js +44 -13
  148. package/src/zeyos/table.js +220 -80
  149. package/src/zeyos/view.js +43 -93
  150. package/styles/base.css +16 -1
  151. package/styles/print.css +1 -1
  152. package/styles/tokens/dark.css +4 -0
  153. package/styles/tokens/semantic.css +56 -28
  154. package/styles/zx.css +3 -0
package/docs/llms.md CHANGED
@@ -61,7 +61,10 @@ Themes: `light` | `dark` | `auto` (follows OS).
61
61
  (`--zx-space-unit`, 4px: `--zx-space-1…8` = 4/8/12/16/20/24/32/40px), radius steps are fractions of
62
62
  one base (`--zx-radius`, 4px: `-sm/-md/-lg/-xl` = 2/3/4/6px; `-full` only for shapes whose roundness
63
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:
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:
65
68
 
66
69
  ```css
67
70
  :root { --zx-space-unit: 3.5px; --zx-control-height: 28px; }
@@ -114,7 +117,9 @@ accent), `--zx-color-bg-chrome` (navigation and inspector columns, one tone off
114
117
  `--zx-color-on-accent-solid` (the one filled primary button per screen),
115
118
  `--zx-color-danger/warning/success/info(+ -bg)`, the categorical tone palette
116
119
  `--zx-color-tone-{blue,teal,green,amber,orange,red,pink,violet,slate}-bg/-fg` (tags, chips, icon
117
- tiles, avatars — neither the accent nor a status), the chart series `--zx-color-chart-1…8`,
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`,
118
123
  `--zx-focus-ring`, `--zx-control-height/-radius`, `--zx-space-*`, `--zx-radius-*`, `--zx-text-*`
119
124
  (`-3xl` for document titles), `--zx-weight-medium/-semibold`, `--zx-tracking-tight/-title`. Never
120
125
  use tier-1 palette tokens (`--zx-gray-*`, `--zx-green-*`, `--zx-emerald-*`, `--zx-tone-*`) or raw
@@ -220,12 +225,20 @@ await records.saveView({ name: 'My open invoices', setDefault: true });
220
225
  behavior. `load` maps stored timestamps to `Date`; `save` validates and creates/updates, maps back
221
226
  to Unix seconds, calls `onSaved`, and shows a success toast. Its toasts (that confirmation and a
222
227
  failed request's report) appear in the theme scope of `scope` — default: the form's own element,
223
- so a form in a scoped dialog toasts inside that scope; `notify: false` raises none, leaving the
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
224
230
  promise's result or rejection to the caller.
225
231
  - `zeyosTable(client, resource, opts)` returns
226
- `{table, load(), setSearch(), setFilters(), loadMore(), count, page, hasMore, destroy()}`. Columns
227
- come from schema metadata, sorting uses `sortMode:'server'`, and page zero replaces rows while
228
- later pages append them using server `limit`/`offset` and `count`. Table presentation options and
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
229
242
  listeners pass through — `variant`, `zebra`, `stickyHeader`, `responsive`, `growing`, `emptyText`,
230
243
  `rowClass`, `rowKind(s)`, `columnVisibility`, `columnReorder`, `hiddenColumns`, `on<event>`;
231
244
  `onrowclick` runs beside `onRowClick`. The binding owns `columns`, `data`, and `sortMode`, and
@@ -234,7 +247,8 @@ await records.saveView({ name: 'My open invoices', setDefault: true });
234
247
  complete projection, and view options. `zeyosView(client, resource, CardView|TableView|KanbanView,
235
248
  opts)` returns `{view, load(), setSearch(), setFilters(), loadMore(), count, page, hasMore,
236
249
  savedViews, ready, restoreDefault(), captureView(), saveView(), applyView(), destroy()}` with
237
- 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
238
252
  even when hidden. Without `savedViews`, the host may persist `view.getViewState()` directly.
239
253
  - `createSavedViewRegistry(transport, {userId,workspaceId,resource})` coordinates private named
240
254
  Table/Card/Kanban configurations as one versioned, atomically replaced document. Entries capture
@@ -264,8 +278,10 @@ instance's zone — a day, not an instant: tables show it with no time, and form
264
278
  so neither the viewer's zone nor daylight saving moves it. Pass the instance's IANA zone as
265
279
  `timeZone` (table, form, view; default: the runtime's own); `unixSecondsToCalendarDay(seconds, tz)`
266
280
  and `calendarDayToUnixSeconds(date, tz)` do the same conversion for application code. Use
267
- `dataFilterStateToFilters(state, defs)` before `tableBinding.setFilters(...)` when wiring a
268
- `DataFilter`.
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.
269
285
 
270
286
  ### Module icons and colours
271
287
 
@@ -303,6 +319,41 @@ registerModules({ tickets: '#f04639', 'my-fork': { label: 'My Fork', icon: 'zeyo
303
319
  throw.
304
320
  - `registerModules(map)` adds forks and weblets or overrides shipped entries (a bare string sets
305
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.
306
357
  <!-- /doc -->
307
358
 
308
359
  ## Custom elements
@@ -384,8 +435,10 @@ a `MenuButton`, where choosing one activates the original control.
384
435
  `setActive(name, active?)`, `destroy()`.
385
436
  - **Events** — `action {name, item}`, emitted for descriptor-built items in addition to their own
386
437
  `onclick`, from the row and from the overflow menu alike.
387
- - **Keyboard** — Tab reaches the toolbar once; ArrowLeft/ArrowRight move between controls and wrap;
388
- 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.
389
442
  - **Pressed items** — an item with `active` renders `aria-pressed="true"` and takes the neutral
390
443
  `--zx-color-bg-current` fill, never the accent.
391
444
  <!-- /doc -->
@@ -524,7 +577,9 @@ Zx owns ranking, focus, request cancellation, and presentation; ZeyOS owns the c
524
577
  permissions, recent-history/cache policy, and the route or command ultimately invoked.
525
578
 
526
579
  - **Constructor** — `new Launcher(null, options)` owns a top-layer dialog in the opener's nearest
527
- 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;
528
583
  `new Launcher(existingDialog, options)` enhances and later restores an existing `<dialog>`.
529
584
  - **Options** — `items`, abortable `sources: [{id, label?, minQuery?, when?, order?, load(query, {signal})}]`,
530
585
  `query`, `debounce: 250`, `minQuery: 0`, `maxResults: 100`, `placeholder`, `label`, `emptyText`,
@@ -584,7 +639,9 @@ A fixed-size, product-agnostic application identity. It keeps glyph geometry sta
584
639
  sidebars, entity results, and recent-item rows while identity colour and material remain explicit
585
640
  options rather than application globals.
586
641
 
587
- - **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`,
588
645
  `iconSize: '52%'`, `label`, `badge`, `selected: false`, `shape: 'tile'|'circle'`,
589
646
  `glass: false|'subtle'|'strong'`, and
590
647
  additional `class` names.
@@ -697,6 +754,11 @@ Explicit duplicate file IDs are rejected; source order is preserved.
697
754
  - **Events** — cancelable `activate {id,index,file,href,event}` and
698
755
  `action {id,fileId,index,action,file,event}`, plus `datachange {items}` after a collection
699
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.
700
762
  <!-- /doc -->
701
763
 
702
764
  <!-- doc:activity-item -->
@@ -738,6 +800,9 @@ whether a feed is newest- or oldest-first.
738
800
  `updateItem(id,values,{silent?})`, `removeItem(id,{silent?})`, `setLoading()`, and `destroy()`.
739
801
  - **Events** — `datachange {items}` after mutation. Each child's cancelable `zx-action` bubbles
740
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>`.
741
806
  - **Grouping** — groups keep first-seen group order and item source order. The exported pure
742
807
  `groupActivityItems()` and `normalizeActivityItems()` helpers do not mutate caller arrays.
743
808
  <!-- /doc -->
@@ -751,7 +816,9 @@ out, or mutates the session.
751
816
 
752
817
  - **Options** — `account: {name, secondary, src?, initials?, status?, statusLabel?}`, `items`,
753
818
  `compact: false`, `label`, and any top/bottom/left/right `placement`.
754
- - **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()`,
755
822
  `getPanel()`, `destroy()`.
756
823
  - **Events** — cancelable `select {value, item}`, `open`, and `close`. A canceled selection does
757
824
  not run the item's callback and leaves the menu open, so an application can reject or confirm an
@@ -763,6 +830,47 @@ out, or mutates the session.
763
830
  would give it, whatever order the stylesheets load in.
764
831
  <!-- /doc -->
765
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`.
872
+ <!-- /doc -->
873
+
766
874
  <!-- doc:select -->
767
875
  ### Select — APG editable combobox
768
876
 
@@ -775,8 +883,9 @@ Unifies single-select, local filtering, and async loading.
775
883
  `clearable: false`, `filter: false | 'local' | async (query)=>items`, `searchKeys`,
776
884
  `minQuery: 0`, `debounce: 200`, `listHeight: 280`, `groupKey`.
777
885
  - **Getters** — `.value`, `.selected`.
778
- - **Methods** — `set(id, {silent})`, `setItems()`, `setFixedItems()`, `setActions()`, `reset()`,
779
- `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()`.
780
889
  - **Events** — `change {value, item}` (`item` is null on clear), preventable
781
890
  `action {id, action, query, select}`, `open`, `close`, `query {query}`, `loaded {items}`.
782
891
  - **Fixed choices** — `fixedItems` pins entries to the top of the list, separated by a rule (the
@@ -1072,7 +1181,10 @@ messages rather than the page, and uses the translucent glass surface tokens wit
1072
1181
  when its last toast closes. One whose host was emptied or left the document is replaced on the
1073
1182
  next toast rather than reused, and a scope that is itself detached falls back to the focused
1074
1183
  element's scope. `Message.disposeRegion(scope)` → boolean removes a scope's region at once,
1075
- toasts and all, for a host torn down while toasts are still showing.
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.
1076
1188
  - **Per-message options** — `kind: 'info'`, `timeout`, `closable: true`, `maxVisible: 5` (extras
1077
1189
  queue, and hovering pauses dismissal).
1078
1190
  - **Action** — `action: { label, onclick?, kind? }` renders one small button (`.zx-btn`,
@@ -1154,7 +1266,16 @@ When a Dock adopts it, the Sheet becomes an opaque layout surface instead.
1154
1266
  the browser through `showModal()`. Floating `'trap-focus'` and `false` sheets use a manual
1155
1267
  Popover top layer, so they stay viewport-anchored even inside a transformed or clipped theme
1156
1268
  scope while the page keeps scrolling and stays interactive. Escape and outside-click are
1157
- 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
1158
1279
  explicitly transparent and non-filtering; `backdrop` is meaningful only when `modal` is `true`.
1159
1280
  A docked Sheet remains a native non-modal dialog in its Dock's flow.
1160
1281
  - **Resizing and detents** — `resizable` makes the inner edge draggable between `min` and `max`.
@@ -1240,7 +1361,25 @@ DOM child of the anchor's nearest Zx theme scope while the browser lifts it into
1240
1361
  - **Hosting** — the host is resolved again on every `open()`. A dropdown created before its anchor
1241
1362
  is in the document keeps its panel detached until the first open instead of parking it on
1242
1363
  `<body>`, so an anchor inserted into a `[data-zx-theme]` region or a `.zx-scope` afterwards still
1243
- gets its theme. Tooltip, MenuButton, and ContextMenu panels follow the same rule.
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.
1244
1383
  - **Methods** — `open()`, `close()`, `toggle()`, `isOpen()`, `setContent()`, `getPanel()`.
1245
1384
  - **Events** — `open`, `close`.
1246
1385
  <!-- /doc -->
@@ -1260,8 +1399,16 @@ A button that opens a `role="menu"`.
1260
1399
  default.
1261
1400
  - `role: 'menuitemcheckbox'|'menuitemradio'` with `checked` exposes an APG checked action. The
1262
1401
  application owns state changes and calls `setItems()` with the new model.
1263
- - **Methods** — `setItems()`, `open({returnFocus})`, `close()`, `setLabel()`, `getTrigger()`,
1264
- `getPanel()`, `focusFirst({returnFocus})`, `focusLast({returnFocus})`.
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})`.
1265
1412
  - **Events** — `select {value, item}`, `open`, `close`.
1266
1413
  - **Focus return** — choosing an item closes the menu and returns focus to the trigger before the
1267
1414
  `select` event and the item's `onselect` run, so a dialog they open returns to the trigger when
@@ -1273,7 +1420,9 @@ A button that opens a `role="menu"`.
1273
1420
  leaves the menu open with focus back on the item, which is the composition seam used by
1274
1421
  `AccountMenu` and rail flyouts.
1275
1422
  - **Keyboard** — ArrowDown/Enter/Space open and focus the first item; ArrowUp opens and focuses the
1276
- 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.
1277
1426
  - **Appearance** — the highlighted item is a neutral step, a checked item keeps its accent check
1278
1427
  mark, and section headings are sentence-case captions. ContextMenu shares the same rules.
1279
1428
  <!-- /doc -->
@@ -1321,7 +1470,7 @@ A description bubble anchored to another element. The panel is a manual popover
1321
1470
  at all and leaves everything to the methods), `disabled` (false).
1322
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.
1323
1472
  - **Events** — `open`, `close`. Both fire only on a real transition, so a `show()` on an open tooltip is silent.
1324
- - **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.
1325
1474
  <!-- /doc -->
1326
1475
 
1327
1476
  <!-- doc:split-view -->
@@ -1356,9 +1505,9 @@ range select.
1356
1505
  render?: (row,i)=>Node|string, sortValue?, headerTitle? }]`, `data`, `rowId: 'ID'`,
1357
1506
  `sort: { id, dir }`, `sortMode: 'local'|'server'`, `selectable: false|'single'|'multi'`,
1358
1507
  `stickyHeader: true`, `height`, `emptyText`, `rowClass`, `zebra: true`, `variant:
1359
- 'default'|'plain'`, and `hierarchy: false|{parentId, column?, expanded?}`. Locale, currency,
1360
- unit, and decimals may be row callbacks; values remain numbers in row data while display
1361
- formatting is locale-aware.
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.
1362
1511
  - **Appearance** — the header sits on the table's surface (no band) in the muted text colour at a
1363
1512
  medium weight, sentence case; the sorted column's label takes the text colour beside its arrow.
1364
1513
  Rows are divided by `--zx-color-border-subtle`, the header by the ordinary border, and every cell
@@ -1379,6 +1528,44 @@ range select.
1379
1528
  top bar, sets `aria-busy`, and is cleared by the next `setData`).
1380
1529
  - **Events** — `rowclick {row, id, index, event}`, `rowdblclick`, `sort {id, dir}`,
1381
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.
1382
1569
  - **Hierarchy** — flat parent references are projected as an ARIA `treegrid`; `getData()` and all
1383
1570
  editing/mutation APIs stay flat and ID-based. Orphans become roots, cycles render once, local
1384
1571
  sort reorders siblings without mixing levels, and growing counts visible rows. Use
@@ -1470,7 +1657,16 @@ range select.
1470
1657
  `{label, target}` object names the chooser's trigger and mounts the disclosure into a host
1471
1658
  element — a list toolbar, say — instead of the view's own. `label` omitted resolves
1472
1659
  `recordView.fields`. This is what lets a translated application use the chooser at all: the
1473
- 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.
1474
1670
 
1475
1671
  `TableView` is the record-oriented entry point above the lower-level `Table`. It extends
1476
1672
  `RecordView` and composes one complete `Table`, so editing, hierarchy, growing, row movement,
@@ -1484,7 +1680,8 @@ responsive stacking, loading, and advanced table events remain available through
1484
1680
  `fieldControls: true`, and `emptyText`. At least one field remains visible.
1485
1681
  - **Shared methods** — `setData()`, `addData()`, `updateRecord()`, `removeRecord()`, `getRecord()`,
1486
1682
  `getData()`, field getters/order/visibility methods, `setSort()`/`getSort()`, selection methods,
1487
- `setLoading()`, `getViewState()`, and `setViewState()`.
1683
+ `setLoading()`, `getViewState()`, `setViewState()`, `getFieldControls()`, and
1684
+ `mountFieldControls(target)`.
1488
1685
  - **Portable state** — `{version:1,fieldOrder,hiddenFields,sort}` is JSON-safe configuration only.
1489
1686
  Records, selection, DOM nodes, rendered values, and callbacks are never serialized. A state from
1490
1687
  one record view may seed another; unknown fields are ignored and new fields append.
@@ -1503,7 +1700,15 @@ portable state as TableView and KanbanView.
1503
1700
 
1504
1701
  - **Options** — the shared RecordView options plus `titleField`, `subtitleField`, `preview`,
1505
1702
  `previewAlt`, `link`, `actions`, `groupBy`, `groupOrder`, `minCardWidth`, `maxColumns`,
1506
- `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()`.
1507
1712
  - **Selection trigger** — `selectionTrigger:'checkbox'` preserves existing selection behavior.
1508
1713
  With `'card'`, selectable backgrounds toggle on click, Enter or Space, Shift+click selects a range,
1509
1714
  and the checkbox is omitted. Links, actions and text selection keep their native behavior. Selection
@@ -1636,7 +1841,18 @@ windowed loader.
1636
1841
  `locale`, `weekStart: 1`, `weekNumbers: true`, `workweek: false`, `agendaDays: 14`,
1637
1842
  `slotDuration: 30`, `slotMinTime: 0`, `slotMaxTime: 1440`, `scrollTime: 480`, `eventLimit: 3`,
1638
1843
  `selectable: false`, `editable: false`, `eventStartEditable`, `eventDurationEditable`,
1639
- `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.
1640
1856
  - **Event input** — generic records default to `{id,title,start,end,allDay,color,location,editable,
1641
1857
  durationEditable}`. Every field has a string-or-function reader (`eventId`, `eventTitle`,
1642
1858
  `eventStart`, …); numeric dates use `dateUnit: 'milliseconds'|'seconds'`. Inputs are normalized
@@ -1683,6 +1899,8 @@ Declarative client-side filter bar producing a filtered array, commonly wired to
1683
1899
 
1684
1900
  - **Options** — `filters: [{ type: 'select'|'text'|'custom', id, label, field(s)|get, options?,
1685
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`).
1686
1904
  - **Methods** — `setData()`, `apply()` → rows, `clear()`, `getState()`, `setState()`,
1687
1905
  `addFilter()`.
1688
1906
  - **Events** — `filter {rows, state}`.
@@ -1700,7 +1918,7 @@ form renders, with no per-application translation layer in between.
1700
1918
  - **Options** — `fields: Record<string, {type, label?, options?, min?, max?, step?, search?}>`,
1701
1919
  `order` (display order; omitted uses object order), `value` (the applied filters, drafted
1702
1920
  from), `mode: 'draft'|'live'`, `serialize: 'seconds'|'ms'|'iso'|(date, edge) => unknown`,
1703
- `emptyText`, `msg`.
1921
+ `timeZone` (IANA zone the days are counted in; null = the runtime's own), `emptyText`, `msg`.
1704
1922
  - **Types** — `text`, `select`, `date`, `date:range`, `int`, `float`, `number`, and
1705
1923
  `int:range` / `float:range` / `number:range`. An unrecognised type renders a visible,
1706
1924
  labelled placeholder naming it and is excluded from the value — never dropped silently.
@@ -1725,7 +1943,10 @@ Five behaviours are deliberate rather than incidental:
1725
1943
 
1726
1944
  `serialize` decides how a date leaves the component, because the answer is a wire-format
1727
1945
  decision rather than an application one. A range's upper bound takes the **end** of its day, or
1728
- 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`.
1729
1950
  <!-- /doc -->
1730
1951
 
1731
1952
  <!-- doc:filter -->
@@ -1742,6 +1963,11 @@ adapter to compile into its own backend query language.
1742
1963
  debounce?}`. Built-in types cover text, number/money, date/datetime, boolean, enum/status/
1743
1964
  priority/country/currency/unit/entity, and tags. Compatible operators and their arity come from
1744
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.
1745
1971
  - **Options** — `fields`, `value`, `operators`, `allowGroups: true`, `maxDepth: 3`,
1746
1972
  `maxConditions: 50`, `autoApply: false`, `readonly`, `disabled`, `applyLabel`, `clearLabel`,
1747
1973
  `searchable: false`, `layout: 'standard'|'compact'`, `rootLogic: null|'and'|'or'`,
@@ -1844,24 +2070,45 @@ breadcrumb.
1844
2070
  mounted twice repeats no id and a field `stage` never collides with a `#stage` elsewhere; the
1845
2071
  label, description, and message ids derive from it (`<id>-label`). Radio groups are named after
1846
2072
  the control id. An `id` in the field options that differs from the key is used as given.
1847
- - **Methods** — `addField(id, opts)`, `getField`/`hasField`/`getFields`,
2073
+ - **Methods** — `addField(id, opts|Field|element|component)`, `getField`/`hasField`/`getFields`,
1848
2074
  `getValues`/`setValues`/`getValue`/`setValue`, `reset`, `clear`, `focus(id)`,
1849
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.
1850
2088
 
1851
2089
  #### Field
1852
2090
 
1853
2091
  - **Options** — `id` (the control's DOM id; generated when omitted), `name` (the field name and
1854
2092
  native control `name`; defaults to `id` on a standalone field), `type: 'text'`, `label`,
1855
- `description`, `value`, `placeholder`, `required`, `disabled`, `options`,
1856
- `layout: 'stack'|'inline'`, `props: {}`.
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: {}`.
1857
2096
  - **Properties** — `controlId` (the control's DOM id), `name`.
1858
2097
  - **Built-in types** — `text`, `password`, `textarea`, `checkbox`, `int`, `float`, `select`,
1859
2098
  `optionlist`, `hidden`, `html` (display content as text: no input frame, a control's height,
1860
2099
  inset like the values around it in a quiet form).
1861
- - **Methods** — `getValue`/`setValue`, `focus`, `reset`, `setDisabled`,
2100
+ - **Methods** — `getValue`/`setValue`, `focus`, `reset`, `setDisabled`, `setReadonly`,
1862
2101
  `setHighlight(msg, kind)`, `clearHighlight`, `getInput`, `own(child)`, plus the statics
1863
2102
  `Field.register(type, adapter)` and `Field.has(type)`.
1864
- - **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.
1865
2112
  - **Labels** — small, medium weight, in the muted text colour: quieter than the values they name.
1866
2113
  With `layout: 'inline'` the label lines up with the control's first line.
1867
2114
 
@@ -2350,7 +2597,7 @@ columns, in what shape. The parts have always existed (`Search`, `SortControl`,
2350
2597
  forgetting the same thing.
2351
2598
 
2352
2599
  - **Options** — `search: {placeholder?, value?, debounce?, clearable?}|false`, `count`,
2353
- `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`),
2354
2601
  `view`, `tools: [{id, label?, icon?, badge?, badgeKind?, pressed?, disabled?, onclick?}]`,
2355
2602
  `label`, `msg`.
2356
2603
  - **Methods** — `setCount()`, `setBadge()`, `setPressed()`, `setToolDisabled()`, `setTools()`,
@@ -2370,18 +2617,90 @@ Three properties are the reason to use this rather than assemble your own:
2370
2617
  The current view and a pressed tool take the neutral `--zx-color-bg-current` fill — a mode the
2371
2618
  list is in, not a pick — never the accent tint.
2372
2619
 
2373
- `bind(view)` is optional sugar for a `TableView` or `CardView`: it feature-detects rather than
2374
- importing them, adopts the view's shape, and mounts a field chooser built with
2375
- `fieldControls: {target}` into the toolbar. The toolbar is fully functional without it.
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.
2376
2681
  <!-- /doc -->
2377
2682
 
2378
2683
  <!-- doc:pagination -->
2379
2684
  ### Pagination
2380
2685
 
2381
- The pager for a server-backed list, shaped to drop straight onto `Table` with
2382
- `sortMode: 'server'` or onto the `zeyosTable` binding — `getState()` hands back the `offset` and
2383
- `pageSize` a query needs. `page` is 1-based and always clamped into `[1, pages]`; a `total` of 0 is
2384
- 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.
2385
2704
 
2386
2705
  - **Options** — `page: 1`, `pageSize: 25`, `total: 0`, `pageSizes: [25, 50, 100]`, `siblings: 1`,
2387
2706
  `boundaries: 1`, `showPageSize: true`, `showSummary: true` (renders "26–50 of 312"),
@@ -2396,8 +2715,9 @@ treated as one empty page rather than zero pages.
2396
2715
  - **Semantics** — `<nav aria-label="Pagination">`; the current page carries `aria-current="page"`;
2397
2716
  previous/next are labelled icon buttons that go `disabled` at the ends; the ellipses are
2398
2717
  `aria-hidden` spans, not buttons.
2399
- - **Appearance** — the current page is the neutral `--zx-color-bg-current` fill with a semibold
2400
- figure: where you are, not a call to action.
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.
2401
2721
  <!-- /doc -->
2402
2722
 
2403
2723
  <!-- doc:navigation-bar -->
@@ -2431,21 +2751,56 @@ inline; every rail state opens descendants toward the workspace in anchored flyo
2431
2751
 
2432
2752
  - **Options** — `items`, `active`, `expanded`, `collapsed`, `collapsible` (`false` removes the
2433
2753
  minimise and expand buttons), `orientation: 'vertical'|'horizontal'`,
2434
- `side: 'left'|'right'|'top'|'bottom'`, `label`,
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`,
2435
2757
  `header`, `footer`, `railHeader`, `railFooter`, `openDelay: 80`, `closeDelay: 160`, and
2436
2758
  `renderIcon(item, context)`. Icon values may also be names, nodes, components, or factories.
2437
- - **Methods** — `setItems()`, `getItems()`, `setActive()`, `setExpanded()`, `getExpanded()`,
2438
- `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})`,
2439
2792
  `setLayout({orientation, side})`, `openFlyout(id, {focus})`, `closeFlyout()`,
2440
2793
  `closeAllFlyouts()`, `isFlyoutOpen()`, `focus()`, `destroy()`. `getItems()` returns copies of
2441
2794
  the normalized tree; a leaf carries no `children` property.
2442
2795
  - **Events** — cancelable `select {item, id, value, href, event}`,
2443
- `branchchange {item, id, expanded, ids}`, `collapsechange {collapsed}`, and
2444
- `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}`.
2445
2799
  - **Presentation state** — horizontal navigation is always effectively minimized and never exposes
2446
2800
  an expand control. Switching layouts does not erase the saved vertical collapsed preference or
2447
- expanded branch IDs. `AppSidebar` is the single public application-navigation component; use
2448
- `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`.
2449
2804
  - **Current state** — the current page is neutral: the `--zx-color-bg-current` fill, the full
2450
2805
  text colour and a semibold label, in the tree, the rail and every flyout; hover is a lighter
2451
2806
  step of the same overlay. A closed branch (or a rail item) over the active route takes the
@@ -2462,11 +2817,81 @@ inline; every rail state opens descendants toward the workspace in anchored flyo
2462
2817
  link or callback is disabled in every presentation.
2463
2818
  <!-- /doc -->
2464
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
+
2465
2887
  <!-- doc:kernel -->
2466
2888
  ### Core helpers
2467
2889
 
2468
2890
  - **Component** — the base class: `on`/`off`/`once`/`emit`, `listen`, `toElement`, `msg`,
2469
- `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`.
2470
2895
  - **DOM** — `h(tag, props, ...children)`, `h.raw(html)`, `htmlEscape`, `resolveElement`.
2471
2896
  `__(tag#id.class, properties, content)` is Zx's compact DOM builder and public alias for
2472
2897
  `compactElement()`. It preserves `D*` data, `S*` style, boolean/built-in property, attribute,
@@ -2483,7 +2908,9 @@ inline; every rail state opens descendants toward the workspace in anchored flyo
2483
2908
  `{update, destroy}`. It anchors a manual popover with CSS anchor positioning where the browser
2484
2909
  supports it and a scroll/resize-tracked fallback where it does not. Options are resolved once,
2485
2910
  so changing a placement means `destroy()` and a fresh `position()` call.
2486
- - **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.
2487
2914
  - **Dates** — `formatDate(d, fmt)`, `parseDate(s, fmt)` (tokens
2488
2915
  `%d %m %Y %y %H %M %S %a %A %b %B %s`), `clampDate`, `isSameDay`, `addDays`, `addMonths`,
2489
2916
  `getWeekStart`.
@@ -2606,12 +3033,20 @@ entry point as the components. See also the Kernel section for `Component`, `h`,
2606
3033
  `copyToClipboard(text)` → `Promise<boolean>` uses the async Clipboard API and falls back to a
2607
3034
  hidden textarea, never rejecting.
2608
3035
  - **Translation** — `setTranslator(fn)`, `setLanguage(lang)`, `getLanguage()`,
2609
- `translate(key, args)`, `printf(str, args)` (`%s` placeholders). Every component routes its
2610
- built-in strings through `translate()`, so one translator covers the library, and
2611
- `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.
2612
3044
  - **Keyboard** — `focusTrap(container)` → `{activate, release}` confines Tab to a container;
2613
- `rovingTabindex(container, itemSelector, {orientation, wrap})` → `{focusFirst, focusLast,
2614
- 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)` →
2615
3050
  `(event|string) => void` resolves buffered printable keystrokes to an item (500 ms buffer,
2616
3051
  repeated-letter cycling).
2617
3052
  <!-- /doc -->
@@ -2668,8 +3103,13 @@ in component CSS.
2668
3103
  (`--zx-dur-fast` 120ms, `--zx-dur-base` 180ms — both 0ms in a reduced-motion region — and the
2669
3104
  curves `--zx-ease`, `--zx-ease-spring`, `--zx-ease-drawer`).
2670
3105
  - **Tier 2** — the semantic set, the only tokens components may use: `--zx-color-*`,
2671
- `--zx-control-*`, `--zx-focus-ring`, `--zx-overlay-shadow`, `--zx-drag-shadow` (an item being
2672
- dragged; its own value in dark, with a faint light rim). The roles that carry the visual
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
2673
3113
  language:
2674
3114
  - **Columns.** `--zx-color-bg-surface` is the working canvas; `--zx-color-bg-chrome` is the
2675
3115
  navigation and inspector tone beside it (one step below the canvas in light, between page and