@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,487 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * The overlay kernel: one z-order, one registry of open overlays, and one document-level
5
+ * dispatcher for Escape, outside presses, and focus leaving an overlay.
6
+ *
7
+ * Before it, every overlay counted its own order and listened to the document itself, and none of
8
+ * them knew about the others: a non-modal Sheet closed itself when an item was chosen in a menu it
9
+ * had opened, and one Escape closed a sheet while leaving its dropdown stranded on a hidden anchor.
10
+ * Here each open overlay is one entry, and two rules decide everything:
11
+ *
12
+ * - **Escape goes to one layer.** The topmost entry that handles Escape gets it, and only that
13
+ * one. An event that arrives `defaultPrevented` was already spent by something inside (a Select
14
+ * closing its own list) and reaches no overlay at all. A modal `<dialog>` handles Escape natively,
15
+ * so reaching one stops the search without touching the event.
16
+ * - **Inside means inside the opener chain.** Each entry records the overlay its anchor sits in as
17
+ * its parent. A press inside a popup anchored from within a sheet is therefore inside the sheet,
18
+ * wherever in the DOM the popup's panel lives; a press outside both dismisses both.
19
+ *
20
+ * Closing an entry also closes the entries opened from it that asked to follow their parent, so a
21
+ * sheet never leaves a dropdown open on an element that is no longer on screen.
22
+ *
23
+ * The document listeners exist only while at least one overlay is open, and go away with the last
24
+ * one through a single AbortController.
25
+ */
26
+
27
+ /**
28
+ * What kind of layer an entry is.
29
+ *
30
+ * - `modal` — a `showModal()` dialog. Escape and the backdrop belong to the browser, and nothing
31
+ * below a modal reacts to presses or keys.
32
+ * - `non-modal` — a dialog that leaves the page live (a floating non-modal Sheet).
33
+ * - `popup` — an anchored panel: a Dropdown, a menu, a flyout.
34
+ * - `tooltip` — a description bubble. Any press outside it dismisses it, whatever lies above, and
35
+ * it is never anybody's parent.
36
+ * - `toast` — a message region. It never takes Escape and is never a parent, and a press inside one
37
+ * dismisses nothing, so acting on a toast does not close the drawer it reports on.
38
+ * @typedef {'modal'|'non-modal'|'popup'|'tooltip'|'toast'} OverlayKind
39
+ */
40
+
41
+ /**
42
+ * @typedef {Object} OverlayEntryOptions
43
+ * @property {OverlayKind} kind Layer kind.
44
+ * @property {Element} root The overlay's own root: the panel, dialog, or region.
45
+ * @property {unknown} [owner=null] Component that owns the entry; informational.
46
+ * @property {Element|null|Array<Element|null|undefined>} [anchor=null] Element(s) the overlay was
47
+ * opened from or is anchored to. The topmost entry whose root contains one becomes the parent.
48
+ * @property {Array<Element|null|undefined>} [inside=[]] Further elements that count as part of the
49
+ * overlay for outside presses and focus — a dropdown's trigger, which toggles it on its own.
50
+ * @property {(event: KeyboardEvent, handle: OverlayHandle) => boolean|void} [onEscape] Handles an
51
+ * Escape routed to this entry. Return `false` to decline, which passes it to the next layer down.
52
+ * The dispatcher calls `preventDefault()` on an Escape an entry handled.
53
+ * @property {(event: Event, handle: OverlayHandle) => void} [onOutsidePointer] Called for a press
54
+ * outside the entry's chain, while nothing above it contained the press.
55
+ * @property {(event: FocusEvent, handle: OverlayHandle) => void} [onFocusOutside] Called when focus
56
+ * moves to an element outside the entry's chain.
57
+ * @property {(handle: OverlayHandle) => void} [onParentClose] Called when the parent entry closes.
58
+ * An entry without it outlives its parent and is re-parented to the grandparent.
59
+ */
60
+
61
+ /**
62
+ * One registered overlay, as the stack keeps it.
63
+ * @typedef {Object} OverlayEntry
64
+ * @property {number} order Z-order; higher is on top. Unique and increasing, never reused.
65
+ * @property {OverlayKind} kind Layer kind.
66
+ * @property {Element} root Overlay root.
67
+ * @property {unknown} owner Owning component.
68
+ * @property {Element[]} anchors Anchor elements.
69
+ * @property {Element[]} inside Extra elements that count as inside.
70
+ * @property {OverlayEntry|null} parent Overlay the anchor sat in when the entry opened.
71
+ * @property {OverlayEntryOptions['onEscape']} [onEscape] Escape handler.
72
+ * @property {OverlayEntryOptions['onOutsidePointer']} [onOutsidePointer] Outside-press handler.
73
+ * @property {OverlayEntryOptions['onFocusOutside']} [onFocusOutside] Focus-outside handler.
74
+ * @property {OverlayEntryOptions['onParentClose']} [onParentClose] Parent-close handler.
75
+ */
76
+
77
+ /**
78
+ * Returned by `push()`: the owner's grip on its entry.
79
+ * @typedef {Object} OverlayHandle
80
+ * @property {OverlayEntry} entry The registered entry.
81
+ * @property {number} order The entry's z-order, for `data-zx-overlay-order`.
82
+ * @property {() => boolean} isOpen Whether the entry is still registered.
83
+ * @property {() => boolean} isTopmost Whether no other entry (toasts aside) sits above it.
84
+ * @property {(node: Node|null|undefined) => boolean} contains Whether a node is inside the entry's
85
+ * chain: its root, its `inside` elements, or any overlay opened from it.
86
+ * @property {() => void} release Unregisters the entry and closes the children that follow it.
87
+ * Safe to call repeatedly.
88
+ */
89
+
90
+ /** Kinds that can never contain another overlay's anchor. */
91
+ const LEAF_KINDS = new Set(['tooltip', 'toast']);
92
+
93
+ /**
94
+ * Whether `node` is `root` or inside it, crossing shadow roots.
95
+ * @param {Node|null|undefined} root Container.
96
+ * @param {Node|null|undefined} node Candidate.
97
+ * @returns {boolean}
98
+ */
99
+ export function containsNode(root, node) {
100
+ if (!root || !node) return false;
101
+ for (let current = /** @type {any} */ (node); current; current = current.parentNode ?? current.host ?? null) {
102
+ if (current === root) return true;
103
+ }
104
+ return false;
105
+ }
106
+
107
+ /**
108
+ * The entry an overlay opened from `anchors` belongs under: the topmost one whose root contains an
109
+ * anchor. Tooltips and toasts are never parents.
110
+ * @param {readonly OverlayEntry[]} entries Registered entries, bottom to top.
111
+ * @param {readonly Element[]} anchors Anchor elements of the overlay being opened.
112
+ * @returns {OverlayEntry|null}
113
+ */
114
+ export function findParent(entries, anchors) {
115
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
116
+ const entry = entries[index];
117
+ if (LEAF_KINDS.has(entry.kind)) continue;
118
+ if (anchors.some((anchor) => containsNode(entry.root, anchor))) return entry;
119
+ }
120
+ return null;
121
+ }
122
+
123
+ /**
124
+ * Whether a node is inside an entry's chain: its root, its `inside` elements, or — recursively —
125
+ * any entry whose parent it is.
126
+ * @param {readonly OverlayEntry[]} entries Registered entries.
127
+ * @param {OverlayEntry} entry Entry to test.
128
+ * @param {Node|null|undefined} node Candidate.
129
+ * @returns {boolean}
130
+ */
131
+ export function insideChain(entries, entry, node) {
132
+ if (!node) return false;
133
+ if (containsNode(entry.root, node)) return true;
134
+ if (entry.inside.some((element) => containsNode(element, node))) return true;
135
+ return entries.some((candidate) => candidate.parent === entry && insideChain(entries, candidate, node));
136
+ }
137
+
138
+ /**
139
+ * The topmost entry, toasts aside, optionally narrowed by a predicate.
140
+ * @param {readonly OverlayEntry[]} entries Registered entries, bottom to top.
141
+ * @param {(entry: OverlayEntry) => boolean} [filter] Extra condition.
142
+ * @returns {OverlayEntry|null}
143
+ */
144
+ export function topmostEntry(entries, filter = () => true) {
145
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
146
+ const entry = entries[index];
147
+ if (entry.kind !== 'toast' && filter(entry)) return entry;
148
+ }
149
+ return null;
150
+ }
151
+
152
+ /**
153
+ * The entries an Escape may be offered to, in the order they are asked: from the top down, those
154
+ * with an Escape handler, stopping at the first modal (whose Escape is the browser's).
155
+ *
156
+ * `within` is the open modal dialog the key was pressed in, when there is one. Everything outside
157
+ * it is inert, so an entry whose root lies outside it is never offered the key — which keeps a
158
+ * modal the stack does not know about (a `<dialog>` an application opened with `showModal()`)
159
+ * from losing its Escape to a popup left open behind it.
160
+ * @param {readonly OverlayEntry[]} entries Registered entries, bottom to top.
161
+ * @param {Node|null} [within=null] Open modal dialog containing the key's target.
162
+ * @returns {OverlayEntry[]}
163
+ */
164
+ export function escapeCandidates(entries, within = null) {
165
+ const candidates = [];
166
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
167
+ const entry = entries[index];
168
+ if (entry.kind === 'modal') break;
169
+ if (within && !containsNode(within, entry.root)) continue;
170
+ if (typeof entry.onEscape === 'function') candidates.push(entry);
171
+ }
172
+ return candidates;
173
+ }
174
+
175
+ /**
176
+ * The entries a press on `node` dismisses, topmost first.
177
+ *
178
+ * A press inside a toast dismisses nothing. Tooltips go on any press outside them. For the rest,
179
+ * the walk runs from the top down and stops at the first entry whose chain contains the press — it
180
+ * landed on that layer, and the layers beneath are covered by it or own it — or at a modal, below
181
+ * which everything is inert. Every entry passed on the way that handles outside presses is
182
+ * dismissed. As for Escape, a press inside an open modal dialog dismisses nothing outside it.
183
+ * @param {readonly OverlayEntry[]} entries Registered entries, bottom to top.
184
+ * @param {Node|null|undefined} node Press target.
185
+ * @param {Node|null} [within=null] Open modal dialog containing the press.
186
+ * @returns {OverlayEntry[]}
187
+ */
188
+ export function outsideTargets(entries, node, within = null) {
189
+ if (entries.some((entry) => entry.kind === 'toast' && containsNode(entry.root, node))) return [];
190
+ const targets = [];
191
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
192
+ const entry = entries[index];
193
+ if (entry.kind === 'tooltip') {
194
+ if (!insideChain(entries, entry, node) && entry.onOutsidePointer) targets.push(entry);
195
+ }
196
+ }
197
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
198
+ const entry = entries[index];
199
+ if (LEAF_KINDS.has(entry.kind)) continue;
200
+ if (insideChain(entries, entry, node)) break;
201
+ if (entry.kind === 'modal') break;
202
+ if (within && !containsNode(within, entry.root)) continue;
203
+ if (entry.onOutsidePointer) targets.push(entry);
204
+ }
205
+ return targets;
206
+ }
207
+
208
+ /**
209
+ * A registry of open overlays. The library uses the one shared instance, `overlayStack`; separate
210
+ * instances exist for tests and for documents other than the global one.
211
+ */
212
+ export class OverlayStack {
213
+ /** @type {OverlayEntry[]} Bottom to top. */
214
+ #entries = [];
215
+ #order = 0;
216
+ /** @type {AbortController|null} */
217
+ #listening = null;
218
+ /** @type {Set<(entries: readonly OverlayEntry[]) => void>} */
219
+ #listeners = new Set();
220
+ /** @type {EventTarget|null} */
221
+ #target;
222
+ /** @type {WeakMap<OverlayEntry, OverlayHandle>} */
223
+ #handles = new WeakMap();
224
+
225
+ /**
226
+ * @param {{target?: EventTarget|null}} [options={}] Where to listen; the global document when
227
+ * omitted. Pass `null` to install no listeners and drive the dispatch methods directly.
228
+ */
229
+ constructor(options = {}) {
230
+ this.#target = options.target === undefined
231
+ ? (typeof document === 'undefined' ? null : document)
232
+ : options.target;
233
+ }
234
+
235
+ /**
236
+ * Registers an open overlay above every other.
237
+ * @param {OverlayEntryOptions} options Entry description.
238
+ * @returns {OverlayHandle}
239
+ */
240
+ push(options) {
241
+ const anchors = toList(options.anchor);
242
+ /** @type {OverlayEntry} */
243
+ const entry = {
244
+ order: ++this.#order,
245
+ kind: options.kind,
246
+ root: options.root,
247
+ owner: options.owner ?? null,
248
+ anchors,
249
+ inside: toList(options.inside),
250
+ parent: options.kind === 'toast' ? null : findParent(this.#entries, anchors),
251
+ onEscape: options.onEscape,
252
+ onOutsidePointer: options.onOutsidePointer,
253
+ onFocusOutside: options.onFocusOutside,
254
+ onParentClose: options.onParentClose
255
+ };
256
+ this.#entries.push(entry);
257
+ /** @type {OverlayHandle} */
258
+ const handle = {
259
+ entry,
260
+ order: entry.order,
261
+ isOpen: () => this.#entries.includes(entry),
262
+ isTopmost: () => topmostEntry(this.#entries) === entry,
263
+ contains: (node) => this.#entries.includes(entry) && insideChain(this.#entries, entry, node),
264
+ release: () => this.#release(entry)
265
+ };
266
+ this.#handles.set(entry, handle);
267
+ this.#listen();
268
+ this.#notify();
269
+ return handle;
270
+ }
271
+
272
+ /**
273
+ * The registered entries, bottom to top. A copy: changing it changes nothing.
274
+ * @returns {OverlayEntry[]}
275
+ */
276
+ entries() {
277
+ return this.#entries.slice();
278
+ }
279
+
280
+ /**
281
+ * The topmost entry, toasts aside.
282
+ * @param {(entry: OverlayEntry) => boolean} [filter] Extra condition.
283
+ * @returns {OverlayEntry|null}
284
+ */
285
+ topmost(filter) {
286
+ return topmostEntry(this.#entries, filter);
287
+ }
288
+
289
+ /**
290
+ * Root of the topmost open modal dialog, or null when none is open. Asks the registry first and
291
+ * falls back to the document, so a `<dialog>` opened with `showModal()` outside the library still
292
+ * counts.
293
+ * @returns {Element|null}
294
+ */
295
+ topmostModal() {
296
+ const entry = topmostEntry(this.#entries, (candidate) => candidate.kind === 'modal'
297
+ && candidate.root.isConnected && matches(candidate.root, ':modal', true));
298
+ if (entry) return entry.root;
299
+ if (typeof document === 'undefined' || typeof document.querySelectorAll !== 'function') return null;
300
+ let modals;
301
+ try {
302
+ modals = document.querySelectorAll('dialog:modal');
303
+ } catch {
304
+ return null;
305
+ }
306
+ return modals.length ? modals[modals.length - 1] : null;
307
+ }
308
+
309
+ /**
310
+ * Whether a node is inside any registered overlay's root or `inside` elements.
311
+ * @param {Node|null|undefined} node Candidate.
312
+ * @returns {boolean}
313
+ */
314
+ containsNode(node) {
315
+ return this.#entries.some((entry) => containsNode(entry.root, node)
316
+ || entry.inside.some((element) => containsNode(element, node)));
317
+ }
318
+
319
+ /**
320
+ * Subscribes to registry changes: called after every push and release.
321
+ * @param {(entries: readonly OverlayEntry[]) => void} listener Change listener.
322
+ * @returns {() => void} Unsubscribes.
323
+ */
324
+ subscribe(listener) {
325
+ this.#listeners.add(listener);
326
+ return () => { this.#listeners.delete(listener); };
327
+ }
328
+
329
+ /**
330
+ * Routes an Escape keydown to the topmost layer that handles it. Ignores other keys and events
331
+ * already `defaultPrevented`. Installed on the document while overlays are open; public so a
332
+ * stack created without a target can be driven directly.
333
+ * @param {KeyboardEvent} event Keydown event.
334
+ * @returns {OverlayEntry|null} The entry that handled it.
335
+ */
336
+ handleKeydown(event) {
337
+ if (event.key !== 'Escape' || event.defaultPrevented || event.isComposing) return null;
338
+ for (const entry of escapeCandidates(this.#entries, modalAround(eventNode(event)))) {
339
+ const handle = this.#handles.get(entry);
340
+ if (!handle || !this.#entries.includes(entry)) continue;
341
+ if (entry.onEscape?.(event, handle) === false) continue;
342
+ event.preventDefault();
343
+ return entry;
344
+ }
345
+ return null;
346
+ }
347
+
348
+ /**
349
+ * Dismisses the layers a press landed outside of. Installed on the document, capture phase, while
350
+ * overlays are open; public so a stack created without a target can be driven directly.
351
+ * @param {Event} event Pointerdown event.
352
+ * @returns {OverlayEntry[]} The entries asked to dismiss, topmost first.
353
+ */
354
+ handlePointerdown(event) {
355
+ const node = eventNode(event);
356
+ const targets = outsideTargets(this.#entries, node, modalAround(node));
357
+ for (const entry of targets) {
358
+ const handle = this.#handles.get(entry);
359
+ if (handle && this.#entries.includes(entry)) entry.onOutsidePointer?.(event, handle);
360
+ }
361
+ return targets;
362
+ }
363
+
364
+ /**
365
+ * Tells the entries that watch focus that it moved outside them.
366
+ * @param {FocusEvent} event Focusin event.
367
+ * @returns {void}
368
+ */
369
+ handleFocusin(event) {
370
+ const node = eventNode(event);
371
+ for (const entry of this.#entries.slice().reverse()) {
372
+ if (typeof entry.onFocusOutside !== 'function' || !this.#entries.includes(entry)) continue;
373
+ if (insideChain(this.#entries, entry, node)) continue;
374
+ const handle = this.#handles.get(entry);
375
+ if (handle) entry.onFocusOutside(event, handle);
376
+ }
377
+ }
378
+
379
+ /** @param {OverlayEntry} entry @returns {void} */
380
+ #release(entry) {
381
+ const index = this.#entries.indexOf(entry);
382
+ if (index < 0) return;
383
+ this.#entries.splice(index, 1);
384
+ const children = this.#entries.filter((candidate) => candidate.parent === entry);
385
+ // Re-parented first, so a child that outlives its parent is still in somebody's chain.
386
+ for (const child of children) child.parent = entry.parent;
387
+ if (this.#entries.length === 0) this.#unlisten();
388
+ this.#notify();
389
+ for (const child of children.reverse()) {
390
+ const handle = this.#handles.get(child);
391
+ if (handle && this.#entries.includes(child)) child.onParentClose?.(handle);
392
+ }
393
+ }
394
+
395
+ /** @returns {void} */
396
+ #listen() {
397
+ if (this.#listening || !this.#target || typeof this.#target.addEventListener !== 'function') return;
398
+ const controller = new AbortController();
399
+ this.#listening = controller;
400
+ const { signal } = controller;
401
+ this.#target.addEventListener('keydown', (event) => this.handleKeydown(/** @type {KeyboardEvent} */ (event)), { signal });
402
+ this.#target.addEventListener('pointerdown', (event) => this.handlePointerdown(event), { capture: true, signal });
403
+ this.#target.addEventListener('focusin', (event) => this.handleFocusin(/** @type {FocusEvent} */ (event)), { signal });
404
+ }
405
+
406
+ /** @returns {void} */
407
+ #unlisten() {
408
+ this.#listening?.abort();
409
+ this.#listening = null;
410
+ }
411
+
412
+ /** @returns {void} */
413
+ #notify() {
414
+ const snapshot = this.#entries.slice();
415
+ for (const listener of [...this.#listeners]) {
416
+ try {
417
+ listener(snapshot);
418
+ } catch (error) {
419
+ queueMicrotask(() => { throw error; });
420
+ }
421
+ }
422
+ }
423
+ }
424
+
425
+ /**
426
+ * The document's overlay stack, shared by every overlay in the library.
427
+ * @type {OverlayStack}
428
+ */
429
+ export const overlayStack = new OverlayStack();
430
+
431
+ /**
432
+ * Registers an open overlay on the shared stack. Shorthand for `overlayStack.push(options)`.
433
+ * @param {OverlayEntryOptions} options Entry description.
434
+ * @returns {OverlayHandle}
435
+ */
436
+ export function pushOverlay(options) {
437
+ return overlayStack.push(options);
438
+ }
439
+
440
+ /**
441
+ * @param {Element|null|undefined|Array<Element|null|undefined>} value
442
+ * @returns {Element[]}
443
+ */
444
+ function toList(value) {
445
+ const list = Array.isArray(value) ? value : [value];
446
+ return /** @type {Element[]} */ (list.filter((item, index) => item != null && list.indexOf(item) === index));
447
+ }
448
+
449
+ /**
450
+ * The innermost open modal dialog around a node, or null.
451
+ * @param {Node|null} node
452
+ * @returns {Element|null}
453
+ */
454
+ function modalAround(node) {
455
+ for (let current = /** @type {any} */ (node); current; current = current.parentNode ?? current.host ?? null) {
456
+ if (current.localName === 'dialog' && typeof current.matches === 'function') {
457
+ try {
458
+ if (current.matches(':modal')) return current;
459
+ } catch {
460
+ return null;
461
+ }
462
+ }
463
+ }
464
+ return null;
465
+ }
466
+
467
+ /** @param {Event} event @returns {Node|null} */
468
+ function eventNode(event) {
469
+ const path = typeof event.composedPath === 'function' ? event.composedPath() : [];
470
+ return /** @type {Node|null} */ (path[0] ?? event.target ?? null);
471
+ }
472
+
473
+ /**
474
+ * `element.matches(selector)`, answering `fallback` where the selector is not supported.
475
+ * @param {Element} element
476
+ * @param {string} selector
477
+ * @param {boolean} fallback
478
+ * @returns {boolean}
479
+ */
480
+ function matches(element, selector, fallback) {
481
+ if (typeof element.matches !== 'function') return fallback;
482
+ try {
483
+ return element.matches(selector);
484
+ } catch {
485
+ return fallback;
486
+ }
487
+ }
@@ -46,9 +46,12 @@ export function position(anchor, floating, options = {}) {
46
46
 
47
47
  floating.setAttribute('popover', 'manual');
48
48
  const supportsAnchors = typeof CSS !== 'undefined' && CSS.supports?.('anchor-name: --zx-a');
49
+ const anchorName = `--${uid('zx-a')}`;
49
50
  if (supportsAnchors) {
50
- const anchorName = `--${uid('zx-a')}`;
51
- /** @type {HTMLElement} */ (anchor).style.setProperty('anchor-name', anchorName);
51
+ // One element can anchor several floating elements at once — a tooltip and a menu on the same
52
+ // trigger — so the name joins the anchor's list rather than replacing it.
53
+ const names = anchorNames(anchor);
54
+ /** @type {HTMLElement} */ (anchor).style.setProperty('anchor-name', [...names, anchorName].join(', '));
52
55
  floating.style.position = 'fixed';
53
56
  floating.style.inset = 'auto';
54
57
  floating.style.margin = '0';
@@ -104,13 +107,29 @@ export function position(anchor, floating, options = {}) {
104
107
  observer?.disconnect();
105
108
  cleanupMode();
106
109
  hidePopover(floating);
107
- restoreAttribute(anchor, 'style', anchorStyle);
110
+ if (supportsAnchors) {
111
+ // Only this name leaves: another floating element may still be anchored to the same one.
112
+ const rest = anchorNames(anchor).filter((name) => name !== anchorName);
113
+ if (rest.length) /** @type {HTMLElement} */ (anchor).style.setProperty('anchor-name', rest.join(', '));
114
+ else /** @type {HTMLElement} */ (anchor).style.removeProperty('anchor-name');
115
+ if (anchorStyle === null && anchor.getAttribute('style') === '') anchor.removeAttribute('style');
116
+ }
108
117
  restoreAttribute(floating, 'style', floatingStyle);
109
118
  restoreAttribute(floating, 'popover', popover);
110
119
  }
111
120
  };
112
121
  }
113
122
 
123
+ /**
124
+ * The anchor names set inline on an element.
125
+ * @param {Element} anchor
126
+ * @returns {string[]}
127
+ */
128
+ function anchorNames(anchor) {
129
+ return String(/** @type {HTMLElement} */ (anchor).style?.getPropertyValue('anchor-name') ?? '')
130
+ .split(',').map((name) => name.trim()).filter((name) => name && name !== 'none');
131
+ }
132
+
114
133
  /** @param {string} placement @param {boolean} rtl @returns {string} */
115
134
  function positionArea(placement, rtl) {
116
135
  return {
package/src/index.js CHANGED
@@ -52,8 +52,9 @@ export {
52
52
  } from './core/dom.js';
53
53
  export { position } from './core/position.js';
54
54
  export { breakpoints, breakpointOf, matchBreakpoint, onBreakpoint } from './core/breakpoint.js';
55
+ export { MOTION_ATTRIBUTE, prefersReducedMotion, watchMotion } from './core/motion.js';
55
56
  export { Http, zeyosService, parseResult } from './core/http.js';
56
- export { setTranslator, setLanguage, getLanguage, translate, printf } from './core/i18n.js';
57
+ export { setTranslator, setLanguage, getLanguage, translate, printf, defaultMessages } from './core/i18n.js';
57
58
  export { focusTrap, rovingTabindex, typeahead } from './core/keyboard.js';
58
59
  export {
59
60
  formatDate, parseDate, clampDate, isSameDay, addDays, addMonths, getWeekStart
@@ -75,8 +76,8 @@ export {
75
76
  export { storage } from './core/storage.js';
76
77
  export { toCsv, downloadBlob, copyToClipboard } from './core/export.js';
77
78
  export { defineElements } from './elements/define.js';
78
- export { button, buttonGroup } from './components/button/button.js';
79
- export { badge, badgeGroup } from './components/badge/badge.js';
79
+ export { button, buttonGroup, setButtonBusy } from './components/button/button.js';
80
+ export { BADGE_TONES, badge, badgeGroup } from './components/badge/badge.js';
80
81
  export { CheckButton } from './components/check-button/check-button.js';
81
82
  export { emptyState } from './components/empty-state/empty-state.js';
82
83
  export { spinner } from './components/loading/spinner.js';
@@ -108,7 +109,7 @@ export { Pagination, paginationRange } from './components/pagination/pagination.
108
109
  export { Search } from './components/search/search.js';
109
110
  export { AppIcon, appIcon } from './components/app-icon/app-icon.js';
110
111
  export { Launcher, rankLauncherItems } from './components/launcher/launcher.js';
111
- export { Avatar, avatarInitials } from './components/avatar/avatar.js';
112
+ export { AVATAR_TONES, Avatar, avatarInitials, avatarTone } from './components/avatar/avatar.js';
112
113
  export {
113
114
  EntityRef, normalizeEntityRef, normalizeEntityRefLink,
114
115
  normalizeEntityRefMetadata, normalizeEntityRefActions
@@ -122,8 +123,10 @@ export {
122
123
  ActivityList, groupActivityItems, normalizeActivityItems
123
124
  } from './components/activity-list/activity-list.js';
124
125
  export { AccountMenu } from './components/account-menu/account-menu.js';
126
+ export { WorkspaceSwitcher } from './components/workspace-switcher/workspace-switcher.js';
125
127
  export { normalizeAppItems } from './internal/app-rail.js';
126
- export { AppSidebar } from './components/app-sidebar/app-sidebar.js';
128
+ export { ModuleBar, partitionModules, resolveModuleColor } from './components/module-bar/module-bar.js';
129
+ export { AppSidebar, presentationGlyph } from './components/app-sidebar/app-sidebar.js';
127
130
  export { Message } from './components/message/message.js';
128
131
  export { Modal } from './components/modal/modal.js';
129
132
  export { Dialog } from './components/dialog/dialog.js';
@@ -134,6 +137,7 @@ export { Tooltip, tooltip, describe } from './components/tooltip/tooltip.js';
134
137
  export { MenuButton } from './components/menu-button/menu-button.js';
135
138
  export { ContextMenu } from './components/context-menu/context-menu.js';
136
139
  export { Select } from './components/select/select.js';
140
+ export { statusIcon, priorityIcon } from './components/select/glyphs.js';
137
141
  export { Checklist } from './components/checklist/checklist.js';
138
142
  export { TagPicker } from './components/tag-picker/tag-picker.js';
139
143
  export { NumberField, parseNumber, snapNumber } from './components/number-field/number-field.js';
@@ -186,6 +190,7 @@ export {
186
190
  } from './components/filter-panel/filter-panel.js';
187
191
  export { SortControl } from './components/sort-control/sort-control.js';
188
192
  export { ListToolbar } from './components/list-toolbar/list-toolbar.js';
193
+ export { ViewSettings } from './components/view-settings/view-settings.js';
189
194
  export { Filter } from './components/filter/filter.js';
190
195
  export {
191
196
  filterOperators, emptyFilterAst, filterCondition, filterGroup,