@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
@@ -0,0 +1,181 @@
1
+ import { ModuleBar } from '../components/module-bar/module-bar.js';
2
+ import { normalizeAppItems } from '../internal/app-rail.js';
3
+ import { moduleIcon } from './icons.js';
4
+ import { mergeDestination, normalizeApplication } from './launcher.js';
5
+ import { moduleInfo } from './modules.js';
6
+
7
+ /** @typedef {import('../components/module-bar/module-bar.js').ModuleItem} ModuleItem */
8
+ /** @typedef {import('../components/module-bar/module-bar.js').ModuleBarOptions} ModuleBarOptions */
9
+ /** @typedef {import('../components/app-sidebar/app-sidebar.js').AppSidebarOptions} AppSidebarOptions */
10
+ /**
11
+ * One entry of a module's menu, as the ZeyOS shell describes it: an object, or an
12
+ * `[id, label, href]` array.
13
+ * @typedef {Object} ZeyosMenuEntry
14
+ * @property {string|number} [id] Entry identifier; a section needs none.
15
+ * @property {string} label Entry label.
16
+ * @property {string} [href] Destination.
17
+ * @property {string} [target] Link browsing context.
18
+ * @property {() => void} [invoke] Application-owned action.
19
+ * @property {string|number} [badge] Count.
20
+ * @property {string|Node} [icon] Icon name or node.
21
+ * @property {boolean} [section] A heading over `children` rather than a destination.
22
+ * @property {Array<ZeyosMenuEntry|Array<any>>} [children] Nested entries.
23
+ */
24
+ /**
25
+ * The ZeyOS shell state a module bar is built from. The application fields are the launcher's
26
+ * (`buildZeyosLauncherConfig()`), so one state object can feed both.
27
+ * @typedef {Object} ZeyosModuleBarState
28
+ * @property {Record<string, string>|Array<string|Record<string, any>>} [modules={}] Permitted module catalogue.
29
+ * @property {Array<Array<any>|Record<string, any>>} [menuApps=[]] Forks, weblets, and authoritative menu entries.
30
+ * @property {string[]} [pinned] Application identifiers on the bar, in order; the rest are under "All modules".
31
+ * @property {string|null} [activeIdentifier=null] Current module, `fork#…`, or `weblet#…` identifier.
32
+ * @property {string|number|null} [active=null] Current menu entry, when it is more precise than the module.
33
+ * @property {(module: string) => boolean} [canUseModule] Permission predicate.
34
+ * @property {(app: unknown) => Record<string, any>|null} [resolveApplication] Maps an application to href/invoke/value metadata.
35
+ * @property {Record<string, Array<ZeyosMenuEntry|Array<any>>>} [menus={}] Each application's menu, keyed by identifier.
36
+ * @property {Record<string, string|number>} [badges={}] Badges keyed by application identifier or menu entry id.
37
+ * @property {Record<string, string>} [groups={}] "All modules" group per application identifier.
38
+ * @property {{forks?: string, weblets?: string}} [labels] Group labels for forks and weblets.
39
+ * @property {(app: unknown, normalized: Record<string, any>) => Node|null} [renderApplicationIcon] Host icon renderer.
40
+ */
41
+ /**
42
+ * @typedef {Object} ZeyosModuleBarOptions
43
+ * @property {boolean} [standardIcons=false] Use stock Font Awesome glyphs instead of the ZeyOS kit.
44
+ * @property {'solid'|'tint'|'plain'} [appearance='solid'] Tile treatment; ZeyOS draws its modules solid.
45
+ */
46
+
47
+ /**
48
+ * Maps ZeyOS shell state to module bar items: every permitted module, fork, and weblet with its
49
+ * kit icon, its identity colour (the payload's, else the registry's), its menu, and its badge.
50
+ * A module with a menu is a panel module; one without is a destination.
51
+ * @param {ZeyosModuleBarState} [state={}] Application-owned shell state.
52
+ * @param {ZeyosModuleBarOptions} [options={}] Mapping options.
53
+ * @returns {ModuleItem[]}
54
+ */
55
+ export function zeyosModuleItems(state = {}, options = {}) {
56
+ const pinned = Array.isArray(state.pinned) ? state.pinned.map(String) : null;
57
+ const menus = state.menus && typeof state.menus === 'object' ? state.menus : {};
58
+ const badges = state.badges && typeof state.badges === 'object' ? state.badges : {};
59
+ const groups = state.groups && typeof state.groups === 'object' ? state.groups : {};
60
+ const labels = { forks: 'Forks', weblets: 'Weblets', ...(state.labels ?? {}) };
61
+ const seen = new Set();
62
+ /** @type {Record<string, any>[]} */
63
+ const items = [];
64
+ const add = (raw, fallbackId = null, fallbackLabel = null) => {
65
+ const app = normalizeApplication(raw, fallbackId, fallbackLabel);
66
+ if (!app || seen.has(app.identifier)) return;
67
+ if (app.type === 'module' && typeof state.canUseModule === 'function' && !state.canUseModule(app.module)) return;
68
+ seen.add(app.identifier);
69
+ const children = menuEntries(menus[app.identifier], badges);
70
+ const destination = children.length ? {} : mergeDestination(app.raw, state.resolveApplication);
71
+ const pin = pinned ? pinned.indexOf(app.identifier) : -1;
72
+ const group = groups[app.identifier]
73
+ ?? (app.type === 'fork' ? labels.forks : app.type === 'weblet' ? labels.weblets : undefined);
74
+ items.push({
75
+ id: app.identifier,
76
+ label: app.label,
77
+ // A factory, so a host renderer or the kit glyph is built per tile rather than shared.
78
+ icon: () => (typeof state.renderApplicationIcon === 'function' ? state.renderApplicationIcon(app.raw, app) : null)
79
+ ?? moduleIcon(app.module, { size: '1em', standard: Boolean(options.standardIcons) }),
80
+ color: app.color ?? moduleInfo(app.module).color,
81
+ module: app.module,
82
+ ...(group ? { group } : {}),
83
+ ...(badges[app.identifier] != null ? { badge: badges[app.identifier] } : {}),
84
+ ...(pinned ? { pinned: pin >= 0 ? pin : false } : {}),
85
+ ...(children.length ? { children } : destination)
86
+ });
87
+ };
88
+ for (const raw of Array.isArray(state.menuApps) ? state.menuApps : []) add(raw);
89
+ if (Array.isArray(state.modules)) {
90
+ for (const module of state.modules) {
91
+ if (typeof module === 'string') add([module, moduleInfo(module).label]);
92
+ else add(module, module?.id ?? module?.module, module?.label);
93
+ }
94
+ } else {
95
+ for (const [module, label] of Object.entries(state.modules ?? {})) add([module, label]);
96
+ }
97
+ return normalizeAppItems(items);
98
+ }
99
+
100
+ /**
101
+ * Builds plain ModuleBar options from ZeyOS shell state.
102
+ * @param {ZeyosModuleBarState} [state={}] Application-owned shell state.
103
+ * @param {ZeyosModuleBarOptions & ModuleBarOptions} [options={}] Mapping and ModuleBar options.
104
+ * @returns {ModuleBarOptions}
105
+ */
106
+ export function buildZeyosModuleBarConfig(state = {}, options = {}) {
107
+ const { standardIcons = false, appearance = 'solid', ...barOptions } = options;
108
+ return {
109
+ ...barOptions,
110
+ items: zeyosModuleItems(state, { standardIcons }),
111
+ active: state.active ?? state.activeIdentifier ?? null,
112
+ appearance
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Builds AppSidebar options for the modules layout from ZeyOS shell state: the module strip beside
118
+ * a panel with the current module's menu.
119
+ * @param {ZeyosModuleBarState} [state={}] Application-owned shell state.
120
+ * @param {ZeyosModuleBarOptions & AppSidebarOptions} [options={}] Mapping and AppSidebar options.
121
+ * @returns {AppSidebarOptions}
122
+ */
123
+ export function buildZeyosAppSidebarConfig(state = {}, options = {}) {
124
+ const { standardIcons = false, appearance = 'solid', moduleBar = {}, ...sidebarOptions } = options;
125
+ return {
126
+ label: 'ZeyOS',
127
+ ...sidebarOptions,
128
+ layout: 'modules',
129
+ items: zeyosModuleItems(state, { standardIcons }),
130
+ active: state.active ?? state.activeIdentifier ?? null,
131
+ moduleBar: { appearance, ...moduleBar }
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Creates a ModuleBar from ZeyOS shell state.
137
+ * @param {Element|string|null} target Bar target, or null to create one.
138
+ * @param {ZeyosModuleBarState} [state={}] Application-owned shell state.
139
+ * @param {ZeyosModuleBarOptions & ModuleBarOptions} [options={}] Mapping and ModuleBar options.
140
+ * @returns {ModuleBar}
141
+ */
142
+ export function zeyosModuleBar(target, state = {}, options = {}) {
143
+ return new ModuleBar(target, buildZeyosModuleBarConfig(state, options));
144
+ }
145
+
146
+ /**
147
+ * Normalizes a ZeyOS menu: objects and `[id, label, href]` arrays, sections, nested entries, and
148
+ * badges looked up by entry id. Entries without a label, and leaves without an id, are dropped.
149
+ * @param {unknown} entries Menu entries.
150
+ * @param {Record<string, string|number>} badges Badges by id.
151
+ * @returns {Record<string, any>[]}
152
+ */
153
+ function menuEntries(entries, badges) {
154
+ if (!Array.isArray(entries)) return [];
155
+ return entries.flatMap((entry) => {
156
+ if (Array.isArray(entry)) {
157
+ const [id, label, href] = entry;
158
+ if (id == null || label == null) return [];
159
+ return [{
160
+ id, label: String(label),
161
+ ...(href != null ? { href: String(href) } : {}),
162
+ ...(badges[String(id)] != null ? { badge: badges[String(id)] } : {})
163
+ }];
164
+ }
165
+ if (!entry || typeof entry !== 'object' || entry.label == null) return [];
166
+ const children = menuEntries(entry.children, badges);
167
+ if (entry.section === true) {
168
+ return children.length ? [{ ...(entry.id != null ? { id: entry.id } : {}), section: true, label: String(entry.label), children }] : [];
169
+ }
170
+ if (entry.id == null) return [];
171
+ const picked = Object.fromEntries(['id', 'href', 'target', 'invoke', 'icon', 'value', 'disabled']
172
+ .filter((key) => entry[key] !== undefined).map((key) => [key, entry[key]]));
173
+ const badge = badges[String(entry.id)] ?? entry.badge;
174
+ return [{
175
+ ...picked,
176
+ label: String(entry.label),
177
+ ...(badge != null ? { badge } : {}),
178
+ ...(children.length ? { children } : {})
179
+ }];
180
+ });
181
+ }
@@ -20,8 +20,10 @@
20
20
  * @property {string} [rawField] Raw ZeyOS field when the UI id is an alias.
21
21
  * @property {string[]} [fields] Raw fields combined as an OR group.
22
22
  * @property {string} [operator] ZeyOS comparison operator such as `>=` or `IN`.
23
- * @property {string} [format] Semantic value format; date/datetime values become Unix seconds.
24
- * @property {string} [type] Semantic value type; date/datetime values become Unix seconds.
23
+ * @property {string} [format] Semantic value format; date/datetime values become Unix seconds. With
24
+ * a `timeZone` conversion option, a `date` (date-only) value is a calendar day and becomes that
25
+ * day's midnight in the zone, like a ZeyOS form stores it; `datetime` values stay instants.
26
+ * @property {string} [type] Semantic value type, read like `format`.
25
27
  * @property {boolean} [includeEmpty=false] Preserve an empty-string value instead of treating it as inactive UI state.
26
28
  * @property {boolean} [includeNull=false] Preserve null instead of treating it as inactive UI state.
27
29
  * @property {(value: unknown, state: Record<string, unknown>) => unknown} [transform] Value transform.
@@ -55,6 +57,159 @@ export function unixSecondsToDate(seconds) {
55
57
  return new Date(value * 1000);
56
58
  }
57
59
 
60
+ /**
61
+ * @typedef {Object} CalendarDay
62
+ * @property {number} year Full year.
63
+ * @property {number} month Zero-based month, as `Date` counts them.
64
+ * @property {number} day Day of the month.
65
+ */
66
+
67
+ /**
68
+ * @typedef {Object} CalendarZone
69
+ * @property {string|undefined} timeZone Canonical IANA zone, or undefined for the runtime's own zone.
70
+ * @property {(milliseconds: number) => CalendarDay} dayOf Calendar day an instant falls on in the zone.
71
+ * @property {(year: number, month: number, day: number) => number} midnightOf Epoch milliseconds of
72
+ * the first moment of a calendar day in the zone (its midnight, or the first minute after a
73
+ * daylight-saving gap that swallows midnight).
74
+ */
75
+
76
+ /**
77
+ * Reads a ZeyOS date-only value as the calendar day it names. Date-only fields — an invoice's
78
+ * `date` and `duedate`, a contact's `birthdate` — hold Unix seconds at midnight of a day in the
79
+ * instance's time zone: a calendar day, not an instant. The result is midnight of that same day in
80
+ * the runtime's own zone, which is what Datebox and DatePicker edit, so the day survives a viewer in
81
+ * another zone and a daylight-saving change.
82
+ * @param {number|string|null|undefined} seconds Unix seconds; numeric strings are accepted.
83
+ * @param {string} [timeZone] IANA zone of the instance, such as `Europe/Vienna`; the runtime's own
84
+ * zone when omitted.
85
+ * @returns {Date|null} Local midnight of the day, or null for a nullish or empty input.
86
+ */
87
+ export function unixSecondsToCalendarDay(seconds, timeZone) {
88
+ if (seconds == null || seconds === '') return null;
89
+ const value = Number(seconds);
90
+ if (!Number.isFinite(value)) throw new TypeError('Expected finite Unix seconds');
91
+ const { year, month, day } = calendarZone(timeZone).dayOf(value * 1000);
92
+ return localMidnight(year, month, day);
93
+ }
94
+
95
+ /**
96
+ * Converts the calendar day of a Date to the Unix seconds a ZeyOS date-only field stores: midnight
97
+ * of that day in the instance's time zone. Only the Date's local year, month, and day count — its
98
+ * time of day is ignored — so a Datebox value round-trips through {@link unixSecondsToCalendarDay}.
99
+ * @param {Date|null|undefined} date Day to store.
100
+ * @param {string} [timeZone] IANA zone of the instance; the runtime's own zone when omitted.
101
+ * @returns {number|null} Unix seconds, or null for a nullish input.
102
+ */
103
+ export function calendarDayToUnixSeconds(date, timeZone) {
104
+ if (date == null) return null;
105
+ if (!(date instanceof Date)) throw new TypeError('Expected a Date');
106
+ if (!Number.isFinite(date.getTime())) throw new RangeError('Invalid Date');
107
+ const midnight = calendarZone(timeZone).midnightOf(date.getFullYear(), date.getMonth(), date.getDate());
108
+ return Math.floor(midnight / 1000);
109
+ }
110
+
111
+ /**
112
+ * Validates an IANA time-zone name.
113
+ * @param {unknown} timeZone Zone name; nullish or empty selects the runtime's own zone.
114
+ * @returns {string|undefined} The canonical zone name, or undefined for the runtime's own zone.
115
+ * @throws {RangeError} When the runtime does not know the zone.
116
+ */
117
+ export function normalizeTimeZone(timeZone) {
118
+ if (timeZone == null || timeZone === '') return undefined;
119
+ try {
120
+ return new Intl.DateTimeFormat('en-US', { timeZone: String(timeZone) }).resolvedOptions().timeZone;
121
+ } catch {
122
+ throw new RangeError(`Unknown time zone "${String(timeZone)}"`);
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Calendar arithmetic for one time zone. The `Intl` formatter is built once, so a table column can
128
+ * hold one zone for all of its cells.
129
+ * @param {string} [timeZone] IANA zone; the runtime's own zone when omitted.
130
+ * @returns {CalendarZone} Day lookup and midnight construction for the zone.
131
+ */
132
+ export function calendarZone(timeZone) {
133
+ const zone = normalizeTimeZone(timeZone);
134
+ if (!zone) {
135
+ return {
136
+ timeZone: undefined,
137
+ dayOf: (milliseconds) => {
138
+ const date = new Date(milliseconds);
139
+ return { year: date.getFullYear(), month: date.getMonth(), day: date.getDate() };
140
+ },
141
+ midnightOf: (year, month, day) => localMidnight(year, month, day).getTime()
142
+ };
143
+ }
144
+
145
+ const formatter = new Intl.DateTimeFormat('en-US', {
146
+ timeZone: zone,
147
+ hourCycle: 'h23',
148
+ year: 'numeric',
149
+ month: 'numeric',
150
+ day: 'numeric',
151
+ hour: 'numeric',
152
+ minute: 'numeric',
153
+ second: 'numeric'
154
+ });
155
+ /** @param {number} milliseconds @returns {CalendarDay & {seconds: number}} */
156
+ const wallClock = (milliseconds) => {
157
+ const parts = {};
158
+ for (const part of formatter.formatToParts(new Date(milliseconds))) parts[part.type] = Number(part.value);
159
+ return {
160
+ year: parts.year,
161
+ month: parts.month - 1,
162
+ day: parts.day,
163
+ seconds: (parts.hour % 24) * 3600 + parts.minute * 60 + parts.second
164
+ };
165
+ };
166
+ /** @param {number} milliseconds @returns {number} The zone's UTC offset at that instant, in milliseconds. */
167
+ const offsetAt = (milliseconds) => {
168
+ const wall = wallClock(milliseconds);
169
+ return utcMidnight(wall.year, wall.month, wall.day) + wall.seconds * 1000
170
+ - Math.floor(milliseconds / 1000) * 1000;
171
+ };
172
+
173
+ return {
174
+ timeZone: zone,
175
+ dayOf: (milliseconds) => {
176
+ const { year, month, day } = wallClock(milliseconds);
177
+ return { year, month, day };
178
+ },
179
+ midnightOf: (year, month, day) => {
180
+ // Midnight is the instant whose wall clock reads the target day at 00:00. Two offset
181
+ // corrections settle it on either side of a daylight-saving change; where a change skips
182
+ // midnight altogether, the earlier candidate lands on the previous evening and the later one
183
+ // on the first minute of the day, which is the one kept.
184
+ const target = utcMidnight(year, month, day);
185
+ const first = target - offsetAt(target);
186
+ const second = target - offsetAt(first);
187
+ let best = null;
188
+ for (const candidate of [second, first]) {
189
+ const wall = wallClock(candidate);
190
+ if (wall.year !== year || wall.month !== month || wall.day !== day) continue;
191
+ if (!best || wall.seconds < best.seconds) best = { candidate, seconds: wall.seconds };
192
+ }
193
+ return best ? best.candidate : second;
194
+ }
195
+ };
196
+ }
197
+
198
+ /** @param {number} year @param {number} month @param {number} day @returns {Date} */
199
+ function localMidnight(year, month, day) {
200
+ const date = new Date(2000, 0, 1);
201
+ date.setFullYear(year, month, day);
202
+ date.setHours(0, 0, 0, 0);
203
+ return date;
204
+ }
205
+
206
+ /** @param {number} year @param {number} month @param {number} day @returns {number} */
207
+ function utcMidnight(year, month, day) {
208
+ const date = new Date(0);
209
+ date.setUTCFullYear(year, month, day);
210
+ return date.getTime();
211
+ }
212
+
58
213
  /**
59
214
  * Converts one or more Zx Table sort records to the signed field notation used by ZeyOS.
60
215
  * A missing sort produces an empty array.
@@ -72,6 +227,16 @@ export function tableSortToQuery(sort) {
72
227
  });
73
228
  }
74
229
 
230
+ /**
231
+ * @typedef {Object} FilterConversionOptions
232
+ * @property {string|null} [timeZone] IANA zone of the ZeyOS instance. When given, values of
233
+ * `date` (date-only) definitions are calendar days: a `Date` counts by its local year, month, and
234
+ * day, a `YYYY-MM-DD` string by its digits, and either becomes midnight of that day in the zone —
235
+ * exactly what `calendarDayToUnixSeconds()` and `zeyosForm()` store, so a viewer in another zone
236
+ * filters `duedate` by the day they picked. `null` names the runtime's own zone. Omitted, dates
237
+ * convert as exact instants, as they always have.
238
+ */
239
+
75
240
  /**
76
241
  * Converts DataFilter state to ZeyOS field predicates. Definitions may rename UI ids,
77
242
  * add comparison operators, combine several fields with OR, or transform values. Blank/null
@@ -79,12 +244,17 @@ export function tableSortToQuery(sort) {
79
244
  * the distinct ZeyOS meanings. Zero and false are always retained.
80
245
  * @param {Record<string, unknown>} state DataFilter state keyed by definition id.
81
246
  * @param {ServerFilterDefinition[]|Record<string, string|ServerFilterDefinition>} [defs=[]] Server mappings.
247
+ * @param {FilterConversionOptions} [options={}] Conversion options, such as the instance zone.
82
248
  * @returns {Record<string, unknown>} ZeyOS `filters` object (plural at the call site).
249
+ * @throws {RangeError} When `options.timeZone` names a zone the runtime does not know.
83
250
  */
84
- export function dataFilterStateToFilters(state, defs = []) {
251
+ export function dataFilterStateToFilters(state, defs = [], options = {}) {
85
252
  if (!state || typeof state !== 'object' || Array.isArray(state)) {
86
253
  throw new TypeError('DataFilter state must be an object');
87
254
  }
255
+ const calendarDays = Boolean(options) && typeof options === 'object'
256
+ && Object.prototype.hasOwnProperty.call(options, 'timeZone');
257
+ const zone = calendarDays ? normalizeTimeZone(options.timeZone) : undefined;
88
258
  const definitions = normalizeDefinitions(state, defs);
89
259
  const filters = {};
90
260
  let groupIndex = 0;
@@ -106,7 +276,7 @@ export function dataFilterStateToFilters(state, defs = []) {
106
276
 
107
277
  let value = typeof definition.transform === 'function'
108
278
  ? definition.transform(original, state)
109
- : normalizeFilterValue(original, definition);
279
+ : normalizeFilterValue(original, definition, calendarDays ? { timeZone: zone } : null);
110
280
  if (value === undefined) continue;
111
281
  if (definition.operator) value = { [definition.operator]: value };
112
282
 
@@ -174,16 +344,30 @@ function normalizeDefinitions(state, defs) {
174
344
  return Object.keys(state).map((id) => ({ id, field: id }));
175
345
  }
176
346
 
177
- /** @param {unknown} value @param {ServerFilterDefinition} definition @returns {unknown} */
178
- function normalizeFilterValue(value, definition) {
347
+ /**
348
+ * @param {unknown} value Filter value.
349
+ * @param {ServerFilterDefinition} definition Server mapping.
350
+ * @param {{timeZone: string|undefined}|null} calendar Zone for date-only values, or null to convert
351
+ * every date as an instant.
352
+ * @returns {unknown}
353
+ */
354
+ function normalizeFilterValue(value, definition, calendar = null) {
179
355
  const semantic = String(definition.format ?? definition.type ?? '').toLowerCase();
180
- if (value instanceof Date && ['date', 'datetime', 'date-time'].includes(semantic)) {
181
- return dateToUnixSeconds(value);
182
- }
183
- if (Array.isArray(value) && ['date', 'datetime', 'date-time'].includes(semantic)) {
184
- return value.map((entry) => entry instanceof Date ? dateToUnixSeconds(entry) : entry);
185
- }
186
- return value;
356
+ if (!['date', 'datetime', 'date-time'].includes(semantic)) return value;
357
+ /** @param {unknown} entry @returns {unknown} */
358
+ const convert = (entry) => {
359
+ if (calendar && semantic === 'date') {
360
+ if (entry instanceof Date) return calendarDayToUnixSeconds(entry, calendar.timeZone);
361
+ const day = typeof entry === 'string' ? /^(\d{4})-(\d{2})-(\d{2})$/.exec(entry.trim()) : null;
362
+ if (day) {
363
+ const midnight = calendarZone(calendar.timeZone).midnightOf(Number(day[1]), Number(day[2]) - 1, Number(day[3]));
364
+ return Math.floor(midnight / 1000);
365
+ }
366
+ return entry;
367
+ }
368
+ return entry instanceof Date ? dateToUnixSeconds(entry) : entry;
369
+ };
370
+ return Array.isArray(value) ? value.map(convert) : convert(value);
187
371
  }
188
372
 
189
373
  /** @param {ZxSort|ZxSort[]|undefined} sort @param {string[]|Record<string, string>|undefined} fields @returns {ZxSort|ZxSort[]|undefined} */