@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
@@ -17,6 +17,9 @@ import { Tooltip } from '../components/tooltip/tooltip.js';
17
17
  * @property {() => void} [invoke] Application-owned non-link action.
18
18
  * @property {boolean} [disabled] Whether activation is unavailable.
19
19
  * @property {AppNavItem[]} [children] Child destinations/actions.
20
+ * @property {boolean} [section] Makes the item a section: a quiet heading over its `children`, which
21
+ * render at the same level. A section is never focused, selected, or collapsed, and needs no `id`.
22
+ * @property {Node|{toElement: () => Node|null}} [actions] A section's trailing controls, such as an add button.
20
23
  */
21
24
  /**
22
25
  * @typedef {Object} AppRailPresenterOptions
@@ -45,6 +48,14 @@ import { Tooltip } from '../components/tooltip/tooltip.js';
45
48
  export class AppRailPresenter extends Component {
46
49
  static cssName = 'app-rail';
47
50
 
51
+ /**
52
+ * BEM block of the rail's own parts (header, list, footer, row, item). A component built on the
53
+ * presenter names its parts after itself; flyout panels keep the `zx-app-rail__` vocabulary,
54
+ * because every rail portals the same panel.
55
+ * @type {string}
56
+ */
57
+ static partBlock = 'zx-app-rail';
58
+
48
59
  /** @type {Readonly<AppRailPresenterOptions>} */
49
60
  static defaults = {
50
61
  items: [],
@@ -93,13 +104,14 @@ export class AppRailPresenter extends Component {
93
104
  root.dataset.orientation = this.options.orientation === 'horizontal' ? 'horizontal' : 'vertical';
94
105
  const sides = root.dataset.orientation === 'horizontal' ? ['top', 'bottom'] : ['left', 'right'];
95
106
  root.dataset.side = sides.includes(this.options.side) ? this.options.side : sides[0];
96
- const header = h('div', { class: 'zx-app-rail__header' });
97
- const footer = h('div', { class: 'zx-app-rail__footer' });
107
+ const block = this._partBlock();
108
+ const header = h('div', { class: `${block}__header` });
109
+ const footer = h('div', { class: `${block}__footer` });
98
110
  appendSlot(header, this.options.header);
99
111
  appendSlot(footer, this.options.footer);
100
112
  header.hidden = !header.hasChildNodes();
101
113
  footer.hidden = !footer.hasChildNodes();
102
- root.replaceChildren(header, h('ul', { ref: 'list', class: 'zx-app-rail__list' }), footer);
114
+ root.replaceChildren(header, h('ul', { ref: 'list', class: `${block}__list` }), footer);
103
115
  this.listen(root, 'keydown', (event) => this._onRailKeydown(/** @type {KeyboardEvent} */ (event)));
104
116
  return root;
105
117
  }
@@ -209,31 +221,28 @@ export class AppRailPresenter extends Component {
209
221
  this._clearArtifacts();
210
222
  this.refs.list.replaceChildren();
211
223
  this._interaction = new RailListenerScope(/** @type {HTMLElement} */ (this.el));
212
- this._items.forEach((item) => {
213
- const hasChildren = Array.isArray(item.children) && item.children.length > 0;
214
- const control = navControl(item, {
215
- className: 'zx-app-rail__item',
216
- index: String(item.id),
217
- iconOnly: true,
218
- disclosure: hasChildren,
219
- disclosureIcon: railDisclosureIcon(this.el.dataset.side),
220
- visual: appNavIcon(item, this.options.renderIcon, {
221
- orientation: this.el.dataset.orientation,
222
- collapsed: true,
223
- side: this.el.dataset.side,
224
- level: 1,
225
- location: 'root'
226
- })
227
- });
228
- const row = h('li', { class: 'zx-app-rail__row' }, control);
224
+ let started = false;
225
+ for (const item of railEntries(this._items)) {
226
+ // A section has no rail control: the rail draws a rule between the groups instead.
227
+ if (item === SECTION_BREAK) {
228
+ if (started) this.refs.list.append(h('li', { class: `${this._partBlock()}__separator`, ariaHidden: 'true' }));
229
+ continue;
230
+ }
231
+ started = true;
232
+ const hasChildren = Array.isArray(item.children) && item.children.length > 0
233
+ && this._flyoutsFor(item);
234
+ const control = this._renderControl(item, hasChildren);
235
+ const row = h('li', { class: `${this._partBlock()}__row` }, control);
229
236
  this.refs.list.append(row);
230
- const tooltip = new Tooltip(control, {
231
- content: item.label,
232
- placement: tooltipPlacement(this.el.dataset.side),
233
- delay: 450
234
- });
235
- this._tooltips.push(tooltip);
236
- this._tooltipById.set(String(item.id), tooltip);
237
+ if (this._tooltipFor(item)) {
238
+ const tooltip = new Tooltip(control, {
239
+ content: item.label,
240
+ placement: tooltipPlacement(this.el.dataset.side),
241
+ delay: 450
242
+ });
243
+ this._tooltips.push(tooltip);
244
+ this._tooltipById.set(String(item.id), tooltip);
245
+ }
237
246
  if (hasChildren) this._buildFlyout(item, control, String(item.id), null);
238
247
  else this._interaction.listen(control, 'click', (event) => {
239
248
  const selected = this._select(item, event);
@@ -243,21 +252,66 @@ export class AppRailPresenter extends Component {
243
252
  }
244
253
  }
245
254
  });
246
- });
247
- this._interaction.listen(document, 'pointerdown', (event) => {
248
- const open = [...this._flyouts.values()].filter((flyout) => flyout.dropdown.isOpen());
249
- if (open.length === 0) return;
250
- const path = event.composedPath();
251
- if (open.some((flyout) => path.includes(flyout.panel) || path.includes(flyout.trigger))) return;
252
- for (const flyout of open) if (flyout.parentKey === null) this.closeFlyout(flyout.key);
253
- });
255
+ }
256
+ // Outside presses need no listener here: each open flyout is a Dropdown on the overlay stack,
257
+ // which closes it on a press outside it and every flyout opened from it, and a nested flyout,
258
+ // anchored inside its parent's panel, counts as part of that parent.
259
+ this._afterRebuild();
254
260
  this._syncActive();
255
261
  }
