@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
@@ -3,7 +3,7 @@
3
3
  max-block-size: calc(100vh - (2 * var(--zx-space-3)));
4
4
  overflow: auto;
5
5
  border: 1px solid var(--zx-color-overlay-border);
6
- border-radius: var(--zx-radius-md);
6
+ border-radius: var(--zx-radius-lg);
7
7
  background-color: var(--zx-color-overlay-surface);
8
8
  background-image: var(--zx-overlay-surface-image);
9
9
  background-clip: padding-box;
@@ -1,11 +1,16 @@
1
1
  import { Component } from '../../core/component.js';
2
2
  import { h, resolveElement } from '../../core/dom.js';
3
- import { overlayHost } from '../../core/overlay-host.js';
3
+ import { connectedOverlayHost, openDialogOf, overlayHost, overlayScope } from '../../core/overlay-host.js';
4
+ import { pushOverlay } from '../../core/overlay-stack.js';
4
5
  import { position } from '../../core/position.js';
5
6
  import { uid } from '../../core/util.js';
6
7
 
7
- const HANDLE_POINTER_DISMISS = Symbol('zxDropdownPointerDismiss');
8
- const HANDLE_ESCAPE_DISMISS = Symbol('zxDropdownEscapeDismiss');
8
+ /** What Tab may land on inside a panel: the controls a reader reaches in sequence. */
9
+ const TABBABLE_SELECTOR = [
10
+ 'a[href]', 'button:not([disabled])', 'input:not([disabled]):not([type="hidden"])',
11
+ 'select:not([disabled])', 'textarea:not([disabled])', 'iframe', '[contenteditable="true"]',
12
+ '[tabindex]'
13
+ ].map((selector) => `${selector}:not([tabindex^="-"])`).join(',');
9
14
 
10
15
  /**
11
16
  * @typedef {Object} DropdownOptions
@@ -15,13 +20,23 @@ const HANDLE_ESCAPE_DISMISS = Symbol('zxDropdownEscapeDismiss');
15
20
  * @property {boolean} [matchWidth=false] Whether the panel matches the anchor width.
16
21
  * @property {'click'|'manual'} [openOn='click'] Trigger behavior.
17
22
  * @property {boolean} [closeOnSelect=false] Whether a click within the panel closes it.
18
- * @property {Element|string|null} [scope=null] Element whose nearest Zx theme scope owns the panel; defaults to the anchor.
23
+ * @property {Element|string|null} [scope=null] Element whose nearest Zx theme scope owns the panel; defaults to the anchor. Resolved on every open, so an anchor connected after construction still gets its scope.
19
24
  * @property {(event: CustomEvent<Record<string, never>>) => void} [onopen] Open event listener.
20
25
  * @property {(event: CustomEvent<Record<string, never>>) => void} [onclose] Close event listener.
21
26
  */
22
27
 
23
28
  /**
24
29
  * Generic anchored floating panel using the browser popover top layer.
30
+ *
31
+ * Open, the panel is one entry on the overlay stack (`core/overlay-stack.js`): Escape closes it
32
+ * only while it is the topmost layer that takes Escape, a press outside it and every overlay opened
33
+ * from it closes it, and it closes with the overlay its anchor sits in. Inside an open dialog the
34
+ * panel is hosted by that dialog, so `showModal()` does not make it inert.
35
+ *
36
+ * The panel follows its trigger in the Tab order: Tab on the anchor of an open panel moves into
37
+ * the panel's first focusable control (or onto the panel itself when it carries a `tabindex`),
38
+ * Tab past the panel's last control continues after the anchor, and Shift+Tab from the first goes
39
+ * back to the anchor. Closing while focus is inside returns it to the anchor.
25
40
  * @fires Dropdown#open
26
41
  * @fires Dropdown#close
27
42
  * @extends {Component<DropdownOptions>}
@@ -46,6 +61,12 @@ export class Dropdown extends Component {
46
61
  #position = null;
47
62
  /** @type {Map<string, string|null>} */
