@zeyos/zx 4.3.4 → 5.0.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 (231) hide show
  1. package/CHANGELOG.md +438 -0
  2. package/README.md +49 -10
  3. package/dist/types/components/activity-item/activity-item.d.ts +351 -0
  4. package/dist/types/components/activity-list/activity-list.d.ts +217 -0
  5. package/dist/types/components/app-sidebar/app-sidebar.d.ts +33 -3
  6. package/dist/types/components/avatar/avatar.d.ts +46 -7
  7. package/dist/types/components/badge/badge.d.ts +21 -1
  8. package/dist/types/components/button/button.d.ts +23 -0
  9. package/dist/types/components/calendar/calendar.d.ts +19 -0
  10. package/dist/types/components/card/card.d.ts +7 -0
  11. package/dist/types/components/card-view/card-view.d.ts +11 -3
  12. package/dist/types/components/card-view/record-card.d.ts +7 -1
  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 +149 -0
  16. package/dist/types/components/code-editor/field-adapter.d.ts +5 -0
  17. package/dist/types/components/context-menu/context-menu.d.ts +18 -2
  18. package/dist/types/components/date-picker/date-picker.d.ts +8 -0
  19. package/dist/types/components/date-picker/time-picker.d.ts +7 -0
  20. package/dist/types/components/datebox/date-range-box.d.ts +5 -1
  21. package/dist/types/components/datebox/datebox.d.ts +7 -2
  22. package/dist/types/components/dialog/dialog.d.ts +75 -6
  23. package/dist/types/components/dropdown/dropdown.d.ts +2 -2
  24. package/dist/types/components/entity-ref/entity-ref.d.ts +331 -0
  25. package/dist/types/components/field/field.d.ts +24 -2
  26. package/dist/types/components/fieldset/fieldset.d.ts +19 -1
  27. package/dist/types/components/file-item/file-item.d.ts +262 -0
  28. package/dist/types/components/file-list/file-list.d.ts +171 -0
  29. package/dist/types/components/filter/filter.d.ts +91 -36
  30. package/dist/types/components/filter-panel/filter-panel.d.ts +449 -0
  31. package/dist/types/components/form/form.d.ts +12 -1
  32. package/dist/types/components/kanban-view/kanban-card.d.ts +146 -0
  33. package/dist/types/components/kanban-view/kanban-view.d.ts +170 -4
  34. package/dist/types/components/launcher/launcher.d.ts +278 -26
  35. package/dist/types/components/list-toolbar/list-toolbar.d.ts +599 -0
  36. package/dist/types/components/menu-button/menu-button.d.ts +37 -42
  37. package/dist/types/components/message/message.d.ts +126 -17
  38. package/dist/types/components/modal/modal.d.ts +61 -2
  39. package/dist/types/components/navigation-bar/navigation-bar.d.ts +105 -21
  40. package/dist/types/components/panel/panel.d.ts +7 -0
  41. package/dist/types/components/search/search.d.ts +7 -0
  42. package/dist/types/components/select/color.d.ts +12 -0
  43. package/dist/types/components/select/glyphs.d.ts +124 -0
  44. package/dist/types/components/select/select.d.ts +15 -3
  45. package/dist/types/components/sort-control/sort-control.d.ts +105 -0
  46. package/dist/types/components/stat-tile/stat-tile.d.ts +288 -0
  47. package/dist/types/components/tabbox/tabbox.d.ts +16 -0
  48. package/dist/types/components/table/table.d.ts +190 -1
  49. package/dist/types/components/table-view/table-view.d.ts +11 -5
  50. package/dist/types/components/tag-picker/tag-picker.d.ts +21 -4
  51. package/dist/types/components/timebox/timebox.d.ts +7 -0
  52. package/dist/types/components/tree/tree.d.ts +13 -0
  53. package/dist/types/components/value-list/value-list.d.ts +8 -0
  54. package/dist/types/components/view/record-view.d.ts +96 -5
  55. package/dist/types/core/breakpoint.d.ts +1 -1
  56. package/dist/types/core/icons.d.ts +37 -22
  57. package/dist/types/core/motion.d.ts +59 -0
  58. package/dist/types/core/overlay-host.d.ts +15 -1
  59. package/dist/types/index.d.ts +16 -3
  60. package/dist/types/internal/app-rail.d.ts +10 -0
  61. package/dist/types/internal/menu-items.d.ts +134 -0
  62. package/dist/types/zeyos/connect.d.ts +6 -0
  63. package/dist/types/zeyos/form.d.ts +50 -9
  64. package/dist/types/zeyos/index.d.ts +1 -1
  65. package/dist/types/zeyos/query.d.ts +79 -0
  66. package/dist/types/zeyos/schema.d.ts +24 -1
  67. package/dist/types/zeyos/table.d.ts +100 -14
  68. package/dist/types/zeyos/view.d.ts +10 -0
  69. package/dist/zx-zeyos.esm.js +2206 -489
  70. package/dist/zx-zeyos.esm.min.js +56 -1
  71. package/dist/zx.css +3322 -724
  72. package/dist/zx.esm.js +12244 -5809
  73. package/dist/zx.esm.min.js +58 -3
  74. package/dist/zx.global.js +12246 -5811
  75. package/dist/zx.global.min.js +58 -3
  76. package/dist/zx.min.css +1 -1
  77. package/docs/api.json +1683 -146
  78. package/docs/llms.md +777 -110
  79. package/docs/llms.txt +14 -8
  80. package/package.json +1 -1
  81. package/src/components/account-menu/account-menu.css +35 -10
  82. package/src/components/activity-item/activity-item.css +137 -0
  83. package/src/components/activity-item/activity-item.js +602 -0
  84. package/src/components/activity-list/activity-list.css +99 -0
  85. package/src/components/activity-list/activity-list.js +409 -0
  86. package/src/components/app-icon/app-icon.css +1 -1
  87. package/src/components/app-rail/app-rail.css +191 -22
  88. package/src/components/app-sidebar/app-sidebar.css +349 -24
  89. package/src/components/app-sidebar/app-sidebar.js +86 -11
  90. package/src/components/aurora/aurora.css +5 -3
  91. package/src/components/avatar/avatar.css +32 -7
  92. package/src/components/avatar/avatar.js +72 -8
  93. package/src/components/badge/badge.css +54 -2
  94. package/src/components/badge/badge.js +29 -2
  95. package/src/components/button/button.css +74 -8
  96. package/src/components/button/button.js +44 -1
  97. package/src/components/calendar/calendar.css +46 -46
  98. package/src/components/calendar/calendar.js +33 -3
  99. package/src/components/card/card.css +2 -2
  100. package/src/components/card/card.js +12 -1
  101. package/src/components/card-view/card-view.css +10 -7
  102. package/src/components/card-view/card-view.js +25 -9
  103. package/src/components/card-view/record-card.js +7 -3
  104. package/src/components/chart/chart.css +4 -1
  105. package/src/components/chart/chart.js +12 -0
  106. package/src/components/check-button/check-button.css +10 -2
  107. package/src/components/checklist/checklist.css +9 -2
  108. package/src/components/checklist/checklist.js +11 -5
  109. package/src/components/checklist/field-adapter.js +3 -1
  110. package/src/components/code-editor/code-editor.css +68 -0
  111. package/src/components/code-editor/code-editor.js +306 -0
  112. package/src/components/code-editor/field-adapter.js +34 -0
  113. package/src/components/context-menu/context-menu.css +87 -5
  114. package/src/components/context-menu/context-menu.js +22 -30
  115. package/src/components/copy/copy-button.css +6 -0
  116. package/src/components/copy/copy-input.css +8 -3
  117. package/src/components/data-filter/data-filter.css +5 -4
  118. package/src/components/date-picker/date-picker.css +6 -6
  119. package/src/components/date-picker/date-picker.js +22 -8
  120. package/src/components/date-picker/date-range-picker.css +3 -3
  121. package/src/components/date-picker/month-picker.js +3 -3
  122. package/src/components/date-picker/time-picker.css +1 -1
  123. package/src/components/date-picker/time-picker.js +20 -8
  124. package/src/components/datebox/date-range-box.css +79 -2
  125. package/src/components/datebox/date-range-box.js +13 -2
  126. package/src/components/datebox/datebox.css +79 -2
  127. package/src/components/datebox/datebox.js +13 -2
  128. package/src/components/dialog/dialog.css +26 -0
  129. package/src/components/dialog/dialog.js +174 -70
  130. package/src/components/dropdown/dropdown.css +1 -1
  131. package/src/components/dropdown/dropdown.js +7 -5
  132. package/src/components/empty-state/empty-state.css +2 -2
  133. package/src/components/entity-ref/entity-ref.css +182 -0
  134. package/src/components/entity-ref/entity-ref.js +496 -0
  135. package/src/components/field/field.css +28 -9
  136. package/src/components/field/field.js +68 -26
  137. package/src/components/field-upload/field-upload.css +1 -1
  138. package/src/components/fieldset/fieldset.css +5 -3
  139. package/src/components/fieldset/fieldset.js +28 -5
  140. package/src/components/file-item/file-item.css +88 -0
  141. package/src/components/file-item/file-item.js +505 -0
  142. package/src/components/file-list/file-list.css +51 -0
  143. package/src/components/file-list/file-list.js +320 -0
  144. package/src/components/filter/filter.css +27 -1
  145. package/src/components/filter/filter.js +205 -47
  146. package/src/components/filter-panel/filter-panel.css +104 -0
  147. package/src/components/filter-panel/filter-panel.js +964 -0
  148. package/src/components/finder/finder.css +15 -13
  149. package/src/components/form/form.css +5 -4
  150. package/src/components/form/form.js +11 -2
  151. package/src/components/grid/grid.css +1 -1
  152. package/src/components/groupbox/groupbox.css +1 -1
  153. package/src/components/kanban-view/kanban-card.js +247 -0
  154. package/src/components/kanban-view/kanban-view.css +226 -8
  155. package/src/components/kanban-view/kanban-view.js +255 -12
  156. package/src/components/launcher/launcher.css +107 -25
  157. package/src/components/launcher/launcher.js +557 -63
  158. package/src/components/list-toolbar/list-toolbar.css +194 -0
  159. package/src/components/list-toolbar/list-toolbar.js +851 -0
  160. package/src/components/master-panel/master-panel.css +2 -0
  161. package/src/components/menu-button/menu-button.css +87 -5
  162. package/src/components/menu-button/menu-button.js +86 -46
  163. package/src/components/message/message.css +38 -5
  164. package/src/components/message/message.js +230 -17
  165. package/src/components/modal/modal.css +15 -0
  166. package/src/components/modal/modal.js +159 -3
  167. package/src/components/navigation-bar/navigation-bar.css +95 -11
  168. package/src/components/navigation-bar/navigation-bar.js +171 -4
  169. package/src/components/number-field/field-adapter.js +1 -0
  170. package/src/components/number-field/number-field.css +70 -0
  171. package/src/components/number-field/number-field.js +2 -0
  172. package/src/components/pagination/pagination.css +8 -6
  173. package/src/components/panel/panel.css +1 -1
  174. package/src/components/panel/panel.js +13 -2
  175. package/src/components/rating/rating.css +7 -0
  176. package/src/components/search/search.js +17 -5
  177. package/src/components/select/color.js +22 -0
  178. package/src/components/select/field-adapter.js +6 -1
  179. package/src/components/select/glyphs.js +165 -0
  180. package/src/components/select/select.css +138 -45
  181. package/src/components/select/select.js +86 -47
  182. package/src/components/sheet/sheet.css +11 -8
  183. package/src/components/slider/slider.css +16 -2
  184. package/src/components/slider/slider.js +1 -1
  185. package/src/components/sort-control/sort-control.css +20 -0
  186. package/src/components/sort-control/sort-control.js +140 -0
  187. package/src/components/stat-tile/stat-tile.css +208 -0
  188. package/src/components/stat-tile/stat-tile.js +601 -0
  189. package/src/components/tabbox/tabbox.css +32 -27
  190. package/src/components/tabbox/tabbox.js +17 -2
  191. package/src/components/table/table.css +155 -31
  192. package/src/components/table/table.js +386 -33
  193. package/src/components/table-view/table-view.css +8 -2
  194. package/src/components/table-view/table-view.js +21 -6
  195. package/src/components/tag-picker/tag-picker.css +86 -6
  196. package/src/components/tag-picker/tag-picker.js +25 -8
  197. package/src/components/timebox/timebox.css +22 -2
  198. package/src/components/timebox/timebox.js +18 -5
  199. package/src/components/toggle/toggle.css +12 -1
  200. package/src/components/toolbar/toolbar.js +2 -2
  201. package/src/components/tooltip/tooltip.css +4 -0
  202. package/src/components/tooltip/tooltip.js +10 -3
  203. package/src/components/tree/tree.css +25 -16
  204. package/src/components/tree/tree.js +31 -0
  205. package/src/components/value-list/value-list.css +3 -3
  206. package/src/components/value-list/value-list.js +22 -6
  207. package/src/components/view/record-view.css +5 -0
  208. package/src/components/view/record-view.js +100 -7
  209. package/src/core/breakpoint.js +1 -1
  210. package/src/core/icons.js +259 -57
  211. package/src/core/motion.js +147 -0
  212. package/src/core/overlay-host.js +21 -2
  213. package/src/index.js +30 -3
  214. package/src/internal/app-rail.js +18 -1
  215. package/src/internal/menu-items.js +142 -0
  216. package/src/zeyos/connect.js +6 -2
  217. package/src/zeyos/form.js +67 -27
  218. package/src/zeyos/index.js +1 -1
  219. package/src/zeyos/query.js +153 -0
  220. package/src/zeyos/schema.js +144 -27
  221. package/src/zeyos/table.js +68 -5
  222. package/src/zeyos/view.js +6 -0
  223. package/styles/base.css +56 -0
  224. package/styles/icons.css +11 -1
  225. package/styles/print.css +192 -0
  226. package/styles/tokens/dark.css +113 -37
  227. package/styles/tokens/global.css +121 -27
  228. package/styles/tokens/semantic.css +100 -31
  229. package/styles/tokens/themes.css +7 -6
  230. package/styles/zx.css +11 -1
  231. package/styles/tokens/density.css +0 -9
