@zeyos/zx 5.0.0 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/CHANGELOG.md +164 -0
  2. package/README.md +2 -1
  3. package/dist/types/components/account-menu/account-menu.d.ts +8 -0
  4. package/dist/types/components/activity-list/activity-list.d.ts +9 -1
  5. package/dist/types/components/app-icon/app-icon.d.ts +14 -2
  6. package/dist/types/components/app-sidebar/app-sidebar.d.ts +288 -14
  7. package/dist/types/components/calendar/calendar.d.ts +67 -5
  8. package/dist/types/components/card-view/card-view.d.ts +7 -4
  9. package/dist/types/components/card-view/record-card.d.ts +11 -0
  10. package/dist/types/components/context-menu/context-menu.d.ts +6 -2
  11. package/dist/types/components/data-filter/data-filter.d.ts +4 -4
  12. package/dist/types/components/dropdown/dropdown.d.ts +22 -7
  13. package/dist/types/components/field/field.d.ts +30 -0
  14. package/dist/types/components/fieldset/external-adapter.d.ts +83 -0
  15. package/dist/types/components/fieldset/fieldset.d.ts +7 -0
  16. package/dist/types/components/file-list/file-list.d.ts +35 -3
  17. package/dist/types/components/filter/filter.d.ts +127 -3
  18. package/dist/types/components/filter-panel/filter-panel.d.ts +36 -7
  19. package/dist/types/components/launcher/launcher.d.ts +8 -0
  20. package/dist/types/components/list-toolbar/list-toolbar.d.ts +15 -6
  21. package/dist/types/components/menu-button/menu-button.d.ts +6 -0
  22. package/dist/types/components/message/message.d.ts +14 -1
  23. package/dist/types/components/modal/modal.d.ts +30 -0
  24. package/dist/types/components/module-bar/module-bar.d.ts +395 -0
  25. package/dist/types/components/pagination/pagination.d.ts +17 -3
  26. package/dist/types/components/select/select.d.ts +6 -0
  27. package/dist/types/components/sheet/sheet.d.ts +15 -0
  28. package/dist/types/components/table/table.d.ts +265 -17
  29. package/dist/types/components/view/record-view.d.ts +59 -2
  30. package/dist/types/components/view-settings/view-settings.d.ts +879 -0
  31. package/dist/types/components/workspace-switcher/workspace-switcher.d.ts +327 -0
  32. package/dist/types/core/component.d.ts +12 -2
  33. package/dist/types/core/i18n.d.ts +27 -3
  34. package/dist/types/core/keyboard.d.ts +98 -1
  35. package/dist/types/core/messages.d.ts +382 -0
  36. package/dist/types/core/overlay-host.d.ts +28 -1
  37. package/dist/types/core/overlay-stack.d.ts +260 -0
  38. package/dist/types/index.d.ts +5 -2
  39. package/dist/types/internal/app-rail.d.ts +38 -0
  40. package/dist/types/internal/menu-items.d.ts +71 -3
  41. package/dist/types/zeyos/index.d.ts +1 -0
  42. package/dist/types/zeyos/launcher.d.ts +9 -0
  43. package/dist/types/zeyos/module-bar.d.ts +176 -0
  44. package/dist/types/zeyos/query.d.ts +31 -5
  45. package/dist/types/zeyos/table.d.ts +245 -8
  46. package/dist/types/zeyos/view.d.ts +16 -2
  47. package/dist/zx-zeyos.esm.js +4422 -430
  48. package/dist/zx-zeyos.esm.min.js +5 -5
  49. package/dist/zx.css +1417 -134
  50. package/dist/zx.esm.js +6986 -830
  51. package/dist/zx.esm.min.js +8 -8
  52. package/dist/zx.global.js +6986 -830
  53. package/dist/zx.global.min.js +8 -8
  54. package/dist/zx.min.css +1 -1
  55. package/docs/api.json +794 -17
  56. package/docs/llms.md +503 -63
  57. package/docs/llms.txt +6 -2
  58. package/package.json +1 -1
  59. package/src/components/account-menu/account-menu.js +12 -0
  60. package/src/components/activity-list/activity-list.js +27 -1
  61. package/src/components/app-icon/app-icon.css +13 -2
  62. package/src/components/app-icon/app-icon.js +11 -1
  63. package/src/components/app-rail/app-rail.css +19 -0
  64. package/src/components/app-sidebar/app-sidebar.css +204 -3
  65. package/src/components/app-sidebar/app-sidebar.js +646 -40
  66. package/src/components/breadcrumb/breadcrumb.css +1 -1
  67. package/src/components/calendar/calendar.css +1 -1
  68. package/src/components/calendar/calendar.js +26 -52
  69. package/src/components/card/card.css +2 -2
  70. package/src/components/card-view/card-view.css +5 -5
  71. package/src/components/card-view/card-view.js +13 -9
  72. package/src/components/card-view/record-card.js +14 -3
  73. package/src/components/code-editor/code-editor.css +1 -1
  74. package/src/components/code-editor/field-adapter.js +9 -2
  75. package/src/components/context-menu/context-menu.js +8 -2
  76. package/src/components/copy/copy-input.css +1 -1
  77. package/src/components/data-filter/data-filter.js +2 -2
  78. package/src/components/date-picker/time-picker.css +2 -2
  79. package/src/components/dock/dock.css +5 -4
  80. package/src/components/dropdown/dropdown.js +157 -72
  81. package/src/components/field/field.js +33 -3
  82. package/src/components/fieldset/external-adapter.js +363 -0
  83. package/src/components/fieldset/fieldset.js +10 -59
  84. package/src/components/file-list/file-list.js +144 -21
  85. package/src/components/filter/filter.css +86 -2
  86. package/src/components/filter/filter.js +300 -8
  87. package/src/components/filter-panel/filter-panel.js +149 -32
  88. package/src/components/finder/finder.css +1 -1
  89. package/src/components/kanban-view/kanban-view.css +3 -3
  90. package/src/components/kanban-view/kanban-view.js +5 -2
  91. package/src/components/launcher/launcher.css +1 -1
  92. package/src/components/launcher/launcher.js +29 -3
  93. package/src/components/list-toolbar/list-toolbar.js +39 -13
  94. package/src/components/master-panel/master-panel.js +0 -1
  95. package/src/components/menu-button/menu-button.css +63 -0
  96. package/src/components/menu-button/menu-button.js +97 -11
  97. package/src/components/message/message.js +60 -20
  98. package/src/components/modal/modal.css +1 -1
  99. package/src/components/modal/modal.js +66 -13
  100. package/src/components/module-bar/module-bar.css +486 -0
  101. package/src/components/module-bar/module-bar.js +1074 -0
  102. package/src/components/multi-value-editor/multi-value-editor.css +1 -1
  103. package/src/components/number-field/field-adapter.js +2 -1
  104. package/src/components/pagination/pagination.css +5 -5
  105. package/src/components/pagination/pagination.js +20 -4
  106. package/src/components/panel/panel.css +3 -2
  107. package/src/components/questionnaire/questionnaire.css +4 -4
  108. package/src/components/rating/field-adapter.js +2 -1
  109. package/src/components/rating/rating.css +1 -1
  110. package/src/components/search/search.css +4 -1
  111. package/src/components/select/select.js +9 -0
  112. package/src/components/sheet/sheet.css +1 -1
  113. package/src/components/sheet/sheet.js +74 -32
  114. package/src/components/slider/field-adapter.js +6 -1
  115. package/src/components/slider/slider.css +2 -2
  116. package/src/components/split-view/split-view.css +1 -1
  117. package/src/components/stat-tile/stat-tile.css +1 -1
  118. package/src/components/stepper/stepper.css +1 -1
  119. package/src/components/tabbox/tabbox.css +9 -5
  120. package/src/components/table/table.css +86 -11
  121. package/src/components/table/table.js +628 -57
  122. package/src/components/tag-picker/field-adapter.js +2 -1
  123. package/src/components/tooltip/tooltip.js +35 -17
  124. package/src/components/tree/tree.css +1 -1
  125. package/src/components/view/record-view.css +1 -1
  126. package/src/components/view/record-view.js +178 -18
  127. package/src/components/view-settings/view-settings.css +285 -0
  128. package/src/components/view-settings/view-settings.js +1830 -0
  129. package/src/components/workspace-switcher/workspace-switcher.css +188 -0
  130. package/src/components/workspace-switcher/workspace-switcher.js +463 -0
  131. package/src/core/component.js +88 -17
  132. package/src/core/fontawesome.js +5 -1
  133. package/src/core/i18n.js +47 -4
  134. package/src/core/icons.js +12 -2
  135. package/src/core/keyboard.js +206 -11
  136. package/src/core/messages.js +427 -0
  137. package/src/core/overlay-host.js +64 -9
  138. package/src/core/overlay-stack.js +487 -0
  139. package/src/core/position.js +22 -3
  140. package/src/index.js +5 -2
  141. package/src/internal/app-rail.js +134 -53
  142. package/src/internal/menu-items.js +61 -3
  143. package/src/zeyos/form.js +5 -3
  144. package/src/zeyos/index.js +3 -0
  145. package/src/zeyos/launcher.js +8 -3
  146. package/src/zeyos/module-bar.js +181 -0
  147. package/src/zeyos/query.js +44 -13
  148. package/src/zeyos/table.js +220 -80
  149. package/src/zeyos/view.js +43 -93
  150. package/styles/base.css +16 -1
  151. package/styles/print.css +1 -1
  152. package/styles/tokens/dark.css +4 -0
  153. package/styles/tokens/semantic.css +56 -28
  154. package/styles/zx.css +3 -0