48
63
  #anchorAttributes = new Map();
64
+ /** @type {import('../../core/overlay-stack.js').OverlayHandle|null} */
65
+ #overlay = null;
66
+ /** @type {((event: KeyboardEvent) => void)|null} Escape behaviour a composing component supplies. */
67
+ #escapeHandler = null;
68
+ /** Whether the panel joins the Tab order after its anchor; menus and rail flyouts opt out. */
69
+ #tabReach = true;
49
70
  #destroyed = false;
50
71
 
51
72
  /**
@@ -58,7 +79,10 @@ export class Dropdown extends Component {
58
79
  if (!resolved) throw new TypeError('Dropdown anchor could not be resolved');
59
80
  super(null, options);
60
81
  this.#anchor = resolved;
61
- overlayHost(this.options.scope ?? resolved).append(this.el);
82
+ // An anchor that is not in the document yet has no theme scope to read; the panel then waits
83
+ // detached and finds its host on the first open, instead of settling on <body>. A closed panel
84
+ // rests in the theme scope even when the anchor is in a dialog; it moves in only while open.
85
+ connectedOverlayHost(this.options.scope ?? resolved, { dialog: false })?.append(this.el);
62
86
  this.#rememberAnchorAttribute('aria-expanded');
63
87
  this.#rememberAnchorAttribute('aria-controls');
64
88
  this.#anchor.setAttribute('aria-expanded', 'false');
@@ -71,6 +95,8 @@ export class Dropdown extends Component {
71
95
  this.listen(this.el, 'click', () => {
72
96
  if (this.options.closeOnSelect) this.close();
73
97
  });
98
+ this.listen(this.#anchor, 'keydown', (event) => this.#onAnchorKeydown(/** @type {KeyboardEvent} */ (event)));
99
+ this.listen(this.el, 'keydown', (event) => this.#onPanelKeydown(/** @type {KeyboardEvent} */ (event)));
74
100
  }
75
101
 
76
102
  /**
@@ -93,7 +119,7 @@ export class Dropdown extends Component {
93
119
  */
