@zeyos/zx 2.2.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/CHANGELOG.md +197 -0
  2. package/DESIGN-SYSTEM.md +136 -0
  3. package/MIGRATION.md +155 -4
  4. package/README.md +51 -14
  5. package/dist/types/compat/map/data.d.ts +3 -2
  6. package/dist/types/components/account-menu/account-menu.d.ts +144 -0
  7. package/dist/types/components/app-rail/app-rail.d.ts +250 -0
  8. package/dist/types/components/app-sidebar/app-sidebar.d.ts +169 -0
  9. package/dist/types/components/avatar/avatar.d.ts +100 -0
  10. package/dist/types/components/chart/chart.d.ts +321 -0
  11. package/dist/types/components/date-picker/month-picker.d.ts +6 -0
  12. package/dist/types/components/dialog/dialog.d.ts +9 -0
  13. package/dist/types/components/dock/dock.d.ts +495 -0
  14. package/dist/types/components/dropdown/dropdown.d.ts +2 -2
  15. package/dist/types/components/field/field.d.ts +14 -0
  16. package/dist/types/components/field-upload/field-upload.d.ts +37 -4
  17. package/dist/types/components/form/form.d.ts +25 -3
  18. package/dist/types/components/grid/grid.d.ts +172 -0
  19. package/dist/types/components/launcher/launcher.d.ts +300 -0
  20. package/dist/types/components/layout/layout.d.ts +4 -4
  21. package/dist/types/components/menu-button/menu-button.d.ts +10 -2
  22. package/dist/types/components/modal/modal.d.ts +25 -0
  23. package/dist/types/components/multi-value-editor/multi-value-editor.d.ts +74 -9
  24. package/dist/types/components/questionnaire/items.d.ts +327 -0
  25. package/dist/types/components/questionnaire/questionnaire.d.ts +449 -0
  26. package/dist/types/components/select/select.d.ts +36 -1
  27. package/dist/types/components/sheet/sheet-stack.d.ts +167 -0
  28. package/dist/types/components/sheet/sheet.d.ts +294 -0
  29. package/dist/types/components/split-view/split-view.d.ts +2 -21
  30. package/dist/types/components/tabbox/tabbox.d.ts +12 -3
  31. package/dist/types/components/table/table.d.ts +219 -12
  32. package/dist/types/components/tooltip/tooltip.d.ts +2 -2
  33. package/dist/types/components/value-list/value-list.d.ts +24 -3
  34. package/dist/types/core/dom.d.ts +114 -0
  35. package/dist/types/core/drag-axis.d.ts +111 -0
  36. package/dist/types/core/position.d.ts +4 -2
  37. package/dist/types/index.d.ts +12 -2
  38. package/dist/zx-compat.esm.js +943 -311
  39. package/dist/zx-compat.esm.min.js +1 -1
  40. package/dist/zx-compat.global.js +7893 -2252
  41. package/dist/zx-compat.global.min.js +3 -3
  42. package/dist/zx-zeyos.esm.js +826 -114
  43. package/dist/zx-zeyos.esm.min.js +1 -1
  44. package/dist/zx.css +1720 -243
  45. package/dist/zx.esm.js +8227 -2599
  46. package/dist/zx.esm.min.js +3 -3
  47. package/dist/zx.global.js +8227 -2599
  48. package/dist/zx.global.min.js +3 -3
  49. package/dist/zx.min.css +1 -1
  50. package/docs/api.json +2597 -255
  51. package/docs/llms.md +475 -37
  52. package/docs/llms.txt +21 -9
  53. package/package.json +4 -2
  54. package/src/compat/globals.js +3 -1
  55. package/src/compat/map/data.js +31 -9
  56. package/src/components/account-menu/account-menu.css +70 -0
  57. package/src/components/account-menu/account-menu.js +208 -0
  58. package/src/components/app-rail/app-rail.css +107 -0
  59. package/src/components/app-rail/app-rail.js +668 -0
  60. package/src/components/app-sidebar/app-sidebar.css +68 -0
  61. package/src/components/app-sidebar/app-sidebar.js +388 -0
  62. package/src/components/avatar/avatar.css +61 -0
  63. package/src/components/avatar/avatar.js +222 -0
  64. package/src/components/button/button.css +0 -1
  65. package/src/components/chart/chart.css +61 -0
  66. package/src/components/chart/chart.js +518 -0
  67. package/src/components/date-picker/date-range-picker.css +7 -0
  68. package/src/components/date-picker/month-picker.js +9 -0
  69. package/src/components/dialog/dialog.js +11 -4
  70. package/src/components/dock/dock.css +228 -0
  71. package/src/components/dock/dock.js +977 -0
  72. package/src/components/dropdown/dropdown.js +1 -1
  73. package/src/components/field/field.js +2 -0
  74. package/src/components/field-upload/field-upload.js +9 -4
  75. package/src/components/fieldset/fieldset.css +7 -0
  76. package/src/components/form/form.css +7 -0
  77. package/src/components/form/form.js +6 -3
  78. package/src/components/grid/grid.css +6 -0
  79. package/src/components/grid/grid.js +213 -0
  80. package/src/components/launcher/launcher.css +100 -0
  81. package/src/components/launcher/launcher.js +557 -0
  82. package/src/components/layout/layout.js +2 -2
  83. package/src/components/menu-button/menu-button.js +27 -2
  84. package/src/components/modal/modal.css +5 -1
  85. package/src/components/modal/modal.js +37 -2
  86. package/src/components/multi-value-editor/multi-value-editor.css +99 -15
  87. package/src/components/multi-value-editor/multi-value-editor.js +237 -19
  88. package/src/components/navigation-bar/navigation-bar.css +7 -0
  89. package/src/components/questionnaire/items.js +370 -0
  90. package/src/components/questionnaire/questionnaire.css +387 -0
  91. package/src/components/questionnaire/questionnaire.js +1033 -0
  92. package/src/components/search/search.css +10 -2
  93. package/src/components/select/select.css +6 -1
  94. package/src/components/select/select.js +99 -11
  95. package/src/components/sheet/sheet-stack.js +304 -0
  96. package/src/components/sheet/sheet.css +351 -0
  97. package/src/components/sheet/sheet.js +551 -0
  98. package/src/components/split-view/split-view.js +76 -161
  99. package/src/components/stepper/stepper.css +12 -2
  100. package/src/components/tabbox/tabbox.css +13 -3
  101. package/src/components/tabbox/tabbox.js +34 -10
  102. package/src/components/table/table.css +43 -0
  103. package/src/components/table/table.js +523 -45
  104. package/src/components/tooltip/tooltip.js +5 -2
  105. package/src/components/value-list/value-list.js +6 -3
  106. package/src/core/dom.js +302 -0
  107. package/src/core/drag-axis.js +203 -0
  108. package/src/core/position.js +68 -22
  109. package/src/index.js +19 -2
  110. package/styles/tokens/dark.css +15 -13
  111. package/styles/tokens/global.css +17 -5
  112. package/styles/tokens/semantic.css +14 -8
  113. package/styles/tokens/themes.css +72 -0
  114. package/styles/zx.css +11 -1
  115. package/dist/types/components/permission/permission.d.ts +0 -93
  116. package/src/components/permission/permission.css +0 -47
  117. package/src/components/permission/permission.js +0 -198