@@ -3,8 +3,12 @@ import { h, restoreTarget, snapshotTarget } from '../../core/dom.js';
3
3
  import { icon } from '../../core/icons.js';
4
4
  import { typeahead } from '../../core/keyboard.js';
5
5
  import { position } from '../../core/position.js';
6
- import { isCssColor, uid } from '../../core/util.js';
6
+ import { uid } from '../../core/util.js';
7
+ import { itemColor } from './color.js';
7
8
  import { matchItems } from './filter.js';
9
+ import {
10
+ DEFAULT_STATUSES, STATUS_INDICATORS, STATUS_TONES, normalizePriorityLevel, priorityIcon, statusIcon
11
+ } from './glyphs.js';
8
12
 
9
13
  /** @typedef {string|number|boolean|symbol|bigint|null|undefined} Primitive */
10
14
  /** @typedef {Record<string, any>|Primitive} SelectItem */
@@ -27,6 +31,7 @@ import { matchItems } from './filter.js';
27
31
  * @property {unknown} [value=null] Initially selected ID or item.
28
32
  * @property {boolean} [disabled=false] Whether interaction is disabled.
29
33
  * @property {string} [placeholder=''] Empty control text.
34
+ * @property {string} [label=''] Accessible combobox name; falls back to the placeholder.
30
35
  * @property {boolean} [clearable=false] Whether selection may be cleared.