@@ -97,7 +97,7 @@
97
97
  }
98
98
 
99
99
  .zx-multi-value-editor__handle:focus-visible {
100
- outline: none;
100
+ outline-color: transparent;
101
101
  box-shadow: var(--zx-focus-ring);
102
102
  }
103
103
 
@@ -35,7 +35,8 @@ export function registerNumberFieldAdapter() {
35
35
  }
36
36
  },
37
37
  focus: () => numberField.focus(),
38
- setDisabled: (value) => (value ? numberField.disable() : numberField.enable())
38
+ setDisabled: (value) => (value ? numberField.disable() : numberField.enable()),
39
+ setReadonly: (value) => { numberField.setReadonly(value); }
39
40
  };
40
41
  });
41
42
  }
@@ -1,8 +1,8 @@
1
1
  /*
2
2
  * Pagination — the footer bar of a server-backed list.
3
3
  *
4
- * One row that wraps: the summary is pushed to the start, the page-size select and the pager sit
5
- * at the end. Page buttons are ordinary `.zx-btn` controls so they inherit the library's control
4
+ * One row that wraps: the summary and the page-size select describe the slice and sit at the
5
+ * start, the pager sits alone at the end. Page buttons are ordinary `.zx-btn` controls so they inherit the library's control
6
6
  * height, radius, and focus ring; the current page takes the neutral current fill — it is where
7
7
  * you are, not a call to action.
8
8
  */