@@ -0,0 +1,68 @@
1
+ .zx-app-sidebar {
2
+ display: flex;
3
+ min-block-size: 0;
4
+ border: 1px solid var(--zx-color-border);
5
+ background: var(--zx-color-bg-raised);
6
+ color: var(--zx-color-text);
7
+ }
8
+
9
+ .zx-app-sidebar__expanded {
10
+ display: grid;
11
+ grid-template-rows: auto minmax(0, 1fr) auto;
12
+ inline-size: min(280px, 100vw);
13
+ min-block-size: 0;
14
+ }
15
+
16
+ .zx-app-sidebar__expanded[hidden],
17
+ .zx-app-sidebar__rail[hidden] { display: none; }
18
+
19
+ .zx-app-sidebar__header {
20
+ display: grid;
21
+ grid-template-columns: minmax(0, 1fr) auto;
22
+ align-items: center;
23
+ gap: var(--zx-space-2);
24
+ min-block-size: calc(var(--zx-control-height) + var(--zx-space-3));
25
+ border-block-end: 1px solid var(--zx-color-border);
26
+ padding: var(--zx-space-2);
27
+ }
28
+
29
+ .zx-app-sidebar__header-content { min-inline-size: 0; }
30
+ .zx-app-sidebar__nav { min-block-size: 0; overflow: auto; padding: var(--zx-space-2); }
31
+ .zx-app-sidebar__list,
32
+ .zx-app-sidebar__sublist { display: grid; gap: var(--zx-space-1); margin: 0; padding: 0; list-style: none; }
33
+ .zx-app-sidebar__sublist { margin-block-start: var(--zx-space-1); }
34
+ .zx-app-sidebar__sublist[hidden] { display: none; }
35
+
36
+ .zx-app-sidebar__item {
37
+ display: grid;
38
+ grid-template-columns: auto minmax(0, 1fr) auto auto;
39
+ align-items: center;
40
+ gap: var(--zx-space-2);
41
+ inline-size: 100%;
42
+ min-block-size: var(--zx-control-height);
43
+ border-color: transparent;
44
+ border-radius: var(--zx-radius-md);
45
+ background: transparent;
46
+ padding-inline: var(--zx-space-3);
47
+ color: inherit;
48
+ text-align: start;
49
+ text-decoration: none;
50
+ }
51
+
52
+ .zx-app-sidebar__row[data-level="2"] > .zx-app-sidebar__item { padding-inline-start: calc(var(--zx-space-3) + var(--zx-space-5)); }
53
+ .zx-app-sidebar__row[data-level="3"] > .zx-app-sidebar__item { padding-inline-start: calc(var(--zx-space-3) + var(--zx-space-7)); }
54
+ .zx-app-sidebar__item:hover,
55
+ .zx-app-sidebar__item:focus-visible,
56
+ .zx-app-sidebar__item[data-active-descendant="true"],
57
+ .zx-app-sidebar__item[aria-current="page"] { background: var(--zx-color-bg-hover); color: var(--zx-color-accent); }
58
+ .zx-app-sidebar__item[aria-disabled="true"] { color: var(--zx-color-text-muted); cursor: not-allowed; opacity: .65; }
59
+ .zx-app-sidebar__item-icon,
60
+ .zx-app-sidebar__item-disclosure { display: inline-flex; }
61
+ .zx-app-sidebar__item-badge { color: var(--zx-color-text-muted); font-size: var(--zx-text-xs); }
62
+
63
+ .zx-app-sidebar__footer {
64
+ border-block-start: 1px solid var(--zx-color-border);
65
+ padding: var(--zx-space-2);
66
+ }
67
+ .zx-app-sidebar__footer[hidden] { display: none; }
68
+ .zx-app-sidebar__rail-header { display: grid; gap: var(--zx-space-1); }
@@ -0,0 +1,388 @@
1
+ import { Component } from '../../core/component.js';
2
+ import { h } from '../../core/dom.js';
3
+ import { icon } from '../../core/icons.js';
4
+ import { uid } from '../../core/util.js';
5
+ import { AppRail, normalizeAppItems } from '../app-rail/app-rail.js';
6
+
7
+ /** @typedef {import('../app-rail/app-rail.js').AppNavItem} AppNavItem */
8
+ /** @typedef {import('../app-rail/app-rail.js').AppNavSelectDetail} AppNavSelectDetail */
9
+ /**
10
+ * @typedef {Object} AppSidebarOptions
11
+ * @property {AppNavItem[]} [items=[]] Application navigation tree.
12
+ * @property {string|number|null} [active=null] Active item id.
13
+ * @property {Array<string|number>} [expanded=[]] Initially expanded branch ids.
14
+ * @property {boolean} [collapsed=false] Whether the minimized AppRail is shown.
15
+ * @property {'left'|'right'} [side='left'] Workspace side.
16
+ * @property {string} [label='Applications'] Navigation accessible label.
17
+ * @property {Node|string|number|{toElement: () => Node|null}|null} [header=null] Expanded header content.
18
+ * @property {Node|string|number|{toElement: () => Node|null}|null} [footer=null] Expanded sticky footer content.
19
+ * @property {Node|string|number|{toElement: () => Node|null}|null} [railHeader=null] Minimized rail header content.
20
+ * @property {Node|string|number|{toElement: () => Node|null}|null} [railFooter=null] Minimized rail footer content.
21
+ * @property {(event: CustomEvent<AppNavSelectDetail>) => void} [onselect] Preventable action listener.
22
+ * @property {(event: CustomEvent<{item: AppNavItem|null, id: string|number|null, expanded: boolean|null, ids: Array<string|number>}>) => void} [onbranchchange] Branch-state listener; bulk replacement reports null state.
23
+ * @property {(event: CustomEvent<{collapsed: boolean}>) => void} [oncollapsechange] Presentation-state listener.
24
+ */
25
+
26
+ /**
27
+ * Expanded vertical application navigation; the only Xenon shell mode with inline descendants.
28
+ * @fires AppSidebar#select
29
+ * @fires AppSidebar#branchchange
30
+ * @fires AppSidebar#collapsechange
31
+ * @extends {Component<AppSidebarOptions>}
32
+ */
33
+ export class AppSidebar extends Component {
34
+ static cssName = 'app-sidebar';
35
+
36
+ /** @type {Readonly<AppSidebarOptions>} */
37
+ static defaults = {
38
+ items: [],
39
+ active: null,
40
+ expanded: [],
41
+ collapsed: false,
42
+ side: 'left',
43
+ label: 'Applications',
44
+ header: null,
45
+ footer: null,
46
+ railHeader: null,
47
+ railFooter: null
48
+ };
49
+
50
+ /**
51
+ * Creates or enhances an application-sidebar host.
52
+ * @param {Element|string|null} target Sidebar target.
53
+ * @param {AppSidebarOptions} [options={}] Sidebar options.
54
+ */
55
+ constructor(target = null, options = {}) {
56
+ super(target, options);
57
+ this.listen(this.refs.rail, 'zx-select', (event) => event.stopPropagation());
58
+ const expandButton = h('button', {
59
+ class: 'zx-app-sidebar__rail-toggle zx-icon-btn',
60
+ type: 'button',
61
+ ariaLabel: 'Expand application sidebar'
62
+ }, icon(this.el.dataset.side === 'right' ? 'chevron-left' : 'chevron-right'));
63
+ this._railExpandButton = expandButton;
64
+ const railHeader = h('div', { class: 'zx-app-sidebar__rail-header' }, expandButton);
65
+ appendSlot(railHeader, this.options.railHeader);
66
+ this._rail = new AppRail(this.refs.rail, {
67
+ items: this._items,
68
+ active: this._active,
69
+ orientation: 'vertical',
70
+ side: this.el.dataset.side,
71
+ label: this.options.label,
72
+ header: railHeader,
73
+ footer: this.options.railFooter,
74
+ onselect: (event) => {
75
+ const selected = this.emit('select', event.detail);
76
+ if (selected.defaultPrevented) event.preventDefault();
77
+ }
78
+ });
79
+ this.listen(expandButton, 'click', () => {
80
+ this.expand();
81
+ });
82
+ this._renderTree();
83
+ this.setCollapsed(Boolean(this.options.collapsed), { silent: true });
84
+ }
85
+
86
+ /** @returns {HTMLElement} */
87
+ render() {
88
+ this._createdRoot = this.el === null;
89
+ const root = /** @type {HTMLElement} */ (this.el ?? h('aside'));
90
+ this.el = root;
91
+ this._original = this._createdRoot ? null : {
92
+ attributes: Array.from(root.attributes, (attribute) => [attribute.name, attribute.value]),
93
+ children: Array.from(root.childNodes)
94
+ };
95
+ this._items = normalizeAppItems(this.options.items);
96
+ this._active = this.options.active;
97
+ this._expanded = new Set(Array.isArray(this.options.expanded) ? this.options.expanded.map(String) : []);
98
+ this._collapsed = Boolean(this.options.collapsed);
99
+ root.dataset.side = this.options.side === 'right' ? 'right' : 'left';
100
+
101
+ const headerContent = h('div', { class: 'zx-app-sidebar__header-content' });
102
+ appendSlot(headerContent, this.options.header);
103
+ const collapseButton = h('button', {
104
+ ref: 'collapse',
105
+ class: 'zx-app-sidebar__collapse zx-icon-btn',
106
+ type: 'button',
107
+ ariaLabel: 'Minimize application sidebar'
108
+ }, icon(root.dataset.side === 'right' ? 'chevron-right' : 'chevron-left'));
109
+ const footer = h('footer', { class: 'zx-app-sidebar__footer' });
110
+ appendSlot(footer, this.options.footer);
111
+ footer.hidden = !footer.hasChildNodes();
112
+ const expanded = h('div', { ref: 'expanded', class: 'zx-app-sidebar__expanded' },
113
+ h('header', { class: 'zx-app-sidebar__header' }, headerContent, collapseButton),
114
+ h('nav', { class: 'zx-app-sidebar__nav', ariaLabel: this.options.label },
115
+ h('ul', { ref: 'list', class: 'zx-app-sidebar__list' })),
116
+ footer);
117
+ const rail = h('div', { ref: 'rail', class: 'zx-app-sidebar__rail' });
118
+ root.replaceChildren(expanded, rail);
119
+ this.listen(collapseButton, 'click', () => {
120
+ this.collapse();
121
+ });
122
+ this.listen(this.refs.list, 'click', (event) => this._onTreeClick(event));
123
+ return root;
124
+ }
125
+
126
+ /** Replaces the navigation tree in both expanded and rail presentations. @param {AppNavItem[]} items @returns {this} */
127
+ setItems(items) {
128
+ this._items = normalizeAppItems(items);
129
+ this._reconcileExpanded();
130
+ this._renderTree();
131
+ this._rail.setItems(this._items);
132
+ return this;
133
+ }
134
+
135
+ /** Sets the active id in both presentations without routing. @param {string|number|null} id @returns {this} */
136
+ setActive(id) {
137
+ this._active = id;
138
+ this._syncActive();
139
+ this._rail.setActive(id);
140
+ return this;
141
+ }
142
+
143
+ /** Replaces expanded branch ids. @param {Array<string|number>} ids @param {{silent?: boolean}} [options={}] @returns {this} */
144
+ setExpanded(ids, { silent = false } = {}) {
145
+ this._expanded = new Set(Array.isArray(ids) ? ids.map(String) : []);
146
+ this._reconcileExpanded();
147
+ this._renderTree();
148
+ if (!silent) this.emit('branchchange', {
149
+ item: null,
150
+ id: null,
151
+ expanded: null,
152
+ ids: this.getExpanded()
153
+ });
154
+ return this;
155
+ }
156
+
157
+ /** Returns expanded branch ids in navigation order. @returns {Array<string|number>} */
158
+ getExpanded() {
159
+ const ordered = [];
160
+ walkItems(this._items, (item) => {
161
+ if (this._expanded.has(String(item.id))) ordered.push(item.id);
162
+ });
163
+ return ordered;
164
+ }
165
+
166
+ /** Toggles one inline branch. @param {string|number} id @param {{silent?: boolean}} [options={}] @returns {this} */
167
+ toggleBranch(id, { silent = false } = {}) {
168
+ const item = findItem(this._items, id);
169
+ if (!item?.children?.length) return this;
170
+ const key = String(id);
171
+ const expanded = !this._expanded.has(key);
172
+ if (expanded) this._expanded.add(key);
173
+ else this._expanded.delete(key);
174
+ this._renderTree();
175
+ if (!silent) this.emit('branchchange', { item, id: item.id, expanded, ids: this.getExpanded() });
176
+ return this;
177
+ }
178
+
179
+ /** Shows the expanded sidebar presentation. @param {{silent?: boolean}} [options={}] @returns {this} */
180
+ expand(options = {}) {
181
+ return this.setCollapsed(false, options);
182
+ }
183
+
184
+ /** Shows the minimized AppRail presentation. @param {{silent?: boolean}} [options={}] @returns {this} */
185
+ collapse(options = {}) {
186
+ return this.setCollapsed(true, options);
187
+ }
188
+
189
+ /** Sets expanded/minimized presentation while preserving branch state. @param {boolean} collapsed @param {{silent?: boolean}} [options={}] @returns {this} */
190
+ setCollapsed(collapsed, { silent = false } = {}) {
191
+ const next = Boolean(collapsed);
192
+ const changed = next !== this._collapsed;
193
+ const active = document.activeElement;
194
+ const focusRailToggle = next && Boolean(active && this.refs.expanded.contains(active));
195
+ const focusSidebarToggle = !next && this._rail?._containsNode(active);
196
+ if (!next) this._rail?.closeAllFlyouts();
197
+ this._collapsed = next;
198
+ this.el.dataset.collapsed = String(next);
199
+ this.refs.expanded.hidden = next;
200
+ this.refs.rail.hidden = !next;
201
+ if (focusRailToggle) this._railExpandButton?.focus();
202
+ else if (focusSidebarToggle) this.refs.collapse.focus();
203
+ if (changed && !silent) this.emit('collapsechange', { collapsed: next });
204
+ return this;
205
+ }
206
+
207
+ /** Reports whether the rail presentation is active. @returns {boolean} */
208
+ isCollapsed() {
209
+ return this._collapsed;
210
+ }
211
+
212
+ /** Returns the composed minimized rail. @returns {AppRail} */
213
+ getRail() {
214
+ return this._rail;
215
+ }
216
+
217
+ /** Destroys the rail and restores/removes the sidebar host. @returns {void} */
218
+ destroy() {
219
+ const original = this._original;
220
+ this._rail.destroy();
221
+ super.destroy();
222
+ if (!this._createdRoot && original) {
223
+ for (const attribute of Array.from(this.el.attributes)) this.el.removeAttribute(attribute.name);
224
+ for (const [name, value] of original.attributes) this.el.setAttribute(name, value);
225
+ this.el.replaceChildren(...original.children);
226
+ }
227
+ }
228
+
229
+ /** @returns {void} */
230
+ _renderTree() {
231
+ const focused = this.refs.list.contains(document.activeElement)
232
+ ? /** @type {Element} */ (document.activeElement).closest('[data-app-nav-id]')?.getAttribute('data-app-nav-id')
233
+ : null;
234
+ this.refs.list.replaceChildren(...this._items.map((item) => this._treeRow(item, 1)));
235
+ this._syncActive();
236
+ if (focused !== null) {
237
+ const replacement = [...this.refs.list.querySelectorAll('[data-app-nav-id]')]
238
+ .find((control) => control.getAttribute('data-app-nav-id') === focused) ?? null;
239
+ visibleTreeControl(replacement)?.focus();
240
+ }
241
+ }
242
+
243
+ /** @param {AppNavItem} item @param {number} level @returns {HTMLElement} */
244
+ _treeRow(item, level) {
245
+ const children = item.children ?? [];
246
+ const hasChildren = children.length > 0;
247
+ const key = String(item.id);
248
+ const expanded = hasChildren && this._expanded.has(key);
249
+ const childId = hasChildren ? uid('zx-app-sidebar-branch') : null;
250
+ const control = navControl(item, hasChildren, expanded, childId);
251
+ const row = h('li', {
252
+ class: 'zx-app-sidebar__row',
253
+ dataset: { level }
254
+ }, control);
255
+ if (hasChildren) {
256
+ row.append(h('ul', {
257
+ class: 'zx-app-sidebar__sublist',
258
+ id: childId,
259
+ hidden: !expanded
260
+ }, children.map((child) => this._treeRow(child, level + 1))));
261
+ }
262
+ return row;
263
+ }
264
+
265
+ /** Handles dynamic tree controls without retaining detached nodes after a rerender. @param {Event} event @returns {void} */
266
+ _onTreeClick(event) {
267
+ const control = /** @type {Element|null} */ (event.target)?.closest?.('[data-app-nav-id]');
268
+ if (!control || !this.refs.list.contains(control)) return;
269
+ const item = findItem(this._items, control.getAttribute('data-app-nav-id'));
270
+ if (!item || item.disabled) {
271
+ event.preventDefault();
272
+ return;
273
+ }
274
+ if (item.children?.length) this.toggleBranch(item.id);
275
+ else this._select(item, event);
276
+ }
277
+
278
+ /** @param {AppNavItem} item @param {Event} event @returns {void} */
279
+ _select(item, event) {
280
+ if (item.disabled) {
281
+ event.preventDefault();
282
+ return;
283
+ }
284
+ const selected = this.emit('select', {
285
+ item,
286
+ id: item.id,
287
+ value: item.value ?? item.id,
288
+ href: item.href ?? null,
289
+ event
290
+ });
291
+ if (selected.defaultPrevented) event.preventDefault();
292
+ else if (!item.href) {
293
+ event.preventDefault();
294
+ item.invoke?.();
295
+ }
296
+ }
297
+
298
+ /** @returns {void} */
299
+ _syncActive() {
300
+ const activeId = this._active == null ? null : String(this._active);
301
+ for (const control of this.refs.list.querySelectorAll('[data-app-nav-id]')) {
302
+ const item = findItem(this._items, control.getAttribute('data-app-nav-id'));
303
+ const direct = activeId !== null && control.getAttribute('data-app-nav-id') === activeId;
304
+ const descendant = activeId !== null && item
305
+ ? containsItemId(item.children ?? [], activeId) : false;
306
+ if (direct) {
307
+ control.setAttribute('aria-current', 'page');
308
+ } else control.removeAttribute('aria-current');
309
+ if (descendant) control.dataset.activeDescendant = 'true';
310
+ else delete control.dataset.activeDescendant;
311
+ }
312
+ }
313
+
314
+ /** @returns {void} */
315
+ _reconcileExpanded() {
316
+ const branchIds = new Set();
317
+ walkItems(this._items, (item) => { if (item.children?.length) branchIds.add(String(item.id)); });
318
+ for (const id of this._expanded) if (!branchIds.has(id)) this._expanded.delete(id);
319
+ }
320
+ }
321
+
322
+ /** @param {AppNavItem} item @param {boolean} disclosure @param {boolean} expanded @param {string|null} controls @returns {HTMLElement} */
323
+ function navControl(item, disclosure, expanded, controls) {
324
+ const tag = item.href && !disclosure ? 'a' : 'button';
325
+ return /** @type {HTMLElement} */ (h(tag, {
326
+ class: 'zx-app-sidebar__item',
327
+ type: tag === 'button' ? 'button' : null,
328
+ href: tag === 'a' ? item.href : null,
329
+ target: tag === 'a' ? item.target ?? null : null,
330
+ ariaDisabled: item.disabled ? 'true' : null,
331
+ ariaExpanded: disclosure ? String(expanded) : null,
332
+ ariaControls: controls,
333
+ dataset: { appNavId: String(item.id) }
334
+ },
335
+ item.icon ? h('span', { class: 'zx-app-sidebar__item-icon', ariaHidden: 'true' }, icon(item.icon)) : null,
336
+ h('span', { class: 'zx-app-sidebar__item-label' }, item.label),
337
+ item.badge != null ? h('span', { class: 'zx-app-sidebar__item-badge' }, String(item.badge)) : null,
338
+ disclosure ? h('span', { class: 'zx-app-sidebar__item-disclosure', ariaHidden: 'true' },
339
+ icon(expanded ? 'chevron-down' : 'chevron-right')) : null));
340
+ }
341
+
342
+ /** Returns a control or the nearest owning disclosure when that control is inside a collapsed branch. @param {Element|null} control @returns {HTMLElement|null} */
343
+ function visibleTreeControl(control) {
344
+ let candidate = /** @type {HTMLElement|null} */ (control);
345
+ while (candidate) {
346
+ const hiddenList = candidate.closest('.zx-app-sidebar__sublist[hidden]');
347
+ if (!hiddenList) return candidate;
348
+ candidate = /** @type {HTMLElement|null} */ (hiddenList.parentElement?.firstElementChild ?? null);
349
+ }
350
+ return null;
351
+ }
352
+
353
+ /** @param {AppNavItem[]} items @param {string|number} id @returns {AppNavItem|null} */
354
+ function findItem(items, id) {
355
+ for (const item of items) {
356
+ if (String(item.id) === String(id)) return item;
357
+ const child = findItem(item.children ?? [], id);
358
+ if (child) return child;
359
+ }
360
+ return null;
361
+ }
362
+
363
+ /** @param {AppNavItem[]} items @param {(item: AppNavItem) => void} visit @returns {void} */
364
+ function walkItems(items, visit) {
365
+ for (const item of items) {
366
+ visit(item);
367
+ walkItems(item.children ?? [], visit);
368
+ }
369
+ }
370
+
371
+ /** @param {AppNavItem[]} items @param {string} id @returns {boolean} */
372
+ function containsItemId(items, id) {
373
+ return items.some((item) => String(item.id) === id || containsItemId(item.children ?? [], id));
374
+ }
375
+
376
+ /** @param {Element} target @param {unknown} content @returns {void} */
377
+ function appendSlot(target, content) {
378
+ if (content == null) return;
379
+ if (typeof content === 'string' || typeof content === 'number') target.append(document.createTextNode(String(content)));
380
+ else if (typeof content === 'object' && typeof content.toElement === 'function') {
381
+ const element = content.toElement();
382
+ if (element) target.append(element);
383
+ } else if (typeof content === 'object' && typeof content.nodeType === 'number') target.append(content);
384
+ }
385
+
386
+ /** Fired when a destination/action is activated. @event AppSidebar#select @type {CustomEvent<AppNavSelectDetail>} */
387
+ /** Fired when an inline branch changes. @event AppSidebar#branchchange @type {CustomEvent<{item: AppNavItem|null, id: string|number|null, expanded: boolean|null, ids: Array<string|number>}>} */
388
+ /** Fired when expanded/minimized presentation changes. @event AppSidebar#collapsechange @type {CustomEvent<{collapsed: boolean}>} */
@@ -0,0 +1,61 @@
1
+ .zx-avatar {
2
+ --zx-avatar-size: var(--zx-control-height);
3
+ position: relative;
4
+ display: inline-grid;
5
+ flex: none;
6
+ place-items: center;
7
+ inline-size: var(--zx-avatar-size);
8
+ block-size: var(--zx-avatar-size);
9
+ overflow: visible;
10
+ color: var(--zx-color-text);
11
+ vertical-align: middle;
12
+ }
13
+
14
+ .zx-avatar[data-size="sm"] { --zx-avatar-size: calc(var(--zx-control-height) * .75); }
15
+ .zx-avatar[data-size="lg"] { --zx-avatar-size: calc(var(--zx-control-height) * 1.25); }
16
+ .zx-avatar[data-shape="circle"] { border-radius: 50%; }
17
+ .zx-avatar[data-shape="rounded"] { border-radius: var(--zx-radius-md); }
18
+ .zx-avatar[data-shape="square"] { border-radius: 0; }
19
+
20
+ .zx-avatar__image,
21
+ .zx-avatar__fallback {
22
+ inline-size: 100%;
23
+ block-size: 100%;
24
+ border-radius: inherit;
25
+ }
26
+
27
+ .zx-avatar__image {
28
+ display: block;
29
+ object-fit: cover;
30
+ }
31
+
32
+ .zx-avatar__image[hidden],
33
+ .zx-avatar__fallback[hidden],
34
+ .zx-avatar__status[hidden] {
35
+ display: none;
36
+ }
37
+
38
+ .zx-avatar__fallback {
39
+ display: grid;
40
+ place-items: center;
41
+ border: 1px solid var(--zx-color-border);
42
+ background: var(--zx-color-bg-subtle);
43
+ font-size: calc(var(--zx-avatar-size) * .34);
44
+ font-weight: 700;
45
+ line-height: 1;
46
+ }
47
+
48
+ .zx-avatar__status {
49
+ position: absolute;
50
+ inset-inline-end: -1px;
51
+ inset-block-end: -1px;
52
+ inline-size: max(8px, calc(var(--zx-avatar-size) * .28));
53
+ block-size: max(8px, calc(var(--zx-avatar-size) * .28));
54
+ border: 2px solid var(--zx-color-bg-raised);
55
+ border-radius: 50%;
56
+ background: var(--zx-color-text-muted);
57
+ }
58
+
59
+ .zx-avatar__status[data-status="online"] { background: var(--zx-color-success); }
60
+ .zx-avatar__status[data-status="away"] { background: var(--zx-color-warning); }
61
+ .zx-avatar__status[data-status="busy"] { background: var(--zx-color-danger); }