@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.
- package/CHANGELOG.md +266 -1
- package/README.md +7 -3
- package/dist/types/components/activity-item/activity-item.d.ts +351 -0
- package/dist/types/components/activity-list/activity-list.d.ts +195 -0
- package/dist/types/components/app-sidebar/app-sidebar.d.ts +28 -0
- package/dist/types/components/button/button.d.ts +8 -0
- package/dist/types/components/calendar/calendar-model.d.ts +22 -0
- package/dist/types/components/calendar/calendar.d.ts +19 -0
- package/dist/types/components/card/card.d.ts +7 -0
- package/dist/types/components/card-view/card-view.d.ts +12 -4
- package/dist/types/components/card-view/record-card.d.ts +53 -5
- package/dist/types/components/code-editor/code-editor.d.ts +160 -0
- package/dist/types/components/code-editor/field-adapter.d.ts +5 -0
- package/dist/types/components/context-menu/context-menu.d.ts +18 -2
- package/dist/types/components/date-picker/date-picker.d.ts +8 -0
- package/dist/types/components/date-picker/time-picker.d.ts +7 -0
- package/dist/types/components/dialog/dialog.d.ts +29 -0
- package/dist/types/components/entity-ref/entity-ref.d.ts +331 -0
- package/dist/types/components/file-item/file-item.d.ts +234 -0
- package/dist/types/components/file-list/file-list.d.ts +171 -0
- package/dist/types/components/filter/filter.d.ts +91 -36
- package/dist/types/components/filter-panel/filter-panel.d.ts +449 -0
- package/dist/types/components/kanban-view/kanban-card.d.ts +138 -0
- package/dist/types/components/kanban-view/kanban-policy.d.ts +268 -0
- package/dist/types/components/kanban-view/kanban-view.d.ts +837 -26
- package/dist/types/components/launcher/launcher.d.ts +19 -12
- package/dist/types/components/list-toolbar/list-toolbar.d.ts +599 -0
- package/dist/types/components/menu-button/menu-button.d.ts +4 -37
- package/dist/types/components/message/message.d.ts +7 -0
- package/dist/types/components/modal/modal.d.ts +48 -0
- package/dist/types/components/navigation-bar/navigation-bar.d.ts +105 -21
- package/dist/types/components/panel/panel.d.ts +7 -0
- package/dist/types/components/search/search.d.ts +7 -0
- package/dist/types/components/select/select.d.ts +4 -0
- package/dist/types/components/sort-control/sort-control.d.ts +105 -0
- package/dist/types/components/stat-tile/stat-tile.d.ts +277 -0
- package/dist/types/components/tabbox/tabbox.d.ts +7 -0
- package/dist/types/components/table/table.d.ts +144 -0
- package/dist/types/components/table-view/table-view.d.ts +11 -5
- package/dist/types/components/timebox/timebox.d.ts +7 -0
- package/dist/types/components/value-list/value-list.d.ts +8 -0
- package/dist/types/components/view/record-view.d.ts +96 -5
- package/dist/types/core/component.d.ts +6 -1
- package/dist/types/core/icons.d.ts +17 -0
- package/dist/types/index.d.ts +13 -1
- package/dist/types/internal/app-rail.d.ts +10 -0
- package/dist/types/internal/menu-items.d.ts +134 -0
- package/dist/zx-zeyos.esm.js +651 -88
- package/dist/zx-zeyos.esm.min.js +8 -1
- package/dist/zx.css +6266 -3900
- package/dist/zx.esm.js +8487 -1805
- package/dist/zx.esm.min.js +10 -3
- package/dist/zx.global.js +8487 -1805
- package/dist/zx.global.min.js +10 -3
- package/dist/zx.min.css +1 -1
- package/docs/api.json +1811 -247
- package/docs/llms.md +345 -40
- package/docs/llms.txt +11 -7
- package/package.json +1 -2
- package/src/components/activity-item/activity-item.css +137 -0
- package/src/components/activity-item/activity-item.js +599 -0
- package/src/components/activity-list/activity-list.css +64 -0
- package/src/components/activity-list/activity-list.js +375 -0
- package/src/components/app-rail/app-rail.css +227 -19
- package/src/components/app-sidebar/app-sidebar.css +414 -24
- package/src/components/app-sidebar/app-sidebar.js +65 -5
- package/src/components/button/button.css +8 -0
- package/src/components/button/button.js +6 -1
- package/src/components/calendar/calendar-model.js +45 -0
- package/src/components/calendar/calendar.css +30 -10
- package/src/components/calendar/calendar.js +41 -13
- package/src/components/card/card.js +12 -1
- package/src/components/card-view/card-view.css +212 -9
- package/src/components/card-view/card-view.js +32 -16
- package/src/components/card-view/record-card.js +98 -21
- package/src/components/code-editor/code-editor.css +67 -0
- package/src/components/code-editor/code-editor.js +288 -0
- package/src/components/code-editor/field-adapter.js +34 -0
- package/src/components/context-menu/context-menu.css +80 -3
- package/src/components/context-menu/context-menu.js +22 -30
- package/src/components/date-picker/date-picker.js +22 -8
- package/src/components/date-picker/month-picker.js +3 -3
- package/src/components/date-picker/time-picker.js +20 -8
- package/src/components/dialog/dialog.js +88 -66
- package/src/components/dropdown/dropdown.js +2 -2
- package/src/components/entity-ref/entity-ref.css +182 -0
- package/src/components/entity-ref/entity-ref.js +496 -0
- package/src/components/file-item/file-item.css +88 -0
- package/src/components/file-item/file-item.js +371 -0
- package/src/components/file-list/file-list.css +51 -0
- package/src/components/file-list/file-list.js +320 -0
- package/src/components/filter/filter.css +27 -1
- package/src/components/filter/filter.js +199 -45
- package/src/components/filter-panel/filter-panel.css +104 -0
- package/src/components/filter-panel/filter-panel.js +964 -0
- package/src/components/kanban-view/kanban-card.js +239 -0
- package/src/components/kanban-view/kanban-policy.js +412 -0
- package/src/components/kanban-view/kanban-view.css +674 -36
- package/src/components/kanban-view/kanban-view.js +1762 -240
- package/src/components/launcher/launcher.js +59 -27
- package/src/components/list-toolbar/list-toolbar.css +189 -0
- package/src/components/list-toolbar/list-toolbar.js +851 -0
- package/src/components/menu-button/menu-button.css +80 -3
- package/src/components/menu-button/menu-button.js +21 -37
- package/src/components/message/message.css +14 -12
- package/src/components/message/message.js +13 -2
- package/src/components/modal/modal.css +15 -0
- package/src/components/modal/modal.js +141 -0
- package/src/components/navigation-bar/navigation-bar.css +77 -2
- package/src/components/navigation-bar/navigation-bar.js +171 -4
- package/src/components/panel/panel.js +13 -2
- package/src/components/search/search.js +17 -5
- package/src/components/select/field-adapter.js +4 -1
- package/src/components/select/select.js +3 -0
- package/src/components/sort-control/sort-control.css +17 -0
- package/src/components/sort-control/sort-control.js +140 -0
- package/src/components/stat-tile/stat-tile.css +213 -0
- package/src/components/stat-tile/stat-tile.js +590 -0
- package/src/components/tabbox/tabbox.js +12 -1
- package/src/components/table/table.css +51 -2
- package/src/components/table/table.js +298 -19
- package/src/components/table-view/table-view.css +8 -2
- package/src/components/table-view/table-view.js +21 -6
- package/src/components/tag-picker/tag-picker.css +3 -1
- package/src/components/timebox/timebox.js +18 -5
- package/src/components/value-list/value-list.js +22 -6
- package/src/components/view/record-view.css +5 -0
- package/src/components/view/record-view.js +100 -7
- package/src/core/component.js +8 -3
- package/src/core/icons.js +74 -5
- package/src/index.js +33 -3
- package/src/internal/app-rail.js +19 -1
- package/src/internal/menu-items.js +142 -0
- package/styles/print.css +192 -0
- package/styles/zx.css +11 -0
- 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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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:
|
|
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:
|
|
149
|
+
column-gap: 0;
|
|
92
150
|
inline-size: 100%;
|
|
93
|
-
min-block-size: var(--zx-
|
|
94
|
-
border
|
|
95
|
-
border-radius: var(--zx-radius
|
|
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-
|
|
98
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
.zx-app-rail__flyout-item
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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 {
|