31
36
  * @property {false|'local'|((query: string) => Promise<SelectItem[]>|SelectItem[])} [filter=false] Filtering mode.
32
37
  * @property {Array<SelectValueReader>|null} [searchKeys=null] Fields searched by local filtering.
@@ -48,7 +53,9 @@ import { matchItems } from './filter.js';
48
53
  * @property {string} [recentLabel='Recent'] Heading above recent choices.
49
54
  * @property {boolean|SelectAction} [create=false] Optional create command at the bottom.
50
55
  * @property {string|((item: SelectItem)=>unknown)} [iconKey='icon'] Item icon reader.
51
- * @property {string|((item: SelectItem)=>unknown)} [colorKey='color'] Optional icon colour reader.
56
+ * @property {string|((item: SelectItem)=>unknown)} [colorKey='color'] Optional icon colour reader: a CSS
57
+ * colour or a token reference such as `var(--zx-color-tone-blue-fg)`. Without one the glyph is a
58
+ * neutral tile in muted ink.
52
59
  * @property {string|((item: SelectItem)=>unknown)} [subtitleKey='description'] Secondary-text reader.
53
60
  * @property {((item: SelectItem, context:{selected:boolean})=>Node|string|null)|null} [renderIcon=null] Entity icon renderer.
54
61
  */