256
262
 
263
+ /** @returns {string} The BEM block of the rail's own parts. @protected */
264
+ _partBlock() {
265
+ return /** @type {typeof AppRailPresenter} */ (this.constructor).partBlock;
266
+ }
267
+
268
+ /**
269
+ * Builds one top-level rail control. The control must carry `data-app-nav-id` and
270
+ * `data-app-nav-value-id`, which keyboard handling, flyouts, and the active state read.
271
+ * @param {AppNavItem} item Top-level item.
272
+ * @param {boolean} hasFlyout Whether the control opens a flyout of the item's children.
273
+ * @returns {HTMLElement}
274
+ * @protected
275
+ */
276
+ _renderControl(item, hasFlyout) {
277
+ return navControl(item, {
278
+ className: 'zx-app-rail__item',
279
+ index: String(item.id),
280
+ iconOnly: true,
281
+ disclosure: hasFlyout,
282
+ disclosureIcon: railDisclosureIcon(this.el.dataset.side),
283
+ visual: appNavIcon(item, this.options.renderIcon, {
284
+ orientation: this.el.dataset.orientation,
285
+ collapsed: true,
286
+ side: this.el.dataset.side,
287
+ level: 1,
288
+ location: 'root'
289
+ })
290
+ });
291
+ }
292
+
293
+ /** Whether an item with children presents them in a flyout. @param {AppNavItem} item @returns {boolean} @protected */
294
+ _flyoutsFor(item) {
295
+ return true;
296
+ }
297
+
298
+ /** Whether a top-level control gets a hover/focus tooltip naming it. @param {AppNavItem} item @returns {boolean} @protected */
299
+ _tooltipFor(item) {
300
+ return true;
301
+ }
302
+
303
+ /** Runs after every rebuild, before the active state is applied. @returns {void} @protected */
304
+ _afterRebuild() {}
305
+
257
306
  /** @param {AppNavItem} item @param {HTMLElement} trigger @param {string} key @param {string|null} parentKey @returns {void} */
