@zeyos/zx 4.4.0 → 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 (182) hide show
  1. package/CHANGELOG.md +283 -0
  2. package/README.md +42 -10
  3. package/dist/types/components/activity-list/activity-list.d.ts +22 -0
  4. package/dist/types/components/app-sidebar/app-sidebar.d.ts +5 -3
  5. package/dist/types/components/avatar/avatar.d.ts +46 -7
  6. package/dist/types/components/badge/badge.d.ts +21 -1
  7. package/dist/types/components/button/button.d.ts +15 -0
  8. package/dist/types/components/chart/chart.d.ts +10 -0
  9. package/dist/types/components/checklist/checklist.d.ts +9 -0
  10. package/dist/types/components/code-editor/code-editor.d.ts +6 -17
  11. package/dist/types/components/datebox/date-range-box.d.ts +5 -1
  12. package/dist/types/components/datebox/datebox.d.ts +7 -2
  13. package/dist/types/components/dialog/dialog.d.ts +46 -6
  14. package/dist/types/components/dropdown/dropdown.d.ts +2 -2
  15. package/dist/types/components/entity-ref/entity-ref.d.ts +2 -2
  16. package/dist/types/components/field/field.d.ts +24 -2
  17. package/dist/types/components/fieldset/fieldset.d.ts +19 -1
  18. package/dist/types/components/file-item/file-item.d.ts +34 -6
  19. package/dist/types/components/form/form.d.ts +12 -1
  20. package/dist/types/components/kanban-view/kanban-card.d.ts +8 -0
  21. package/dist/types/components/launcher/launcher.d.ts +260 -15
  22. package/dist/types/components/list-toolbar/list-toolbar.d.ts +1 -1
  23. package/dist/types/components/menu-button/menu-button.d.ts +33 -5
  24. package/dist/types/components/message/message.d.ts +119 -17
  25. package/dist/types/components/modal/modal.d.ts +13 -2
  26. package/dist/types/components/select/color.d.ts +12 -0
  27. package/dist/types/components/select/glyphs.d.ts +124 -0
  28. package/dist/types/components/select/select.d.ts +11 -3
  29. package/dist/types/components/stat-tile/stat-tile.d.ts +16 -5
  30. package/dist/types/components/tabbox/tabbox.d.ts +9 -0
  31. package/dist/types/components/table/table.d.ts +46 -1
  32. package/dist/types/components/tag-picker/tag-picker.d.ts +21 -4
  33. package/dist/types/components/tree/tree.d.ts +13 -0
  34. package/dist/types/core/breakpoint.d.ts +1 -1
  35. package/dist/types/core/icons.d.ts +23 -25
  36. package/dist/types/core/motion.d.ts +59 -0
  37. package/dist/types/core/overlay-host.d.ts +15 -1
  38. package/dist/types/index.d.ts +5 -3
  39. package/dist/types/zeyos/connect.d.ts +6 -0
  40. package/dist/types/zeyos/form.d.ts +50 -9
  41. package/dist/types/zeyos/index.d.ts +1 -1
  42. package/dist/types/zeyos/query.d.ts +79 -0
  43. package/dist/types/zeyos/schema.d.ts +24 -1
  44. package/dist/types/zeyos/table.d.ts +100 -14
  45. package/dist/types/zeyos/view.d.ts +10 -0
  46. package/dist/zx-zeyos.esm.js +1621 -443
  47. package/dist/zx-zeyos.esm.min.js +53 -5
  48. package/dist/zx.css +1411 -660
  49. package/dist/zx.esm.js +1976 -703
  50. package/dist/zx.esm.min.js +56 -8
  51. package/dist/zx.global.js +1978 -705
  52. package/dist/zx.global.min.js +56 -8
  53. package/dist/zx.min.css +1 -1
  54. package/docs/api.json +219 -34
  55. package/docs/llms.md +472 -110
  56. package/docs/llms.txt +3 -3
  57. package/package.json +1 -1
  58. package/src/components/account-menu/account-menu.css +35 -10
  59. package/src/components/activity-item/activity-item.css +1 -1
  60. package/src/components/activity-item/activity-item.js +3 -0
  61. package/src/components/activity-list/activity-list.css +38 -3
  62. package/src/components/activity-list/activity-list.js +34 -0
  63. package/src/components/app-icon/app-icon.css +1 -1
  64. package/src/components/app-rail/app-rail.css +40 -79
  65. package/src/components/app-sidebar/app-sidebar.css +109 -174
  66. package/src/components/app-sidebar/app-sidebar.js +21 -6
  67. package/src/components/aurora/aurora.css +5 -3
  68. package/src/components/avatar/avatar.css +32 -7
  69. package/src/components/avatar/avatar.js +72 -8
  70. package/src/components/badge/badge.css +54 -2
  71. package/src/components/badge/badge.js +29 -2
  72. package/src/components/button/button.css +67 -9
  73. package/src/components/button/button.js +38 -0
  74. package/src/components/calendar/calendar.css +46 -46
  75. package/src/components/card/card.css +2 -2
  76. package/src/components/card-view/card-view.css +9 -7
  77. package/src/components/chart/chart.css +4 -1
  78. package/src/components/chart/chart.js +12 -0
  79. package/src/components/check-button/check-button.css +10 -2
  80. package/src/components/checklist/checklist.css +9 -2
  81. package/src/components/checklist/checklist.js +11 -5
  82. package/src/components/checklist/field-adapter.js +3 -1
  83. package/src/components/code-editor/code-editor.css +6 -5
  84. package/src/components/code-editor/code-editor.js +19 -1
  85. package/src/components/context-menu/context-menu.css +9 -4
  86. package/src/components/copy/copy-button.css +6 -0
  87. package/src/components/copy/copy-input.css +8 -3
  88. package/src/components/data-filter/data-filter.css +5 -4
  89. package/src/components/date-picker/date-picker.css +6 -6
  90. package/src/components/date-picker/date-range-picker.css +3 -3
  91. package/src/components/date-picker/time-picker.css +1 -1
  92. package/src/components/datebox/date-range-box.css +79 -2
  93. package/src/components/datebox/date-range-box.js +13 -2
  94. package/src/components/datebox/datebox.css +79 -2
  95. package/src/components/datebox/datebox.js +13 -2
  96. package/src/components/dialog/dialog.css +26 -0
  97. package/src/components/dialog/dialog.js +99 -17
  98. package/src/components/dropdown/dropdown.css +1 -1
  99. package/src/components/dropdown/dropdown.js +5 -3
  100. package/src/components/empty-state/empty-state.css +2 -2
  101. package/src/components/entity-ref/entity-ref.css +1 -1
  102. package/src/components/entity-ref/entity-ref.js +1 -1
  103. package/src/components/field/field.css +28 -9
  104. package/src/components/field/field.js +68 -26
  105. package/src/components/field-upload/field-upload.css +1 -1
  106. package/src/components/fieldset/fieldset.css +5 -3
  107. package/src/components/fieldset/fieldset.js +28 -5
  108. package/src/components/file-item/file-item.js +144 -10
  109. package/src/components/filter/filter.js +7 -3
  110. package/src/components/filter-panel/filter-panel.css +1 -1
  111. package/src/components/finder/finder.css +15 -13
  112. package/src/components/form/form.css +5 -4
  113. package/src/components/form/form.js +11 -2
  114. package/src/components/grid/grid.css +1 -1
  115. package/src/components/groupbox/groupbox.css +1 -1
  116. package/src/components/kanban-view/kanban-card.js +11 -3
  117. package/src/components/kanban-view/kanban-view.css +39 -15
  118. package/src/components/kanban-view/kanban-view.js +4 -4
  119. package/src/components/launcher/launcher.css +107 -25
  120. package/src/components/launcher/launcher.js +503 -41
  121. package/src/components/list-toolbar/list-toolbar.css +13 -8
  122. package/src/components/list-toolbar/list-toolbar.js +1 -1
  123. package/src/components/master-panel/master-panel.css +2 -0
  124. package/src/components/menu-button/menu-button.css +9 -4
  125. package/src/components/menu-button/menu-button.js +65 -9
  126. package/src/components/message/message.css +33 -3
  127. package/src/components/message/message.js +217 -15
  128. package/src/components/modal/modal.js +18 -3
  129. package/src/components/navigation-bar/navigation-bar.css +18 -9
  130. package/src/components/number-field/field-adapter.js +1 -0
  131. package/src/components/number-field/number-field.css +70 -0
  132. package/src/components/number-field/number-field.js +2 -0
  133. package/src/components/pagination/pagination.css +8 -6
  134. package/src/components/panel/panel.css +1 -1
  135. package/src/components/rating/rating.css +7 -0
  136. package/src/components/select/color.js +22 -0
  137. package/src/components/select/field-adapter.js +2 -0
  138. package/src/components/select/glyphs.js +165 -0
  139. package/src/components/select/select.css +138 -45
  140. package/src/components/select/select.js +83 -47
  141. package/src/components/sheet/sheet.css +11 -8
  142. package/src/components/slider/slider.css +16 -2
  143. package/src/components/slider/slider.js +1 -1
  144. package/src/components/sort-control/sort-control.css +4 -1
  145. package/src/components/stat-tile/stat-tile.css +41 -46
  146. package/src/components/stat-tile/stat-tile.js +17 -6
  147. package/src/components/tabbox/tabbox.css +32 -27
  148. package/src/components/tabbox/tabbox.js +5 -1
  149. package/src/components/table/table.css +104 -29
  150. package/src/components/table/table.js +90 -16
  151. package/src/components/tag-picker/tag-picker.css +83 -5
  152. package/src/components/tag-picker/tag-picker.js +25 -8
  153. package/src/components/timebox/timebox.css +22 -2
  154. package/src/components/toggle/toggle.css +12 -1
  155. package/src/components/toolbar/toolbar.js +2 -2
  156. package/src/components/tooltip/tooltip.css +4 -0
  157. package/src/components/tooltip/tooltip.js +10 -3
  158. package/src/components/tree/tree.css +25 -16
  159. package/src/components/tree/tree.js +31 -0
  160. package/src/components/value-list/value-list.css +3 -3
  161. package/src/components/view/record-view.js +1 -1
  162. package/src/core/breakpoint.js +1 -1
  163. package/src/core/icons.js +193 -60
  164. package/src/core/motion.js +147 -0
  165. package/src/core/overlay-host.js +21 -2
  166. package/src/index.js +5 -3
  167. package/src/internal/app-rail.js +1 -2
  168. package/src/zeyos/connect.js +6 -2
  169. package/src/zeyos/form.js +67 -27
  170. package/src/zeyos/index.js +1 -1
  171. package/src/zeyos/query.js +153 -0
  172. package/src/zeyos/schema.js +144 -27
  173. package/src/zeyos/table.js +68 -5
  174. package/src/zeyos/view.js +6 -0
  175. package/styles/base.css +56 -0
  176. package/styles/icons.css +11 -1
  177. package/styles/tokens/dark.css +113 -37
  178. package/styles/tokens/global.css +121 -27
  179. package/styles/tokens/semantic.css +100 -31
  180. package/styles/tokens/themes.css +7 -6
  181. package/styles/zx.css +0 -1
  182. package/styles/tokens/density.css +0 -9
