@zeyos/zx 4.3.0 → 4.4.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 (136) hide show
  1. package/CHANGELOG.md +266 -1
  2. package/README.md +7 -3
  3. package/dist/types/components/activity-item/activity-item.d.ts +351 -0
  4. package/dist/types/components/activity-list/activity-list.d.ts +195 -0
  5. package/dist/types/components/app-sidebar/app-sidebar.d.ts +28 -0
  6. package/dist/types/components/button/button.d.ts +8 -0
  7. package/dist/types/components/calendar/calendar-model.d.ts +22 -0
  8. package/dist/types/components/calendar/calendar.d.ts +19 -0
  9. package/dist/types/components/card/card.d.ts +7 -0
  10. package/dist/types/components/card-view/card-view.d.ts +12 -4
  11. package/dist/types/components/card-view/record-card.d.ts +53 -5
  12. package/dist/types/components/code-editor/code-editor.d.ts +160 -0
  13. package/dist/types/components/code-editor/field-adapter.d.ts +5 -0
  14. package/dist/types/components/context-menu/context-menu.d.ts +18 -2
  15. package/dist/types/components/date-picker/date-picker.d.ts +8 -0
  16. package/dist/types/components/date-picker/time-picker.d.ts +7 -0
  17. package/dist/types/components/dialog/dialog.d.ts +29 -0
  18. package/dist/types/components/entity-ref/entity-ref.d.ts +331 -0
  19. package/dist/types/components/file-item/file-item.d.ts +234 -0
  20. package/dist/types/components/file-list/file-list.d.ts +171 -0
  21. package/dist/types/components/filter/filter.d.ts +91 -36
  22. package/dist/types/components/filter-panel/filter-panel.d.ts +449 -0
  23. package/dist/types/components/kanban-view/kanban-card.d.ts +138 -0
  24. package/dist/types/components/kanban-view/kanban-policy.d.ts +268 -0
  25. package/dist/types/components/kanban-view/kanban-view.d.ts +837 -26
  26. package/dist/types/components/launcher/launcher.d.ts +19 -12
  27. package/dist/types/components/list-toolbar/list-toolbar.d.ts +599 -0
  28. package/dist/types/components/menu-button/menu-button.d.ts +4 -37
  29. package/dist/types/components/message/message.d.ts +7 -0
  30. package/dist/types/components/modal/modal.d.ts +48 -0
  31. package/dist/types/components/navigation-bar/navigation-bar.d.ts +105 -21
  32. package/dist/types/components/panel/panel.d.ts +7 -0
  33. package/dist/types/components/search/search.d.ts +7 -0
  34. package/dist/types/components/select/select.d.ts +4 -0
  35. package/dist/types/components/sort-control/sort-control.d.ts +105 -0
  36. package/dist/types/components/stat-tile/stat-tile.d.ts +277 -0
  37. package/dist/types/components/tabbox/tabbox.d.ts +7 -0
  38. package/dist/types/components/table/table.d.ts +144 -0
  39. package/dist/types/components/table-view/table-view.d.ts +11 -5
  40. package/dist/types/components/timebox/timebox.d.ts +7 -0
  41. package/dist/types/components/value-list/value-list.d.ts +8 -0
  42. package/dist/types/components/view/record-view.d.ts +96 -5
  43. package/dist/types/core/component.d.ts +6 -1
  44. package/dist/types/core/icons.d.ts +17 -0
  45. package/dist/types/index.d.ts +13 -1
  46. package/dist/types/internal/app-rail.d.ts +10 -0
  47. package/dist/types/internal/menu-items.d.ts +134 -0
  48. package/dist/zx-zeyos.esm.js +651 -88
  49. package/dist/zx-zeyos.esm.min.js +8 -1
  50. package/dist/zx.css +6266 -3900
  51. package/dist/zx.esm.js +8487 -1805
  52. package/dist/zx.esm.min.js +10 -3
  53. package/dist/zx.global.js +8487 -1805
  54. package/dist/zx.global.min.js +10 -3
  55. package/dist/zx.min.css +1 -1
  56. package/docs/api.json +1811 -247
  57. package/docs/llms.md +345 -40
  58. package/docs/llms.txt +11 -7
  59. package/package.json +1 -2
  60. package/src/components/activity-item/activity-item.css +137 -0
  61. package/src/components/activity-item/activity-item.js +599 -0
  62. package/src/components/activity-list/activity-list.css +64 -0
  63. package/src/components/activity-list/activity-list.js +375 -0
  64. package/src/components/app-rail/app-rail.css +227 -19
  65. package/src/components/app-sidebar/app-sidebar.css +414 -24
  66. package/src/components/app-sidebar/app-sidebar.js +65 -5
  67. package/src/components/button/button.css +8 -0
  68. package/src/components/button/button.js +6 -1
  69. package/src/components/calendar/calendar-model.js +45 -0
  70. package/src/components/calendar/calendar.css +30 -10
  71. package/src/components/calendar/calendar.js +41 -13
  72. package/src/components/card/card.js +12 -1
  73. package/src/components/card-view/card-view.css +212 -9
  74. package/src/components/card-view/card-view.js +32 -16
  75. package/src/components/card-view/record-card.js +98 -21
  76. package/src/components/code-editor/code-editor.css +67 -0
  77. package/src/components/code-editor/code-editor.js +288 -0
  78. package/src/components/code-editor/field-adapter.js +34 -0
  79. package/src/components/context-menu/context-menu.css +80 -3
  80. package/src/components/context-menu/context-menu.js +22 -30
  81. package/src/components/date-picker/date-picker.js +22 -8
  82. package/src/components/date-picker/month-picker.js +3 -3
  83. package/src/components/date-picker/time-picker.js +20 -8
  84. package/src/components/dialog/dialog.js +88 -66
  85. package/src/components/dropdown/dropdown.js +2 -2
  86. package/src/components/entity-ref/entity-ref.css +182 -0
  87. package/src/components/entity-ref/entity-ref.js +496 -0
  88. package/src/components/file-item/file-item.css +88 -0
  89. package/src/components/file-item/file-item.js +371 -0
  90. package/src/components/file-list/file-list.css +51 -0
  91. package/src/components/file-list/file-list.js +320 -0
  92. package/src/components/filter/filter.css +27 -1
  93. package/src/components/filter/filter.js +199 -45
  94. package/src/components/filter-panel/filter-panel.css +104 -0
  95. package/src/components/filter-panel/filter-panel.js +964 -0
  96. package/src/components/kanban-view/kanban-card.js +239 -0
  97. package/src/components/kanban-view/kanban-policy.js +412 -0
  98. package/src/components/kanban-view/kanban-view.css +674 -36
  99. package/src/components/kanban-view/kanban-view.js +1762 -240
  100. package/src/components/launcher/launcher.js +59 -27
  101. package/src/components/list-toolbar/list-toolbar.css +189 -0
  102. package/src/components/list-toolbar/list-toolbar.js +851 -0
  103. package/src/components/menu-button/menu-button.css +80 -3
  104. package/src/components/menu-button/menu-button.js +21 -37
  105. package/src/components/message/message.css +14 -12
  106. package/src/components/message/message.js +13 -2
  107. package/src/components/modal/modal.css +15 -0
  108. package/src/components/modal/modal.js +141 -0
  109. package/src/components/navigation-bar/navigation-bar.css +77 -2
  110. package/src/components/navigation-bar/navigation-bar.js +171 -4
  111. package/src/components/panel/panel.js +13 -2
  112. package/src/components/search/search.js +17 -5
  113. package/src/components/select/field-adapter.js +4 -1
  114. package/src/components/select/select.js +3 -0
  115. package/src/components/sort-control/sort-control.css +17 -0
  116. package/src/components/sort-control/sort-control.js +140 -0
  117. package/src/components/stat-tile/stat-tile.css +213 -0
  118. package/src/components/stat-tile/stat-tile.js +590 -0
  119. package/src/components/tabbox/tabbox.js +12 -1
  120. package/src/components/table/table.css +51 -2
  121. package/src/components/table/table.js +298 -19
  122. package/src/components/table-view/table-view.css +8 -2
  123. package/src/components/table-view/table-view.js +21 -6
  124. package/src/components/tag-picker/tag-picker.css +3 -1
  125. package/src/components/timebox/timebox.js +18 -5
  126. package/src/components/value-list/value-list.js +22 -6
  127. package/src/components/view/record-view.css +5 -0
  128. package/src/components/view/record-view.js +100 -7
  129. package/src/core/component.js +8 -3
  130. package/src/core/icons.js +74 -5
  131. package/src/index.js +33 -3
  132. package/src/internal/app-rail.js +19 -1
  133. package/src/internal/menu-items.js +142 -0
  134. package/styles/print.css +192 -0
  135. package/styles/zx.css +11 -0
  136. package/DESIGN-SYSTEM.md +0 -182