@@ -68,18 +75,6 @@ const PRIORITY_KEYS = [
68
75
  ];
69
76
  const PRIORITY_FALLBACKS = ['Lowest', 'Low', 'Medium', 'High', 'Highest'];
70
77
 
71
- const DEFAULT_STATUSES = Object.freeze([
72
- ['backlog', 'status.backlog', 'Backlog', 'dotted', 'muted'],
73
- ['todo', 'status.todo', 'Todo', 'ring', 'neutral'],
74
- ['in-progress', 'status.inProgress', 'In Progress', 'progress', 'warning'],
75
- ['in-review', 'status.inReview', 'In Review', 'review', 'info'],
76
- ['ready', 'status.ready', 'Ready to Merge', 'ready', 'accent'],
77
- ['waiting', 'status.waiting', 'Waiting for feedback', 'waiting', 'warning'],
78
- ['done', 'status.done', 'Done', 'done', 'info'],
79
- ['canceled', 'status.canceled', 'Canceled', 'canceled', 'muted'],
80
- ['duplicate', 'status.duplicate', 'Duplicate', 'duplicate', 'muted']
81
- ]);
82
-
83
78
  /**
84
79
  * APG editable combobox with optional local or asynchronous filtering.
85
80
  *
@@ -109,6 +104,7 @@ export class Select extends Component {
109
104
  value: null,
110
105
  disabled: false,
111
106
  placeholder: '',
107
+ label: '',
112
108
  clearable: false,
113
109
  filter: false,
114
110
  searchKeys: null,
@@ -123,7 +119,7 @@ export class Select extends Component {
123
119
  render() {
124
120
  // Instance state initialized here because render() runs inside the base
125
121
  // constructor, before class-field initializers would run (and clobber it).
126
- this._items = []; this._visibleItems = []; this._fixedItems = []; this._visibleFixed = []; this._actions = []; this._selected = null; this._optionEntries = []; this._activeIndex = -1; this._open = false; this._disabled = false; this._loading = false; this._requestSequence = 0; this._queryTimer = null; this._position = null; this._createdRoot = false; this._root = null; this._snapshot = null;
122
+ this._items = []; this._visibleItems = []; this._fixedItems = []; this._visibleFixed = []; this._actions = []; this._selected = null; this._optionEntries = []; this._activeIndex = -1; this._open = false; this._disabled = false; this._loading = false; this._requestSequence = 0; this._queryTimer = null; this._position = null; this._createdRoot = false; this._root = null; this._snapshot = null; this._pointerModality = false;
127
123
  this._createdRoot = this.el === null;
128
124
  const root = /** @type {HTMLElement} */ (this.el ?? h('div'));
129
125
  this.el = root;