@@ -22,8 +22,8 @@
22
22
  white-space: nowrap;
23
23
  }
24
24
 
25
- /* Absorbs the slack, so summary and controls end up at opposite ends of a wide bar and simply
26
- stack when there is no room. */
25
+ /* Absorbs the slack, so the summary and page size end up at the start and the pager at the end of
26
+ a wide bar, and they simply stack when there is no room. */
27
27
  .zx-pagination__spacer {
28
28
  flex: 1 1 var(--zx-space-4);
29
29
  }
@@ -43,7 +43,7 @@
43
43
  }
44
44
 
45
45
  .zx-pagination__select:focus-visible {
46
- outline: 0;
46
+ outline-color: transparent;
47
47
  box-shadow: var(--zx-focus-ring);
48
48
  }
49
49
 
@@ -95,9 +95,23 @@ export function paginationRange({ page = 1, pages = 1, siblings = 1, boundaries
95
95
 
96
96
  /**
97
97
  * The pager for a server-backed list. It owns no data: it reports which slice the user asked for
98
- * and lets the application fetch it, which is exactly the shape `zeyosTable()` expects —
99
- * `offset`/`pageSize` for `load()` in `pages` mode, or one `change` per `loadMore()` in
100
- * `loadmore` mode.
98
+ * — `page`, `pageSize`, and the zero-based row `offset = (page - 1) * pageSize` — and lets the
99
+ * application fetch it.
100
+ *
101
+ * The pager is one-based and defaults to 25 rows per page. The ZeyOS bindings are zero-based,
102
+ * default to 50 rows, and append pages above zero unless told to replace, so connecting the two
103
+ * takes a small mapping rather than a direct hand-off:
104
+ *
105
+ * ```js
106
+ * const binding = zeyosTable(client, 'contacts', { pageSize: 25 });
107
+ * const pager = new Pagination(host, { pageSize: binding.pageSize });
108
+ * pager.on('change', async ({ detail }) => {
109
+ * const result = await binding.load({ page: detail.page - 1, pageSize: detail.pageSize, replace: true });
110
+ * pager.setTotal(result.count ?? 0);
111
+ * });
112
+ * ```
113
+ *
114
+ * In `loadmore` mode each `change` stands for one `binding.loadMore()`, which appends.
101
115
  *
102
116
  * `page` is clamped into `[1, pages]` after every state change, so shrinking `total` under the
103
117
  * user's feet can never leave the pager pointing past the end of the data.
@@ -163,8 +177,10 @@ export class Pagination extends Component {
163
177
  if (this.options.showSummary) {
164
178
  children.push(h('span', { class: 'zx-pagination__summary', ref: 'summary', ariaLive: 'polite' }));
165
179
  }
166
- children.push(h('span', { class: 'zx-pagination__spacer' }));
180
+ // The summary and the page size describe the slice and sit together at the start; the pager
181
+ // walks the slice and sits alone at the end, where the next-page action is reached last.
167
182
  if (this.options.showPageSize) children.push(this._renderPageSize());
183
+ children.push(h('span', { class: 'zx-pagination__spacer' }));
168
184
  children.push(this.options.mode === 'pages' ? this._renderPages() : this._renderLoadMore());
169
185
  root.replaceChildren(...children);
170
186
 
@@ -3,7 +3,7 @@
3
3
  border: 1px solid var(--zx-color-border);
4
4
  border-radius: var(--zx-radius-lg);
5
5
  background: var(--zx-color-bg-surface);
6
- box-shadow: var(--zx-shadow-1);
6
+ box-shadow: var(--zx-surface-shadow);
7
7
  }
8
8
 
9
9
  /* The header is a row, not one big button: the collapse control takes the title area and the
@@ -49,7 +49,8 @@ button.zx-panel__toggle:hover {
49
49
 
50
50
  /* Inset, because the panel clips its own overflow and would cut an outside ring. */
51
51
  button.zx-panel__toggle:focus-visible {
52
- outline: 0;
52
+ outline-color: transparent;
53
+ outline-offset: -2px;
53
54
  box-shadow: inset var(--zx-focus-ring);
54
55
  }
55
56
 
@@ -51,11 +51,11 @@
51
51
  }
52
52
 
53
53
  .zx-questionnaire__item:focus {
54
- outline: none;
54
+ outline-color: transparent;
55
55
  }
56
56
 
57
57
  .zx-questionnaire__item:focus-visible {
58
- outline: none;
58
+ outline-color: transparent;
59
59
  box-shadow: var(--zx-focus-ring);
60
60
  border-radius: var(--zx-questionnaire-radius);
61
61
  }
@@ -275,7 +275,7 @@
275
275
  }