@@ -75,7 +75,9 @@ export class Timebox extends Component {
75
75
  const content = h('div', {
76
76
  class: 'zx-timebox__content',
77
77
  role: 'group',
78
- ariaLabel: this.options.seconds ? 'Duration in hours, minutes, and seconds' : 'Duration in hours and minutes'
78
+ ariaLabel: this.options.seconds
79
+ ? this._message('timebox.durationWithSeconds', 'Duration in hours, minutes, and seconds')
80
+ : this._message('timebox.duration', 'Duration in hours and minutes')
79
81
  });
80
82
  this._content = content;
81
83
 
@@ -84,7 +86,7 @@ export class Timebox extends Component {
84
86
  ref: 'sign',
85
87
  class: 'zx-timebox__sign',
86
88
  type: 'button',
87
- ariaLabel: 'Toggle duration sign',
89
+ ariaLabel: this._message('timebox.toggleSign', 'Toggle duration sign'),
88
90
  ariaPressed: 'false'
89
91
  }, '+'));
90
92
  this.listen(this.refs.sign, 'click', () => {
@@ -95,12 +97,12 @@ export class Timebox extends Component {
95
97
  });
96
98
  }
97
99
 
98
- this._addSegment(content, 'hours', 'Hours', false);
100
+ this._addSegment(content, 'hours', this._message('timebox.hours', 'Hours'), false);
99
101
  content.append(h('span', { class: 'zx-timebox__separator', ariaHidden: 'true' }, ':'));
100
- this._addSegment(content, 'minutes', 'Minutes', true);
102
+ this._addSegment(content, 'minutes', this._message('timebox.minutes', 'Minutes'), true);
101
103
  if (this.options.seconds) {
102
104
  content.append(h('span', { class: 'zx-timebox__separator', ariaHidden: 'true' }, ':'));
103
- this._addSegment(content, 'seconds', 'Seconds', true);
105
+ this._addSegment(content, 'seconds', this._message('timebox.seconds', 'Seconds'), true);
104
106
  }
105
107
  root.append(content);
106
108
  this.set(this.options.value, this.options.unit, { silent: true });
@@ -237,6 +239,17 @@ export class Timebox extends Component {
237
239
  _emitChange() {
238
240
  this.emit('change', { value: this.get(this.options.unit) });
239
241
  }
242
+
243
+ /**
244
+ * Resolves a message through the host translator, falling back to the built-in English text.
245
+ * @param {string} key Message key.
246
+ * @param {string} fallback English default.
247
+ * @returns {string}
248
+ */
249
+ _message(key, fallback) {
250
+ const message = this.msg(key);
251
+ return message === key ? fallback : message;
252
+ }
240
253
  }
