@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
@@ -0,0 +1,375 @@
1
+ // @ts-check
2
+ import { Component } from '../../core/component.js';
3
+ import { h, restoreTarget, snapshotTarget } from '../../core/dom.js';
4
+ import { uid } from '../../core/util.js';
5
+ import { skeleton, skeletonText } from '../skeleton/skeleton.js';
6
+ import { ActivityItem } from '../activity-item/activity-item.js';
7
+
8
+ /** @typedef {import('../activity-item/activity-item.js').ActivityItemOptions} ActivityItemOptions */
9
+ /** @typedef {string|number|Node|Component|null} ActivityListContent */
10
+ /** @typedef {string|((item:ActivityItemOptions,index:number)=>unknown)|null} ActivityGroupReader */
11
+ /** @typedef {(group:unknown,items:ActivityItemOptions[])=>ActivityListContent} ActivityGroupLabel */
12
+
13
+ /**
14
+ * @typedef {Object} ActivityGroup
15
+ * @property {unknown} id Group identity returned by the configured reader.
16
+ * @property {ActivityItemOptions[]} items Activities in stable source order.
17
+ */
18
+
19
+ /**
20
+ * @typedef {Object} ActivityListOptions
21
+ * @property {ActivityItemOptions[]} [items=[]] Initial activities in host-supplied chronological order.
22
+ * @property {ActivityGroupReader} [groupBy=null] Property or resolver used for stable grouping.
23
+ * @property {ActivityGroupLabel|null} [groupLabel=null] Group-heading renderer.
24
+ * @property {string} [label='Activity'] Accessible collection name.
25
+ * @property {boolean} [loading=false] Whether initial or incremental activity is loading.
26
+ * @property {number} [loadingCount=3] Placeholder rows shown for an empty loading list.
27
+ * @property {string} [loadingText='Loading activity'] Loading status text.
28
+ * @property {ActivityListContent} [emptyText='No activity yet'] Empty-state content.
29
+ * @property {1|2|3|4|5|6} [groupHeadingLevel=2] Semantic group-heading level.
30
+ * @property {1|2|3|4|5|6} [itemHeadingLevel=3] Default item-heading level.
31
+ * @property {(event:CustomEvent<{items:ActivityItemOptions[]}>)=>void} [ondatachange]
32
+ */
33
+
34
+ /**
35
+ * Accessible chronological activity collection with stable grouping, loading/empty states, and
36
+ * incremental update methods. Ordering stays host-controlled so paginated and live server results
37
+ * are never silently re-sorted. Item `zx-action` events bubble through the list naturally.
38
+ *
39
+ * @fires ActivityList#datachange
40
+ * @extends {Component<ActivityListOptions>}
41
+ */
42
+ export class ActivityList extends Component {
43
+ static cssName = 'activity-list';
44
+
45
+ /** @type {Readonly<ActivityListOptions>} */
46
+ static defaults = {
47
+ items: [],
48
+ groupBy: null,
49
+ groupLabel: null,
50
+ label: 'Activity',
51
+ loading: false,
52
+ loadingCount: 3,
53
+ loadingText: 'Loading activity',
54
+ emptyText: 'No activity yet',
55
+ groupHeadingLevel: 2,
56
+ itemHeadingLevel: 3
57
+ };
58
+
59
+ /**
60
+ * Creates or enhances an activity collection.
61
+ * @param {Element|string|null} [target=null] Existing container, selector, or null.
62
+ * @param {ActivityListOptions} [options={}] Collection options.
63
+ */
64
+ constructor(target = null, options = {}) {
65
+ super(target, options);
66
+ }
67
+
68
+ /** @returns {HTMLElement} */
69
+ render() {
70
+ const created = !this.el;
71
+ const root = /** @type {HTMLElement} */ (this.el ?? h('section'));
72
+ this.el = root;
73
+ this._createdRoot = created;
74
+ this._original = created ? null : snapshotTarget(root);
75
+ this._cleaned = false;
76
+ this._items = normalizeActivityItems(this.options.items);
77
+ this._loading = Boolean(this.options.loading);
78
+ this._itemComponents = [];
79
+ this._groupSequence = 0;
80
+ root.setAttribute('aria-label', String(this.options.label || 'Activity'));
81
+ try {
82
+ this._renderList();
83
+ return root;
84
+ } catch (error) {
85
+ this._destroyItems();
86
+ if (!created) restoreTarget(root, this._original);
87
+ throw error;
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Replaces all activities.
93
+ * @param {ActivityItemOptions[]} items Activities in desired chronological order.
94
+ * @param {{silent?:boolean}} [options={}] Event behavior.
95
+ * @returns {this}
96
+ */
97
+ setItems(items, options = {}) {
98
+ this._items = normalizeActivityItems(items);
99
+ this._renderList();
100
+ if (!options.silent) this._emitDataChange();
101
+ return this;
102
+ }
103
+
104
+ /** Returns shallow descriptor copies in current display order. @returns {ActivityItemOptions[]} */
105
+ getItems() {
106
+ return this._items.map(cloneActivityDescriptor);
107
+ }
108
+
109
+ /** Returns a shallow descriptor copy by stable host ID. @param {unknown} id @returns {ActivityItemOptions|null} */
110
+ getItem(id) {
111
+ const item = this._items.find((candidate) => Object.is(candidate.id, id));
112
+ return item ? cloneActivityDescriptor(item) : null;
113
+ }
114
+
115
+ /**
116
+ * Adds activities after the current source order for older-page or forward-stream updates.
117
+ * @param {ActivityItemOptions|ActivityItemOptions[]} items New activities.
118
+ * @param {{silent?:boolean}} [options={}] Event behavior.
119
+ * @returns {this}
120
+ */
121
+ appendItems(items, options = {}) {
122
+ const additions = normalizeActivityItems(Array.isArray(items) ? items : [items]);
123
+ if (additions.length === 0) return this;
124
+ this._items = normalizeActivityItems([...this._items, ...additions]);
125
+ this._renderList();
126
+ if (!options.silent) this._emitDataChange();
127
+ return this;
128
+ }
129
+
130
+ /**
131
+ * Adds activities before the current source order for newest-first live updates.
132
+ * @param {ActivityItemOptions|ActivityItemOptions[]} items New activities.
133
+ * @param {{silent?:boolean}} [options={}] Event behavior.
134
+ * @returns {this}
135
+ */
136
+ prependItems(items, options = {}) {
137
+ const additions = normalizeActivityItems(Array.isArray(items) ? items : [items]);
138
+ if (additions.length === 0) return this;
139
+ this._items = normalizeActivityItems([...additions, ...this._items]);
140
+ this._renderList();
141
+ if (!options.silent) this._emitDataChange();
142
+ return this;
143
+ }
144
+
145
+ /**
146
+ * Patches the first activity whose ID is `Object.is`-equal to the requested ID.
147
+ * @param {unknown} id Stable host identifier.
148
+ * @param {Partial<ActivityItemOptions>} values Replacement fields.
149
+ * @param {{silent?:boolean}} [options={}] Event behavior.
150
+ * @returns {this}
151
+ */
152
+ updateItem(id, values, options = {}) {
153
+ if (!values || typeof values !== 'object' || Array.isArray(values)) {
154
+ throw new TypeError('ActivityList update values must be an object');
155
+ }
156
+ const index = this._items.findIndex((candidate) => Object.is(candidate.id, id));
157
+ if (index < 0) return this;
158
+ const next = this._items.slice();
159
+ next[index] = { ...next[index], ...values };
160
+ this._items = normalizeActivityItems(next);
161
+ this._renderList();
162
+ if (!options.silent) this._emitDataChange();
163
+ return this;
164
+ }
165
+
166
+ /**
167
+ * Removes the first activity whose ID is `Object.is`-equal to the requested ID.
168
+ * @param {unknown} id Stable host identifier.
169
+ * @param {{silent?:boolean}} [options={}] Event behavior.
170
+ * @returns {this}
171
+ */
172
+ removeItem(id, options = {}) {
173
+ const index = this._items.findIndex((candidate) => Object.is(candidate.id, id));
174
+ if (index < 0) return this;
175
+ this._items.splice(index, 1);
176
+ this._renderList();
177
+ if (!options.silent) this._emitDataChange();
178
+ return this;
179
+ }
180
+
181
+ /** Sets initial/incremental loading presentation. @param {boolean} loading @returns {this} */
182
+ setLoading(loading) {
183
+ const next = Boolean(loading);
184
+ if (next === this._loading) return this;
185
+ this._loading = next;
186
+ this._renderList();
187
+ return this;
188
+ }
189
+
190
+ /** Restores an enhanced target exactly, or removes an owned list. @returns {void} */
191
+ destroy() {
192
+ if (this._cleaned) return;
193
+ this._cleaned = true;
194
+ this._destroyItems();
195
+ super.destroy();
196
+ if (!this._createdRoot) restoreTarget(this.el, this._original);
197
+ }
198
+
199
+ /** @private @returns {void} */
200
+ _renderList() {
201
+ this._destroyItems();
202
+ this._groupSequence = 0;
203
+ const root = /** @type {HTMLElement} */ (this.el);
204
+ root.setAttribute('aria-busy', String(this._loading));
205
+ /** @type {HTMLElement[]} */
206
+ const content = [];
207
+ try {
208
+ if (this._items.length > 0) {
209
+ const groupBy = this.options.groupBy ?? null;
210
+ const groups = groupActivityItems(this._items, groupBy);
211
+ const grouped = groupBy !== null;
212
+ for (const group of groups) content.push(this._createGroup(group, grouped));
213
+ } else if (!this._loading) {
214
+ content.push(h('div', {
215
+ class: 'zx-activity-list__empty', role: 'status'
216
+ }, listContent(this.options.emptyText, 'ActivityList emptyText')));
217
+ }
218
+ if (this._loading) content.push(this._createLoading(this._items.length === 0));
219
+ root.replaceChildren(...content);
220
+ } catch (error) {
221
+ this._destroyItems();
222
+ throw error;
223
+ }
224
+ }
225
+
226
+ /** @private @param {ActivityGroup} group @param {boolean} grouped @returns {HTMLElement} */
227
+ _createGroup(group, grouped) {
228
+ const list = h('ol', { class: 'zx-activity-list__items' });
229
+ for (const item of group.items) {
230
+ const component = new ActivityItem(null, {
231
+ ...item,
232
+ headingLevel: item.headingLevel ?? this.options.itemHeadingLevel
233
+ });
234
+ this._itemComponents.push(component);
235
+ list.append(component.toElement());
236
+ }
237
+ if (!grouped) return list;
238
+ this._groupSequence += 1;
239
+ const headingId = uid(`zx-activity-list-group-${this._groupSequence}`);
240
+ const label = this.options.groupLabel
241
+ ? this.options.groupLabel(group.id, group.items.map(cloneActivityDescriptor))
242
+ : String(group.id ?? 'Other');
243
+ list.setAttribute('aria-labelledby', headingId);
244
+ return h('section', { class: 'zx-activity-list__group', ariaLabelledby: headingId },
245
+ h(`h${normalizeHeadingLevel(this.options.groupHeadingLevel, 2)}`, {
246
+ class: 'zx-activity-list__group-heading', id: headingId
247
+ }, listContent(label, 'ActivityList groupLabel')), list);
248
+ }
249
+
250
+ /** @private @param {boolean} placeholders @returns {HTMLElement} */
251
+ _createLoading(placeholders) {
252
+ const region = h('div', {
253
+ class: 'zx-activity-list__loading', role: 'status', ariaLive: 'polite'
254
+ }, h('span', { class: 'zx-activity-list__loading-label' }, String(this.options.loadingText)));
255
+ if (!placeholders) return region;
256
+ const rows = h('div', { class: 'zx-activity-list__skeletons', ariaHidden: 'true' });
257
+ const count = normalizeLoadingCount(this.options.loadingCount);
258
+ for (let index = 0; index < count; index += 1) {
259
+ rows.append(h('div', { class: 'zx-activity-list__skeleton' },
260
+ skeleton({ width: '2.25rem', height: '2.25rem', radius: 'full' }),
261
+ skeletonText({ lines: 2, heading: true, lastLineWidth: index % 2 ? '58%' : '72%' })));
262
+ }
263
+ region.append(rows);
264
+ return region;
265
+ }
266
+
267
+ /** @private @returns {void} */
268
+ _destroyItems() {
269
+ for (const item of this._itemComponents) item.destroy();
270
+ this._itemComponents.length = 0;
271
+ }
272
+
273
+ /** @private @returns {void} */
274
+ _emitDataChange() {
275
+ this.emit('datachange', { items: this.getItems() });
276
+ }
277
+ }
278
+
279
+ /**
280
+ * Groups descriptors in first-seen group order while preserving source order within every group.
281
+ * A null reader returns one ungrouped collection. Neither descriptors nor the input array mutate.
282
+ * @param {ActivityItemOptions[]} items Activity descriptors.
283
+ * @param {ActivityGroupReader} groupBy Group reader.
284
+ * @returns {ActivityGroup[]}
285
+ */
286
+ export function groupActivityItems(items, groupBy = null) {
287
+ const source = normalizeActivityItems(items);
288
+ if (groupBy === null) return source.length ? [{ id: null, items: source }] : [];
289
+ if (typeof groupBy !== 'string' && typeof groupBy !== 'function') {
290
+ throw new TypeError('ActivityList groupBy must be a property, function, or null');
291
+ }
292
+ const groups = new Map();
293
+ source.forEach((item, index) => {
294
+ const id = typeof groupBy === 'function' ? groupBy(item, index) : item[groupBy];
295
+ if (!groups.has(id)) groups.set(id, []);
296
+ groups.get(id).push(item);
297
+ });
298
+ return Array.from(groups, ([id, grouped]) => ({ id, items: grouped }));
299
+ }
300
+
301
+ /**
302
+ * Validates and shallow-clones activity descriptors, including each slot array. Explicit
303
+ * duplicate IDs are rejected while id-less entries remain valid.
304
+ * @param {unknown} value Candidate descriptor array.
305
+ * @returns {ActivityItemOptions[]}
306
+ */
307
+ export function normalizeActivityItems(value) {
308
+ if (!Array.isArray(value)) throw new TypeError('ActivityList items must be an array');
309
+ const ids = new Set();
310
+ return value.map((item) => {
311
+ if (!item || typeof item !== 'object' || Array.isArray(item)) {
312
+ throw new TypeError('ActivityList items must be descriptor objects');
313
+ }
314
+ for (const key of ['metadata', 'attachments', 'actions']) {
315
+ if (item[key] !== undefined && !Array.isArray(item[key])) {
316
+ throw new TypeError(`ActivityList item ${key} must be an array`);
317
+ }
318
+ }
319
+ const descriptor = /** @type {ActivityItemOptions} */ (item);
320
+ if (descriptor.id !== null && descriptor.id !== undefined) {
321
+ if (ids.has(descriptor.id)) throw new RangeError(`Duplicate ActivityList id: ${String(descriptor.id)}`);
322
+ ids.add(descriptor.id);
323
+ }
324
+ return cloneActivityDescriptor(descriptor);
325
+ });
326
+ }
327
+
328
+ /** @param {ActivityItemOptions} item @returns {ActivityItemOptions} */
329
+ function cloneActivityDescriptor(item) {
330
+ const avatar = item.avatar && isPlainObject(item.avatar) ? { ...item.avatar } : item.avatar;
331
+ return {
332
+ ...item,
333
+ avatar,
334
+ metadata: [...(item.metadata ?? [])],
335
+ attachments: [...(item.attachments ?? [])],
336
+ actions: [...(item.actions ?? [])]
337
+ };
338
+ }
339
+
340
+ /** @param {ActivityListContent} value @param {string} label @returns {Node|string} */
341
+ function listContent(value, label) {
342
+ if (value === null) return '';
343
+ if (typeof value === 'string' || typeof value === 'number') return String(value);
344
+ if (value && typeof value === 'object'
345
+ && typeof (/** @type {{nodeType?:unknown}} */ (value)).nodeType === 'number') {
346
+ return /** @type {Node} */ (value);
347
+ }
348
+ if (value instanceof Component) return value.toElement();
349
+ throw new TypeError(`${label} must be text, a Node, a Component, or null`);
350
+ }
351
+
352
+ /** @param {unknown} value @returns {boolean} */
353
+ function isPlainObject(value) {
354
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
355
+ return typeof (/** @type {{nodeType?:unknown}} */ (value)).nodeType !== 'number'
356
+ && !(value instanceof Component);
357
+ }
358
+
359
+ /** @param {unknown} value @param {1|2|3|4|5|6} fallback @returns {1|2|3|4|5|6} */
360
+ function normalizeHeadingLevel(value, fallback) {
361
+ const level = Math.trunc(Number(value));
362
+ return /** @type {1|2|3|4|5|6} */ (level >= 1 && level <= 6 ? level : fallback);
363
+ }
364
+
365
+ /** @param {unknown} value @returns {number} */
366
+ function normalizeLoadingCount(value) {
367
+ const count = Math.trunc(Number(value));
368
+ return Number.isFinite(count) ? Math.max(1, Math.min(12, count)) : 3;
369
+ }
370
+
371
+ /**
372
+ * Activity descriptors changed through a list mutation method.
373
+ * @event ActivityList#datachange
374
+ * @type {CustomEvent<{items:ActivityItemOptions[]}>}
375
+ */
@@ -57,8 +57,7 @@
57
57
  color: var(--zx-color-accent);
58
58
  }
59
59
 
60
- .zx-app-rail__item[aria-disabled="true"],
61
- .zx-app-rail__flyout-item[aria-disabled="true"] {
60
+ .zx-app-rail__item[aria-disabled="true"] {
62
61
  color: var(--zx-color-text-muted);
63
62
  cursor: not-allowed;
64
63
  opacity: .65;
@@ -81,31 +80,240 @@
81
80
  }
82
81
  .zx-app-rail__item-disclosure { position: absolute; inset-inline-end: 1px; inset-block-end: 1px; display: inline-flex; }
83
82
 
84
- .zx-app-rail__popover { min-inline-size: 220px; padding: var(--zx-space-2); }
85
- .zx-app-rail__flyout-title { padding: var(--zx-space-2) var(--zx-space-3); color: var(--zx-color-text-muted); font-size: var(--zx-text-xs); font-weight: 700; }
86
- .zx-app-rail__flyout-list { display: grid; gap: var(--zx-space-1); margin: 0; padding: 0; list-style: none; }
83
+ /*
84
+ * The flyout.
85
+ *
86
+ * A minimized sidebar is the same navigation as an expanded one, so the panel a rail item opens
87
+ * has to read as a slice of the tree rather than as a menu that happens to hang off it: the same
88
+ * icon gutter, the same item height, labels that wrap instead of truncating, and the same active
89
+ * recipe — filled pill, docked marker, heavier label.
90
+ *
91
+ * The panel is portaled to the overlay host, so `.zx-app-sidebar` descendant selectors and the
92
+ * `--zx-app-sidebar-*` scale declared on that root cannot reach it. The scale is re-declared here
93
+ * against the same density-aware tier-2 tokens the sidebar derives it from, so the two surfaces
94
+ * move together when a token moves rather than agreeing on a number frozen twice.
95
+ */
96
+ .zx-app-rail__popover {
97
+ --zx-app-rail-gutter: var(--zx-space-2);
98
+ --zx-app-rail-icon: var(--zx-space-6);
99
+ --zx-app-rail-item-gap: var(--zx-space-3);
100
+ --zx-app-rail-item-height: calc(var(--zx-control-height) + var(--zx-space-2));
101
+ --zx-app-rail-pad-x: var(--zx-space-3);
102
+ --zx-app-rail-row-gap: var(--zx-space-1);
103
+ --zx-app-rail-radius: var(--zx-radius-xl);
104
+ --zx-app-rail-marker: 3px;
105
+ --zx-app-rail-hover: color-mix(in srgb, var(--zx-color-text) 7%, transparent);
106
+ --zx-app-rail-press: color-mix(in srgb, var(--zx-color-text) 14%, transparent);
107
+ min-inline-size: 240px;
108
+ }
109
+
110
+ /* `styles/zx.css` imports this file before `dropdown.css`, so the two properties the flyout has
111
+ to take back from the generic overlay surface — the padding, which is the gutter the marker
112
+ docks into, and the width the panel may grow to, which is the tree's own 280px — have to
113
+ out-specify it instead of merely following it. The viewport clamp the dropdown guarantees is
114
+ carried over rather than dropped. */
115
+ .zx-dropdown.zx-app-rail__popover {
116
+ max-inline-size: min(280px, calc(100vw - (2 * var(--zx-space-3))));
117
+ padding: var(--zx-space-2) var(--zx-app-rail-gutter);
118
+ }
119
+
120
+ /* The branch's own label, naming what the panel is a slice of. It aligns with the row padding
121
+ rather than with the labels under it: a caption for the list, not a row in it. */
122
+ .zx-app-rail__flyout-title {
123
+ padding: var(--zx-space-1) var(--zx-app-rail-pad-x) var(--zx-space-2);
124
+ color: var(--zx-color-text-muted);
125
+ font-size: var(--zx-text-xs);
126
+ font-weight: 700;
127
+ line-height: 1.3;
128
+ }
129
+
130
+ .zx-app-rail__flyout-list {
131
+ display: grid;
132
+ gap: var(--zx-app-rail-row-gap);
133
+ margin: 0;
134
+ padding: 0;
135
+ list-style: none;
136
+ }
137
+
138
+ /* Same bargain the tree makes: a list with no icons at all does not pay for the gutter. */
139
+ .zx-app-rail__flyout-list:not(:has(.zx-app-rail__flyout-item-icon)) {
140
+ --zx-app-rail-icon: 0px;
141
+ --zx-app-rail-item-gap: 0px;
142
+ }
143
+
87
144
  .zx-app-rail__flyout-item {
145
+ position: relative;
88
146
  display: grid;
89
- grid-template-columns: auto minmax(0, 1fr) auto auto;
147
+ grid-template-columns: calc(var(--zx-app-rail-icon) + var(--zx-app-rail-item-gap)) minmax(0, 1fr) auto auto;
90
148
  align-items: center;
91
- gap: var(--zx-space-2);
149
+ column-gap: 0;
92
150
  inline-size: 100%;
93
- min-block-size: var(--zx-control-height);
94
- border-color: transparent;
95
- border-radius: var(--zx-radius-md);
151
+ min-block-size: var(--zx-app-rail-item-height);
152
+ border: 0;
153
+ border-radius: var(--zx-app-rail-radius);
96
154
  background: transparent;
97
- padding-inline: var(--zx-control-pad-x);
98
- color: inherit;
155
+ padding-block: var(--zx-space-1);
156
+ padding-inline: var(--zx-app-rail-pad-x);
157
+ color: var(--zx-color-text);
158
+ font-size: var(--zx-text-md);
159
+ font-weight: 500;
160
+ line-height: 1.35;
99
161
  text-align: start;
100
162
  text-decoration: none;
101
163
  }
102
- .zx-app-rail__flyout-item:hover,
103
- .zx-app-rail__flyout-item:focus-visible,
104
- .zx-app-rail__flyout-item[aria-current="page"] { background: var(--zx-color-overlay-hover); color: var(--zx-color-accent); }
105
- .zx-app-rail__flyout-item-icon { display: inline-flex; grid-column: 1; }
106
- .zx-app-rail__flyout-item-label { grid-column: 2; }
107
- .zx-app-rail__flyout-item-badge { grid-column: 3; color: var(--zx-color-text-muted); font-size: var(--zx-text-xs); }
108
- .zx-app-rail__flyout-item-disclosure { display: inline-flex; grid-column: 4; color: var(--zx-color-text-muted); }
164
+
165
+ @media (pointer: coarse) {
166
+ .zx-app-rail__flyout-item { min-block-size: 40px; }
167
+ }
168
+
169
+ /* The marker, docked to the panel's own leading edge exactly as the tree's is docked to the
170
+ sidebar's: it sits in the panel's gutter, so every row lines up however deep the flyout chain
171
+ runs. On a right-hand rail the panel opens toward the workspace and the marker moves to the
172
+ edge facing the screen, which is the rule `.zx-app-sidebar[data-side="right"]` states. */
173
+ .zx-app-rail__flyout-item::before {
174
+ content: "";
175
+ position: absolute;
176
+ inset-block: 50%;
177
+ inset-inline-start: calc(-1 * var(--zx-app-rail-gutter));
178
+ inline-size: var(--zx-app-rail-marker);
179
+ border-start-end-radius: var(--zx-radius-full);
180
+ border-end-end-radius: var(--zx-radius-full);
181
+ background: var(--zx-color-accent);
182
+ opacity: 0;
183
+ }
184
+
185
+ .zx-app-rail__popover[data-side="right"] .zx-app-rail__flyout-item::before {
186
+ inset-inline: auto calc(-1 * var(--zx-app-rail-gutter));
187
+ border-radius: 0;
188
+ border-start-start-radius: var(--zx-radius-full);
189
+ border-end-start-radius: var(--zx-radius-full);
190
+ }
191
+
192
+ .zx-app-rail__flyout-item[aria-current="page"]::before {
193
+ inset-block: 25%;
194
+ opacity: 1;
195
+ }
196
+
197
+ .zx-app-rail__flyout-item[data-active-descendant="true"][aria-expanded="false"]::before {
198
+ inset-block: 38%;
199
+ opacity: .55;
200
+ }
201
+
202
+ .zx-app-rail__flyout-item-icon {
203
+ grid-column: 1;
204
+ display: flex;
205
+ align-items: center;
206
+ justify-content: center;
207
+ inline-size: var(--zx-app-rail-icon);
208
+ color: var(--zx-color-text-muted);
209
+ }
210
+
211
+ .zx-app-rail__flyout-item-label {
212
+ grid-column: 2;
213
+ min-inline-size: 0;
214
+ hyphens: auto;
215
+ overflow-wrap: break-word;
216
+ }
217
+
218
+ .zx-app-rail__flyout-item-badge {
219
+ grid-column: 3;
220
+ margin-inline-start: var(--zx-space-2);
221
+ color: var(--zx-color-text-muted);
222
+ font-size: var(--zx-text-xs);
223
+ font-variant-numeric: tabular-nums;
224
+ line-height: 1;
225
+ }
226
+
227
+ .zx-app-rail__flyout-item-disclosure {
228
+ grid-column: 4;
229
+ display: inline-flex;
230
+ margin-inline-start: var(--zx-space-1);
231
+ color: var(--zx-color-text-muted);
232
+ }
233
+
234
+ .zx-app-rail__flyout-item:hover { background: var(--zx-app-rail-hover); }
235
+ .zx-app-rail__flyout-item:hover .zx-app-rail__flyout-item-icon,
236
+ .zx-app-rail__flyout-item:hover .zx-app-rail__flyout-item-disclosure { color: var(--zx-color-text); }
237
+ .zx-app-rail__flyout-item:active { background: var(--zx-app-rail-press); }
238
+
239
+ /* An open nested branch holds the hover fill: the chain that leads to the panel in front stays
240
+ readable while the pointer is two panels away. The rail item does the same thing. */
241
+ .zx-app-rail__flyout-item[aria-expanded="true"]:not([aria-current="page"]) {
242
+ background: var(--zx-app-rail-hover);
243
+ }
244
+ .zx-app-rail__flyout-item[aria-expanded="true"]:not([aria-current="page"]) .zx-app-rail__flyout-item-icon,
245
+ .zx-app-rail__flyout-item[aria-expanded="true"]:not([aria-current="page"]) .zx-app-rail__flyout-item-disclosure {
246
+ color: var(--zx-color-text);
247
+ }
248
+
249
+ .zx-app-rail__flyout-item[data-active-descendant="true"][aria-expanded="false"] {
250
+ color: var(--zx-color-accent);
251
+ font-weight: 600;
252
+ }
253
+
254
+ .zx-app-rail__flyout-item[aria-current="page"] {
255
+ background: var(--zx-color-accent);
256
+ color: var(--zx-color-on-accent);
257
+ font-weight: 600;
258
+ }
259
+
260
+ .zx-app-rail__flyout-item[aria-current="page"]:hover {
261
+ background: color-mix(in srgb, var(--zx-color-text) 8%, var(--zx-color-accent));
262
+ }
263
+
264
+ .zx-app-rail__flyout-item[aria-current="page"]:active {
265
+ background: color-mix(in srgb, var(--zx-color-text) 16%, var(--zx-color-accent));
266
+ }
267
+
268
+ /* An accent glow around an accent fill is a ring nobody can see, so the pill draws an inner line
269
+ in its own text colour first — the same repair the tree makes. */
270
+ .zx-app-rail__flyout-item[aria-current="page"]:focus-visible {
271
+ box-shadow: inset 0 0 0 2px var(--zx-color-on-accent), var(--zx-focus-ring);
272
+ }
273
+
274
+ .zx-app-rail__flyout-item[aria-current="page"] .zx-app-rail__flyout-item-icon,
275
+ .zx-app-rail__flyout-item[aria-current="page"] .zx-app-rail__flyout-item-badge,
276
+ .zx-app-rail__flyout-item[aria-current="page"] .zx-app-rail__flyout-item-disclosure {
277
+ color: inherit;
278
+ }
279
+
280
+ /* Unavailable, and still readable: the icon carries the dimming so the label can keep the muted
281
+ text role instead of falling under 3:1 with the rest of the row. */
282
+ .zx-app-rail__flyout-item[aria-disabled="true"] {
283
+ color: var(--zx-color-text-muted);
284
+ cursor: not-allowed;
285
+ }
286
+
287
+ .zx-app-rail__flyout-item[aria-disabled="true"] .zx-app-rail__flyout-item-icon { opacity: .55; }
288
+
289
+ .zx-app-rail__flyout-item[aria-disabled="true"]:hover,
290
+ .zx-app-rail__flyout-item[aria-disabled="true"]:active { background: transparent; }
291
+
292
+ @media (prefers-reduced-motion: no-preference) {
293
+ .zx-app-rail__flyout-item {
294
+ transition: background-color var(--zx-dur-fast) var(--zx-ease),
295
+ color var(--zx-dur-fast) var(--zx-ease),
296
+ box-shadow var(--zx-dur-fast) var(--zx-ease);
297
+ }
298
+
299
+ .zx-app-rail__flyout-item::before {
300
+ transition: opacity var(--zx-dur-base) var(--zx-ease),
301
+ inset-block var(--zx-dur-base) var(--zx-ease);
302
+ }
303
+
304
+ .zx-app-rail__flyout-item:active { transform: scale(.985); }
305
+ }
306
+
307
+ /* Forced colours drop the fill and the marker together; the system's selected pair carries the
308
+ state there instead. */
309
+ @media (forced-colors: active) {
310
+ .zx-app-rail__flyout-item[aria-current="page"] {
311
+ background: Highlight;
312
+ color: HighlightText;
313
+ }
314
+
315
+ .zx-app-rail__flyout-item::before { background: Highlight; }
316
+ }
109
317
 
110
318
  @media (prefers-reduced-transparency: reduce) {
111
319
  .zx-app-rail {