276
276
 
277
277
  .zx-questionnaire__freeform-input:focus-visible {
278
- outline: none;
278
+ outline-color: transparent;
279
279
  border-color: var(--zx-color-accent);
280
280
  box-shadow: var(--zx-focus-ring);
281
281
  }
@@ -301,7 +301,7 @@
301
301
  }
302
302
 
303
303
  .zx-questionnaire__review:focus {
304
- outline: none;
304
+ outline-color: transparent;
305
305
  }
306
306
 
307
307
 
@@ -28,7 +28,8 @@ export function registerRatingFieldAdapter() {
28
28
  }
29
29
  },
30
30
  focus: () => rating.focus(),
31
- setDisabled: (value) => (value ? rating.disable() : rating.enable())
31
+ setDisabled: (value) => (value ? rating.disable() : rating.enable()),
32
+ setReadonly: (value) => { rating.setReadonly(value); }
32
33
  };
33
34
  });
34
35
  }
@@ -76,7 +76,7 @@
76
76
  }
77
77
 
78
78
  .zx-rating__radio:focus-visible {
79
- outline: 0;
79
+ outline-color: transparent;
80
80
  box-shadow: var(--zx-focus-ring);
81
81
  }
82
82
 
@@ -13,8 +13,11 @@
13
13
  }
14
14
 