241
254
 
242
255
  /** @event Timebox#change @type {CustomEvent<{value: number}>} */
@@ -1,5 +1,6 @@
1
1
  import { Component } from '../../core/component.js';
2
2
  import { h } from '../../core/dom.js';
3
+ import { printf } from '../../core/i18n.js';
3
4
  import { uid } from '../../core/util.js';
4
5
 
5
6
  /**
@@ -49,14 +50,14 @@ export class ValueList extends Component {
49
50
  this._list = h('ul', {
50
51
  class: 'zx-value-list__list',
51
52
  role: 'listbox',
52
- ariaLabel: 'Values',
53
+ ariaLabel: this._message('valueList.values', 'Values'),
53
54
  ariaOrientation: 'horizontal'
54
55
  });
55
56
  this._input = h('input', {
56
57
  class: 'zx-value-list__input',
57
58
  type: 'text',
58
59
  placeholder: String(this.options.placeholder ?? ''),
59
- ariaLabel: 'Add value',
60
+ ariaLabel: this._message('valueList.add', 'Add value'),
60
61
  ariaDescribedby: instructionsId
61
62
  });
62
63
  this._status = h('div', {
@@ -67,7 +68,8 @@ export class ValueList extends Component {
67
68
  this._instructions = h('span', {
68
69
  class: 'zx-value-list__instructions',
69
70
  id: instructionsId
70
- }, 'Press Enter to add. Focus a value and press Ctrl plus Left or Right to reorder.');
71
+ }, this._message('valueList.instructions',
72
+ 'Press Enter to add. Focus a value and press Ctrl plus Left or Right to reorder.'));
71
73
  this._ownedNodes.push(this._list, this._input, this._status, this._instructions);
72
74
  root.append(this._list, this._input, this._status, this._instructions);
73
75
 
@@ -120,13 +122,14 @@ export class ValueList extends Component {
120
122
  const normalized = String(value ?? '').trim();
121
123
  if (!normalized) return false;
122
124
  if (this.options.unique && this._values.includes(normalized)) {
123
- this._showError('This value already exists.');
125
+ this._showError(this._message('valueList.duplicate', 'This value already exists.'));
124
126
  return false;
125
127
  }
126
128
  if (typeof this.options.validate === 'function') {
127
129
  const result = this.options.validate(normalized);
128
130
  if (result !== true) {
129
- this._showError(typeof result === 'string' ? result : 'This value is not valid.');
131
+ this._showError(typeof result === 'string' ? result
132
+ : this._message('valueList.invalid', 'This value is not valid.'));
130
133
  return false;
131
134
  }
132
135
  }
@@ -196,7 +199,8 @@ export class ValueList extends Component {
196
199
  draggable: Boolean(this.options.sortable) && !this._disabled,
197
200
  ariaSelected: 'false',
198
201
  ariaDisabled: String(this._disabled),
199
- ariaLabel: this.options.deletable ? `${value}. Press Delete to remove.` : value,
202
+ ariaLabel: this.options.deletable
203
+ ? this._message('valueList.removable', '%1. Press Delete to remove.', value) : value,
200
204
  dataset: { index }
201
205
  }, h('span', { class: 'zx-value-list__value' }, value), this.options.deletable ? h('span', {
202
206
  class: 'zx-value-list__remove',
@@ -302,6 +306,18 @@ export class ValueList extends Component {
302
306
  this._input.setAttribute('aria-invalid', 'true');
303
307
  }
304
308
 
309
+ /**
310
+ * Resolves a message through the host translator, falling back to the built-in English text.
311
+ * @param {string} key Message key.
312
+ * @param {string} fallback Built-in text, with `%1`-style placeholders.
313
+ * @param {...unknown} args Interpolation values.
314
+ * @returns {string}
315
+ */
316
+ _message(key, fallback, ...args) {
317
+ const message = this.msg(key, ...args);
318
+ return message === key ? printf(fallback, args) : message;
319
+ }
320
+
305
321
  /** @returns {void} */
306
322
  destroy() {
307
323
  for (const node of this._ownedNodes ?? []) node.remove();
@@ -17,6 +17,11 @@
17
17
  margin-inline-start: auto;
18
18
  }
19
19
 