@@ -2,6 +2,7 @@
2
2
  import { Component } from '../../core/component.js';
3
3
  import { h, restoreTarget, snapshotTarget } from '../../core/dom.js';
4
4
  import { formatFileSize } from '../../core/format.js';
5
+ import { getIconConfig, iconNames } from '../../core/icons.js';
5
6
  import { isElement } from '../../core/util.js';
6
7
  import { badge } from '../badge/badge.js';
7
8
  import {
@@ -20,9 +21,13 @@ import {
20
21
  * @property {unknown} [id=null] Application-owned file identifier included in events.
21
22
  * @property {string} [name=''] File name.
22
23
  * @property {number|string|null} [size=null] Byte count; invalid and negative values are omitted.
23
- * @property {string} [mime=''] MIME type.
24
+ * @property {string} [mime=''] MIME type. Shown as a readable type ("PDF", "Image"), never as
25
+ * the raw string, and used to pick the icon.
26
+ * @property {string|null} [typeLabel=null] Type shown beside the size; null derives it from the
27
+ * MIME type (or the name's extension when the MIME type is unfamiliar), an empty string hides it.
24
28
  * @property {EntityRefContent} [subtitle=null] Optional supporting text before generated metadata.
25
- * @property {EntityRefIcon} [icon='file'] File icon name, node, component, or lazy renderer.
29
+ * @property {EntityRefIcon} [icon='file'] File icon name, node, component, or lazy renderer. The
30
+ * generic `'file'` glyph gives way to one for the MIME type when the active icon set has it.
26
31
  * @property {EntityRefMetadata[]} [metadata=[]] Additional labelled metadata.
27
32
  * @property {EntityRefLinkValue} [link=null] Optional safe native file/download link.
28
33
  * @property {EntityRefActionEntry[]} [actions=[]] File actions; application policy stays external.
@@ -30,7 +35,7 @@ import {
30
35
  * @property {string|null} [statusLabel=null] Overrides the generated state label; an empty string hides it.
31
36
  * @property {number|null} [progress=null] Completed percentage, clamped to 0–100.
32
37
  * @property {boolean} [indeterminate=false] Whether active progress has no known amount.
33
- * @property {'sm'|'md'|'lg'} [density='md'] File-identity presentation size.
38
+ * @property {'sm'|'md'|'lg'} [identitySize='md'] Size of the file's icon and name.
34
39
  * @property {boolean} [wrap=false] Whether long identity text may wrap.
35
40
  * @property {string} [actionsLabel='File actions'] Accessible name for secondary actions.
36
41
  * @property {string} [locale] Locale passed to `formatFileSize`.
@@ -46,6 +51,7 @@ import {
46
51
  * @property {string} name File name.
47
52
  * @property {number|null} size Byte count.
48
53
  * @property {string} mime MIME type.
54
+ * @property {string|null} typeLabel Explicit type label, or null for the one derived from `mime`.
49
55
  * @property {EntityRefContent} subtitle Supporting content.
50
56
  * @property {EntityRefIcon} icon File visual.
51
57
  * @property {EntityRefMetadata[]} metadata Additional metadata.
@@ -55,7 +61,7 @@ import {
55
61
  * @property {string|null} statusLabel Explicit state label, or null for the generated label.
56
62
  * @property {number|null} progress Completed percentage.
57
63
  * @property {boolean} indeterminate Unknown-progress state.
58
- * @property {'sm'|'md'|'lg'} density Identity size.
64
+ * @property {'sm'|'md'|'lg'} identitySize Size of the file's icon and name.
59
65
  * @property {boolean} wrap Wrapping state.
60
66
  * @property {string} actionsLabel Action-group label.
61
67
  * @property {string|undefined} locale File-size locale.
@@ -91,6 +97,7 @@ export class FileItem extends Component {
91
97
  name: '',
92
98
  size: null,
93
99
  mime: '',
100
+ typeLabel: null,
94
101
  subtitle: null,
95
102
  icon: 'file',
96
103
  metadata: [],
@@ -100,7 +107,7 @@ export class FileItem extends Component {
100
107
  statusLabel: null,
101
108
  progress: null,
102
109
  indeterminate: false,
103
- density: 'md',
110
+ identitySize: 'md',
104
111
  wrap: false,
105
112
  actionsLabel: 'File actions'
106
113
  };
@@ -207,7 +214,7 @@ export class FileItem extends Component {
207
214
  const track = /** @type {HTMLElement} */ (this.refs.progressTrack);
208
215
  const fill = /** @type {HTMLElement} */ (this.refs.progressFill);
209
216
  root.dataset.status = state.status;
210
- root.dataset.density = state.density;
217
+ root.dataset.identitySize = state.identitySize;
211
218
  this._entity.set(entityOptions(state));
212
219
 
213
220
  const label = fileStatusLabel(state);
@@ -282,6 +289,7 @@ export function normalizeFileItem(values = {}) {
282
289
  name: String(source.name ?? ''),
283
290
  size: rawSize !== null && Number.isFinite(rawSize) && rawSize >= 0 ? rawSize : null,
284
291
  mime: String(source.mime ?? ''),
292
+ typeLabel: source.typeLabel == null ? null : String(source.typeLabel),
285
293
  subtitle: normalizeFileContent(source.subtitle),
286
294
  icon: source.icon === undefined ? 'file' : source.icon,
287
295
  metadata: normalizeEntityRefMetadata(source.metadata),
@@ -292,7 +300,7 @@ export function normalizeFileItem(values = {}) {
292
300
  progress: rawProgress === null || !Number.isFinite(rawProgress)
293
301
  ? null : Math.min(100, Math.max(0, rawProgress)),
294
302
  indeterminate: Boolean(source.indeterminate),
295
- density: source.density === 'sm' || source.density === 'lg' ? source.density : 'md',
303
+ identitySize: source.identitySize === 'sm' || source.identitySize === 'lg' ? source.identitySize : 'md',
296
304
  wrap: Boolean(source.wrap),
297
305
  actionsLabel: String(source.actionsLabel || 'File actions'),
298
306
  locale: normalizeLocale(source.locale)
@@ -302,7 +310,8 @@ export function normalizeFileItem(values = {}) {
302
310
  /** @param {FileItemState} state @returns {import('../entity-ref/entity-ref.js').EntityRefOptions} */
303
311
  function entityOptions(state) {
304
312
  const metadata = [];
305
- if (state.mime) metadata.push({ label: 'Type', value: state.mime, showLabel: false });
313
+ const type = state.typeLabel ?? fileTypeLabel(state.mime, state.name);
314
+ if (type) metadata.push({ label: 'Type', value: type, showLabel: false });
306
315
  if (state.size !== null) {
307
316
  metadata.push({
308
317
  label: 'Size',
@@ -315,16 +324,141 @@ function entityOptions(state) {
315
324
  id: state.id,
316
325
  title: state.name,
317
326
  subtitle: state.subtitle,
318
- icon: state.icon,
327
+ icon: state.icon === 'file' ? fileTypeIcon(state.mime) : state.icon,
319
328
  metadata,
320
329
  link: state.link,
321
330
  actions: state.actions,
322
- size: state.density,
331
+ size: state.identitySize,
323
332
  wrap: state.wrap,
324
333
  actionsLabel: state.actionsLabel
325
334
  };
326
335
  }
327
336
 
337
+ /**
338
+ * What a MIME type is called and drawn as. Labels are what a reader recognises — a format's own
339
+ * acronym where it has a well-known one, the family otherwise — and icons are Font Awesome names
340
+ * in order of preference, used only when the active icon set can draw them.
341
+ * @type {Readonly<Record<string, {label: string, icons: string[]}>>}
342
+ */
343
+ const FILE_KINDS = Object.freeze({
344
+ pdf: { label: 'PDF', icons: ['file-pdf'] },
345
+ csv: { label: 'CSV', icons: ['file-csv', 'file-excel'] },
346
+ tsv: { label: 'TSV', icons: ['file-csv', 'file-excel'] },
347
+ spreadsheet: { label: 'Spreadsheet', icons: ['file-excel'] },
348
+ document: { label: 'Document', icons: ['file-word', 'file-lines'] },
349
+ presentation: { label: 'Presentation', icons: ['file-powerpoint'] },
350
+ text: { label: 'Text', icons: ['file-lines'] },
351
+ markdown: { label: 'Markdown', icons: ['file-lines'] },
352
+ html: { label: 'HTML', icons: ['file-code', 'code'] },
353
+ css: { label: 'CSS', icons: ['file-code', 'code'] },
354
+ javascript: { label: 'JavaScript', icons: ['file-code', 'code'] },
355
+ json: { label: 'JSON', icons: ['file-code', 'code'] },
356
+ xml: { label: 'XML', icons: ['file-code', 'code'] },
357
+ zip: { label: 'ZIP', icons: ['file-zipper'] },
358
+ gzip: { label: 'GZIP', icons: ['file-zipper'] },
359
+ tar: { label: 'TAR', icons: ['file-zipper'] },
360
+ rar: { label: 'RAR', icons: ['file-zipper'] },
361
+ sevenZip: { label: '7Z', icons: ['file-zipper'] },
362
+ email: { label: 'Email', icons: ['envelope'] },
363
+ calendar: { label: 'Calendar', icons: ['calendar'] },
364
+ contact: { label: 'Contact', icons: ['address-card'] },
365
+ image: { label: 'Image', icons: ['file-image', 'image'] },
366
+ video: { label: 'Video', icons: ['file-video', 'video'] },
367
+ audio: { label: 'Audio', icons: ['file-audio'] },
368
+ font: { label: 'Font', icons: ['font'] }
369
+ });
370
+
371
+ /** Exact MIME types, lower case and without parameters, mapped to their kind. */
372
+ const MIME_KINDS = new Map(Object.entries({
373
+ 'application/pdf': 'pdf',
374
+ 'text/csv': 'csv',
375
+ 'application/csv': 'csv',
376
+ 'text/comma-separated-values': 'csv',
377
+ 'text/tab-separated-values': 'tsv',
378
+ 'application/vnd.ms-excel': 'spreadsheet',
379
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'spreadsheet',
380
+ 'application/vnd.oasis.opendocument.spreadsheet': 'spreadsheet',
381
+ 'application/msword': 'document',
382
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'document',
383
+ 'application/vnd.oasis.opendocument.text': 'document',
384
+ 'application/rtf': 'document',
385
+ 'text/rtf': 'document',
386
+ 'application/vnd.ms-powerpoint': 'presentation',
387
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'presentation',
388
+ 'application/vnd.oasis.opendocument.presentation': 'presentation',
389
+ 'text/plain': 'text',
390
+ 'text/markdown': 'markdown',
391
+ 'text/x-markdown': 'markdown',
392
+ 'text/html': 'html',
393
+ 'application/xhtml+xml': 'html',
394
+ 'text/css': 'css',
395
+ 'text/javascript': 'javascript',
396
+ 'application/javascript': 'javascript',
397
+ 'application/json': 'json',
398
+ 'application/ld+json': 'json',
399
+ 'application/xml': 'xml',
400
+ 'text/xml': 'xml',
401
+ 'application/zip': 'zip',
402
+ 'application/x-zip-compressed': 'zip',
403
+ 'application/gzip': 'gzip',
404
+ 'application/x-gzip': 'gzip',
405
+ 'application/x-tar': 'tar',
406
+ 'application/vnd.rar': 'rar',
407
+ 'application/x-rar-compressed': 'rar',
408
+ 'application/x-7z-compressed': 'sevenZip',
409
+ 'message/rfc822': 'email',
410
+ 'text/calendar': 'calendar',
411
+ 'text/vcard': 'contact',
412
+ 'text/x-vcard': 'contact'
413
+ }));
414
+
415
+ /** Top-level MIME types that name a whole family. */
416
+ const MIME_FAMILIES = new Map([
417
+ ['image', 'image'], ['video', 'video'], ['audio', 'audio'], ['font', 'font'], ['text', 'text']
418
+ ]);
419
+
420
+ /**
421
+ * @param {string} mime MIME type, possibly with parameters.
422
+ * @returns {{label: string, icons: string[]}|null}
423
+ */
424
+ function fileKind(mime) {
425
+ const essence = String(mime ?? '').split(';')[0].trim().toLowerCase();
426
+ if (!essence) return null;
427
+ const kind = MIME_KINDS.get(essence) ?? MIME_FAMILIES.get(essence.split('/')[0]);
428
+ return kind ? FILE_KINDS[kind] : null;
429
+ }
430
+
431
+ /**
432
+ * A readable name for a file's type: "PDF", "CSV", "Image", "Spreadsheet". An unfamiliar MIME type
433
+ * falls back to the name's extension in capitals ("EXE"); without a MIME type, or with neither,
434
+ * the result is empty — a raw MIME string is never shown.
435
+ * @param {string} mime MIME type, possibly with parameters (`text/csv; charset=utf-8`).
436
+ * @param {string} [name=''] File name, consulted only when the MIME type is unfamiliar.
437
+ * @returns {string}
438
+ */
439
+ export function fileTypeLabel(mime, name = '') {
440
+ if (!String(mime ?? '').trim()) return '';
441
+ const kind = fileKind(mime);
442
+ if (kind) return kind.label;
443
+ const extension = /\.([a-z0-9]{1,5})$/i.exec(String(name ?? ''))?.[1];
444
+ return extension ? extension.toUpperCase() : '';
445
+ }
446
+
447
+ /**
448
+ * The icon name for a MIME type: a type-specific glyph when the active icon set can draw one,
449
+ * `'file'` otherwise. Font Awesome resolves every name it is given; the bundled inline set only
450
+ * those it carries or that `registerIcons()` added, so a missing glyph never shows a placeholder.
451
+ * @param {string} mime MIME type, possibly with parameters.
452
+ * @returns {string}
453
+ */
454
+ export function fileTypeIcon(mime) {
455
+ const candidates = fileKind(mime)?.icons ?? [];
456
+ if (!candidates.length) return 'file';
457
+ if (getIconConfig().provider === 'fontawesome') return candidates[0];
458
+ const available = new Set(iconNames());
459
+ return candidates.find((name) => available.has(name)) ?? 'file';
460
+ }
461
+
328
462
  /** @param {FileItemState} state @returns {string} */
329
463
  function fileStatusLabel(state) {
330
464
  return state.statusLabel === null ? STATUS_LABELS[state.status] : state.statusLabel;
@@ -295,7 +295,7 @@ export class Filter extends Component {
295
295
  actionButton('add', this._value.root.id, 'plus', this._message('filter.addFilter', 'Add filter'), { ref: 'add', disabled: this._blocked() }),
296
296
  this.options.allowGroups ? actionButton('add-group', this._value.root.id, 'folder', this._message('filter.addGroup', 'Add group'), { disabled: this._blocked() }) : null,
297
297
  actionButton('clear', '', 'x', String(this.options.clearLabel ?? this._message('filter.clear', 'Clear')), { disabled: this._blocked() }),
298
- this.options.showApply ? actionButton('apply', '', 'check', String(this.options.applyLabel ?? this._message('filter.apply', 'Apply filters')), { class: 'zx-button--primary', disabled: this._disabled }) : null
298
+ this.options.showApply ? actionButton('apply', '', 'check', String(this.options.applyLabel ?? this._message('filter.apply', 'Apply filters')), { kind: 'primary', disabled: this._disabled }) : null
299
299
  );
300
300
  this.el.replaceChildren(status, tree, actions);
301
301
  this.refs.status = status;
@@ -631,9 +631,13 @@ export class Filter extends Component {
631
631
  }
632
632
  }
633
633
 
634
- /** @param {string} action @param {string} id @param {string} iconName @param {string} label @param {Record<string,any>} [props={}] @returns {HTMLElement} */
634
+ /** @param {string} action @param {string} id @param {string} iconName @param {string} label @param {Record<string,any>} [props={}] `kind` picks the Button kind; the rest are element properties. @returns {HTMLElement} */
635
635
  function actionButton(action, id, iconName, label, props = {}) {
636
- return h('button', { type: 'button', class: ['zx-button', 'zx-button--sm', props.class].filter(Boolean), ...props, dataset: { filterAction: action, nodeId: id } }, icon(iconName, { size: 13 }), h('span', {}, label));
636
+ const { kind, ...rest } = props;
637
+ return h('button', {
638
+ type: 'button', class: 'zx-btn', ...rest,
639
+ dataset: { filterAction: action, nodeId: id, size: 'sm', ...(kind ? { kind } : {}) }
640
+ }, icon(iconName), h('span', {}, label));
637
641
  }
638
642
 
639
643
  /** @param {FilterField} field @param {any} node @param {any} value @param {boolean} multiple @param {boolean} disabled @param {number} [index=0] @param {FilterValueMessages} [messages=BUILT_IN_VALUE_MESSAGES] @returns {HTMLSelectElement|null} */
@@ -26,7 +26,7 @@
26
26
  .zx-filter-panel__label {
27
27
  color: var(--zx-color-text-muted);
28
28
  font-size: var(--zx-text-sm);
29
- font-weight: 650;
29
+ font-weight: var(--zx-weight-medium);
30
30
  }
31
31
 
32
32
  .zx-filter-panel__input {
@@ -17,7 +17,7 @@
17
17
  flex: none;
18
18
  min-inline-size: 0;
19
19
  flex-direction: column;
20
- border-inline-end: 1px solid var(--zx-color-border);
20
+ border-inline-end: 1px solid var(--zx-color-border-subtle);
21
21
  }
22
22
 
23
23
  .zx-finder__list {
@@ -32,9 +32,12 @@
32
32
  display: flex;
33
33
  align-items: center;
34
34
  gap: var(--zx-space-2);
35
- min-block-size: calc(var(--zx-control-height) - 6px);
36
- padding-inline: var(--zx-space-2);
37
- border-radius: var(--zx-radius-md);
35
+ /* A browsing row: a space-1 margin round one line of text, and the smallest radius, so a
36
+ column of them reads as a list rather than a stack of buttons. */
37
+ min-block-size: calc(var(--zx-control-height) - var(--zx-space-1));
38
+ padding: var(--zx-space-1) var(--zx-space-2);
39
+ border-radius: var(--zx-radius-sm);
40
+ line-height: 1.25;
38
41
  cursor: pointer;
39
42
  }
40
43
 
@@ -42,10 +45,12 @@
42
45
  background: var(--zx-color-bg-hover);
43
46
  }
44
47
 
48
+ /* The selected row of each column is the path you are on, not a pick: the neutral current fill,
49
+ the full text colour and a heavier label. */
45
50
  .zx-finder__row[aria-selected="true"] {
46
- background: var(--zx-color-bg-selected);
47
- color: var(--zx-color-accent);
48
- font-weight: 550;
51
+ background: var(--zx-color-bg-current);
52
+ color: var(--zx-color-text);
53
+ font-weight: var(--zx-weight-semibold);
49
54
  }
50
55
 
51
56
  .zx-finder__row:focus-visible {
@@ -53,19 +58,16 @@
53
58
  box-shadow: var(--zx-focus-ring);
54
59
  }
55
60
 
61
+ /* Icons are quieter than labels, folders included; the current row's icon takes the text colour. */
56
62
  .zx-finder__icon {
57
63
  display: inline-grid;
58
64
  place-items: center;
59
65
  flex: none;
60
- color: var(--zx-color-text-muted);
61
- }
62
-
63
- .zx-finder__row[data-branch] .zx-finder__icon {
64
- color: var(--zx-color-warning);
66
+ color: var(--zx-color-text-subtle);
65
67
  }
66
68
 
67
69
  .zx-finder__row[aria-selected="true"] .zx-finder__icon {
68
- color: inherit;
70
+ color: var(--zx-color-text);
69
71
  }
70
72
 
71
73
  .zx-finder__label {
@@ -29,11 +29,12 @@
29
29
  }
30
30
 
31
31
  .zx-form__actions .zx-btn[type="submit"] {
32
- border-color: var(--zx-color-accent);
33
- background: var(--zx-color-accent);
34
- color: var(--zx-color-on-accent);
32
+ border-color: var(--zx-color-accent-solid);
33
+ background: var(--zx-color-accent-solid);
34
+ color: var(--zx-color-on-accent-solid);
35
35
  }
36
36
 
37
37
  .zx-form__actions .zx-btn[type="submit"]:hover {
38
- background: var(--zx-color-accent-hover);
38
+ border-color: var(--zx-color-accent-solid-hover);
39
+ background: var(--zx-color-accent-solid-hover);
39
40
  }
@@ -1,6 +1,7 @@
1
1
  import { Component } from '../../core/component.js';
2
2
  import { h } from '../../core/dom.js';
3
3
  import { icon as createIcon } from '../../core/icons.js';
4
+ import { uid } from '../../core/util.js';
4
5
  import { Fieldset } from '../fieldset/fieldset.js';
5
6
  import { coerceFloat, coerceInteger } from '../field/field.js';
6
7
 
@@ -22,6 +23,9 @@ import { coerceFloat, coerceInteger } from '../field/field.js';
22
23
  * @property {Array<Fieldset|import('../fieldset/fieldset.js').FieldsetOptions>} [fieldsets=[]] Initial fieldsets.
23
24
  * @property {FormAction[]} [actions=[]] Form action descriptors.
24
25
  * @property {boolean} [novalidate=true] Native `novalidate` state.
26
+ * @property {string|null} [idPrefix=null] Prefix of the DOM ids generated for the fields of the
27
+ * fieldsets this form builds, as `<idPrefix>-<key>`. Null generates one per form instance, so a
28
+ * form mounted twice never repeats an id. A fieldset's own `idPrefix` wins.
25
29
  * @property {(event: CustomEvent<{values: Record<string, unknown>}>) => void} [onsubmit] Submit listener.
26
30
  * @property {(event: CustomEvent<{errors: Record<string, string>}>) => void} [oninvalid] Invalid listener.
27
31
  * @property {(event: CustomEvent<{id: string, value: unknown}>) => void} [onchange] Field change listener.
@@ -42,6 +46,7 @@ export class Form extends Component {
42
46
  fieldsets: [],
43
47
  actions: [],
44
48
  novalidate: true,
49
+ idPrefix: null,
45
50
  msg: {
46
51
  'form.required': 'This field is required.',
47
52
  'form.int': 'Enter a whole number.',
@@ -56,6 +61,7 @@ export class Form extends Component {
56
61
  this._ownedFieldsets = new Set();
57
62
  this._origins = new Map();
58
63
  this._ownedNodes = [];
64
+ this._idPrefix = this.options.idPrefix ? String(this.options.idPrefix) : uid('zx-form');
59
65
  this._novalidateAttribute = root.getAttribute('novalidate');
60
66
  root.toggleAttribute('novalidate', Boolean(this.options.novalidate));
61
67
 
@@ -81,13 +87,16 @@ export class Form extends Component {
81
87
  }
82
88
 
83
89
  /**
84
- * Adds an existing fieldset or constructs one from options.
90
+ * Adds an existing fieldset or constructs one from options. A constructed fieldset generates its
91
+ * field ids from the form's `idPrefix` unless its options carry their own.
85
92
  * @param {Fieldset|import('../fieldset/fieldset.js').FieldsetOptions} fieldset Fieldset source.
86
93
  * @returns {Fieldset}
87
94
  */
88
95
  addFieldset(fieldset) {
89
96
  const owned = !(fieldset instanceof Fieldset);
90
- const instance = owned ? new Fieldset(null, fieldset ?? {}) : fieldset;
97
+ const instance = owned
98
+ ? new Fieldset(null, { ...(fieldset ?? {}), idPrefix: fieldset?.idPrefix ?? this._idPrefix })
99
+ : fieldset;
91
100
  if (this._fieldsets.includes(instance)) return instance;
92
101
  if (owned) this._ownedFieldsets.add(instance);
93
102
  else this._origins.set(instance, { parent: instance.el.parentNode, next: instance.el.nextSibling });
@@ -2,5 +2,5 @@
2
2
  the lowercase `.zx-grid` layout helper remains a separate, unchanged primitive. */
3
3
  .zx-table[data-preset="billing-items"] tr.zx-grid-billing__summary > td {
4
4
  background: var(--zx-color-bg-muted);
5
- font-weight: 700;
5
+ font-weight: 650;
6
6
  }
@@ -11,7 +11,7 @@
11
11
  gap: var(--zx-space-2);
12
12
  min-block-size: var(--zx-control-height);
13
13
  padding: var(--zx-space-2) var(--zx-space-4);
14
- font-weight: 650;
14
+ font-weight: var(--zx-weight-semibold);
15
15
  cursor: pointer;
16
16
  user-select: none;
17
17
  }
@@ -1,7 +1,7 @@
1
1
  // @ts-check
2
2
  import { h, safeHref } from '../../core/dom.js';
3
3
  import { icon } from '../../core/icons.js';
4
- import { avatarInitials } from '../avatar/avatar.js';
4
+ import { AVATAR_TONES, avatarInitials, avatarTone } from '../avatar/avatar.js';
5
5
  import { badge } from '../badge/badge.js';
6
6
 
7
7
  /** @typedef {Record<string, any>} KanbanRecord */
@@ -28,6 +28,9 @@ import { badge } from '../badge/badge.js';
28
28
  * @property {string} name Person's name, used as the accessible label.
29
29
  * @property {string|null} src Safe image URL, or null for the initials fallback.
30
30
  * @property {string} initials Fallback initials.
31
+ * @property {import('../avatar/avatar.js').AvatarTone} tone Fallback tint: a descriptor's own
32
+ * `tone` when it names one of `AVATAR_TONES`, otherwise `avatarTone(name)` — the colour the same
33
+ * person has in every Avatar.
31
34
  */
32
35
 
33
36
  /** Semantic intents an indicator may carry, in the order they are documented. */
@@ -132,6 +135,8 @@ export function createKanbanProgress(progress, label, valueText) {
132
135
  /**
133
136
  * Normalizes one or more responsible people. Names, name lists, and descriptor objects all
134
137
  * resolve to the same shape, and an unsafe image URL degrades to initials rather than failing.
138
+ * A descriptor may name its `tone`; otherwise the tone is derived from the name exactly as Avatar
139
+ * derives it.
135
140
  * @param {unknown} value Raw field value.
136
141
  * @returns {KanbanAssignee[]} Normalized people, in the order supplied.
137
142
  */
@@ -148,7 +153,8 @@ export function resolveKanbanAssignees(value) {
148
153
  if (!name) return null;
149
154
  const src = safeHref(source.src ?? source.image ?? null);
150
155
  const initials = source.initials == null ? avatarInitials(name) : String(source.initials);
151
- return { name, src, initials };
156
+ const tone = AVATAR_TONES.includes(source.tone) ? source.tone : avatarTone(name);
157
+ return { name, src, initials, tone };
152
158
  }).filter(Boolean));
153
159
  }
154
160
 
@@ -185,8 +191,10 @@ export function createKanbanAvatars(people, max, groupLabel, overflowLabel) {
185
191
  const split = kanbanAvatarLimit(max, people.length);
186
192
  const shown = people.slice(0, split.shown);
187
193
  const hidden = split.hidden;
194
+ // Each face carries its tone, so the initials sit on the tint the person has in every Avatar.
188
195
  const faces = shown.map((person) => h('span', {
189
- class: 'zx-kanban-view__avatar', ariaHidden: 'true', title: person.name || undefined
196
+ class: 'zx-kanban-view__avatar', ariaHidden: 'true', title: person.name || undefined,
197
+ dataset: { tone: person.tone ?? avatarTone(person.name) }
190
198
  }, person.src
191
199
  ? h('img', { class: 'zx-kanban-view__avatar-image', src: person.src, alt: '', draggable: false })
192
200
  : h('span', { class: 'zx-kanban-view__avatar-initials' }, person.initials)));
@@ -120,8 +120,8 @@
120
120
  }
121
121
 
122
122
  .zx-kanban-view__move-handle[aria-pressed="true"] {
123
- background: var(--zx-color-accent-subtle);
124
- color: var(--zx-color-accent);
123
+ background: var(--zx-color-bg-current);
124
+ color: var(--zx-color-text);
125
125
  cursor: grabbing;
126
126
  }
127
127
 
@@ -348,7 +348,7 @@
348
348
  }
349
349
 
350
350
  .zx-kanban-view .zx-record-card__eyebrow-group[data-card-slot="eyebrow-start"] .zx-record-card__field {
351
- border-radius: var(--zx-radius-full);
351
+ border-radius: var(--zx-radius-sm);
352
352
  background: var(--zx-color-bg-muted);
353
353
  padding: calc(var(--zx-space-1) / 2) var(--zx-space-2);
354
354
  }
@@ -419,6 +419,12 @@
419
419
  .zx-kanban-view .zx-record-card[data-grabbed="true"] {
420
420
  outline: 2px solid var(--zx-color-accent);
421
421
  outline-offset: 2px;
422
+ /* A card picked up from the keyboard is the item being dragged, so it lifts like one. */
423
+ box-shadow: var(--zx-drag-shadow);
424
+ }
425
+
426
+ .zx-kanban-view .zx-record-card[data-grabbed="true"]:focus-visible {
427
+ box-shadow: var(--zx-focus-ring), var(--zx-drag-shadow);
422
428
  }
423
429
 
424
430
  .zx-kanban-view__empty {
@@ -470,10 +476,6 @@
470
476
  }
471
477
  }
472
478
 
473
- [data-zx-density="compact"] .zx-kanban-view {
474
- --zx-kanban-card-padding-inline: var(--zx-space-2);
475
- }
476
-
477
479
  @media (prefers-reduced-motion: no-preference) {
478
480
  .zx-kanban-view__collapse,
479
481
  .zx-kanban-view__move-handle,
@@ -651,7 +653,8 @@
651
653
  border: 1px solid var(--zx-color-border);
652
654
  border-radius: var(--zx-radius-md);
653
655
  background: var(--zx-color-bg-raised);
654
- box-shadow: var(--zx-shadow-2);
656
+ /* The item in flight: lifted above every resting card by the shared drag shadow. */
657
+ box-shadow: var(--zx-drag-shadow);
655
658
  padding: var(--zx-space-1) var(--zx-space-2);
656
659
  color: var(--zx-color-text);
657
660
  font-size: var(--zx-text-xs);
@@ -704,7 +707,6 @@
704
707
  color: var(--zx-color-text-muted);
705
708
  font-size: var(--zx-text-xs);
706
709
  font-variant-numeric: tabular-nums;
707
- letter-spacing: .02em;
708
710
  text-overflow: ellipsis;
709
711
  white-space: nowrap;
710
712
  }
@@ -767,6 +769,11 @@
767
769
  min-inline-size: 0;
768
770
  }
769
771
 
772
+ /*
773
+ * A face is a small Avatar: the initials on the tint of the tone the component derives from the
774
+ * name with Avatar's own hash (`data-tone`), so a person has the same colour on the board as
775
+ * everywhere else. The surface-coloured edge is what separates overlapping faces.
776
+ */
770
777
  .zx-kanban-view__avatar {
771
778
  display: inline-grid;
772
779
  flex: none;
@@ -776,17 +783,27 @@
776
783
  overflow: hidden;
777
784
  border: 1px solid var(--zx-color-bg-surface);
778
785
  border-radius: var(--zx-radius-full);
779
- background: var(--zx-color-bg-muted);
780
- color: var(--zx-color-text-muted);
786
+ background: var(--zx-kanban-avatar-bg, var(--zx-color-bg-muted));
787
+ color: var(--zx-kanban-avatar-fg, var(--zx-color-text-muted));
781
788
  }
782
789
 
790
+ .zx-kanban-view__avatar[data-tone="blue"] { --zx-kanban-avatar-bg: var(--zx-color-tone-blue-bg); --zx-kanban-avatar-fg: var(--zx-color-tone-blue-fg); }
791
+ .zx-kanban-view__avatar[data-tone="teal"] { --zx-kanban-avatar-bg: var(--zx-color-tone-teal-bg); --zx-kanban-avatar-fg: var(--zx-color-tone-teal-fg); }
792
+ .zx-kanban-view__avatar[data-tone="green"] { --zx-kanban-avatar-bg: var(--zx-color-tone-green-bg); --zx-kanban-avatar-fg: var(--zx-color-tone-green-fg); }
793
+ .zx-kanban-view__avatar[data-tone="amber"] { --zx-kanban-avatar-bg: var(--zx-color-tone-amber-bg); --zx-kanban-avatar-fg: var(--zx-color-tone-amber-fg); }
794
+ .zx-kanban-view__avatar[data-tone="orange"] { --zx-kanban-avatar-bg: var(--zx-color-tone-orange-bg); --zx-kanban-avatar-fg: var(--zx-color-tone-orange-fg); }
795
+ .zx-kanban-view__avatar[data-tone="red"] { --zx-kanban-avatar-bg: var(--zx-color-tone-red-bg); --zx-kanban-avatar-fg: var(--zx-color-tone-red-fg); }
796
+ .zx-kanban-view__avatar[data-tone="pink"] { --zx-kanban-avatar-bg: var(--zx-color-tone-pink-bg); --zx-kanban-avatar-fg: var(--zx-color-tone-pink-fg); }
797
+ .zx-kanban-view__avatar[data-tone="violet"] { --zx-kanban-avatar-bg: var(--zx-color-tone-violet-bg); --zx-kanban-avatar-fg: var(--zx-color-tone-violet-fg); }
798
+ .zx-kanban-view__avatar[data-tone="slate"] { --zx-kanban-avatar-bg: var(--zx-color-tone-slate-bg); --zx-kanban-avatar-fg: var(--zx-color-tone-slate-fg); }
799
+
783
800
  .zx-kanban-view__avatar + .zx-kanban-view__avatar {
784
801
  margin-inline-start: calc(var(--zx-space-2) * -1);
785
802
  }
786
803
 
787
804
  .zx-kanban-view__avatar[data-overflow="true"] {
788
- background: var(--zx-color-accent-subtle);
789
- color: var(--zx-color-accent);
805
+ background: var(--zx-color-bg-current);
806
+ color: var(--zx-color-text-muted);
790
807
  }
791
808
 
792
809
  .zx-kanban-view__avatar-image {
@@ -795,13 +812,20 @@
795
812
  object-fit: cover;
796
813
  }
797
814
 
815
+ /* Avatar's initials at the small size: 0.4 of the face, never under 10px, semibold. */
798
816
  .zx-kanban-view__avatar-initials {
799
- font-size: var(--zx-text-xs);
800
- font-weight: 600;
817
+ font-size: max(10px, calc(1.5rem * .4));
818
+ font-weight: var(--zx-weight-semibold);
819
+ letter-spacing: 0;
801
820
  line-height: 1;
802
821
  text-transform: uppercase;
803
822
  }
804
823
 
824
+ /* The overflow chip is a count, not a person: it keeps the text size it can be read at. */
825
+ .zx-kanban-view__avatar[data-overflow="true"] .zx-kanban-view__avatar-initials {
826
+ font-size: var(--zx-text-xs);
827
+ }
828
+
805
829
  /* ── Move handle ──────────────────────────────────────────────────────────────────────────── */
806
830
 
807
831
  /* The card is the pointer drag source, so the handle is keyboard furniture: announced and in tab
@@ -1742,12 +1742,12 @@ export class KanbanView extends RecordView {
1742
1742
  /** @returns {HTMLElement} Built-in history controls. */
1743
1743
  _createHistoryControls() {
1744
1744
  this._undoButton = /** @type {HTMLButtonElement} */ (h('button', {
1745
- class: 'zx-button zx-button--sm', type: 'button', disabled: true,
1746
- dataset: { kanbanHistory: 'undo' }
1745
+ class: 'zx-btn', type: 'button', disabled: true,
1746
+ dataset: { kanbanHistory: 'undo', size: 'sm', kind: 'ghost' }
1747
1747
  }, this._text('undo')));
1748
1748
  this._redoButton = /** @type {HTMLButtonElement} */ (h('button', {
1749
- class: 'zx-button zx-button--sm', type: 'button', disabled: true,
1750
- dataset: { kanbanHistory: 'redo' }
1749
+ class: 'zx-btn', type: 'button', disabled: true,
1750
+ dataset: { kanbanHistory: 'redo', size: 'sm', kind: 'ghost' }
1751
1751
  }, this._text('redo')));
1752
1752
  const group = h('div', { class: 'zx-kanban-view__history', role: 'group' },
1753
1753
  this._undoButton, this._redoButton);