@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
@@ -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 {
@@ -147,17 +147,31 @@
147
147
  font-variant-numeric: tabular-nums;
148
148
  }
149
149
 
150
+ /*
151
+ * A mark sits under the thumb's centre at its value — half a thumb in from either end of the rail
152
+ * — and centres its label there, except that a label is never pushed past either end of the
153
+ * track: the outermost ones end flush with it instead of spilling out of the component.
154
+ * `--zx-slider-mark` is the value's position on the scale, from 0 to 1.
155
+ */
150
156
  .zx-slider__marks {
151
157
  position: relative;
158
+ inline-size: 100%;
152
159
  block-size: var(--zx-space-5);
153
160
  margin-block-start: var(--zx-space-1);
161
+ container-type: inline-size;
154
162
  }
155
163
 
156
164
  .zx-slider__mark {
165
+ --zx-slider-mark-at: calc(var(--zx-slider-thumb) / 2
166
+ + (100cqi - var(--zx-slider-thumb)) * var(--zx-slider-mark, 0));
157
167
  position: absolute;
158
168
  inset-block-start: 0;
159
- inset-inline-start: var(--zx-slider-mark, 0%);
160
- transform: translateX(-50%);
169
+ inset-inline-start: var(--zx-slider-mark-at);
170
+ transform: translateX(clamp(
171
+ -1 * var(--zx-slider-mark-at),
172
+ -50%,
173
+ 100cqi - var(--zx-slider-mark-at) - 100%
174
+ ));
161
175
  color: var(--zx-color-text-muted);
162
176
  font-size: var(--zx-text-xs);
163
177
  white-space: nowrap;
@@ -279,7 +279,7 @@ export class Slider extends Component {
279
279
  const label = typeof mark === 'number' ? this._format(value) : String(mark.label ?? this._format(value));
280
280
  element.append(h('span', {
281
281
  class: 'zx-slider__mark',
282
- style: { '--zx-slider-mark': `${this._ratio(value) * 100}%` }
282
+ style: { '--zx-slider-mark': String(this._ratio(value)) }
283
283
  }, label));
284
284
  }
285
285
  return element;
@@ -10,8 +10,11 @@
10
10
  .zx-sort-control__direction {
11
11
  inline-size: var(--zx-control-height);
12
12
  padding: 0;
13
- border-radius: var(--zx-radius-full);
13
+ border-radius: var(--zx-control-radius);
14
14
  }
15
15
 
16
16
  .zx-sort-control__direction:focus-visible { box-shadow: var(--zx-focus-ring); }
17
+ /* The arrow is the current direction, not decoration, so it keeps the text colour a ghost glyph
18
+ would otherwise give up. */
19
+ .zx-sort-control .zx-sort-control__direction > .zx-icon { color: var(--zx-color-text); }
17
20
  .zx-sort-control .zx-select__control { background: var(--zx-color-bg-control); }
@@ -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;
@@ -1,14 +1,17 @@
1
1
  /*
2
- * The tile is a size ladder, not a colour: the value is the loudest thing on it, the label is
3
- * small above it, and the delta and sparkline are quieter than both. `kind` tints the icon and
4
- * the sparkline only — never the value, which has to stay at full text contrast whatever intent
5
- * a server assigns the metric.
2
+ * The tile is a size ladder, not a colour: the value is the loudest thing on it, the label is a
3
+ * quiet sentence-case line above it, and the caption, delta and sparkline are quieter than both.
4
+ * The icon is a muted glyph, not a coloured chip. `kind` tints the sparkline only — never the
5
+ * value, which has to stay at full text contrast whatever intent a server assigns the metric.
6
+ *
7
+ * A column, with the sparkline pushed to the foot: tiles in one row are as tall as the tallest,
8
+ * so their labels, values and sparklines line up whether or not each tile has every part.
6
9
  */
7
10
  .zx-stat-tile {
8
11
  --zx-stat-tile-value-size: var(--zx-text-2xl);
9
- display: grid;
10
- gap: var(--zx-space-2);
11
- align-content: start;
12
+ display: flex;
13
+ flex-direction: column;
14
+ gap: var(--zx-space-1);
12
15
  min-inline-size: 0;
13
16
  padding: var(--zx-space-4);
14
17
  border: 1px solid var(--zx-color-border);
@@ -38,7 +41,7 @@ button.zx-stat-tile:hover {
38
41
 
39
42
  a.zx-stat-tile:focus-visible,
40
43
  button.zx-stat-tile:focus-visible {
41
- outline: 0;
44
+ outline-color: transparent;
42
45
  box-shadow: var(--zx-focus-ring);
43
46
  }
44
47
 
@@ -60,25 +63,19 @@ button.zx-stat-tile:focus-visible {
60
63
  .zx-stat-tile__label {
61
64
  min-inline-size: 0;
62
65
  overflow: hidden;
63
- font-size: var(--zx-text-xs);
64
- font-weight: 600;
65
- letter-spacing: 0.04em;
66
+ font-size: var(--zx-text-sm);
67
+ font-weight: var(--zx-weight-medium);
66
68
  line-height: 1.4;
67
- text-transform: uppercase;
68
69
  text-overflow: ellipsis;
69
70
  white-space: nowrap;
70
71
  color: var(--zx-color-text-muted);
71
72
  }
72
73
 
73
74
  .zx-stat-tile__icon {
74
- display: grid;
75
+ display: inline-grid;
75
76
  flex: none;
76
77
  place-items: center;
77
- inline-size: 1.75rem;
78
- block-size: 1.75rem;
79
- border-radius: var(--zx-radius-full);
80
- background: var(--zx-color-bg-muted);
81
- color: var(--zx-color-text-muted);
78
+ color: var(--zx-color-text-subtle);
82
79
  }
83
80
 
84
81
  /*
@@ -87,21 +84,35 @@ button.zx-stat-tile:focus-visible {
87
84
  */
88
85
  .zx-stat-tile__value {
89
86
  min-inline-size: 0;
87
+ margin-block-start: var(--zx-space-1);
90
88
  font-size: var(--zx-stat-tile-value-size);
91
- font-weight: 600;
89
+ font-weight: 650;
92
90
  font-variant-numeric: tabular-nums;
91
+ letter-spacing: var(--zx-tracking-title);
93
92
  line-height: 1.15;
94
93
  overflow-wrap: break-word;
95
94
  color: var(--zx-color-text);
96
95
  }
97
96
 
97
+ /* What the value is out of, or measured in: one muted line, read with the value. */
98
+ .zx-stat-tile__caption {
99
+ min-inline-size: 0;
100
+ overflow: hidden;
101
+ color: var(--zx-color-text-muted);
102
+ font-size: var(--zx-text-sm);
103
+ font-variant-numeric: tabular-nums;
104
+ line-height: 1.4;
105
+ text-overflow: ellipsis;
106
+ white-space: nowrap;
107
+ }
108
+
98
109
  .zx-stat-tile__delta {
99
110
  display: flex;
100
111
  gap: var(--zx-space-1);
101
112
  align-items: baseline;
102
113
  min-inline-size: 0;
103
114
  font-size: var(--zx-text-xs);
104
- font-weight: 600;
115
+ font-weight: var(--zx-weight-medium);
105
116
  font-variant-numeric: tabular-nums;
106
117
  line-height: 1.4;
107
118
  color: var(--zx-color-text-muted);
@@ -124,6 +135,12 @@ button.zx-stat-tile:focus-visible {
124
135
  align-self: center;
125
136
  }
126
137
 
138
+ .zx-stat-tile__delta-value {
139
+ /* A figure never breaks ("+23" over "pcs"); the comparison beside it gives way first. */
140
+ flex: none;
141
+ white-space: nowrap;
142
+ }
143
+
127
144
  .zx-stat-tile__delta-label {
128
145
  min-inline-size: 0;
129
146
  overflow: hidden;
@@ -136,9 +153,12 @@ button.zx-stat-tile:focus-visible {
136
153
 
137
154
  .zx-stat-tile__trend {
138
155
  display: block;
156
+ flex: none;
157
+ box-sizing: content-box;
139
158
  inline-size: 100%;
140
159
  block-size: 2rem;
141
- margin-block-start: var(--zx-space-1);
160
+ margin-block-start: auto;
161
+ padding-block-start: var(--zx-space-2);
142
162
  color: var(--zx-color-text-placeholder);
143
163
  }
144
164
 
@@ -147,32 +167,7 @@ button.zx-stat-tile:focus-visible {
147
167
  gap: var(--zx-space-2);
148
168
  }
149
169
 
150
- /* Intent. The icon carries it as a tint, the sparkline as its stroke. */
151
- .zx-stat-tile[data-kind="accent"] .zx-stat-tile__icon {
152
- background: var(--zx-color-accent-subtle);
153
- color: var(--zx-color-accent);
154
- }
155
-
156
- .zx-stat-tile[data-kind="success"] .zx-stat-tile__icon {
157
- background: var(--zx-color-success-bg);
158
- color: var(--zx-color-success);
159
- }
160
-
161
- .zx-stat-tile[data-kind="warning"] .zx-stat-tile__icon {
162
- background: var(--zx-color-warning-bg);
163
- color: var(--zx-color-warning);
164
- }
165
-
166
- .zx-stat-tile[data-kind="danger"] .zx-stat-tile__icon {
167
- background: var(--zx-color-danger-bg);
168
- color: var(--zx-color-danger);
169
- }
170
-
171
- .zx-stat-tile[data-kind="info"] .zx-stat-tile__icon {
172
- background: var(--zx-color-info-bg);
173
- color: var(--zx-color-info);
174
- }
175
-
170
+ /* Intent. The sparkline carries it as its stroke; the icon stays a muted glyph. */
176
171
  .zx-stat-tile[data-kind="accent"] .zx-stat-tile__trend {
177
172
  color: var(--zx-color-accent);
178
173
  }
@@ -25,6 +25,7 @@ import { skeleton } from '../skeleton/skeleton.js';
25
25
  * @typedef {Object} StatTileNameParts
26
26
  * @property {string} [label] The metric's name.
27
27
  * @property {string} [value] The formatted value.
28
+ * @property {string} [caption] The context line under the value, e.g. `'of 13,184'`.
28
29
  * @property {string} [delta] The change, already worded for speech (`'up 2'`, not `'+2'`).
29
30
  * @property {string} [deltaLabel] What the change is measured against.
30
31
  */
@@ -36,6 +37,8 @@ import { skeleton } from '../skeleton/skeleton.js';
36
37
  * @property {StatTileFormat} [format='number'] How a numeric value is rendered: a named mode, a
37
38
  * callback receiving the raw value, or null to print the value unformatted.
38
39
  * @property {string|null} [currency=null] ISO 4217 code used by `format: 'currency'`.
40
+ * @property {string} [caption=''] A muted context line under the value — what the number is out
41
+ * of or measured in, e.g. `'of 13,184'` or `'open tickets'`. Part of the accessible name.
39
42
  * @property {string} [locale] BCP 47 tag for every formatter; defaults to the active language.
40
43
  * @property {number|string|null} [delta=null] Change against the previous period. Rendered with
41
44
  * its own sign and an arrow, never with colour alone.
@@ -46,9 +49,10 @@ import { skeleton } from '../skeleton/skeleton.js';
46
49
  * @property {string} [deltaLabel=''] What the change is measured against, e.g. `'vs. last week'`.
47
50
  * @property {number[]} [trend=[]] Values for a decorative sparkline. Fewer than two usable numbers
48
51
  * render nothing.
49
- * @property {string|null} [icon=null] Icon name from `icons.js`, shown beside the label.
50
- * @property {StatTileKind} [kind='neutral'] Semantic intent. It tints the icon and the sparkline
51
- * and never the value, which stays at full text contrast whatever intent a record carries.
52
+ * @property {string|null} [icon=null] Icon name from `icons.js`, shown muted beside the label —
53
+ * a glyph, not a coloured chip.
54
+ * @property {StatTileKind} [kind='neutral'] Semantic intent. It tints the sparkline and never the
55
+ * value, which stays at full text contrast whatever intent a record carries.
52
56
  * @property {string|null} [href=null] Destination. A tile with an href is an `<a>`.
53
57
  * @property {boolean} [clickable=false] Renders a `<button>` without an href. Passing `onclick`
54
58
  * sets this implicitly; set it directly to subscribe with `on('click', …)` instead.
@@ -93,6 +97,7 @@ const defaults = Object.freeze({
93
97
  value: null,
94
98
  format: 'number',
95
99
  currency: null,
100
+ caption: '',
96
101
  delta: null,
97
102
  deltaKind: 'neutral',
98
103
  deltaLabel: '',
@@ -306,17 +311,19 @@ export function statTileTag(options = {}) {
306
311
  }
307
312
 
308
313
  /**
309
- * Composes the tile's accessible name: label, then value, then how it changed.
314
+ * Composes the tile's accessible name: label, then value with its caption, then how it changed.
310
315
  *
311
316
  * The sparkline is decoration and contributes nothing, and the delta arrives already worded —
312
- * `'up 2'` rather than `'+2'` — because a screen reader should not have to interpret a glyph.
317
+ * `'up 2'` rather than `'+2'` — because a screen reader should not have to interpret a glyph. The
318
+ * caption is read with the value it qualifies (`'2,184 of 13,184'`), not as a separate clause.
313
319
  *
314
320
  * @param {StatTileNameParts} [parts={}] The pieces, already formatted and translated.
315
321
  * @returns {string} The name, with empty pieces dropped rather than left as stray punctuation.
316
322
  */
317
323
  export function statTileName(parts = {}) {
324
+ const value = [parts.value, parts.caption].map(trimmed).filter(Boolean).join(' ');
318
325
  const change = [parts.delta, parts.deltaLabel].map(trimmed).filter(Boolean).join(' ');
319
- return [trimmed(parts.label), trimmed(parts.value), change].filter(Boolean).join(', ');
326
+ return [trimmed(parts.label), value, change].filter(Boolean).join(', ');
320
327
  }
321
328
 
322
329
  /**
@@ -391,10 +398,12 @@ function fillStatTile(root, options, message) {
391
398
  currency: options.currency
392
399
  });
393
400
  const deltaLabel = asText(options.deltaLabel);
401
+ const caption = asText(options.caption).trim();
394
402
 
395
403
  applyName(root, statTileName({
396
404
  label,
397
405
  value,
406
+ caption,
398
407
  delta: delta === null ? '' : spokenDelta(delta, message),
399
408
  deltaLabel
400
409
  }));
@@ -407,6 +416,7 @@ function fillStatTile(root, options, message) {
407
416
  // `Element`, not `HTMLElement`: the sparkline is an SVG element and belongs in the same list.
408
417
  /** @type {Element[]} */
409
418
  const children = [header, h('div', { class: 'zx-stat-tile__value' }, value)];
419
+ if (caption) children.push(h('div', { class: 'zx-stat-tile__caption' }, caption));
410
420
  if (delta !== null) children.push(deltaBody(delta, deltaLabel, options.deltaKind));
411
421
  const trend = trendBody(options.trend);
412
422
  if (trend) children.push(trend);
@@ -481,6 +491,7 @@ function loadingBody(options) {
481
491
  h('div', { class: 'zx-stat-tile__header' }, skeleton({ width: '55%', height: '0.75rem' })),
482
492
  skeleton({ width: '70%', height: '1.75rem', radius: 'md' })
483
493
  ];
494
+ if (asText(options.caption).trim()) blocks.push(skeleton({ width: '35%', height: '0.75rem' }));
484
495
  if (options.delta !== null && options.delta !== undefined) {
485
496
  blocks.push(skeleton({ width: '40%', height: '0.75rem' }));
486
497
  }
@@ -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