258
307
  _buildFlyout(item, trigger, key, parentKey) {
259
308
  const list = h('ul', { class: 'zx-app-rail__flyout-list' });
260
- for (const child of item.children ?? []) {
309
+ for (const child of railEntries(item.children ?? [], true)) {
310
+ if (child.section) {
311
+ // A caption, not a destination: keyboard movement skips it with every other non-action.
312
+ list.append(h('li', { class: 'zx-app-rail__flyout-section' }, child.label));
313
+ continue;
314
+ }
261
315
  const childKey = `${key}/${child.id}`;
262
316
  const hasChildren = Boolean(child.children?.length);
263
317
  const control = navControl(child, {
@@ -292,19 +346,34 @@ export class AppRailPresenter extends Component {
292
346
  offset: 6
293
347
  });
294
348
  const actualPanel = dropdown.getPanel();
349
+ // Escape arrives through the overlay stack, which offers it to the topmost flyout only. The
350
+ // rail returns focus itself, so the trigger's focus does not reopen what Escape just closed,
351
+ // and a flyout is reached with the arrow keys rather than Tab: every tile is its own Tab stop.
352
+ dropdown._configure({
353
+ tabReach: false,
354
+ onEscape: () => {
355
+ const hadFocus = actualPanel.contains(document.activeElement);
356
+ this.closeFlyout(key);
357
+ if (hadFocus) this._restoreFocus(trigger);
358
+ }
359
+ });
295
360
  actualPanel.classList.add('zx-app-rail__popover');
296
361
  // The panel is portaled out of the rail, so its stylesheet cannot ask which edge the rail
297
- // occupies. Presentation only: it carries the active marker to the edge facing the screen
298
- // on a right-hand rail, exactly as the expanded tree does.
362
+ // occupies. Presentation only: it tells the panel's styles which side it opened from.
299
363
  actualPanel.dataset.side = this.el.dataset.side;
300
364
  actualPanel.setAttribute('role', 'region');
301
365
  actualPanel.setAttribute('aria-label', this._message('appRail.subNavigation', '%1 sub-navigation', item.label));
302
366
  const flyout = { key, parentKey, item, trigger, panel: actualPanel, dropdown };
303
367
  this._flyouts.set(key, flyout);
368
+ // While a tile's own flyout is open its tooltip says nothing the flyout's title does not, and
369
+ // would take the first Escape meant for the flyout.
370
+ const tooltip = parentKey === null ? this._tooltipById.get(String(item.id)) : null;
304
371
  dropdown.on('open', () => {
372
+ tooltip?.disable();
305
373
  if (!this._silenceFlyoutEvents) this.emit('flyoutchange', { item, id: item.id, open: true });
306
374
  });
307
375
  dropdown.on('close', () => {
376
+ tooltip?.enable();
308
377
  if (!this._silenceFlyoutEvents) this.emit('flyoutchange', { item, id: item.id, open: false });
309
378
  });
310
379
 
@@ -394,20 +463,6 @@ export class AppRailPresenter extends Component {
394
463
  const controls = actions(flyout.panel);
395
464
  const current = /** @type {Element|null} */ (event.target)?.closest?.('a, button');
396
465
  const index = controls.indexOf(current);
397
- if (event.key === 'Escape') {
398
- event.preventDefault();
399
- event.stopPropagation();
400
- const nested = [...this._flyouts.values()].find((candidate) =>
401
- candidate.trigger === current && candidate.dropdown.isOpen());
402
- if (nested) {
403
- this.closeFlyout(nested.key);
404
- this._restoreFocus(nested.trigger);
405
- return;
406
- }
407
- this.closeFlyout(flyout.key);
408
- this._restoreFocus(flyout.trigger);
409
- return;
410
- }
411
466
  const nested = [...this._flyouts.values()].find((candidate) => candidate.trigger === current);
412
467
  const into = this.el.dataset.side === 'right' ? 'ArrowLeft' : 'ArrowRight';
413
468
  const out = into === 'ArrowRight' ? 'ArrowLeft' : 'ArrowRight';
@@ -417,7 +472,8 @@ export class AppRailPresenter extends Component {
417
472
  this.openFlyout(nested.key, { focus: true });
418
473
  return;
419
474
  }
420
- if (flyout.parentKey !== null && event.key === out) {
475
+ // Out of a nested flyout, or out of a top-level one beside a vertical rail, back to its trigger.
476
+ if ((flyout.parentKey !== null || this.el.dataset.orientation === 'vertical') && event.key === out) {
421
477
  event.preventDefault();
422
478
  event.stopPropagation();
423
479
  this.closeFlyout(flyout.key);
@@ -607,6 +663,22 @@ class RailListenerScope extends Component {
607
663
  }
608
664
  }
609
665
 
666
+ /** Marks the boundary of a section in a flattened rail. */
667
+ const SECTION_BREAK = Object.freeze({});
668
+
669
+ /**
670
+ * Flattens sections for a rail: a section's children take its place. With `labels`, each section
671
+ * leaves its own item ahead of its children, for a flyout to caption; otherwise a section break.
672
+ * @param {AppNavItem[]} items Normalized items.
673
+ * @param {boolean} [labels=false] Keep section items as captions.
674
+ * @returns {AppNavItem[]}
675
+ */
676
+ function railEntries(items, labels = false) {
677
+ return items.flatMap((item) => (item.section
678
+ ? [labels ? item : /** @type {AppNavItem} */ (SECTION_BREAK), ...railEntries(item.children ?? [], labels)]
679
+ : [item]));
680
+ }
681
+
610
682
  /**
611
683
  * Normalizes an application navigation tree without mutating caller data.
612
684
  * @param {unknown} items Candidate tree.
@@ -614,13 +686,21 @@ class RailListenerScope extends Component {
614
686
  */
615
687
  export function normalizeAppItems(items) {
616
688
  if (!Array.isArray(items)) return [];
617
- return items.filter((item) => item && typeof item === 'object' && item.id != null && item.label != null)
618
- .map((item) => {
689
+ return items.filter((item) => item && typeof item === 'object' && item.label != null
690
+ && (item.id != null || item.section === true))
691
+ .map((item, index) => {
619
692
  const normalized = {
620
693
  ...item,
694
+ // A section is addressed by nothing, so one written without an id gets a stable one.
695
+ id: item.id ?? `section:${index}:${item.label}`,
621
696
  label: String(item.label),
622
697
  children: Array.isArray(item.children) ? normalizeAppItems(item.children) : []
623
698
  };
699
+ if (item.section === true) {
700
+ normalized.section = true;
701
+ delete normalized.href;
702
+ return normalized;
703
+ }
624
704
  if (item.href == null) return normalized;
625
705
  const href = safeHref(item.href);
626
706
  if (href !== null) normalized.href = href;
@@ -1,5 +1,6 @@
1
1
  import { h, safeHref } from '../core/dom.js';
2
2
  import { icon } from '../core/icons.js';
3
+ import { uid } from '../core/util.js';
3
4
 
4
5
  /** @typedef {'menuitem'|'menuitemcheckbox'|'menuitemradio'} MenuItemRole */
5
6
  /**
@@ -31,7 +32,27 @@ import { icon } from '../core/icons.js';
31
32
  * @property {string} label Visible heading.
32
33
  */
33
34
  /** @typedef {{type: 'separator'}} MenuSeparator */
34
- /** @typedef {MenuItem|MenuHeading|MenuSeparator|'-'} MenuEntry */
35
+ /**
36
+ * One option of a menu choice.
37
+ * @typedef {Object} MenuChoiceOption
38
+ * @property {unknown} value Value the choice takes.
39
+ * @property {string} label Accessible name and tooltip; the option shows its glyph or icon.
40
+ * @property {string} [icon] Kernel icon name.
41
+ * @property {Node|(() => Node)} [glyph] A pictogram drawn instead of an icon.
42
+ * @property {boolean} [disabled=false] Whether the option is unavailable.
43
+ */
44
+ /**
45
+ * A setting chosen in place: a labelled row of two to a few options, one checked — a theme, a
46
+ * layout. Choosing an option keeps the menu open, so its effect shows.
47
+ * @typedef {Object} MenuChoice
48
+ * @property {'choice'} type Entry kind.
49
+ * @property {string} label Visible row label.
50
+ * @property {unknown} [value] Value of the checked option.
51
+ * @property {MenuChoiceOption[]} options The options, in order.
52
+ * @property {(value: unknown, entry: MenuChoice, controller: MenuController) => void} [onselect]
53
+ * Called with the chosen option's value.
54
+ */
55
+ /** @typedef {MenuItem|MenuHeading|MenuSeparator|MenuChoice|'-'} MenuEntry */
35
56
 
36
57
  export const MENU_ITEM_SELECTOR = [
37
58
  '[role="menuitem"]',
@@ -46,7 +67,7 @@ export const MENU_ITEM_SELECTOR = [
46
67
  */
47
68
  export function isMenuItem(entry) {
48
69
  return Boolean(entry && entry !== '-' && typeof entry === 'object'
49
- && !['heading', 'separator'].includes(String(/** @type {{type?: unknown}} */ (entry).type ?? '')));
70
+ && !['heading', 'separator', 'choice'].includes(String(/** @type {{type?: unknown}} */ (entry).type ?? '')));
50
71
  }
51
72
 
52
73
  /**
@@ -64,9 +85,11 @@ export function menuItemRole(item) {
64
85
  * Builds one menu's entries with shared structure and link safety.
65
86
  * @param {MenuEntry[]} entries Entry definitions.
66
87
  * @param {string} block CSS block name, without the `zx-` prefix.
88
+ * @param {{choices?: boolean}} [options={}] Whether the menu handles `choice` entries; a menu that
89
+ * does not leaves them out rather than drawing a row that does nothing.
67
90
  * @returns {HTMLElement[]}
68
91
  */
69
- export function renderMenuEntries(entries, block) {
92
+ export function renderMenuEntries(entries, block, { choices = false } = {}) {
70
93
  const nodes = [];
71
94
  entries.forEach((entry, index) => {
72
95
  if (entry === '-' || (entry && typeof entry === 'object' && entry.type === 'separator')) {
@@ -80,6 +103,10 @@ export function renderMenuEntries(entries, block) {
80
103
  }, String(entry.label ?? '')));
81
104
  return;
82
105
  }
106
+ if (entry && typeof entry === 'object' && entry.type === 'choice') {
107
+ if (choices) nodes.push(renderChoice(/** @type {MenuChoice} */ (entry), index, block));
108
+ return;
109
+ }
83
110
  if (!isMenuItem(entry)) return;
84
111
 
85
112
  const role = menuItemRole(entry);
@@ -140,3 +167,34 @@ export function renderMenuEntries(entries, block) {
140
167
  });
141
168
  return nodes;
142
169
  }
170
+
171
+ /**
172
+ * A choice row: its label, then a segmented group of `menuitemradio` options. The group is named
173
+ * by the label, so a screen reader hears "Theme, Dark, radio, checked, 2 of 3".
174
+ * @param {MenuChoice} entry Choice definition.
175
+ * @param {number} index Entry index.
176
+ * @param {string} block CSS block name, without the `zx-` prefix.
177
+ * @returns {HTMLElement}
178
+ */
179
+ function renderChoice(entry, index, block) {
180
+ const labelId = uid(`zx-${block}-choice`);
181
+ const options = Array.isArray(entry.options) ? entry.options : [];
182
+ return h('div', { class: `zx-${block}__choice`, role: 'group', ariaLabelledby: labelId },
183
+ h('span', { class: `zx-${block}__choice-label`, id: labelId }, String(entry.label ?? '')),
184
+ h('span', { class: `zx-${block}__choice-options` }, options.map((option, position) => {
185
+ const glyph = typeof option.glyph === 'function' ? option.glyph() : option.glyph;
186
+ return h('button', {
187
+ class: `zx-${block}__choice-option`,
188
+ type: 'button',
189
+ role: 'menuitemradio',
190
+ tabindex: '-1',
191
+ title: String(option.label ?? ''),
192
+ ariaLabel: String(option.label ?? ''),
193
+ ariaChecked: String(Object.is(option.value, entry.value)),
194
+ ariaDisabled: option.disabled ? 'true' : null,
195
+ 'data-menu-item': String(index),
196
+ 'data-menu-option': String(position)
197
+ }, glyph && typeof glyph === 'object' && 'nodeType' in glyph ? glyph
198
+ : option.icon ? icon(option.icon) : String(option.label ?? ''));
199
+ })));
200
+ }
@@ -107,8 +107,12 @@ function isRecord(value) {
107
107
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
108
108
  }
109
109
 
110
- /** @param {unknown} error @returns {string} */
111
- function errorMessage(error) {
110
+ /**
111
+ * The text a failed request is reported with: the error's own message, or a generic sentence.
112
+ * @param {unknown} error Rejection reason.
113
+ * @returns {string}
114
+ */
115
+ export function errorMessage(error) {
112
116
  if (error instanceof Error && error.message) return error.message;
113
117
  if (error && typeof error === 'object' && typeof error.message === 'string') return error.message;
114
118
  return String(error ?? 'ZeyOS request failed');
package/src/zeyos/form.js CHANGED
@@ -1,6 +1,9 @@
1
1
  import { Form, Message, Select } from '../index.js';
2
- import { connect } from './connect.js';
3
- import { dateToUnixSeconds, unixSecondsToDate } from './query.js';
2
+ import { overlayScope } from '../core/overlay-host.js';
3
+ import { connect, errorMessage } from './connect.js';
4
+ import {
5
+ calendarDayToUnixSeconds, dateToUnixSeconds, unixSecondsToCalendarDay, unixSecondsToDate
6
+ } from './query.js';
4
7
  import { fieldToZxField, resolveFields } from './schema.js';
5
8
  import { buildZeyosSelectConfig } from './select.js';
6
9
 
@@ -9,11 +12,21 @@ import { buildZeyosSelectConfig } from './select.js';
9
12
  * @property {string[]} [fields] Curated ordered field allow-list.
10
13
  * @property {string[]} [exclude] Field ids to omit.
11
14
  * @property {Record<string, string>} [labels] Per-field label overrides.
15
+ * @property {Record<string, string>} [formats] Per-field semantic format overrides, such as
16
+ * `{ duedate: 'date' }` for a Unix-second field that holds a calendar day.
17
+ * @property {string} [timeZone] IANA time zone of the ZeyOS instance. A date-only field's stored
18
+ * midnight is read as the calendar day it names in this zone, and a picked day is saved as midnight
19
+ * in it; defaults to the runtime's own zone.
12
20
  * @property {string} [title=''] Fieldset legend.
13
21
  * @property {1|2|3} [columns=2] Fieldset column count.
14
22
  * @property {Record<string, unknown>} [value] Initial record using ZeyOS storage values.
15
23
  * @property {unknown} [id] Initial record id to load.
16
24
  * @property {(record: Record<string, unknown>) => unknown|Promise<unknown>} [onSaved] Success callback.
25
+ * @property {Element|string|null} [scope=null] Element or selector whose theme scope shows the
26
+ * binding's toasts — the save confirmation and the report of a failed request. Null uses the form's
27
+ * own element, so a form inside a scoped shell or dialog toasts inside that scope.
28
+ * @property {boolean} [notify=true] Whether the binding raises toasts. `false` keeps it silent:
29
+ * `save()`, `load()`, and `ready` still resolve or reject, and reporting is the caller's.
17
30
  */
18
31
 
19
32
  /**
@@ -22,8 +35,10 @@ import { buildZeyosSelectConfig } from './select.js';
22
35
  * @property {Record<string, Record<string, any>>} fields Field descriptors keyed by id.
23
36
  * @property {Array<{id: string, resource: string}>} entityFields Referenced-resource fields.
24
37
  * @property {Record<string, any>} formOptions Plain Zx Form options.
25
- * @property {(record: Record<string, unknown>) => Record<string, unknown>} mapFromRecord Maps Unix seconds to Date values.
26
- * @property {(values: Record<string, unknown>) => Record<string, unknown>} mapToRecord Maps Date and enum values to API storage values.
38
+ * @property {(record: Record<string, unknown>) => Record<string, unknown>} mapFromRecord Maps Unix
39
+ * seconds to Date values: a date-only field to local midnight of its calendar day in `timeZone`.
40
+ * @property {(values: Record<string, unknown>) => Record<string, unknown>} mapToRecord Maps Date and
41
+ * enum values to API storage values: a date-only field to midnight of its day in `timeZone`.
27
42
  */
28
43
 
29
44
  /**
@@ -31,7 +46,8 @@ import { buildZeyosSelectConfig } from './select.js';
31
46
  * @property {Form} form Generated Zx form.
32
47
  * @property {Promise<Record<string, unknown>|null>} ready Initial value/load readiness.
33
48
  * @property {(id: unknown) => Promise<Record<string, unknown>>} load Loads and maps one record.
34
- * @property {() => Promise<Record<string, unknown>|null>} save Validates and creates or updates the record.
49
+ * @property {() => Promise<Record<string, unknown>|null>} save Validates and creates or updates the
50
+ * record, then confirms with a toast in the form's scope unless `notify` is false.
35
51
  * @property {() => Form} getForm Returns the generated Zx Form.
36
52
  * @property {() => void} destroy Destroys the form and all owned fields/selects.
37
53
  */
@@ -39,7 +55,8 @@ import { buildZeyosSelectConfig } from './select.js';
39
55
  /**
40
56
  * Builds the DOM-free form configuration consumed by {@link zeyosForm}. Entity descriptors retain
41
57
  * the `zxselect` type and receive the same async options used by {@link zeyosSelect}; enum options
42
- * come from normalized schema metadata, and the returned mappers handle Unix-second dates.
58
+ * come from normalized schema metadata with readable labels, and the returned mappers handle
59
+ * Unix-second dates — date-only fields as calendar days in `opts.timeZone`.
43
60
  * @param {Record<string, any>} client Injected ZeyOS client instance.
44
61
  * @param {string} resource Resource name such as `transactions`.
45
62
  * @param {ZeyosFormOptions} [opts={}] Schema curation and layout options.
@@ -49,9 +66,14 @@ export function buildZeyosFormConfig(client, resource, opts = {}) {
49
66
  const fieldMeta = resolveFields(client, resource, opts);
50
67
  const fields = {};
51
68
  const entityFields = [];
69
+ /** @type {Map<string, 'date'|'datetime'>} */
70
+ const dateKinds = new Map();
52
71
 
53
72
  for (const meta of fieldMeta) {
54
73
  const descriptor = fieldToZxField(meta);
74
+ if (descriptor.type === 'date' || descriptor.type === 'datetime') {
75
+ dateKinds.set(String(meta.id), descriptor.type);
76
+ }
55
77
  const referencedResource = descriptor.type === 'zxselect' && typeof descriptor.props?.entity === 'string'
56
78
  ? descriptor.props.entity
57
79
  : null;
@@ -78,16 +100,18 @@ export function buildZeyosFormConfig(client, resource, opts = {}) {
78
100
  fields
79
101
  }]
80
102
  },
81
- mapFromRecord: (record) => mapFromRecord(record, fieldMeta),
82
- mapToRecord: (values) => mapToRecord(values, fieldMeta)
103
+ mapFromRecord: (record) => mapFromRecord(record, fieldMeta, dateKinds),
104
+ mapToRecord: (values) => mapToRecord(values, fieldMeta, dateKinds)
83
105
  };
84
106
  }
85
107
 
86
108
  /**
87
109
  * Generates a schema-driven Zx Form and a small async binding object. `load(id)` hydrates entity
88
110
  * labels and converts Unix seconds to Date objects. `save()` validates, maps values back to API
89
- * storage types, calls create/update through the WP14 connection facade, shows a success toast,
90
- * and invokes `onSaved(record)`. Await `ready` when `opts.id` or `opts.value` is supplied.
111
+ * storage types, calls create/update through the WP14 connection facade, invokes
112
+ * `onSaved(record)`, and shows a success toast. Its toasts — that confirmation and the report of a
113
+ * failed request — appear in the theme scope of `opts.scope` or of the form itself, and
114
+ * `notify: false` silences them. Await `ready` when `opts.id` or `opts.value` is supplied.
91
115
  * @param {Record<string, any>} client Injected ZeyOS client instance.
92
116
  * @param {string} resource Resource name such as `transactions`.
93
117
  * @param {ZeyosFormOptions} [opts={}] Schema, initial value, and save options.
@@ -97,8 +121,14 @@ export function zeyosForm(client, resource, opts = {}) {
97
121
  if (opts.onSaved != null && typeof opts.onSaved !== 'function') {
98
122
  throw new TypeError('zeyosForm onSaved must be a function');
99
123
  }
124
+ const notify = opts.notify !== false;
100
125
  const config = buildZeyosFormConfig(client, resource, opts);
101
- const facade = connect(client);
126
+ // The caller's scope, or the form's own element, decides where a toast appears — never whatever
127
+ // element happens to hold focus when a request settles.
128
+ const toastScope = () => opts.scope ?? form.el;
129
+ const facade = connect(client, {
130
+ onError: (error) => notify ? Message.error(errorMessage(error), { scope: toastScope() }) : undefined
131
+ });
102
132
  const form = new Form(null, config.formOptions);
103
133
  let currentId = opts.value?.ID ?? opts.id ?? null;
104
134
  let destroyed = false;
@@ -133,6 +163,10 @@ export function zeyosForm(client, resource, opts = {}) {
133
163
  if (destroyed) return Promise.reject(new Error('Cannot save a destroyed ZeyOS form'));
134
164
  if (savePending) return savePending;
135
165
  if (!form.submit()) return Promise.resolve(null);
166
+ // Resolved now, while the form is in place, and to the theme scope rather than the open
167
+ // dialog the overlay host would be: `onSaved` may close and destroy that dialog, and a toast
168
+ // parked inside it would fall back to an unstyled <body>.
169
+ const host = notify ? overlayScope(toastScope()) : null;
136
170
 
137
171
  savePending = (async () => {
138
172
  let payload;
@@ -157,7 +191,7 @@ export function zeyosForm(client, resource, opts = {}) {
157
191
  throw error;
158
192
  }
159
193
  }
160
- Message.success(`${recordLabel(record, resource)} saved successfully.`);
194
+ if (host) Message.success(`${recordLabel(record, resource)} saved successfully.`, { scope: host });
161
195
  return record;
162
196
  })().finally(() => {
163
197
  savePending = null;
@@ -183,27 +217,41 @@ export function zeyosForm(client, resource, opts = {}) {
183
217
  };
184
218
  }
185
219
 
186
- /** @param {Record<string, unknown>} record @param {Record<string, any>[]} meta @returns {Record<string, unknown>} */
187
- function mapFromRecord(record, meta) {
220
+ /**
221
+ * @param {Record<string, unknown>} record
222
+ * @param {Record<string, any>[]} meta
223
+ * @param {Map<string, 'date'|'datetime'>} dateKinds
224
+ * @returns {Record<string, unknown>}
225
+ */
226
+ function mapFromRecord(record, meta, dateKinds) {
188
227
  const values = {};
189
228
  for (const field of meta) {
190
229
  const id = String(field.id);
191
230
  const value = record[id];
192
- values[id] = isDateField(field) && value != null && value !== ''
193
- ? unixSecondsToDate(/** @type {number|string} */ (value))
194
- : value;
231
+ const kind = dateKinds.get(id);
232
+ if (!kind || value == null || value === '') values[id] = value;
233
+ else if (kind === 'date') values[id] = unixSecondsToCalendarDay(/** @type {number|string} */ (value), field.timeZone);
234
+ else values[id] = unixSecondsToDate(/** @type {number|string} */ (value));
195
235
  }
196
236
  return values;
197
237
  }
198
238
 
199
- /** @param {Record<string, unknown>} values @param {Record<string, any>[]} meta @returns {Record<string, unknown>} */
200
- function mapToRecord(values, meta) {
239
+ /**
240
+ * @param {Record<string, unknown>} values
241
+ * @param {Record<string, any>[]} meta
242
+ * @param {Map<string, 'date'|'datetime'>} dateKinds
243
+ * @returns {Record<string, unknown>}
244
+ */
245
+ function mapToRecord(values, meta, dateKinds) {
201
246
  const record = {};
202
247
  for (const field of meta) {
203
248
  const id = String(field.id);
204
249
  let value = values[id];
205
- if (isDateField(field)) {
206
- value = value == null || value === '' ? null : dateToUnixSeconds(/** @type {Date} */ (value));
250
+ const kind = dateKinds.get(id);
251
+ if (kind) {
252
+ if (value == null || value === '') value = null;
253
+ else if (kind === 'date') value = calendarDayToUnixSeconds(/** @type {Date} */ (value), field.timeZone);
254
+ else value = dateToUnixSeconds(/** @type {Date} */ (value));
207
255
  } else if (hasEnum(field) && isNumericType(field.type) && value !== '' && value != null) {
208
256
  const numeric = Number(value);
209
257
  if (Number.isFinite(numeric)) value = numeric;
@@ -213,12 +261,6 @@ function mapToRecord(values, meta) {
213
261
  return record;
214
262
  }
215
263
 
216
- /** @param {Record<string, any>} meta @returns {boolean} */
217
- function isDateField(meta) {
218
- const descriptor = fieldToZxField(meta);
219
- return descriptor.type === 'date' || descriptor.type === 'datetime';
220
- }
221
-
222
264
  /** @param {Record<string, any>} meta @returns {boolean} */
223
265
  function hasEnum(meta) {
224
266
  return meta.enums != null || meta.enum != null;
@@ -8,7 +8,7 @@ export {
8
8
  } from './schema.js';
9
9
  export {
10
10
  buildListQuery, tableSortToQuery, dataFilterStateToFilters,
11
- dateToUnixSeconds, unixSecondsToDate
11
+ dateToUnixSeconds, unixSecondsToDate, calendarDayToUnixSeconds, unixSecondsToCalendarDay
12
12
  } from './query.js';
13
13
  export { connect } from './connect.js';
14
14
  export {
@@ -20,6 +20,9 @@ export {
20
20
  ZEYOS_LAUNCHER_APPLICATIONS, buildZeyosLauncherConfig, zeyosLauncher,
21
21
  normalizeZeyosLauncherApplications, normalizeZeyosLauncherRecords, zeyosActiveIdentifier
22
22
  } from './launcher.js';
23
+ export {
24
+ zeyosModuleItems, buildZeyosModuleBarConfig, buildZeyosAppSidebarConfig, zeyosModuleBar
25
+ } from './module-bar.js';
23
26
  export {
24
27
  zeyosSelect, buildZeyosSelectConfig, zeyosEntitySelect, buildZeyosEntitySelectConfig
25
28
  } from './select.js';
@@ -224,8 +224,13 @@ export function zeyosActiveIdentifier(state = {}) {
224
224
  return String(state.module ?? '');
225
225
  }
226
226
 
227
- /** @param {unknown} raw @param {string|null} fallbackId @param {unknown} fallbackLabel @returns {Record<string, any>|null} */
228
- function normalizeApplication(raw, fallbackId, fallbackLabel) {
227
+ /**
228
+ * Normalizes one application of the ZeyOS shell catalogue: a `[module, label]` pair, a
229
+ * `[ID, name, module, color]` fork, a weblet row, or an object. Shared with the module bar.
230
+ * @param {unknown} raw @param {string|null} fallbackId @param {unknown} fallbackLabel
231
+ * @returns {Record<string, any>|null}
232
+ */
233
+ export function normalizeApplication(raw, fallbackId, fallbackLabel) {
229
234
  if (Array.isArray(raw)) {
230
235
  if (raw.length < 2) return null;
231
236
  if (raw.length === 2) {
@@ -305,7 +310,7 @@ function normalizeRecord(raw, group, query) {
305
310
  }
306
311
 
307
312
  /** @param {unknown} raw @param {unknown} resolver @returns {Record<string, any>} */
308
- function mergeDestination(raw, resolver) {
313
+ export function mergeDestination(raw, resolver) {
309
314
  const direct = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
310
315
  const resolved = typeof resolver === 'function' ? resolver(raw) : null;
311
316
  const destination = resolved && typeof resolved === 'object' ? resolved : {};