15
15
  /* The input reserves room for the clear button, which floats over its trailing edge, so showing
16
- or hiding the button never changes the control's total width. */
16
+ or hiding the button never changes the control's total width. It takes every bit of width the
17
+ field is given — a toolbar stretches the field, and the submit glyph must stay on its trailing
18
+ edge rather than stopping where the input's own width ran out. */
17
19
  .zx-search__input {
20
+ flex: 1 1 auto;
18
21
  min-inline-size: 12rem;
19
22
  min-block-size: 0;
20
23
  border: 0;
@@ -251,6 +251,15 @@ export class Select extends Component {
251
251
  this.set(id);
252
252
  }
253
253
 
254
+ /**
255
+ * Returns the selected item ID, or null: the same as reading `value`, under the `get()`/`set()`
256
+ * pair the other value components share.
257
+ * @returns {unknown}
258
+ */
259
+ get() {
260
+ return this.value;
261
+ }
262
+
254
263
  /** Selected item, or null. @returns {SelectItem|null} */
255
264
  get selected() {
256
265
  return this._selected;
@@ -249,7 +249,7 @@
249
249
  }
250
250
 
251
251
  .zx-sheet__handle:focus-visible {
252
- outline: none;
252
+ outline-color: transparent;
253
253
  box-shadow: var(--zx-focus-ring);
254
254
  }
255
255
 
@@ -1,6 +1,6 @@
1
1
  import { STEP, STEP_LARGE, dragAxis, resolveSize } from '../../core/drag-axis.js';
2
2
  import { focusTrap } from '../../core/keyboard.js';
3
- import { h } from '../../core/dom.js';
3
+ import { h, resolveElement } from '../../core/dom.js';
4
4
  import { clamp } from '../../core/util.js';
5
5
  import { Dialog } from '../dialog/dialog.js';
6
6
 
@@ -112,36 +112,24 @@ export class Sheet extends Dialog {
112
112
  * field would be undefined at best and a private one would throw.
113
113
  */
114
114
  this._dock = null;
115
+ /** @type {Element|null} The element focus was on when the current `open()` began. */
116
+ this._openedFrom = null;
115
117
  this.setSide(this.options.side);
116
118
  this.el.dataset.backdrop = BACKDROPS.has(this.options.backdrop) ? this.options.backdrop : 'dim';
117
119
  this.el.dataset.modality = modality(this.options.modal);
118
120
  this.refs.close.hidden = !this.options.closeButton;
119
121
 
120
- if (this.options.modal !== true) {
121
- /*
122
- * Escape and outside-click are free for a modal dialog and absent for a non-modal one: the
123
- * browser only wires `cancel` for `showModal()`, and there is no `::backdrop` to click.
124
- */
125
- this.listen(this.el, 'keydown', (event) => {
126
- const keyboardEvent = /** @type {KeyboardEvent} */ (event);
127
- if (keyboardEvent.key !== 'Escape' || !this.options.closable || this._dock) return;
128
- keyboardEvent.preventDefault();
129
- if (!this.emit('cancel').defaultPrevented) this.close();
130
- });
131
- if (this.options.lightDismiss) {
132
- this.listen(document, 'pointerdown', (event) => {
133
- const target = /** @type {Node} */ (event.target);
134
- if (!this.isOpen() || this._dock || this.el.contains(target)) return;
135
- this.close();
136
- });
137
- }
138
- }
139
-
140
122
  /*
141
123
  * Registered after Dialog's own restore listener, so focus lands on the opener either way:
142
124
  * the trap captured the same element, because `open()` activates it before the sheet shows.
143
125
  */
144
126
  this.on('close', () => this.#trap?.deactivate());
127
+ // A sheet that was hosted inside a modal it was opened from goes home once it closes, so it
128
+ // neither sits in that dialog's focus search nor goes with the dialog when it is removed.
129
+ this.on('close', () => {
130
+ const home = super.mountTarget();
131
+ if (!this._dock && this.el.parentElement !== home && this.el.parentElement?.closest('dialog')) home.append(this.el);
132
+ });
145
133
 
146
134
  if (this.#wantsHandle()) this.#buildHandle();
147
135
  }
@@ -320,7 +308,19 @@ export class Sheet extends Dialog {
320
308
  * @returns {Element}
321
309
  */
322
310
  mountTarget() {
323
- return this._dock?.el ?? super.mountTarget();
311
+ if (this._dock) return this._dock.el;
312
+ /*
313
+ * A floating non-modal sheet opened from inside a modal dialog lives inside that dialog: its
314
+ * popover still rises above it, but outside the dialog's subtree `showModal()` would leave it
315
+ * inert. Only asked while opening — `this.el` is unset during `render()` — so a sheet at rest
316
+ * stays in its theme scope. A modal sheet stacks above any dialog wherever it lives.
317
+ */
318
+ if (this.el && this.options.modal !== true) {
319
+ const opener = this._openedFrom ?? resolveElement(this.options.scope);
320
+ const modal = modalAround(opener, this.el);
321
+ if (modal) return modal;
322
+ }
323
+ return super.mountTarget();
324
324
  }
325
325
 
326
326
  /**
@@ -362,11 +362,43 @@ export class Sheet extends Dialog {
362
362
  this.el.returnValue = '';
363
363
  this._present();
364
364
  this.el.dataset.state = 'open';
365
- if (this._dock) delete /** @type {HTMLElement} */ (this.el).dataset.zxOverlayOrder;
366
- else this.el.dataset.zxOverlayOrder = String(nextOverlayOrder());
365
+ this._syncOverlay();
367
366
  }
368
367
 
369
368
 
369
+ /**
370
+ * Describes the presentation for the overlay stack. A docked sheet is part of the layout and
371
+ * stays off it.
372
+ *
373
+ * Escape and outside-click are free for a modal dialog and absent for a non-modal one — the
374
+ * browser only wires `cancel` for `showModal()`, and there is no `::backdrop` to click — so a
375
+ * floating non-modal sheet takes both from the stack. That is what makes it aware of what it
376
+ * opened: Escape reaches it only once no popup above it takes the key, and a press inside a menu
377
+ * anchored in the sheet counts as inside the sheet, wherever the menu's panel lives.
378
+ * @returns {import('../../core/overlay-stack.js').OverlayEntryOptions|null}
379
+ * @protected
380
+ */
381
+ _overlayEntry() {
382
+ if (this._dock) return null;
383
+ const entry = super._overlayEntry();
384
+ if (!entry || this.options.modal === true) return entry;
385
+ return {
386
+ ...entry,
387
+ // An Escape pressed outside the sheet and everything it opened is not addressed to it.
388
+ onEscape: (event) => {
389
+ if (!this.options.closable || !this._overlayContains(/** @type {Node|null} */ (event.target))) return false;
390
+ if (!this.emit('cancel').defaultPrevented) this.close();
391
+ return undefined;
392
+ },
393
+ onOutsidePointer: this.options.lightDismiss ? () => { this.close(); } : undefined,
394
+ // Hosted inside a modal that has now closed, the sheet would stay open but never be seen.
395
+ onParentClose: () => {
396
+ const holder = this.el.parentElement?.closest('dialog');
397
+ if (holder && !holder.open && !holder.matches(':popover-open')) this.close();
398
+ }
399
+ };
400
+ }
401
+
370
402
  /**
371
403
  * Opens the sheet, trapping focus first where the modality asks for it.
372
404
  * @returns {this}
@@ -374,11 +406,17 @@ export class Sheet extends Dialog {
374
406
  */
375
407
  open() {
376
408
  if (this.isOpen()) return this;
409
+ // Read before the trap moves focus: the element the reader was on decides where the sheet lives.
410
+ this._openedFrom = document.activeElement;
377
411
  if (this.options.modal === 'trap-focus') {
378
412
  this.#trap ??= focusTrap(this.el);
379
413
  this.#trap.activate();
380
414
  }
381
- super.open();
415
+ try {
416
+ super.open();
417
+ } finally {
418
+ this._openedFrom = null;
419
+ }
382
420
  // The handle is built while the sheet is still closed, where it measures zero; only now is
383
421
  // there a real size to report to a screen reader.
384
422
  this.#syncHandleAria();
@@ -507,14 +545,18 @@ export class Sheet extends Dialog {
507
545
  }
508
546
 
509
547
  /**
510
- * Next free overlay order. Same bookkeeping `Modal` and `Dropdown` each carry a copy of; the three
511
- * should converge on one kernel helper.
512
- * @returns {number}
548
+ * The open modal dialog around an element, other than the sheet itself, or null.
549
+ * @param {Element|null|undefined} element
550
+ * @param {Element} self
551
+ * @returns {Element|null}
513
552
  */
514
- function nextOverlayOrder() {
515
- const orders = Array.from(document.querySelectorAll('[data-zx-overlay-order]'), (element) =>
516
- Number(element.getAttribute('data-zx-overlay-order')) || 0);
517
- return Math.max(0, ...orders) + 1;
553
+ function modalAround(element, self) {
554
+ if (!element || typeof element.closest !== 'function' || self.contains(element)) return null;
555
+ try {
556
+ return element.closest('dialog:modal');
557
+ } catch {
558
+ return null;
559
+ }
518
560
  }
519
561
 
520
562
  /**
@@ -15,9 +15,13 @@ export function registerSliderFieldAdapter() {
15
15
  ...(options.props ?? {})
16
16
  }));
17
17
  let setting = false;
18
+ // The range input's native `change` bubbles out of the slider after the slider has reported
19
+ // the same commit; without this the Field would emit `change` twice per edit.
20
+ field.listen(slider.el, 'change', (event) => event.stopImmediatePropagation());
18
21
  slider.on('change', (event) => {
19
22
  if (!setting) field.emit('change', { value: event.detail.value });
20
23
  });
24
+ slider.on('input', (event) => field.emit('input', { value: event.detail.value }));
21
25
  return {
22
26
  el: slider.el,
23
27
  get: () => slider.get(),
@@ -30,7 +34,8 @@ export function registerSliderFieldAdapter() {
30
34
  }
31
35
  },
32
36
  focus: () => slider.focus(),
33
- setDisabled: (value) => (value ? slider.disable() : slider.enable())
37
+ setDisabled: (value) => (value ? slider.disable() : slider.enable()),
38
+ setReadonly: (value) => { slider.setReadonly(value); }
34
39
  };
35
40
  });
36
41
  }
@@ -119,7 +119,7 @@
119
119
  border: 2px solid var(--zx-color-bg-surface);
120
120
  border-radius: var(--zx-radius-full);
121
121
  background: var(--zx-color-accent);
122
- box-shadow: var(--zx-shadow-1);
122
+ box-shadow: var(--zx-surface-shadow);
123
123
  }
124
124
 
125
125
  .zx-slider__input::-moz-range-thumb {
@@ -128,7 +128,7 @@
128
128
  border: 2px solid var(--zx-color-bg-surface);
129
129
  border-radius: var(--zx-radius-full);
130
130
  background: var(--zx-color-accent);
131
- box-shadow: var(--zx-shadow-1);
131
+ box-shadow: var(--zx-surface-shadow);
132
132
  }
133
133
 
134
134
  .zx-slider__input:focus-visible {
@@ -99,7 +99,7 @@
99
99
  }
100
100
 
101
101
  .zx-split-view__divider:focus-visible {
102
- outline: 0;
102
+ outline-color: transparent;
103
103
  box-shadow: var(--zx-focus-ring);
104
104
  /* The ring sits outside an 8px-wide element, so it must draw over both panes. */
105
105
  z-index: 1;
@@ -41,7 +41,7 @@ button.zx-stat-tile:hover {
41
41
 
42
42
  a.zx-stat-tile:focus-visible,
43
43
  button.zx-stat-tile:focus-visible {
44
- outline: 0;
44
+ outline-color: transparent;
45
45
  box-shadow: var(--zx-focus-ring);
46
46
  }
47
47
 
@@ -70,7 +70,7 @@ button.zx-stepper__control:disabled {
70
70
  }
71
71
 
72
72
  .zx-stepper__control:focus-visible {
73
- outline: 0;
73
+ outline-color: transparent;
74
74
  box-shadow: var(--zx-focus-ring);
75
75
  }
76
76
 
@@ -113,7 +113,8 @@
113
113
 
114
114
  .zx-tabbox[data-variant="divided"] .zx-tabbox__tab:focus-visible {
115
115
  z-index: 1;
116
- outline: 0;
116
+ outline-color: transparent;
117
+ outline-offset: -2px;
117
118
  box-shadow: inset var(--zx-focus-ring);
118
119
  }
119
120
 
@@ -155,7 +156,8 @@
155
156
 
156
157
  .zx-tabbox[data-variant="bracket"] .zx-tabbox__tab:focus-visible {
157
158
  z-index: 1;
158
- outline: 0;
159
+ outline-color: transparent;
160
+ outline-offset: -2px;
159
161
  box-shadow: inset var(--zx-focus-ring);
160
162
  }
161
163
 
@@ -183,7 +185,8 @@
183
185
 
184
186
  .zx-tabbox[data-variant="line"] .zx-tabbox__tab:focus-visible {
185
187
  z-index: 1;
186
- outline: 0;
188
+ outline-color: transparent;
189
+ outline-offset: -2px;
187
190
  box-shadow: inset var(--zx-focus-ring);
188
191
  }
189
192
 
@@ -220,7 +223,7 @@
220
223
 
221
224
  .zx-tabbox[data-variant="segmented"] .zx-tabbox__tab:focus-visible {
222
225
  z-index: 1;
223
- outline: 0;
226
+ outline-color: transparent;
224
227
  box-shadow: var(--zx-focus-ring);
225
228
  }
226
229
 
@@ -291,6 +294,7 @@
291
294
  }
292
295
 
293
296
  .zx-tabbox__panel:focus-visible {
294
- outline: 0;
297
+ outline-color: transparent;
298
+ outline-offset: -2px;
295
299
  box-shadow: inset var(--zx-focus-ring);
296
300
  }
@@ -108,7 +108,7 @@
108
108
  }
109
109
 
110
110
  .zx-table__column-move:focus-visible {
111
- outline: none;
111
+ outline-color: transparent;
112
112
  box-shadow: var(--zx-focus-ring);
113
113
  }
114
114
 
@@ -298,17 +298,21 @@
298
298
  }
299
299
 
300
300
  .zx-table__sort-button {
301
+ /* At least 24px tall (WCAG 2.5.8 target size) without making the header any taller: the height
302
+ the label's line lacks is padding, handed back by a negative margin of the same size. */
303
+ --zx-table-sort-slack: max(0px, (24px - 1lh) / 2);
301
304
  display: flex;
302
305
  align-items: center;
303
306
  justify-content: space-between;
304
307
  gap: var(--zx-space-2);
305
308
  inline-size: 100%;
306
- min-block-size: auto;
309
+ min-block-size: 24px;
307
310
  border: 0;
308
311
  border-radius: var(--zx-radius-sm);
309
312
  background: inherit;
310
313
  color: inherit;
311
- padding: 0;
314
+ margin-block: calc(-1 * var(--zx-table-sort-slack));
315
+ padding: var(--zx-table-sort-slack) 0;
312
316
  /* The header's type, not a control's: base.css sets buttons in the control size and weight, and
313
317
  the browser resets their tracking, so a sortable label would not match its unsortable
314
318
  neighbours. */
@@ -405,13 +409,77 @@
405
409
  }
406
410
 
407
411
 
412
+ /*
413
+ * The selection checkboxes are drawn rather than native. A native checkbox paints its glyph across
414
+ * its whole box, so the only way to give it a 24px target (WCAG 2.5.8) would be a 24px glyph. Here
415
+ * the box stays the --zx-space-4 square it always was and a transparent border around it makes up
416
+ * the rest of the target — the inset shadow that draws the box's edge sits inside that border.
417
+ * Forced colours get the native control back, which the system paints in its own colours.
418
+ */
408
419
  .zx-table__checkbox {
409
- display: block;
420
+ --zx-table-checkbox-slack: max(0px, (24px - var(--zx-space-4)) / 2);
421
+ appearance: none;
422
+ display: grid;
423
+ place-content: center;
424
+ box-sizing: content-box;
410
425
  inline-size: var(--zx-space-4);
411
- min-block-size: auto;
412
426
  block-size: var(--zx-space-4);
427
+ min-block-size: auto;
413
428
  margin: 0 auto;
414
- accent-color: var(--zx-color-accent);
429
+ border: var(--zx-table-checkbox-slack) solid transparent;
430
+ border-radius: calc(var(--zx-radius-sm) + var(--zx-table-checkbox-slack));
431
+ padding: 0;
432
+ background: var(--zx-color-bg-control) padding-box;
433
+ box-shadow: inset 0 0 0 1px var(--zx-color-text-subtle);
434
+ color: var(--zx-color-on-accent);
435
+ cursor: pointer;
436
+ }
437
+
438
+ .zx-table__checkbox::before {
439
+ content: "";
440
+ inline-size: calc(var(--zx-space-4) * .625);
441
+ block-size: calc(var(--zx-space-4) * .625);
442
+ background: currentColor;
443
+ clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0, 43% 62%);
444
+ visibility: hidden;
445
+ }
446
+
447
+ .zx-table__checkbox:hover:not(:disabled) {
448
+ box-shadow: inset 0 0 0 1px var(--zx-color-text-muted);
449
+ }
450
+
451
+ .zx-table__checkbox:is(:checked, :indeterminate) {
452
+ background-color: var(--zx-color-accent);
453
+ box-shadow: inset 0 0 0 1px var(--zx-color-accent);
454
+ }
455
+
456
+ .zx-table__checkbox:is(:checked, :indeterminate)::before {
457
+ visibility: visible;
458
+ }
459
+
460
+ .zx-table__checkbox:indeterminate::before {
461
+ clip-path: inset(40% 5%);
462
+ }
463
+
464
+ .zx-table__checkbox:focus-visible {
465
+ outline-color: transparent;
466
+ outline-offset: calc(-1 * var(--zx-table-checkbox-slack));
467
+ box-shadow: inset 0 0 0 1px var(--zx-color-accent), var(--zx-focus-ring);
468
+ }
469
+
470
+ .zx-table__checkbox:disabled {
471
+ opacity: .5;
472
+ cursor: not-allowed;
473
+ }
474
+
475
+ @media (forced-colors: active) {
476
+ .zx-table__checkbox {
477
+ appearance: auto;
478
+ }
479
+
480
+ .zx-table__checkbox::before {
481
+ content: none;
482
+ }
415
483
  }
416
484
 
417
485
  .zx-table__empty {
@@ -455,7 +523,7 @@
455
523
  }
456
524
 
457
525
  .zx-table__tree-toggle:focus-visible {
458
- outline: 0;
526
+ outline-color: transparent;
459
527
  box-shadow: var(--zx-focus-ring);
460
528
  }
461
529
 
@@ -519,13 +587,20 @@
519
587
  cursor: cell;
520
588
  }
521
589
 
522
- /* Cells carry a roving tabindex, so their ring must sit inside the cell box — an outer ring would
523
- be painted over by the neighbouring cell. */
524
- .zx-table__body td[data-editable="true"]:focus-visible {
525
- outline: 0;
590
+ /* Rows and cells carry the grid's roving tabindex, so their ring sits inside the box — an outer
591
+ ring would be painted over by the neighbouring row or cell, or clipped by the scroll region. The
592
+ outline stays, transparent, for forced colours to paint. */
593
+ .zx-table__body :is(tr[data-row], td, th)[tabindex]:focus-visible {
594
+ outline-color: transparent;
595
+ outline-offset: -2px;
526
596
  box-shadow: inset var(--zx-focus-ring);
527
597
  }
528
598
 
599
+ /* A row moved to by keyboard scrolls clear of the sticky header rather than behind it. */
600
+ .zx-table__body > tr[data-row] {
601
+ scroll-margin-block: calc(var(--zx-control-height) + var(--zx-space-2)) var(--zx-space-2);
602
+ }
603
+
529
604
  .zx-table__body td[data-editing="true"] {
530
605
  position: relative;
531
606
  }