@@ -136,6 +132,7 @@ export class Select extends Component {
136
132
  class: 'zx-select__input',
137
133
  type: 'text',
138
134
  role: 'combobox',
135
+ ariaLabel: String(this.options.label || this.options.placeholder || 'Select'),
139
136
  autocomplete: 'off',
140
137
  spellcheck: false,
141
138
  ariaExpanded: 'false',
@@ -206,7 +203,22 @@ export class Select extends Component {
206
203
 
207
204
  this.listen(input, 'click', () => this.open());
208
205
  this.listen(input, 'input', () => this._onInput());
209
- this.listen(input, 'keydown', (event) => this._onKeydown(/** @type {KeyboardEvent} */ (event)));
206
+ this.listen(input, 'keydown', (event) => {
207
+ if (!isModifierKey(/** @type {KeyboardEvent} */ (event))) root.removeAttribute('data-focus-origin');
208
+ this._onKeydown(/** @type {KeyboardEvent} */ (event));
209
+ });
210
+ // A text input matches :focus-visible however it was focused, read-only or not, so a click on
211
+ // a read-only Select would draw the keyboard ring. Track how the user last interacted and mark
212
+ // a pointer focus; the ring then follows keyboard focus only, and returns on the first key.
213
+ this.listen(input, 'focus', () => {
214
+ if (this._pointerModality && /** @type {HTMLInputElement} */ (input).readOnly) {
215
+ root.setAttribute('data-focus-origin', 'pointer');
216
+ } else root.removeAttribute('data-focus-origin');
217
+ });
218
+ this.listen(input, 'blur', () => root.removeAttribute('data-focus-origin'));
219
+ this.listen(document, 'keydown', (event) => {
220
+ if (!isModifierKey(/** @type {KeyboardEvent} */ (event))) this._pointerModality = false;
221
+ }, { capture: true });
210
222
  this.listen(toggle, 'click', () => {
211
223
  this.focus();
212
224
  if (this._open) this.close();
@@ -219,6 +231,7 @@ export class Select extends Component {
219
231
  this.listen(panel, 'pointerdown', (event) => event.preventDefault());
220
232
  this.listen(panel, 'click', (event) => this._onOptionClick(event));
221
233
  this.listen(document, 'pointerdown', (event) => {
234
+ this._pointerModality = true;
222
235
  if (this._open && !root.contains(/** @type {Node} */ (event.target))) this.close();
223
236
  }, { capture: true });
224
237
 
@@ -378,26 +391,44 @@ export class Select extends Component {
378
391
  }
379
392
 
380
393
  /**
381
- * Creates the five-level priority preset.
394
+ * Creates the five-level priority preset: values 0 (lowest) to 4 (highest), each drawn as grey
395
+ * signal bars filled to its level, except the highest, which is the one coloured level — a
396
+ * danger square with an exclamation mark. A "No priority" row (value null, a row of dashes)
397
+ * heads the list and labels the empty control; `clearable: false` removes it.
382
398
  * @param {Element|string|null} target Component target.
383
- * @param {SelectOptions} [options={}] Select overrides other than fixed items and renderer.
399
+ * @param {SelectOptions} [options={}] Select overrides other than items and renderers.
400
+ * `clearable` defaults to true here; `noneLabel` and `placeholder` default to the
401
+ * `priority.none` message ("No priority").
384
402
  * @returns {Select}
385
403
  */
386
404
  static priority(target, options = {}) {
387
405
  const requestedValue = options.value ?? null;
388
- const select = new Select(target, {
406
+ const clearable = options.clearable ?? true;
407
+ /** @type {Select|null} */
408
+ let select = null;
409
+ // The constructor renders the list once before `select` is assigned; the label is settled by
410
+ // the setItems() below, which renders it again.
411
+ const noneLabel = () => options.noneLabel
412
+ ?? (select ? select._message('priority.none', 'No priority') : 'No priority');
413
+ select = new Select(target, {
389
414
  ...options,
415
+ clearable,
390
416
  value: null,
391
417
  items: [],
392
418
  renderItem(item) {
393
419
  return priorityContent(item, true);
394
420
  },
421
+ renderNone: options.renderNone ?? (() => priorityContent({ level: null, name: noneLabel() }, true)),
395
422
  renderValueAdornment(item) { return priorityIndicator(item.level); }
396
423
  });
424
+ if (clearable && !options.placeholder) {
425
+ /** @type {HTMLInputElement} */ (select.refs.input).placeholder = noneLabel();
426
+ }
427
+ const preset = select;
397
428
  select.setItems(PRIORITY_KEYS.map((key, level) => {
398
- let name = level === 2 ? select.msg('priority.medium') : key;
399
- if (name === 'priority.medium') name = select._message(key, PRIORITY_FALLBACKS[level]);
400
- else if (level !== 2) name = select._message(key, PRIORITY_FALLBACKS[level]);
429
+ let name = level === 2 ? preset.msg('priority.medium') : key;
430
+ if (name === 'priority.medium') name = preset._message(key, PRIORITY_FALLBACKS[level]);
431
+ else if (level !== 2) name = preset._message(key, PRIORITY_FALLBACKS[level]);
401
432
  return { ID: level, name, level };
402
433
  }));
403
434
  select.set(requestedValue, { silent: true });
@@ -450,10 +481,10 @@ export class Select extends Component {
450
481
  const value = readSelectItem(item, iconKey);
451
482
  if (value && typeof value === 'object' && typeof value.nodeType === 'number') return value.cloneNode(true);
452
483
  if (typeof value !== 'string' || !value) return null;
453
- const color = readSelectItem(item, colorKey);
484
+ const color = itemColor(readSelectItem(item, colorKey));
454
485
  return h('span', {
455
486
  class: 'zx-select__entity-glyph',
456
- style: isCssColor(color) ? { '--zx-select-entity-color': String(color) } : null,
487
+ style: color ? { '--zx-select-entity-color': color } : null,
457
488
  ariaHidden: 'true'
458
489
  }, icon(value, { size: 14 }));
459
490
  };
@@ -989,28 +1020,26 @@ function priorityContent(item, label) {
989
1020
  label ? h('span', {}, String(item.name)) : null);
990
1021
  }
991
1022
 
992
- /** @param {unknown} level @returns {Node} */
1023
+ /**
1024
+ * The priority glyph `priorityIcon()` draws, carrying the Select's own class as well so styles
1025
+ * written against it keep applying.
1026
+ * @param {unknown} level Priority level 0–4, or null for none.
1027
+ * @returns {Node}
1028
+ */
993
1029
  function priorityIndicator(level) {
994
- const normalized = Math.max(0, Math.min(4, Math.trunc(Number(level) || 0)));
995
- return h('span', {
996
- class: 'zx-select__priority-icon',
997
- dataset: { level: normalized },
998
- ariaHidden: 'true'
999
- }, Array.from({ length: 5 }, (_, index) => h('span', {
1000
- dataset: { filled: String(index <= normalized) }
1001
- })));
1030
+ const glyph = priorityIcon(/** @type {number|null} */ (normalizePriorityLevel(level)));
1031
+ glyph.classList.add('zx-select__priority-icon');
1032
+ return glyph;
1002
1033
  }
1003
1034
 
1004
1035
  /** @param {unknown} input @returns {Record<string, any>|null} */
1005
1036
  function normalizeStatusItem(input) {
1006
1037
  if (!input || typeof input !== 'object' || input.ID == null || input.name == null) return null;
1007
- const indicators = ['dotted', 'ring', 'progress', 'review', 'ready', 'waiting', 'done', 'canceled', 'duplicate'];
1008
- const tones = ['muted', 'neutral', 'warning', 'info', 'accent', 'success', 'danger'];
1009
1038
  return {
1010
1039
  ...input,
1011
1040
  name: String(input.name),
1012
- indicator: indicators.includes(String(input.indicator)) ? String(input.indicator) : 'ring',
1013
- tone: tones.includes(String(input.tone)) ? String(input.tone) : 'muted',
1041
+ indicator: STATUS_INDICATORS.includes(String(input.indicator)) ? String(input.indicator) : 'ring',
1042
+ tone: STATUS_TONES.includes(String(input.tone)) ? String(input.tone) : 'muted',
1014
1043
  description: input.description == null ? '' : String(input.description),
1015
1044
  shortcut: input.shortcut == null ? '' : String(input.shortcut)
1016
1045
  };
@@ -1026,17 +1055,16 @@ function statusContent(item) {
1026
1055
  item.shortcut ? h('kbd', { class: 'zx-select__status-shortcut' }, item.shortcut) : null);
1027
1056
  }
1028
1057
 
1029
- /** @param {Record<string, any>} item @returns {Node} */
1058
+ /**
1059
+ * The status glyph `statusIcon()` draws for a settled status item, carrying the Select's own class
1060
+ * as well so styles written against it keep applying.
1061
+ * @param {Record<string, any>} item Normalized status item.
1062
+ * @returns {Node}
1063
+ */
1030
1064
  function statusIndicator(item) {
1031
- const indicator = String(item.indicator ?? 'ring');
1032
- const marker = h('span', {
1033
- class: 'zx-select__status-indicator',
1034
- dataset: { indicator, tone: String(item.tone ?? 'muted') },
1035
- ariaHidden: 'true'
1036
- });
1037
- if (indicator === 'done') marker.append(icon('check', { size: 8 }));
1038
- if (indicator === 'canceled') marker.append(icon('x', { size: 7 }));
1039
- return marker;
1065
+ const glyph = statusIcon({ indicator: item.indicator ?? 'ring', tone: item.tone ?? 'muted' });
1066
+ glyph.classList.add('zx-select__status-indicator');
1067
+ return glyph;
1040
1068
  }
1041
1069
 
1042
1070
  /** Change emitted when the selected value changes. @event Select#change @type {CustomEvent<SelectChangeDetail>} */
@@ -1058,6 +1086,17 @@ function readItem(item, reader) {
1058
1086
  return item[reader];
1059
1087
  }
1060
1088
 
1089
+ /**
1090
+ * Reports a key press that says nothing about how the user is moving focus: a lone modifier, or a
1091
+ * shortcut chord the page or the browser handles.
1092
+ * @param {KeyboardEvent} event Key event.
1093
+ * @returns {boolean}
1094
+ */
1095
+ function isModifierKey(event) {
1096
+ return ['Shift', 'Control', 'Alt', 'Meta', 'CapsLock'].includes(event.key)
1097
+ || event.ctrlKey || event.metaKey;
1098
+ }
1099
+
1061
1100
  /** @param {KeyboardEvent} event @returns {boolean} */
1062
1101
  function isPrintable(event) {
1063
1102
  return event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey;
@@ -57,8 +57,8 @@
57
57
  inset-block-start: 0;
58
58
  inset-block-end: auto;
59
59
  border-block-end: 1px solid var(--zx-color-overlay-border);
60
- border-end-start-radius: var(--zx-sheet-radius, var(--zx-radius-xl));
61
- border-end-end-radius: var(--zx-sheet-radius, var(--zx-radius-xl));
60
+ border-end-start-radius: var(--zx-sheet-radius, var(--zx-radius-lg));
61
+ border-end-end-radius: var(--zx-sheet-radius, var(--zx-radius-lg));
62
62
  --zx-sheet-enter: 0 -100%;
63
63
  }
64
64
 
@@ -66,8 +66,8 @@
66
66
  inset-block-start: auto;
67
67
  inset-block-end: 0;
68
68
  border-block-start: 1px solid var(--zx-color-overlay-border);
69
- border-start-start-radius: var(--zx-sheet-radius, var(--zx-radius-xl));
70
- border-start-end-radius: var(--zx-sheet-radius, var(--zx-radius-xl));
69
+ border-start-start-radius: var(--zx-sheet-radius, var(--zx-radius-lg));
70
+ border-start-end-radius: var(--zx-sheet-radius, var(--zx-radius-lg));
71
71
  --zx-sheet-enter: 0 100%;
72
72
  }
73
73
 
@@ -105,11 +105,14 @@
105
105
  * `display: none` immediately — so `display` and `overlay` transition with `allow-discrete`, which
106
106
  * defers the discrete jump until the rest of the transition has finished. `@starting-style` gives
107
107
  * the entry its from-state. Where either is unsupported the sheet simply appears and disappears.
108
+ *
109
+ * The slide runs on the drawer curve — most of the distance at once, then a settle — while the
110
+ * fade keeps the ordinary ease, so the panel arrives decisively without flashing in.
108
111
  */
109
112
  @media (prefers-reduced-motion: no-preference) {
110
113
  .zx-sheet {
111
114
  transition:
112
- translate var(--zx-dur-base) var(--zx-ease),
115
+ translate var(--zx-dur-base) var(--zx-ease-drawer),
113
116
  opacity var(--zx-dur-base) var(--zx-ease),
114
117
  display var(--zx-dur-base) allow-discrete,
115
118
  overlay var(--zx-dur-base) allow-discrete;
@@ -193,7 +196,7 @@
193
196
  @media (prefers-reduced-motion: no-preference) {
194
197
  .zx-sheet[data-docked] {
195
198
  transition:
196
- flex-basis var(--zx-dur-base) var(--zx-ease),
199
+ flex-basis var(--zx-dur-base) var(--zx-ease-drawer),
197
200
  opacity var(--zx-dur-base) var(--zx-ease),
198
201
  display var(--zx-dur-base) allow-discrete,
199
202
  overlay var(--zx-dur-base) allow-discrete;
@@ -363,8 +366,8 @@
363
366
  @media (prefers-reduced-motion: no-preference) {
364
367
  .zx-sheet[data-stack] {
365
368
  transition:
366
- transform var(--zx-dur-base) var(--zx-ease),
367
- translate var(--zx-dur-base) var(--zx-ease),
369
+ transform var(--zx-dur-base) var(--zx-ease-drawer),
370
+ translate var(--zx-dur-base) var(--zx-ease-drawer),
368
371
  opacity var(--zx-dur-base) var(--zx-ease),
369
372
  display var(--zx-dur-base) allow-discrete,
370
373
  overlay var(--zx-dur-base) allow-discrete;
@@ -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;
@@ -0,0 +1,20 @@
1
+ .zx-sort-control {
2
+ display: inline-grid;
3
+ grid-template-columns: auto minmax(0, 1fr);
4
+ align-items: center;
5
+ gap: var(--zx-space-1);
6
+ min-inline-size: 0;
7
+ inline-size: 100%;
8
+ }
9
+
10
+ .zx-sort-control__direction {
11
+ inline-size: var(--zx-control-height);
12
+ padding: 0;
13
+ border-radius: var(--zx-control-radius);
14
+ }
15
+
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); }
20
+ .zx-sort-control .zx-select__control { background: var(--zx-color-bg-control); }
@@ -0,0 +1,140 @@
1
+ import { Component } from '../../core/component.js';
2
+ import { h, restoreTarget, snapshotTarget } from '../../core/dom.js';
3
+ import { icon } from '../../core/icons.js';
4
+ import { button } from '../button/button.js';
5
+ import { Select } from '../select/select.js';
6
+
7
+ /** @typedef {{id:string,label:string}} SortField */
8
+ /** @typedef {{id:string,dir:'asc'|'desc'}} SortValue */
9
+ /**
10
+ * @typedef {Object} SortControlOptions
11
+ * @property {SortField[]} [fields=[]] Available sort fields, in display order.
12
+ * @property {SortValue|null} [value=null] Initial sort; null chooses the first field ascending.
13
+ * @property {string} [label='Sort by'] Accessible field label and direction-name prefix.
14
+ * @property {{asc?:string,desc?:string}} [labels={}] Current/next direction labels, defaulting to Ascending/Descending.
15
+ * @property {boolean} [disabled=false] Whether interaction is disabled.
16
+ * @property {(event:CustomEvent<{value:SortValue}>)=>void} [onchange] Atomic field/direction change.
17
+ */
18
+
19
+ /**
20
+ * A searchable field selector with an adjacent direction toggle and one shared sort value.
21
+ * @fires SortControl#change
22
+ * @extends {Component<SortControlOptions>}
23
+ */
24
+ export class SortControl extends Component {
25
+ static cssName = 'sort-control';
26
+ /** @type {SortControlOptions} */
27
+ static defaults = { fields: [], value: null, label: 'Sort by', labels: {}, disabled: false };
28
+
29
+ /** @returns {HTMLElement} */
30
+ render() {
31
+ const root = /** @type {HTMLElement} */ (this.el ?? h('div'));
32
+ this._createdRoot = this.el === null;
33
+ this._snapshot = this._createdRoot ? null : snapshotTarget(root);
34
+ this.el = root;
35
+ this._value = normalizeSortValue(this.options.value, this.options.fields);
36
+ this._disabled = Boolean(this.options.disabled || !this.options.fields.length);
37
+ this._direction = button({ kind: 'ghost', disabled: this._disabled });
38
+ this._direction.classList.add('zx-sort-control__direction');
39
+ this.listen(this._direction, 'click', () => {
40
+ if (this._disabled || !this._value) return;
41
+ this.setValue({ id: this._value.id, dir: this._value.dir === 'asc' ? 'desc' : 'asc' });
42
+ });
43
+ this._select = new Select(null, {
44
+ items: sortChoices(this.options.fields), valueKey: 'id', labelKey: 'label',
45
+ value: this._value?.id ?? null, label: this.options.label,
46
+ filter: 'local', disabled: this._disabled,
47
+ onchange: ({ detail }) => {
48
+ if (!detail.item) return;
49
+ this.setValue({ id: detail.item.id, dir: this._value?.dir ?? 'asc' });
50
+ }
51
+ });
52
+ // The owned Select's DOM event must not duplicate the composite's atomic event.
53
+ this.listen(this._select.el, 'zx-change', (event) => event.stopPropagation());
54
+ root.replaceChildren(this._direction, this._select.el);
55
+ this._syncDirection();
56
+ return root;
57
+ }
58
+
59
+ /** Returns a defensive copy of the current sort. @returns {SortValue|null} */
60
+ getValue() { return this._value ? { ...this._value } : null; }
61
+
62
+ /**
63
+ * Replaces the field and direction atomically; rejects unavailable fields and directions.
64
+ * @param {SortValue|null} value Sort state, or null for the first field ascending.
65
+ * @param {{silent?:boolean}} [options={}] Suppress change when silent.
66
+ * @returns {this}
67
+ */
68
+ setValue(value, { silent = false } = {}) {
69
+ const next = normalizeSortValue(value, this.options.fields);
70
+ const changed = sortKey(next) !== sortKey(this._value);
71
+ this._value = next;
72
+ this._select.set(next?.id ?? null, { silent: true });
73
+ this._syncDirection();
74
+ if (changed && !silent) this.emit('change', { value: this.getValue() });
75
+ return this;
76
+ }
77
+
78
+ /** Enables both controls when fields are available. @returns {this} */
79
+ enable() {
80
+ this._disabled = !this.options.fields.length;
81
+ if (!this._disabled) this._select.enable();
82
+ this._direction.disabled = this._disabled;
83
+ return this;
84
+ }
85
+ /** Disables both controls and closes the option panel. @returns {this} */
86
+ disable() {
87
+ this._disabled = true;
88
+ this._select.disable();
89
+ this._direction.disabled = true;
90
+ return this;
91
+ }
92
+
93
+ /** Updates the icon and accessible current/next direction text. @private @returns {void} */
94
+ _syncDirection() {
95
+ const dir = this._value?.dir ?? 'asc';
96
+ const next = dir === 'asc' ? 'desc' : 'asc';
97
+ const labels = { asc: 'Ascending', desc: 'Descending', ...this.options.labels };
98
+ this._direction.replaceChildren(icon(dir === 'asc' ? 'arrow-up-wide-short' : 'arrow-down-wide-short'));
99
+ this._direction.dataset.direction = dir;
100
+ this._direction.setAttribute('aria-label', `${this.options.label}: ${labels[dir]}`);
101
+ this._direction.title = `${labels[dir]} → ${labels[next]}`;
102
+ }
103
+
104
+ /** Focuses the searchable control. @returns {this} */
105
+ focus() { this._select.focus(); return this; }
106
+ /** Releases the owned Select and restores an enhanced target. @returns {void} */
107
+ destroy() {
108
+ this._select?.destroy();
109
+ super.destroy();
110
+ if (!this._createdRoot) restoreTarget(this.el, this._snapshot);
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Validates a sort against available fields without retaining caller-owned state.
116
+ * @param {SortValue|null} value Requested sort.
117
+ * @param {SortField[]} fields Available fields.
118
+ * @returns {SortValue|null} Normalized sort, or null for an empty catalogue.
119
+ */
120
+ export function normalizeSortValue(value, fields) {
121
+ if (!fields.length) return null;
122
+ if (value == null) return { id: String(fields[0].id), dir: 'asc' };
123
+ if (!fields.some((field) => String(field.id) === String(value.id))) throw new RangeError('Unknown sort field');
124
+ if (value.dir !== 'asc' && value.dir !== 'desc') throw new RangeError('Unknown sort direction');
125
+ return { id: String(value.id), dir: value.dir };
126
+ }
127
+
128
+ /**
129
+ * Builds one searchable record per field without retaining caller-owned objects.
130
+ * @param {SortField[]} fields Available fields.
131
+ * @returns {SortField[]} Choice records.
132
+ */
133
+ export function sortChoices(fields) {
134
+ return fields.map((field) => ({ id: String(field.id), label: field.label }));
135
+ }
136
+
137
+ /** @param {SortValue|null} value Sort state. @returns {string} Stable choice key. */
138
+ function sortKey(value) { return value ? JSON.stringify([value.id, value.dir]) : ''; }
139
+
140
+ /** @event SortControl#change @type {CustomEvent<{value:SortValue}>} */