20
+ /* Mounted into a host toolbar the view does not own: the host decides where it sits. */
21
+ .zx-record-view__field-controls[data-mounted="external"] {
22
+ margin-inline-start: 0;
23
+ }
24
+
20
25
  .zx-record-view__field-controls > summary {
21
26
  display: inline-flex;
22
27
  align-items: center;
@@ -1,7 +1,8 @@
1
1
  // @ts-check
2
2
  import { Component } from '../../core/component.js';
3
- import { h, restoreTarget, snapshotTarget } from '../../core/dom.js';
3
+ import { h, resolveElement, restoreTarget, snapshotTarget } from '../../core/dom.js';
4
4
  import { icon } from '../../core/icons.js';
5
+ import { printf } from '../../core/i18n.js';
5
6
  import { sortRows } from '../table/sort.js';
6
7
 
7
8
  /** @typedef {Record<string, any>} ViewRecord */
@@ -31,6 +32,23 @@ import { sortRows } from '../table/sort.js';
31
32
  * @property {ViewSortDirection} dir Sort direction.
32
33
  */
33
34
 
35
+ /**
36
+ * Trigger customisation for the shared field chooser. Passing this object in place of `true` keeps
37
+ * the control and its state model exactly as they are; only its wording and mount point change.
38
+ * @typedef {Object} ViewFieldControlsConfig
39
+ * @property {string} [label] Trigger label. Omitted resolves `recordView.fields`, English `Fields`.
40
+ * @property {Element|string|null} [target=null] Element or selector the disclosure is mounted into
41
+ * instead of the view's own toolbar. An unresolvable target falls back to the view.
42
+ */
43
+
44
+ /**
45
+ * @typedef {Object} ViewFieldControlsSettings
46
+ * @property {boolean} enabled Whether a concrete view renders the chooser at all.
47
+ * @property {boolean} custom Whether the option was given as a `ViewFieldControlsConfig` object.
48
+ * @property {string|null} label Configured trigger label, or null to resolve the message.
49
+ * @property {Element|string|null} target Configured mount point, before resolution.
50
+ */
51
+
34
52
  /**
35
53
  * @typedef {Object} RecordViewState
36
54
  * @property {1} version State schema version.
@@ -50,7 +68,10 @@ import { sortRows } from '../table/sort.js';
50
68
  * @property {unknown[]} [selection=[]] Initially selected record ids.
51
69
  * @property {string[]} [fieldOrder=[]] Field ids in preferred order.
52
70
  * @property {string[]} [hiddenFields=[]] Initially hidden field ids.
53
- * @property {boolean} [fieldControls=true] Whether concrete views show the shared field chooser.
71
+ * @property {boolean|ViewFieldControlsConfig} [fieldControls=true] Whether concrete views show the
72
+ * shared field chooser. `true` renders it in the view's own toolbar and `false` omits it; a
73
+ * `ViewFieldControlsConfig` object renders the same control with a translated `label` and, with
74
+ * `target`, mounts it into a host element such as a list toolbar.
54
75
  * @property {string|Node|(() => string|Node)|null} [emptyText=null] Empty-result content.
55
76
  * @property {(event: CustomEvent<Record<string, unknown>>) => void} [onrecordclick]
56
77
  * @property {(event: CustomEvent<Record<string, unknown>>) => void} [onrecorddblclick]
@@ -117,6 +138,7 @@ export class RecordView extends Component {
117
138
  this._recordViewDestroyed = true;
118
139
  const root = this.el;
119
140
  const snapshot = this._recordViewOriginal;
141
+ this._removeViewFieldControls();
120
142
  super.destroy();
121
143
  if (snapshot && root) restoreTarget(root, snapshot);
122
144
  }
@@ -139,6 +161,9 @@ export class RecordView extends Component {
139
161
  this._viewSelectionAnchor = null;
140
162
  this._viewFieldControls = null;
141
163
  this._viewFieldList = null;
164
+ this._viewFieldSettings = normalizeViewFieldControls(this.options.fieldControls);
165
+ // Resolved once, here, so the mount point cannot drift between render and destroy.
166
+ this._viewFieldControlsTarget = resolveElement(this._viewFieldSettings.target);
142
167
  this._viewInitialized = true;
143
168
  if (this._viewSort && this.options.sortMode === 'local') this._sortViewData();
144
169
  this._setInitialSelection(this.options.selection);
@@ -467,13 +492,15 @@ export class RecordView extends Component {
467
492
  /**
468
493
  * Builds the shared show/hide and ordering disclosure for a concrete view toolbar.
469
494
  * @protected
470
- * @param {string} [label='Fields'] Trigger label.
495
+ * @param {string|null} [label=null] Explicit trigger label. Omitted resolves the configured
496
+ * `fieldControls.label` and then the `recordView.fields` message.
471
497
  * @returns {HTMLElement} Disclosure element.
472
498
  */
473
- _createViewFieldControls(label = 'Fields') {
499
+ _createViewFieldControls(label = null) {
500
+ const text = label ?? this._viewFieldControlsLabel();
474
501
  const list = h('div', { class: 'zx-record-view__field-list' });
475
502
  const details = h('details', { class: 'zx-record-view__field-controls' },
476
- h('summary', { class: 'zx-button zx-button--sm' }, icon('fields', { size: 13 }), label),
503
+ h('summary', { class: 'zx-button zx-button--sm' }, icon('fields', { size: 13 }), text),
477
504
  list);
478
505
  this._viewFieldControls = details;
479
506
  this._viewFieldList = list;
@@ -498,6 +525,58 @@ export class RecordView extends Component {
498
525
  return details;
499
526
  }
500
527
 
528
+ /**
529
+ * Builds the shared field disclosure and mounts it where `fieldControls` asks for it: the host
530
+ * element named by `target`, or the view's own toolbar when no target resolves.
531
+ * @protected
532
+ * @param {Element} toolbar The view's own toolbar.
533
+ * @param {string|null} [label=null] Explicit trigger label, for views that own their wording.
534
+ * @returns {HTMLElement} The mounted disclosure.
535
+ */
536
+ _mountViewFieldControls(toolbar, label = null) {
537
+ const controls = this._createViewFieldControls(label);
538
+ const target = this._viewFieldControlsTarget;
539
+ if (target) controls.dataset.mounted = 'external';
540
+ (target ?? toolbar).append(controls);
541
+ return controls;
542
+ }
543
+
544
+ /**
545
+ * Removes the shared field disclosure from wherever it was mounted. An external `target` keeps
546
+ * every node it had before the view was created.
547
+ * @protected
548
+ * @returns {void}
549
+ */
550
+ _removeViewFieldControls() {
551
+ this._viewFieldControls?.remove?.();
552
+ this._viewFieldControls = null;
553
+ this._viewFieldList = null;
554
+ this._viewFieldControlsTarget = null;
555
+ }
556
+
557
+ /**
558
+ * Resolves the field-chooser trigger label: the configured one, else the translated message.
559
+ * @protected
560
+ * @returns {string} Trigger label.
561
+ */
562
+ _viewFieldControlsLabel() {
563
+ const settings = this._viewFieldSettings ?? normalizeViewFieldControls(this.options.fieldControls);
564
+ return settings.label ?? this._message('recordView.fields', 'Fields');
565
+ }
566
+
567
+ /**
568
+ * Resolves a message through the host translator, falling back to the built-in English text.
569
+ * @protected
570
+ * @param {string} key Message key.
571
+ * @param {string} fallback Built-in text, with `%1`-style placeholders.
572
+ * @param {...unknown} args Interpolation values.
573
+ * @returns {string} Translated or built-in text.
574
+ */
575
+ _message(key, fallback, ...args) {
576
+ const message = this.msg(key, ...args);
577
+ return message === key ? printf(fallback, args) : message;
578
+ }
579
+
501
580
  /** Concrete renderers override this hook. @protected @param {string} _reason Refresh reason. @returns {void} */
502
581
  _refreshView(_reason) {}
503
582
 
@@ -567,14 +646,14 @@ export class RecordView extends Component {
567
646
  const up = h('button', {
568
647
  class: 'zx-record-view__field-move',
569
648
  type: 'button',
570
- ariaLabel: `Move ${field.label} up`,
649
+ ariaLabel: this._message('recordView.moveFieldUp', 'Move %1 up', field.label),
571
650
  disabled: index === 0,
572
651
  dataset: { viewFieldAction: 'up', fieldId: field.id }
573
652
  }, icon('chevron-up', { size: 11 }));
574
653
  const down = h('button', {
575
654
  class: 'zx-record-view__field-move',
576
655
  type: 'button',
577
- ariaLabel: `Move ${field.label} down`,
656
+ ariaLabel: this._message('recordView.moveFieldDown', 'Move %1 down', field.label),
578
657
  disabled: index === fields.length - 1,
579
658
  dataset: { viewFieldAction: 'down', fieldId: field.id }
580
659
  }, icon('chevron-down', { size: 11 }));
@@ -593,6 +672,20 @@ export class RecordView extends Component {
593
672
  }
594
673
  }
595
674
 
675
+ /**
676
+ * Reads the `fieldControls` option in its three shapes. `true` and `false` keep their exact
677
+ * meaning; an object additionally carries the trigger label and the host element to mount into.
678
+ * @param {boolean|ViewFieldControlsConfig|null|undefined} value Raw option value.
679
+ * @returns {ViewFieldControlsSettings} Normalized settings.
680
+ */
681
+ export function normalizeViewFieldControls(value) {
682
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
683
+ const label = value.label == null ? null : String(value.label);
684
+ return { enabled: true, custom: true, label, target: value.target ?? null };
685
+ }
686
+ return { enabled: Boolean(value), custom: false, label: null, target: null };
687
+ }
688
+
596
689
  /**
597
690
  * Validates and clones field descriptors.
598
691
  * @param {ViewField[]} fields Fields.
@@ -157,21 +157,26 @@ export class Component extends EventTarget {
157
157
  * Dispatches a component event and a bubbling, composed `zx-*` DOM event.
158
158
  * @param {string} type Lowercase event name.
159
159
  * @param {Record<string, unknown>} [detail={}] Single event detail object.
160
+ * @param {{honorDomCancellation?:boolean}} [options={}] Mirrored DOM-event behavior. Components
161
+ * with a cancelable mutation boundary may opt into treating `preventDefault()` on the bubbling
162
+ * `zx-*` event exactly like cancellation of the component event.
160
163
  * @returns {Event & {detail: Record<string, unknown>}}
161
164
  * @fires Component#event
162
165
  */
163
- emit(type, detail = {}) {
166
+ emit(type, detail = {}, options = {}) {
164
167
  if (detail === null || typeof detail !== 'object' || Array.isArray(detail)) {
165
168
  throw new TypeError('Event detail must be an object');
166
169
  }
167
170
  const componentEvent = createCustomEvent(type, detail, { cancelable: true });
168
171
  this.dispatchEvent(componentEvent);
169
172
  if (this.el) {
170
- this.el.dispatchEvent(createCustomEvent(`zx-${type}`, detail, {
173
+ const domEvent = createCustomEvent(`zx-${type}`, detail, {
171
174
  bubbles: true,
172
175
  composed: true,
173
176
  cancelable: true
174
- }));
177
+ });
178
+ this.el.dispatchEvent(domEvent);
179
+ if (options.honorDomCancellation && domEvent.defaultPrevented) componentEvent.preventDefault();
175
180
  }
176
181
  return componentEvent;
177
182
  }
package/src/core/icons.js CHANGED
@@ -22,6 +22,9 @@ const SVG_NS = 'http://www.w3.org/2000/svg';
22
22
  * @property {string} [style] Font Awesome style for this icon only (`'solid'`, `'duotone'`, …).
23
23
  * @property {string} [family] Font Awesome family for this icon only (`'classic'`, `'sharp'`, …).
24
24
  * @property {boolean} [fixedWidth] Adds Font Awesome's `fa-fw` for column-aligned glyphs.
25
+ * @property {boolean} [strict=false] Throws `RangeError` on a name the bundled glyph set does not
26
+ * carry, instead of returning the placeholder. For call sites that can afford to fail — a test,
27
+ * a build-time check — rather than a screen rendering server data.
25
28
  */
26
29
 
27
30
  /**
@@ -34,6 +37,8 @@ const SVG_NS = 'http://www.w3.org/2000/svg';
34
37
  * @type {Readonly<Record<string, [string, string]>>}
35
38
  */
36
39
  export const icons = Object.freeze({
40
+ 'arrow-up-wide-short': ['0 0 576 512', 'M151.6 42.4C145.5 35.8 137 32 128 32s-17.5 3.8-23.6 10.4l-88 96c-11.9 13-11.1 33.3 2 45.2s33.3 11.1 45.2-2L96 146.3 96 448c0 17.7 14.3 32 32 32s32-14.3 32-32l0-301.7 32.4 35.4c11.9 13 32.2 13.9 45.2 2s13.9-32.2 2-45.2l-88-96zM320 480l32 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-32 0c-17.7 0-32 14.3-32 32s14.3 32 32 32zm0-128l96 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-96 0c-17.7 0-32 14.3-32 32s14.3 32 32 32zm0-128l160 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-160 0c-17.7 0-32 14.3-32 32s14.3 32 32 32zm0-128l224 0c17.7 0 32-14.3 32-32s-14.3-32-32-32L320 32c-17.7 0-32 14.3-32 32s14.3 32 32 32z'],
41
+ 'arrow-down-wide-short': ['0 0 576 512', 'M151.6 469.6C145.5 476.2 137 480 128 480s-17.5-3.8-23.6-10.4l-88-96c-11.9-13-11.1-33.3 2-45.2s33.3-11.1 45.2 2L96 365.7 96 64c0-17.7 14.3-32 32-32s32 14.3 32 32l0 301.7 32.4-35.4c11.9-13 32.2-13.9 45.2-2s13.9 32.2 2 45.2l-88 96zM320 480c-17.7 0-32-14.3-32-32s14.3-32 32-32l32 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-32 0zm0-128c-17.7 0-32-14.3-32-32s14.3-32 32-32l96 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-96 0zm0-128c-17.7 0-32-14.3-32-32s14.3-32 32-32l160 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-160 0zm0-128c-17.7 0-32-14.3-32-32s14.3-32 32-32l224 0c17.7 0 32 14.3 32 32s-14.3 32-32 32L320 96z'],
37
42
  'chevron-down': ['0 0 512 512', 'M233.4 406.6c12.5 12.5 32.8 12.5 45.3 0l192-192c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L256 338.7 86.6 169.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l192 192z'],
38
43
  'chevron-up': ['0 0 512 512', 'M233.4 105.4c12.5-12.5 32.8-12.5 45.3 0l192 192c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L256 173.3 86.6 342.6c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3l192-192z'],
39
44
  'chevron-left': ['0 0 320 512', 'M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l192 192c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L109.3 256 246.6 118.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-192 192z'],
@@ -188,9 +193,17 @@ export function iconNames() {
188
193
  * its renderer itself — `'fa:user'`, `'duotone:user'`, `'kit:zeyos-notes'`, `'builtin:check'`, or
189
194
  * a literal Font Awesome class list such as `'fa-solid fa-user'`.
190
195
  *
196
+ * **A name this cannot resolve never throws.** Icon names routinely arrive from a server, and one
197
+ * unconfigured name must not take down the screen it appears on. An inline glyph the bundled set
198
+ * does not carry renders as the placeholder described in `missingIcon()` and is reported once per
199
+ * distinct name through `console.warn`; pass `{ strict: true }` to get a `RangeError` instead.
200
+ * Only the inline-SVG path can fail this way: Font Awesome resolves names in the browser at render
201
+ * time, so a name under a loaded kit or stylesheet is never "unknown" here and never warns.
202
+ *
191
203
  * @param {string} name Icon name or spec.
192
204
  * @param {IconOptions} [options={}] Display and accessibility options.
193
205
  * @returns {SVGSVGElement|HTMLElement} An `<svg>` for inline glyphs, an `<i>` for Font Awesome.
206
+ * @throws {RangeError} With `{ strict: true }` only, when the bundled set has no such glyph.
194
207
  */
195
208
  export function icon(name, options = {}) {
196
209
  const spec = parseIconSpec(name);
@@ -218,18 +231,37 @@ export function icon(name, options = {}) {
218
231
  }
219
232
 
220
233
  /**
221
- * Renders one of the bundled inline SVG glyphs.
234
+ * Renders one of the bundled inline SVG glyphs, or the placeholder when there is no such glyph.
222
235
  * @param {string} name Resolved glyph name.
223
236
  * @param {IconOptions} options Display and accessibility options.
224
237
  * @returns {SVGSVGElement}
238
+ * @throws {RangeError} With `options.strict`, when no glyph carries that name.
225
239
  */
226
240
  function builtinIcon(name, options) {
227
241
  const entry = extraGlyphs[name] ?? icons[name];
228
- if (!entry) throw new RangeError(`Unknown icon: ${name}`);
242
+ if (!entry) {
243
+ if (options.strict) throw new RangeError(`Unknown icon: ${name}`);
244
+ warnMissingIcon(name);
245
+ return missingIcon(name, options);
246
+ }
229
247
  const [viewBox, pathData] = entry;
248
+ const svg = iconSvg(viewBox, options);
249
+ const path = document.createElementNS(SVG_NS, 'path');
250
+ path.setAttribute('d', pathData);
251
+ svg.append(path);
252
+ return svg;
253
+ }
254
+
255
+ /**
256
+ * Builds the `<svg>` root every inline glyph shares, so the placeholder is the same element in
257
+ * the same box as the icon it stands in for.
258
+ * @param {string} viewBox The glyph's view box.
259
+ * @param {IconOptions} options Display and accessibility options.
260
+ * @returns {SVGSVGElement}
261
+ */
262
+ function iconSvg(viewBox, options) {
230
263
  const { size = 16 } = options;
231
264
  const svg = document.createElementNS(SVG_NS, 'svg');
232
- const path = document.createElementNS(SVG_NS, 'path');
233
265
  svg.setAttribute('class', classList('zx-icon', options.class));
234
266
  svg.setAttribute('viewBox', viewBox);
235
267
  svg.setAttribute('width', String(size));
@@ -237,11 +269,48 @@ function builtinIcon(name, options) {
237
269
  svg.setAttribute('fill', 'currentColor');
238
270
  svg.setAttribute('focusable', 'false');
239
271
  applyLabel(svg, options.label ?? null);
240
- path.setAttribute('d', pathData);
241
- svg.append(path);
242
272
  return svg;
243
273
  }
244
274
 
275
+ /** View box the placeholder reserves — the square most Font Awesome glyphs are drawn in. */
276
+ const PLACEHOLDER_VIEW_BOX = '0 0 512 512';
277
+
278
+ /**
279
+ * The element an unresolvable name renders as: the ordinary `zx-icon` root at the requested size,
280
+ * carrying no path.
281
+ *
282
+ * Empty rather than a fallback glyph on purpose. The box is what layout needs — a row of icons
283
+ * keeps its alignment and nothing reflows — while a question mark or an outlined square would
284
+ * read as an icon somebody chose, and would be the wrong one wherever colour or shape carries
285
+ * meaning. The gap is addressed to the developer instead, through the `console.warn` and through
286
+ * `data-zx-icon-missing`, which names it in devtools and lets a test or a page audit find every
287
+ * one of them with a single selector.
288
+ * @param {string} name Resolved glyph name that could not be found.
289
+ * @param {IconOptions} options Display and accessibility options.
290
+ * @returns {SVGSVGElement}
291
+ */
292
+ function missingIcon(name, options) {
293
+ const svg = iconSvg(PLACEHOLDER_VIEW_BOX, options);
294
+ svg.setAttribute('data-zx-icon-missing', name);
295
+ return svg;
296
+ }
297
+
298
+ /** @type {Set<string>} Names already reported, so each gap is warned about exactly once. */
299
+ const warnedMissing = new Set();
300
+
301
+ /**
302
+ * Reports a missing glyph once per distinct name. A list of four hundred rows built from the same
303
+ * unconfigured name is one gap to fix, so it is one line in the console, not four hundred.
304
+ * @param {string} name Resolved glyph name that could not be found.
305
+ * @returns {void}
306
+ */
307
+ function warnMissingIcon(name) {
308
+ if (warnedMissing.has(name)) return;
309
+ warnedMissing.add(name);
310
+ console.warn(`[zx.icon] Unknown icon "${name}" — rendered an empty placeholder. `
311
+ + 'Add it with registerIcons(), or render Font Awesome with useFontAwesome().');
312
+ }
313
+
245
314
  /**
246
315
  * Renders a Font Awesome icon element. The glyph itself comes from the kit or stylesheet the
247
316
  * application loaded; Zx only writes the classes.
package/src/index.js CHANGED
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { registerChecklistFieldAdapter } from './components/checklist/field-adapter.js';
7
+ import { registerCodeEditorFieldAdapter } from './components/code-editor/field-adapter.js';
7
8
  import { registerDatePickerFieldAdapters } from './components/date-picker/field-adapter.js';
8
9
  import { registerDateRangeFieldAdapter } from './components/date-picker/range-field-adapter.js';
9
10
  import { registerDateboxFieldAdapters } from './components/datebox/field-adapter.js';
@@ -25,6 +26,7 @@ import { registerValueListFieldAdapter } from './components/value-list/field-ada
25
26
  export function registerFieldAdapters() {
26
27
  registerSelectFieldAdapter();
27
28
  registerChecklistFieldAdapter();
29
+ registerCodeEditorFieldAdapter();
28
30
  registerDateboxFieldAdapters();
29
31
  registerDatePickerFieldAdapters();
30
32
  registerDateRangeFieldAdapter();
@@ -85,10 +87,12 @@ export { stack, grid, aspect } from './components/layout/layout.js';
85
87
  export { truncate, isTruncated } from './components/truncate/truncate.js';
86
88
  export { copyButton } from './components/copy/copy-button.js';
87
89
  export { CopyInput } from './components/copy/copy-input.js';
90
+ export { CodeEditor, indentSelection, editorPosition } from './components/code-editor/code-editor.js';
88
91
  export { Toolbar } from './components/toolbar/toolbar.js';
89
92
  export { Toggle } from './components/toggle/toggle.js';
90
93
  export { Groupbox } from './components/groupbox/groupbox.js';
91
94
  export { Card } from './components/card/card.js';
95
+ export { StatTile, statTile } from './components/stat-tile/stat-tile.js';
92
96
  export { Aurora } from './components/aurora/aurora.js';
93
97
  export { Panel } from './components/panel/panel.js';
94
98
  export { MasterPanel } from './components/master-panel/master-panel.js';
@@ -105,6 +109,18 @@ export { Search } from './components/search/search.js';
105
109
  export { AppIcon, appIcon } from './components/app-icon/app-icon.js';
106
110
  export { Launcher, rankLauncherItems } from './components/launcher/launcher.js';
107
111
  export { Avatar, avatarInitials } from './components/avatar/avatar.js';
112
+ export {
113
+ EntityRef, normalizeEntityRef, normalizeEntityRefLink,
114
+ normalizeEntityRefMetadata, normalizeEntityRefActions
115
+ } from './components/entity-ref/entity-ref.js';
116
+ export { FileItem, normalizeFileItem } from './components/file-item/file-item.js';
117
+ export { FileList, normalizeFileListItems } from './components/file-list/file-list.js';
118
+ export {
119
+ ActivityItem, resolveActivityActions, normalizeActivityAction, normalizeActivityTimestamp
120
+ } from './components/activity-item/activity-item.js';
121
+ export {
122
+ ActivityList, groupActivityItems, normalizeActivityItems
123
+ } from './components/activity-list/activity-list.js';
108
124
  export { AccountMenu } from './components/account-menu/account-menu.js';
109
125
  export { normalizeAppItems } from './internal/app-rail.js';
110
126
  export { AppSidebar } from './components/app-sidebar/app-sidebar.js';
@@ -136,9 +152,9 @@ export {
136
152
  export { Timebox, splitTime, joinTime } from './components/timebox/timebox.js';
137
153
  export { Calendar } from './components/calendar/calendar.js';
138
154
  export {
139
- CALENDAR_VIEWS, addCalendarDays, calendarDayDifference, calendarDayKey,
140
- calendarEventIntersects, calendarEventSpansDays, calendarRange, cloneCalendarEvent,
141
- layoutCalendarSpans, layoutTimedCalendarEvents, normalizeCalendarEvents,
155
+ CALENDAR_VIEWS, addCalendarDays, addCalendarMonths, calendarDayDifference, calendarDayKey,
156
+ calendarEventIntersects, calendarEventSpansDays, calendarPageDate, calendarRange,
157
+ cloneCalendarEvent, layoutCalendarSpans, layoutTimedCalendarEvents, normalizeCalendarEvents,
142
158
  shiftCalendarEvent, startOfCalendarDay, startOfCalendarWeek
143
159
  } from './components/calendar/calendar-model.js';
144
160
  export {
@@ -149,6 +165,15 @@ export { Table, reorderTableRows } from './components/table/table.js';
149
165
  export { TableView } from './components/table-view/table-view.js';
150
166
  export { CardView } from './components/card-view/card-view.js';
151
167
  export { KanbanView } from './components/kanban-view/kanban-view.js';
168
+ export {
169
+ absoluteKanbanIndex, allowsKanbanTransition, createKanbanHistory, evaluateKanbanMove,
170
+ kanbanSearchTerms, locateKanbanRecord, matchesKanbanSearch, normalizeKanbanRules,
171
+ reorderKanbanRecords, resolveKanbanRules
172
+ } from './components/kanban-view/kanban-policy.js';
173
+ export {
174
+ createKanbanAvatars, createKanbanCardHead, createKanbanIndicator, createKanbanProgress,
175
+ kanbanAvatarLimit, resolveKanbanAssignees, resolveKanbanIndicator, resolveKanbanProgress
176
+ } from './components/kanban-view/kanban-card.js';
152
177
  export { Grid, billingColumns, isBillingLine } from './components/grid/grid.js';
153
178
  export {
154
179
  Chart, ChartJsAdapter, chartSummaryTable, cloneChartValue, isEmptyChartData
@@ -156,6 +181,11 @@ export {
156
181
  export { TreeView } from './components/tree/tree.js';
157
182
  export { Finder } from './components/finder/finder.js';
158
183
  export { DataFilter } from './components/data-filter/data-filter.js';
184
+ export {
185
+ FilterPanel, isActiveValue, pruneValue, serializeFilterValue
186
+ } from './components/filter-panel/filter-panel.js';
187
+ export { SortControl } from './components/sort-control/sort-control.js';
188
+ export { ListToolbar } from './components/list-toolbar/list-toolbar.js';
159
189
  export { Filter } from './components/filter/filter.js';
160
190
  export {
161
191
  filterOperators, emptyFilterAst, filterCondition, filterGroup,
@@ -1,4 +1,5 @@
1
1
  import { Component } from '../core/component.js';
2
+ import { printf } from '../core/i18n.js';
2
3
  import { h, safeHref } from '../core/dom.js';
3
4
  import { icon } from '../core/icons.js';
4
5
  import { Dropdown } from '../components/dropdown/dropdown.js';
@@ -292,8 +293,12 @@ export class AppRailPresenter extends Component {
292
293
  });
293
294
  const actualPanel = dropdown.getPanel();
294
295
  actualPanel.classList.add('zx-app-rail__popover');
296
+ // The panel is portaled out of the rail, so its stylesheet cannot ask which edge the rail
297
+ // occupies. Presentation only: it carries the active marker to the edge facing the screen
298
+ // on a right-hand rail, exactly as the expanded tree does.
299
+ actualPanel.dataset.side = this.el.dataset.side;
295
300
  actualPanel.setAttribute('role', 'region');
296
- actualPanel.setAttribute('aria-label', `${item.label} sub-navigation`);
301
+ actualPanel.setAttribute('aria-label', this._message('appRail.subNavigation', '%1 sub-navigation', item.label));
297
302
  const flyout = { key, parentKey, item, trigger, panel: actualPanel, dropdown };
298
303
  this._flyouts.set(key, flyout);
299
304
  dropdown.on('open', () => {
@@ -573,6 +578,19 @@ export class AppRailPresenter extends Component {
573
578
  this._interaction = null;
574
579
  this._flyouts = new Map();
575
580
  }
581
+ /**
582
+ * Resolves one message through `msg()`, which falls back to the host translator. Copied from
583
+ * `Pagination` rather than invented: the rail's flyout label was the last user-visible string
584
+ * in the navigation stack with no way to change it.
585
+ * @param {string} key Message key.
586
+ * @param {string} fallback English default.
587
+ * @param {...unknown} args Interpolation arguments.
588
+ * @returns {string}
589
+ */
590
+ _message(key, fallback, ...args) {
591
+ const message = this.msg(key, ...args);
592
+ return message === key ? printf(fallback, args) : message;
593
+ }
576
594
  }
577
595
 
578
596
  /** Owns listeners attached to one dynamic rail render so `setItems()` can release them immediately. */