94
120
  open() {
95
121
  if (this.isOpen() || this.#destroyed) return this;
96
- const host = overlayHost(this.options.scope ?? this.#anchor);
122
+ const host = this.#host();
97
123
  if (this.el.parentElement !== host) host.append(this.el);
98
124
  this.#position = position(this.#anchor, this.el, {
99
125
  placement: this.options.placement,
@@ -101,9 +127,18 @@ export class Dropdown extends Component {
101
127
  matchWidth: Boolean(this.options.matchWidth)
102
128
  });
103
129
  this.el.dataset.state = 'open';
104
- this.el.dataset.zxOverlayOrder = String(nextOverlayOrder());
130
+ this.#overlay = pushOverlay({
131
+ kind: 'popup',
132
+ root: this.el,
133
+ owner: this,
134
+ anchor: [this.#anchor, resolveElement(this.options.scope)],
135
+ inside: [this.#anchor],
136
+ onEscape: (event) => this.#onEscape(event),
137
+ onOutsidePointer: () => this.close(),
138
+ onParentClose: () => this.close()
139
+ });
140
+ this.el.dataset.zxOverlayOrder = String(this.#overlay.order);
105
141
  this.#anchor.setAttribute('aria-expanded', 'true');
106
- ensureDismissManager();
107
142
  this.emit('open');
108
143
  return this;
109
144
  }
@@ -115,13 +150,24 @@ export class Dropdown extends Component {
115
150
  */
116
151
  close() {
117
152
  if (!this.isOpen()) return this;
153
+ const hadFocus = this.el.contains(document.activeElement);
118
154
  this.#position?.destroy();
119
155
  this.#position = null;
120
156
  this.el.dataset.state = 'closed';
121
157
  delete this.el.dataset.zxOverlayOrder;
158
+ const overlay = this.#overlay;
159
+ this.#overlay = null;
122
160
  this.#anchor.setAttribute('aria-expanded', 'false');
161
+ // A panel that took the Tab order hands focus back rather than dropping it on <body>. Menus and
162
+ // flyouts decide where their own focus goes.
163
+ if (hadFocus && this.#tabReach && this.#anchor instanceof HTMLElement) this.#anchor.focus();
164
+ // Back to the theme scope: a closed panel left inside a dialog would sit in that dialog's
165
+ // focus search and go with it when the dialog is removed.
166
+ if (openDialogOf(this.el.parentElement) && this.el.parentElement?.isConnected) {
167
+ overlayScope(this.options.scope ?? this.#anchor).append(this.el);
168
+ }
169
+ overlay?.release();
123
170
  this.emit('close');
124
- destroyUnusedDismissManager();
125
171
  return this;
126
172
  }
127
173
 
@@ -172,50 +218,78 @@ export class Dropdown extends Component {
172
218
  super.destroy();
173
219
  }
174
220
 
175
- /** @param {PointerEvent} event @returns {void} */
176
- [HANDLE_POINTER_DISMISS](event) {
177
- if (!this.isOpen()) return;
178
- const path = event.composedPath();
179
- if (path.includes(this.el) || path.includes(this.#anchor)) return;
180
- this.close();
221
+ /**
222
+ * Internal seam for components built on a Dropdown (MenuButton, ContextMenu, the app rail):
223
+ * replaces what Escape does when the overlay stack routes it to this panel, and takes the panel
224
+ * out of the anchor's Tab order — a menu is reached with the arrow keys, not Tab.
225
+ * @param {{onEscape?: ((event: KeyboardEvent) => void)|null, tabReach?: boolean}} settings
226
+ * @returns {this}
227
+ * @internal
228
+ */
229
+ _configure(settings) {
230
+ if ('onEscape' in settings) this.#escapeHandler = settings.onEscape ?? null;
231
+ if ('tabReach' in settings) this.#tabReach = Boolean(settings.tabReach);
232
+ return this;
233
+ }
234
+
235
+ /** @returns {Element} Where the panel lives while open. */
236
+ #host() {
237
+ const dialog = openDialogOf(this.#anchor) ?? openDialogOf(resolveElement(this.options.scope));
238
+ return dialog ?? overlayHost(this.options.scope ?? this.#anchor);
181
239
  }
182
240
 
183
241
  /** @param {KeyboardEvent} event @returns {void} */
184
- [HANDLE_ESCAPE_DISMISS](event) {
185
- if (event.defaultPrevented || event.key !== 'Escape' || !this.isOpen()) return;
186
- event.preventDefault();
242
+ #onEscape(event) {
243
+ if (this.#escapeHandler) {
244
+ this.#escapeHandler(event);
245
+ return;
246
+ }
187
247
  const restoreFocus = this.el.contains(document.activeElement);
188
248
  this.close();
189
249
  if (restoreFocus && this.#anchor instanceof HTMLElement) this.#anchor.focus();
190
250
  }
191
251
 
192
- /** @param {string} name @returns {void} */
193
- #rememberAnchorAttribute(name) {
194
- this.#anchorAttributes.set(name, this.#anchor.getAttribute(name));
252
+ /** @param {KeyboardEvent} event @returns {void} */
253
+ #onAnchorKeydown(event) {
254
+ if (event.key !== 'Tab' || event.shiftKey || event.defaultPrevented || !this.#tabReach || !this.isOpen()) return;
255
+ const target = tabbables(this.el)[0] ?? (this.el.hasAttribute('tabindex') ? this.el : null);
256
+ if (!(target instanceof HTMLElement)) return;
257
+ event.preventDefault();
258
+ target.focus();
195
259
  }
196
- }
197
260
 
198
- /** Shared document-level light-dismiss listener owned only while a dropdown is open. */
199
- class DropdownDismissManager extends Component {
200
- /** Creates the transient listener owner. */
201
- constructor() {
202
- super(null);
203
- this.listen(document, 'pointerdown', (event) => {
204
- const component = topmostOverlayComponent();
205
- if (component instanceof Dropdown) component[HANDLE_POINTER_DISMISS](event);
206
- });
207
- this.listen(document, 'keydown', (event) => {
208
- if (event.defaultPrevented || event.key !== 'Escape') return;
209
- const component = topmostOverlayComponent();
210
- if (component instanceof Dropdown) component[HANDLE_ESCAPE_DISMISS](event);
211
- });
261
+ /** @param {KeyboardEvent} event @returns {void} */
262
+ #onPanelKeydown(event) {
263
+ if (event.key !== 'Tab' || event.defaultPrevented || !this.#tabReach || !this.isOpen()) return;
264
+ const items = tabbables(this.el);
265
+ const active = document.activeElement;
266
+ /*
267
+ * Judged by position, not identity: focus may rest on something Tab itself would never stop
268
+ * at — a composite's `tabindex="-1"` part, a roving item — and what matters is whether any
269
+ * real stop is left in the panel on the side Tab is moving to.
270
+ */
271
+ const ahead = active && active !== this.el
272
+ ? items.filter((item) => !item.contains(active) && (event.shiftKey
273
+ ? Boolean(active.compareDocumentPosition(item) & Node.DOCUMENT_POSITION_PRECEDING)
274
+ : Boolean(active.compareDocumentPosition(item) & Node.DOCUMENT_POSITION_FOLLOWING)
275
+ && !active.contains(item)))
276
+ : (event.shiftKey ? [] : items);
277
+ if (ahead.length > 0) return;
278
+ if (event.shiftKey) {
279
+ if (!(this.#anchor instanceof HTMLElement)) return;
280
+ event.preventDefault();
281
+ this.#anchor.focus();
282
+ return;
283
+ }
284
+ const next = followingTabbable(this.#anchor, this.el);
285
+ if (!next) return;
286
+ event.preventDefault();
287
+ next.focus();
212
288
  }
213
289
 
214
- /** @returns {HTMLElement} */
215
- render() {
216
- const marker = h('span', { hidden: true, 'data-zx-dropdown-dismiss-manager': '' });
217
- document.body.append(marker);
218
- return marker;
290
+ /** @param {string} name @returns {void} */
291
+ #rememberAnchorAttribute(name) {
292
+ this.#anchorAttributes.set(name, this.#anchor.getAttribute(name));
219
293
  }
220
294
  }
221
295
 
@@ -249,41 +323,54 @@ function replaceContent(target, content) {
249
323
  }
250
324
  }
251
325
 
252
- /** @returns {Component|null} */
253
- function topmostOverlayComponent() {
254
- let topmost = null;
255
- let highest = -1;
256
- for (const candidate of document.querySelectorAll('[data-zx-overlay-order]')) {
257
- const order = Number(candidate.getAttribute('data-zx-overlay-order')) || 0;
258
- if (order > highest) {
259
- highest = order;
260
- topmost = candidate;
261
- }
262
- }
263
- return topmost ? Component.from(topmost) : null;
264
- }
265
-
266
- /** @returns {number} */
267
- function nextOverlayOrder() {
268
- const orders = Array.from(document.querySelectorAll('[data-zx-overlay-order]'), (element) =>
269
- Number(element.getAttribute('data-zx-overlay-order')) || 0
270
- );
271
- return Math.max(0, ...orders) + 1;
326
+ /**
327
+ * The controls inside a container Tab would visit, in document order.
328
+ * @param {Element} container
329
+ * @returns {HTMLElement[]}
330
+ */
331
+ function tabbables(container) {
332
+ return /** @type {HTMLElement[]} */ (Array.from(container.querySelectorAll(TABBABLE_SELECTOR))
333
+ .filter((element) => isTabbable(element)));
272
334
  }
273
335
 
274
- /** @returns {DropdownDismissManager} */
275
- function ensureDismissManager() {
276
- const marker = document.querySelector('[data-zx-dropdown-dismiss-manager]');
277
- const existing = marker ? Component.from(marker) : null;
278
- return existing instanceof DropdownDismissManager ? existing : new DropdownDismissManager();
336
+ /**
337
+ * The control Tab reaches after `anchor`, skipping the panel that follows it.
338
+ * @param {Element} anchor
339
+ * @param {Element} panel
340
+ * @returns {HTMLElement|null}
341
+ */
342
+ function followingTabbable(anchor, panel) {
343
+ // Inside a modal dialog nothing beyond it can take focus, so the search stays within it.
344
+ const scope = openDialogOf(anchor) ?? anchor.ownerDocument.body ?? anchor.ownerDocument.documentElement;
345
+ const all = tabbables(scope).filter((element) => !panel.contains(element));
346
+ const index = all.findIndex((element) => element === anchor || anchor.contains(element));
347
+ if (index < 0) return null;
348
+ let next = index + 1;
349
+ while (next < all.length && anchor.contains(all[next])) next += 1;
350
+ return all[next] ?? null;
279
351
  }
280
352
 
281
- /** @returns {void} */
282
- function destroyUnusedDismissManager() {
283
- if (document.querySelector('.zx-dropdown[data-state="open"]')) return;
284
- const marker = document.querySelector('[data-zx-dropdown-dismiss-manager]');
285
- const manager = marker ? Component.from(marker) : null;
286
- if (manager instanceof DropdownDismissManager) manager.destroy();
353
+ /**
354
+ * Whether sequential navigation stops at an element: focusable, a non-negative tab index, not
355
+ * disabled, hidden, inert or `aria-hidden`, rendered, and — for a radio — the one stop its group
356
+ * offers (the checked radio, or the first when none is checked).
357
+ * @param {Element} element
358
+ * @returns {boolean}
359
+ */
360
+ function isTabbable(element) {
361
+ const html = /** @type {HTMLElement} */ (element);
362
+ if (typeof html.focus !== 'function' || html.tabIndex < 0) return false;
363
+ if (/** @type {HTMLButtonElement} */ (html).disabled) return false;
364
+ if (element.closest('[inert], [hidden], [aria-hidden="true"], fieldset[disabled]')) return false;
365
+ if (html instanceof HTMLInputElement && html.type === 'radio' && html.name && !html.checked) {
366
+ const scope = html.form ?? html.getRootNode();
367
+ const group = /** @type {HTMLInputElement[]} */ (Array.from(
368
+ /** @type {ParentNode} */ (scope).querySelectorAll?.('input[type="radio"]') ?? []
369
+ )).filter((radio) => radio.name === html.name && (radio.form ?? radio.getRootNode()) === scope);
370
+ if (group.some((radio) => radio.checked) || group[0] !== html) return false;
371
+ }
372
+ if (typeof html.checkVisibility === 'function') return html.checkVisibility({ visibilityProperty: true });
373
+ return html.getClientRects().length > 0;
287
374
  }
288
375
 
289
376
  /** @param {Element} element @param {string} name @param {string|null} value @returns {void} */
@@ -11,14 +11,14 @@
11
11
 
12
12
  .zx-empty-state__icon {
13
13
  display: flex;
14
- color: var(--zx-color-text-placeholder);
14
+ color: var(--zx-color-text-subtle);
15
15
  }
16
16
 
17
17
  .zx-empty-state__title {
18
18
  margin: 0;
19
19
  color: var(--zx-color-text);
20
20
  font-size: var(--zx-text-lg);
21
- font-weight: 600;
21
+ font-weight: var(--zx-weight-semibold);
22
22
  line-height: 1.4;
23
23
  }
24
24
 
@@ -52,7 +52,7 @@
52
52
 
53
53
  .zx-entity-ref__title {
54
54
  color: var(--zx-color-text);
55
- font-weight: 600;
55
+ font-weight: var(--zx-weight-medium);
56
56
  line-height: 1.25;
57
57
  }
58
58
 
@@ -52,7 +52,7 @@ import { isElement } from '../../core/util.js';
52
52
  * @property {EntityRefMetadata[]} [metadata=[]] Labelled dense metadata.
53
53
  * @property {EntityRefLinkValue} [link=null] Optional native primary link.
54
54
  * @property {EntityRefActionEntry[]} [actions=[]] Secondary controls; element actions keep their own behavior.
55
- * @property {'sm'|'md'|'lg'} [size='md'] Density-independent identity size.
55
+ * @property {'sm'|'md'|'lg'} [size='md'] Size of the icon and the identity text.
56
56
  * @property {boolean} [wrap=false] Whether long identity text may wrap instead of truncating.
57
57
  * @property {string} [actionsLabel='Entity actions'] Accessible name for secondary actions.
58
58
  * @property {(event: CustomEvent<{id: unknown, entity: Readonly<EntityRefState>, href: string, event: MouseEvent}>) => void} [onactivate]
@@ -1,5 +1,8 @@
1
+ /* Beside a taller field in a multi-column row the field's cell grows, but its label and control
2
+ stay at the top of it rather than spreading over the extra height. */
1
3
  .zx-field {
2
4
  display: grid;
5
+ align-content: start;
3
6
  gap: var(--zx-space-1);
4
7
  min-inline-size: 0;
5
8
  }
@@ -10,15 +13,24 @@
10
13
  align-items: start;
11
14
  }
12
15
 
16
+ /* Beside its control, a label lines up with the control's first line rather than its top edge, so
17
+ "Status" and "In review" share a baseline — and still sits by the first row of a control that
18
+ grows taller. */
19
+ .zx-field[data-layout="inline"] .zx-field__label {
20
+ padding-block-start: max(0px, (var(--zx-control-height) - 1lh) / 2);
21
+ }
22
+
13
23
  .zx-field[data-field-type="hidden"] {
14
24
  display: none;
15
25
  }
16
26
 
27
+ /* Labels are quieter than the values they name: the small size, a medium weight, the muted text
28
+ colour. The value is what a reader came for. */
17
29
  .zx-field__label {
18
30
  min-inline-size: 0;
19
- color: var(--zx-color-text);
31
+ color: var(--zx-color-text-muted);
20
32
  font-size: var(--zx-text-sm);
21
- font-weight: 600;
33
+ font-weight: var(--zx-weight-medium);
22
34
  }
23
35
 
24
36
  .zx-field__required,
@@ -101,17 +113,24 @@
101
113
  border-color: var(--zx-color-danger);
102
114
  }
103
115
 
104
- .zx-field__static,
105
- .zx-field__error-control {
116
+ /* Display content is not an input, so it draws no frame and no fill. It keeps a control's height
117
+ and centres its first line in it, so it sits level with the fields beside it and with an inline
118
+ label; in a quiet form it is inset like the quiet values around it. */
119
+ .zx-field__static {
106
120
  min-block-size: var(--zx-control-height);
107
- padding: var(--zx-space-2) var(--zx-control-pad-x);
108
- border: 1px solid var(--zx-color-border);
109
- border-radius: var(--zx-control-radius);
110
- background: var(--zx-color-bg-surface);
121
+ padding-block: max(0px, (var(--zx-control-height) - 1lh) / 2);
122
+ font-size: var(--zx-control-font-size);
123
+ }
124
+
125
+ [data-zx-fields="quiet"] .zx-field__static {
126
+ padding-inline: calc(var(--zx-control-pad-x) + 1px);
111
127
  }
112
128
 
113
129
  .zx-field__error-control {
114
- border-color: var(--zx-color-danger);
130
+ min-block-size: var(--zx-control-height);
131
+ padding: var(--zx-space-2) var(--zx-control-pad-x);
132
+ border: 1px solid var(--zx-color-danger);
133
+ border-radius: var(--zx-control-radius);
115
134
  background: var(--zx-color-danger-bg);
116
135
  color: var(--zx-color-danger);
117
136
  }