@zeyos/zx 2.3.0 → 4.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.
- package/CHANGELOG.md +138 -0
- package/DESIGN-SYSTEM.md +96 -0
- package/README.md +29 -39
- package/dist/types/components/account-menu/account-menu.d.ts +144 -0
- package/dist/types/components/app-sidebar/app-sidebar.d.ts +243 -0
- package/dist/types/components/avatar/avatar.d.ts +100 -0
- package/dist/types/components/card/card.d.ts +128 -0
- package/dist/types/components/chart/chart.d.ts +322 -0
- package/dist/types/components/date-picker/month-picker.d.ts +6 -0
- package/dist/types/components/dialog/dialog.d.ts +9 -0
- package/dist/types/components/dock/dock.d.ts +495 -0
- package/dist/types/components/dropdown/dropdown.d.ts +2 -2
- package/dist/types/components/field/field.d.ts +14 -0
- package/dist/types/components/field-upload/field-upload.d.ts +37 -4
- package/dist/types/components/form/form.d.ts +25 -3
- package/dist/types/components/grid/grid.d.ts +172 -0
- package/dist/types/components/launcher/launcher.d.ts +359 -0
- package/dist/types/components/layout/layout.d.ts +4 -4
- package/dist/types/components/menu-button/menu-button.d.ts +10 -2
- package/dist/types/components/modal/modal.d.ts +25 -0
- package/dist/types/components/multi-value-editor/multi-value-editor.d.ts +7 -1
- package/dist/types/components/questionnaire/items.d.ts +327 -0
- package/dist/types/components/questionnaire/questionnaire.d.ts +449 -0
- package/dist/types/components/select/select.d.ts +51 -0
- package/dist/types/components/sheet/sheet-stack.d.ts +167 -0
- package/dist/types/components/sheet/sheet.d.ts +294 -0
- package/dist/types/components/split-view/split-view.d.ts +2 -21
- package/dist/types/components/table/table.d.ts +223 -15
- package/dist/types/components/tooltip/tooltip.d.ts +2 -2
- package/dist/types/components/value-list/value-list.d.ts +24 -3
- package/dist/types/core/dom.d.ts +122 -0
- package/dist/types/core/drag-axis.d.ts +111 -0
- package/dist/types/core/position.d.ts +4 -2
- package/dist/types/index.d.ts +13 -2
- package/dist/types/internal/app-rail.d.ts +265 -0
- package/dist/types/zeyos/icons.d.ts +5 -0
- package/dist/types/zeyos/index.d.ts +2 -1
- package/dist/types/zeyos/launcher.d.ts +147 -0
- package/dist/types/zeyos/select.d.ts +39 -0
- package/dist/zx-zeyos.esm.js +1870 -133
- package/dist/zx-zeyos.esm.min.js +1 -1
- package/dist/zx.css +2006 -267
- package/dist/zx.esm.js +8987 -2757
- package/dist/zx.esm.min.js +3 -3
- package/dist/zx.global.js +8987 -2757
- package/dist/zx.global.min.js +3 -3
- package/dist/zx.min.css +1 -1
- package/docs/api.json +4396 -2344
- package/docs/llms.md +463 -86
- package/docs/llms.txt +32 -19
- package/package.json +10 -17
- package/src/components/account-menu/account-menu.css +70 -0
- package/src/components/account-menu/account-menu.js +208 -0
- package/src/components/app-rail/app-rail.css +107 -0
- package/src/components/app-sidebar/app-sidebar.css +80 -0
- package/src/components/app-sidebar/app-sidebar.js +588 -0
- package/src/components/avatar/avatar.css +61 -0
- package/src/components/avatar/avatar.js +222 -0
- package/src/components/button/button.css +0 -1
- package/src/components/card/card.css +149 -0
- package/src/components/card/card.js +285 -0
- package/src/components/chart/chart.css +61 -0
- package/src/components/chart/chart.js +523 -0
- package/src/components/date-picker/month-picker.js +9 -0
- package/src/components/dialog/dialog.js +11 -4
- package/src/components/dock/dock.css +228 -0
- package/src/components/dock/dock.js +977 -0
- package/src/components/dropdown/dropdown.js +1 -1
- package/src/components/field/field.js +2 -0
- package/src/components/field-upload/field-upload.js +9 -4
- package/src/components/form/form.js +6 -3
- package/src/components/grid/grid.css +6 -0
- package/src/components/grid/grid.js +220 -0
- package/src/components/launcher/launcher.css +210 -0
- package/src/components/launcher/launcher.js +765 -0
- package/src/components/layout/layout.js +2 -2
- package/src/components/menu-button/menu-button.js +27 -2
- package/src/components/modal/modal.css +5 -1
- package/src/components/modal/modal.js +37 -2
- package/src/components/multi-value-editor/multi-value-editor.js +2 -1
- package/src/components/questionnaire/items.js +370 -0
- package/src/components/questionnaire/questionnaire.css +387 -0
- package/src/components/questionnaire/questionnaire.js +1033 -0
- package/src/components/select/field-adapter.js +9 -4
- package/src/components/select/select.css +144 -11
- package/src/components/select/select.js +259 -27
- package/src/components/sheet/sheet-stack.js +304 -0
- package/src/components/sheet/sheet.css +351 -0
- package/src/components/sheet/sheet.js +551 -0
- package/src/components/split-view/split-view.js +76 -161
- package/src/components/table/table.css +43 -0
- package/src/components/table/table.js +531 -52
- package/src/components/tooltip/tooltip.js +5 -2
- package/src/components/value-list/value-list.js +6 -3
- package/src/core/dom.js +322 -0
- package/src/core/drag-axis.js +203 -0
- package/src/core/position.js +68 -22
- package/src/index.js +20 -2
- package/src/internal/app-rail.js +718 -0
- package/src/zeyos/icons.js +8 -3
- package/src/zeyos/index.js +7 -1
- package/src/zeyos/launcher.js +338 -0
- package/src/zeyos/modules.js +1 -0
- package/src/zeyos/select.js +115 -1
- package/styles/tokens/dark.css +2 -3
- package/styles/tokens/global.css +6 -6
- package/styles/tokens/modules.css +1 -0
- package/styles/tokens/semantic.css +5 -3
- package/styles/tokens/themes.css +1 -1
- package/styles/zx.css +11 -1
- package/MIGRATION.md +0 -598
- package/dist/types/compat/base.d.ts +0 -156
- package/dist/types/compat/globals.d.ts +0 -20
- package/dist/types/compat/index.d.ts +0 -128
- package/dist/types/compat/map/basic.d.ts +0 -175
- package/dist/types/compat/map/data.d.ts +0 -102
- package/dist/types/compat/map/date-time.d.ts +0 -80
- package/dist/types/compat/map/factory.d.ts +0 -9
- package/dist/types/compat/map/form.d.ts +0 -98
- package/dist/types/compat/map/helpers.d.ts +0 -12
- package/dist/types/compat/map/http.d.ts +0 -36
- package/dist/types/compat/map/layout.d.ts +0 -106
- package/dist/types/compat/map/options.d.ts +0 -80
- package/dist/types/compat/map/select.d.ts +0 -95
- package/dist/types/compat/map/stubs.d.ts +0 -32
- package/dist/types/compat/map/utilities.d.ts +0 -37
- package/dist/types/compat-entry.d.ts +0 -5
- package/dist/types/components/permission/permission.d.ts +0 -96
- package/dist/zx-compat.esm.js +0 -17484
- package/dist/zx-compat.esm.min.js +0 -1
- package/dist/zx-compat.global.js +0 -23635
- package/dist/zx-compat.global.min.js +0 -3
- package/src/compat/base.js +0 -291
- package/src/compat/globals.js +0 -165
- package/src/compat/index.js +0 -84
- package/src/compat/map/basic.js +0 -464
- package/src/compat/map/data.js +0 -232
- package/src/compat/map/date-time.js +0 -177
- package/src/compat/map/factory.js +0 -45
- package/src/compat/map/form.js +0 -330
- package/src/compat/map/helpers.js +0 -46
- package/src/compat/map/http.js +0 -117
- package/src/compat/map/layout.js +0 -256
- package/src/compat/map/options.js +0 -267
- package/src/compat/map/select.js +0 -198
- package/src/compat/map/stubs.js +0 -26
- package/src/compat/map/utilities.js +0 -113
- package/src/compat-entry.js +0 -11
- package/src/components/permission/permission.css +0 -47
- package/src/components/permission/permission.js +0 -201
|
@@ -0,0 +1,588 @@
|
|
|
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 {
|
|
6
|
+
AppRailPresenter as MinimizedPresenter, appNavIcon, normalizeAppItems
|
|
7
|
+
} from '../../internal/app-rail.js';
|
|
8
|
+
|
|
9
|
+
/** @typedef {import('../../internal/app-rail.js').AppNavItem} AppNavItem */
|
|
10
|
+
/** @typedef {import('../../internal/app-rail.js').AppNavSelectDetail} AppNavSelectDetail */
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {Object} AppSidebarOptions
|
|
13
|
+
* @property {AppNavItem[]} [items=[]] Application navigation tree.
|
|
14
|
+
* @property {string|number|null} [active=null] Active item id.
|
|
15
|
+
* @property {Array<string|number>} [expanded=[]] Initially expanded branch ids.
|
|
16
|
+
* @property {boolean} [collapsed=false] Whether the minimized presentation is shown.
|
|
17
|
+
* @property {boolean} [collapsible=true] Whether vertical navigation exposes a mode toggle.
|
|
18
|
+
* @property {'vertical'|'horizontal'} [orientation='vertical'] Navigation flow; horizontal is always minimized.
|
|
19
|
+
* @property {'left'|'right'|'top'|'bottom'} [side='left'] Workspace edge occupied by navigation.
|
|
20
|
+
* @property {string} [label='Applications'] Navigation accessible label.
|
|
21
|
+
* @property {Node|string|number|{toElement: () => Node|null}|null} [header=null] Expanded header content.
|
|
22
|
+
* @property {Node|string|number|{toElement: () => Node|null}|null} [footer=null] Expanded sticky footer content.
|
|
23
|
+
* @property {Node|string|number|{toElement: () => Node|null}|null} [railHeader=null] Minimized leading content.
|
|
24
|
+
* @property {Node|string|number|{toElement: () => Node|null}|null} [railFooter=null] Minimized trailing content.
|
|
25
|
+
* @property {number} [openDelay=80] Minimized flyout hover-open delay.
|
|
26
|
+
* @property {number} [closeDelay=160] Minimized flyout crossing grace.
|
|
27
|
+
* @property {(item: AppNavItem, context: Record<string, any>) => Node|null} [renderIcon] Optional host icon renderer.
|
|
28
|
+
* @property {(event: CustomEvent<AppNavSelectDetail>) => void} [onselect] Preventable action listener.
|
|
29
|
+
* @property {(event: CustomEvent<{item: AppNavItem|null, id: string|number|null, expanded: boolean|null, ids: Array<string|number>}>) => void} [onbranchchange] Branch-state listener.
|
|
30
|
+
* @property {(event: CustomEvent<{collapsed: boolean}>) => void} [oncollapsechange] Presentation-state listener.
|
|
31
|
+
* @property {(event: CustomEvent<{item: AppNavItem, id: string|number, open: boolean}>) => void} [onflyoutchange] Minimized flyout-state listener.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* One application navigation component. Vertical expanded descendants render inline; minimized
|
|
36
|
+
* vertical and horizontal descendants use pointer-, focus-, and keyboard-accessible flyouts.
|
|
37
|
+
* @fires AppSidebar#select
|
|
38
|
+
* @fires AppSidebar#branchchange
|
|
39
|
+
* @fires AppSidebar#collapsechange
|
|
40
|
+
* @fires AppSidebar#flyoutchange
|
|
41
|
+
* @extends {Component<AppSidebarOptions>}
|
|
42
|
+
*/
|
|
43
|
+
export class AppSidebar extends Component {
|
|
44
|
+
static cssName = 'app-sidebar';
|
|
45
|
+
|
|
46
|
+
/** @type {Readonly<AppSidebarOptions>} */
|
|
47
|
+
static defaults = {
|
|
48
|
+
items: [],
|
|
49
|
+
active: null,
|
|
50
|
+
expanded: [],
|
|
51
|
+
collapsed: false,
|
|
52
|
+
collapsible: true,
|
|
53
|
+
orientation: 'vertical',
|
|
54
|
+
side: 'left',
|
|
55
|
+
label: 'Applications',
|
|
56
|
+
header: null,
|
|
57
|
+
footer: null,
|
|
58
|
+
railHeader: null,
|
|
59
|
+
railFooter: null,
|
|
60
|
+
openDelay: 80,
|
|
61
|
+
closeDelay: 160,
|
|
62
|
+
renderIcon: null
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Creates or enhances an application-navigation host.
|
|
67
|
+
* @param {Element|string|null} [target=null] Navigation target.
|
|
68
|
+
* @param {AppSidebarOptions} [options={}] Navigation options.
|
|
69
|
+
*/
|
|
70
|
+
constructor(target = null, options = {}) {
|
|
71
|
+
super(target, options);
|
|
72
|
+
this._rail = null;
|
|
73
|
+
this._railExpandButton = null;
|
|
74
|
+
this._renderMode();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** @returns {HTMLElement} */
|
|
78
|
+
render() {
|
|
79
|
+
this._createdRoot = this.el === null;
|
|
80
|
+
const root = /** @type {HTMLElement} */ (this.el ?? h('aside'));
|
|
81
|
+
this.el = root;
|
|
82
|
+
this._original = this._createdRoot ? null : {
|
|
83
|
+
attributes: Array.from(root.attributes, (attribute) => [attribute.name, attribute.value]),
|
|
84
|
+
children: Array.from(root.childNodes)
|
|
85
|
+
};
|
|
86
|
+
this._items = normalizeAppItems(this.options.items);
|
|
87
|
+
this._active = this.options.active;
|
|
88
|
+
this._expanded = new Set(Array.isArray(this.options.expanded)
|
|
89
|
+
? this.options.expanded.map(String) : []);
|
|
90
|
+
this._collapsed = Boolean(this.options.collapsed);
|
|
91
|
+
this._orientation = this.options.orientation === 'horizontal' ? 'horizontal' : 'vertical';
|
|
92
|
+
this._side = normalizeSide(this._orientation, this.options.side);
|
|
93
|
+
this._modeAbort = null;
|
|
94
|
+
root.replaceChildren();
|
|
95
|
+
return root;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Replaces the navigation tree in the active presentation. @param {AppNavItem[]} items @returns {this} */
|
|
99
|
+
setItems(items) {
|
|
100
|
+
this._items = normalizeAppItems(items);
|
|
101
|
+
this._reconcileExpanded();
|
|
102
|
+
if (this._rail) this._rail.setItems(this._items);
|
|
103
|
+
else this._renderTree();
|
|
104
|
+
return this;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Returns a shallow copy of normalized items. @returns {AppNavItem[]} */
|
|
108
|
+
getItems() {
|
|
109
|
+
return this._items.map((item) => ({
|
|
110
|
+
...item,
|
|
111
|
+
children: item.children?.map((child) => ({ ...child }))
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Sets the active id without routing. @param {string|number|null} id @returns {this} */
|
|
116
|
+
setActive(id) {
|
|
117
|
+
this._active = id;
|
|
118
|
+
if (this._rail) this._rail.setActive(id);
|
|
119
|
+
else this._syncActive();
|
|
120
|
+
return this;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Replaces expanded branch ids. @param {Array<string|number>} ids @param {{silent?: boolean}} [options={}] @returns {this} */
|
|
124
|
+
setExpanded(ids, { silent = false } = {}) {
|
|
125
|
+
this._expanded = new Set(Array.isArray(ids) ? ids.map(String) : []);
|
|
126
|
+
this._reconcileExpanded();
|
|
127
|
+
if (!this._rail) this._renderTree();
|
|
128
|
+
if (!silent) this.emit('branchchange', {
|
|
129
|
+
item: null,
|
|
130
|
+
id: null,
|
|
131
|
+
expanded: null,
|
|
132
|
+
ids: this.getExpanded()
|
|
133
|
+
});
|
|
134
|
+
return this;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Returns expanded branch ids in navigation order. @returns {Array<string|number>} */
|
|
138
|
+
getExpanded() {
|
|
139
|
+
const ordered = [];
|
|
140
|
+
walkItems(this._items, (item) => {
|
|
141
|
+
if (this._expanded.has(String(item.id))) ordered.push(item.id);
|
|
142
|
+
});
|
|
143
|
+
return ordered;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Toggles one inline branch. @param {string|number} id @param {{silent?: boolean}} [options={}] @returns {this} */
|
|
147
|
+
toggleBranch(id, { silent = false } = {}) {
|
|
148
|
+
const item = findItem(this._items, id);
|
|
149
|
+
if (!item?.children?.length) return this;
|
|
150
|
+
const key = String(id);
|
|
151
|
+
const expanded = !this._expanded.has(key);
|
|
152
|
+
if (expanded) this._expanded.add(key);
|
|
153
|
+
else this._expanded.delete(key);
|
|
154
|
+
if (!this._rail) this._renderTree();
|
|
155
|
+
if (!silent) this.emit('branchchange', {
|
|
156
|
+
item,
|
|
157
|
+
id: item.id,
|
|
158
|
+
expanded,
|
|
159
|
+
ids: this.getExpanded()
|
|
160
|
+
});
|
|
161
|
+
return this;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Shows the expanded vertical presentation. @param {{silent?: boolean}} [options={}] @returns {this} */
|
|
165
|
+
expand(options = {}) {
|
|
166
|
+
return this.setCollapsed(false, options);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Shows the minimized presentation. @param {{silent?: boolean}} [options={}] @returns {this} */
|
|
170
|
+
collapse(options = {}) {
|
|
171
|
+
return this.setCollapsed(true, options);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Sets the saved vertical expanded/minimized state. @param {boolean} collapsed @param {{silent?: boolean}} [options={}] @returns {this} */
|
|
175
|
+
setCollapsed(collapsed, { silent = false } = {}) {
|
|
176
|
+
const previous = this.isCollapsed();
|
|
177
|
+
const active = document.activeElement;
|
|
178
|
+
const outgoingOwnedFocus = Boolean(active
|
|
179
|
+
&& (this.el.contains(active) || this._rail?._containsNode(active)));
|
|
180
|
+
this._collapsed = Boolean(collapsed);
|
|
181
|
+
const next = this.isCollapsed();
|
|
182
|
+
if (previous !== next) {
|
|
183
|
+
this._renderMode();
|
|
184
|
+
if (outgoingOwnedFocus) {
|
|
185
|
+
if (next) this._railExpandButton?.focus();
|
|
186
|
+
else this.refs.collapse?.focus();
|
|
187
|
+
}
|
|
188
|
+
if (!silent) this.emit('collapsechange', { collapsed: next });
|
|
189
|
+
}
|
|
190
|
+
return this;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Reports the effective presentation state; horizontal navigation is always minimized. @returns {boolean} */
|
|
194
|
+
isCollapsed() {
|
|
195
|
+
return this._orientation === 'horizontal' || this._collapsed;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Changes navigation flow/edge without replacing state.
|
|
200
|
+
* @param {{orientation?: 'vertical'|'horizontal', side?: 'left'|'right'|'top'|'bottom'}} layout
|
|
201
|
+
* @param {{silent?: boolean}} [options]
|
|
202
|
+
* @returns {this}
|
|
203
|
+
*/
|
|
204
|
+
setLayout(layout = {}, { silent = false } = {}) {
|
|
205
|
+
const orientation = layout.orientation === 'horizontal' ? 'horizontal'
|
|
206
|
+
: layout.orientation === 'vertical' ? 'vertical' : this._orientation;
|
|
207
|
+
const side = normalizeSide(orientation, layout.side ?? this._side);
|
|
208
|
+
if (orientation === this._orientation && side === this._side) return this;
|
|
209
|
+
const previous = this.isCollapsed();
|
|
210
|
+
const active = document.activeElement;
|
|
211
|
+
const outgoingOwnedFocus = Boolean(active
|
|
212
|
+
&& (this.el.contains(active) || this._rail?._containsNode(active)));
|
|
213
|
+
const focusedId = outgoingOwnedFocus && active instanceof Element
|
|
214
|
+
? active.closest('[data-app-nav-id]')?.getAttribute('data-app-nav-id') ?? null : null;
|
|
215
|
+
this._orientation = orientation;
|
|
216
|
+
this._side = side;
|
|
217
|
+
this._renderMode();
|
|
218
|
+
const next = this.isCollapsed();
|
|
219
|
+
if (outgoingOwnedFocus) {
|
|
220
|
+
const replacement = focusedId === null ? null
|
|
221
|
+
: [...this.el.querySelectorAll('[data-app-nav-id]')]
|
|
222
|
+
.find((control) => control.getAttribute('data-app-nav-id') === focusedId) ?? null;
|
|
223
|
+
if (replacement instanceof HTMLElement) replacement.focus();
|
|
224
|
+
else if (next) this.focus();
|
|
225
|
+
else if (this.refs.collapse instanceof HTMLElement && !this.refs.collapse.hidden) {
|
|
226
|
+
this.refs.collapse.focus();
|
|
227
|
+
} else visibleTreeControls(this.refs.list)[0]?.focus();
|
|
228
|
+
}
|
|
229
|
+
if (previous !== next && !silent) this.emit('collapsechange', { collapsed: next });
|
|
230
|
+
return this;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Opens a minimized descendant flyout. @param {string|number} id @param {{focus?: boolean}} [options={}] @returns {this} */
|
|
234
|
+
openFlyout(id, options = {}) {
|
|
235
|
+
this._rail?.openFlyout(id, options);
|
|
236
|
+
return this;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Closes a minimized descendant flyout. @param {string|number} id @returns {this} */
|
|
240
|
+
closeFlyout(id) {
|
|
241
|
+
this._rail?.closeFlyout(id);
|
|
242
|
+
return this;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Closes every minimized flyout. @returns {this} */
|
|
246
|
+
closeAllFlyouts() {
|
|
247
|
+
this._rail?.closeAllFlyouts();
|
|
248
|
+
return this;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Reports whether a minimized flyout is open. @param {string|number} id @returns {boolean} */
|
|
252
|
+
isFlyoutOpen(id) {
|
|
253
|
+
return this._rail?.isFlyoutOpen(id) ?? false;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Focuses the first navigation action. @returns {this} */
|
|
257
|
+
focus() {
|
|
258
|
+
if (this._rail) this._rail.focus();
|
|
259
|
+
else visibleTreeControls(this.refs.list)[0]?.focus();
|
|
260
|
+
return this;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Destroys flyouts and restores/removes the host. @returns {void} */
|
|
264
|
+
destroy() {
|
|
265
|
+
const original = this._original;
|
|
266
|
+
this._modeAbort?.abort();
|
|
267
|
+
this._modeAbort = null;
|
|
268
|
+
this._rail?.destroy();
|
|
269
|
+
this._rail = null;
|
|
270
|
+
super.destroy();
|
|
271
|
+
if (!this._createdRoot && original) {
|
|
272
|
+
for (const attribute of Array.from(this.el.attributes)) this.el.removeAttribute(attribute.name);
|
|
273
|
+
for (const [name, value] of original.attributes) this.el.setAttribute(name, value);
|
|
274
|
+
this.el.replaceChildren(...original.children);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Mounts exactly one expanded tree or minimized flyout presentation. @returns {void} */
|
|
279
|
+
_renderMode() {
|
|
280
|
+
this._modeAbort?.abort();
|
|
281
|
+
this._modeAbort = new AbortController();
|
|
282
|
+
this._rail?.destroy();
|
|
283
|
+
this._rail = null;
|
|
284
|
+
this._railExpandButton = null;
|
|
285
|
+
for (const key of ['expanded', 'list', 'collapse', 'rail']) delete this.refs[key];
|
|
286
|
+
this.el.replaceChildren();
|
|
287
|
+
this.el.dataset.orientation = this._orientation;
|
|
288
|
+
this.el.dataset.side = this._side;
|
|
289
|
+
this.el.dataset.collapsed = String(this.isCollapsed());
|
|
290
|
+
if (this.isCollapsed()) this._renderRail();
|
|
291
|
+
else this._renderExpanded();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** @returns {void} */
|
|
295
|
+
_renderExpanded() {
|
|
296
|
+
const headerContent = h('div', { class: 'zx-app-sidebar__header-content' });
|
|
297
|
+
appendSlot(headerContent, this.options.header);
|
|
298
|
+
const collapseButton = h('button', {
|
|
299
|
+
class: 'zx-app-sidebar__collapse zx-icon-btn',
|
|
300
|
+
type: 'button',
|
|
301
|
+
ariaLabel: 'Minimize application sidebar'
|
|
302
|
+
}, icon(this._side === 'right' ? 'chevron-right' : 'chevron-left'));
|
|
303
|
+
collapseButton.hidden = !this.options.collapsible;
|
|
304
|
+
const footer = h('footer', { class: 'zx-app-sidebar__footer' });
|
|
305
|
+
appendSlot(footer, this.options.footer);
|
|
306
|
+
footer.hidden = !footer.hasChildNodes();
|
|
307
|
+
const list = h('ul', { class: 'zx-app-sidebar__list' });
|
|
308
|
+
const expanded = h('div', { class: 'zx-app-sidebar__expanded' },
|
|
309
|
+
h('header', { class: 'zx-app-sidebar__header' }, headerContent, collapseButton),
|
|
310
|
+
h('nav', { class: 'zx-app-sidebar__nav', ariaLabel: this.options.label }, list),
|
|
311
|
+
footer);
|
|
312
|
+
this.refs.expanded = expanded;
|
|
313
|
+
this.refs.list = list;
|
|
314
|
+
this.refs.collapse = collapseButton;
|
|
315
|
+
this.el.append(expanded);
|
|
316
|
+
this._listenMode(collapseButton, 'click', () => this.collapse());
|
|
317
|
+
this._listenMode(list, 'click', (event) => this._onTreeClick(event));
|
|
318
|
+
this._listenMode(list, 'keydown', (event) => this._onTreeKeydown(/** @type {KeyboardEvent} */ (event)));
|
|
319
|
+
this._renderTree();
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** @returns {void} */
|
|
323
|
+
_renderRail() {
|
|
324
|
+
const railHost = h('nav', { class: 'zx-app-sidebar__rail' });
|
|
325
|
+
this.el.append(railHost);
|
|
326
|
+
this.refs.rail = railHost;
|
|
327
|
+
const showToggle = this._orientation === 'vertical' && Boolean(this.options.collapsible);
|
|
328
|
+
const railHeader = h('div', { class: 'zx-app-sidebar__rail-header' });
|
|
329
|
+
if (showToggle) {
|
|
330
|
+
const expandButton = h('button', {
|
|
331
|
+
class: 'zx-app-sidebar__rail-toggle zx-icon-btn',
|
|
332
|
+
type: 'button',
|
|
333
|
+
ariaLabel: 'Expand application sidebar'
|
|
334
|
+
}, icon(this._side === 'right' ? 'chevron-left' : 'chevron-right'));
|
|
335
|
+
railHeader.append(expandButton);
|
|
336
|
+
this._railExpandButton = expandButton;
|
|
337
|
+
this._listenMode(expandButton, 'click', () => this.expand());
|
|
338
|
+
}
|
|
339
|
+
appendSlot(railHeader, this.options.railHeader);
|
|
340
|
+
this._rail = new MinimizedPresenter(railHost, {
|
|
341
|
+
items: this._items,
|
|
342
|
+
active: this._active,
|
|
343
|
+
orientation: this._orientation,
|
|
344
|
+
side: this._side,
|
|
345
|
+
label: this.options.label,
|
|
346
|
+
header: railHeader.hasChildNodes() ? railHeader : null,
|
|
347
|
+
footer: this.options.railFooter,
|
|
348
|
+
openDelay: this.options.openDelay,
|
|
349
|
+
closeDelay: this.options.closeDelay,
|
|
350
|
+
renderIcon: this.options.renderIcon,
|
|
351
|
+
onselect: (event) => {
|
|
352
|
+
const selected = this.emit('select', event.detail);
|
|
353
|
+
if (selected.defaultPrevented) event.preventDefault();
|
|
354
|
+
},
|
|
355
|
+
onflyoutchange: (event) => this.emit('flyoutchange', event.detail)
|
|
356
|
+
});
|
|
357
|
+
this._listenMode(railHost, 'zx-select', (event) => event.stopPropagation());
|
|
358
|
+
this._listenMode(railHost, 'zx-flyoutchange', (event) => event.stopPropagation());
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** @param {EventTarget} target @param {string} type @param {EventListener} listener @returns {void} */
|
|
362
|
+
_listenMode(target, type, listener) {
|
|
363
|
+
target.addEventListener(type, listener, { signal: this._modeAbort.signal });
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** @returns {void} */
|
|
367
|
+
_renderTree() {
|
|
368
|
+
if (!this.refs.list) return;
|
|
369
|
+
const focused = this.refs.list.contains(document.activeElement)
|
|
370
|
+
? /** @type {Element} */ (document.activeElement).closest('[data-app-nav-id]')
|
|
371
|
+
?.getAttribute('data-app-nav-id')
|
|
372
|
+
: null;
|
|
373
|
+
this.refs.list.replaceChildren(...this._items.map((item) => this._treeRow(item, 1)));
|
|
374
|
+
this._syncActive();
|
|
375
|
+
if (focused !== null) {
|
|
376
|
+
const replacement = [...this.refs.list.querySelectorAll('[data-app-nav-id]')]
|
|
377
|
+
.find((control) => control.getAttribute('data-app-nav-id') === focused) ?? null;
|
|
378
|
+
visibleTreeControl(replacement)?.focus();
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** @param {AppNavItem} item @param {number} level @returns {HTMLElement} */
|
|
383
|
+
_treeRow(item, level) {
|
|
384
|
+
const children = item.children ?? [];
|
|
385
|
+
const hasChildren = children.length > 0;
|
|
386
|
+
const key = String(item.id);
|
|
387
|
+
const expanded = hasChildren && this._expanded.has(key);
|
|
388
|
+
const childId = hasChildren ? uid('zx-app-sidebar-branch') : null;
|
|
389
|
+
const visual = appNavIcon(item, this.options.renderIcon, {
|
|
390
|
+
orientation: this._orientation,
|
|
391
|
+
collapsed: false,
|
|
392
|
+
side: this._side,
|
|
393
|
+
level,
|
|
394
|
+
location: level === 1 ? 'root' : 'inline'
|
|
395
|
+
});
|
|
396
|
+
const control = navControl(item, hasChildren, expanded, childId, visual);
|
|
397
|
+
const row = h('li', {
|
|
398
|
+
class: 'zx-app-sidebar__row',
|
|
399
|
+
dataset: { level }
|
|
400
|
+
}, control);
|
|
401
|
+
if (hasChildren) {
|
|
402
|
+
row.append(h('ul', {
|
|
403
|
+
class: 'zx-app-sidebar__sublist',
|
|
404
|
+
id: childId,
|
|
405
|
+
hidden: !expanded
|
|
406
|
+
}, children.map((child) => this._treeRow(child, level + 1))));
|
|
407
|
+
}
|
|
408
|
+
return row;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** @param {Event} event @returns {void} */
|
|
412
|
+
_onTreeClick(event) {
|
|
413
|
+
const control = /** @type {Element|null} */ (event.target)?.closest?.('[data-app-nav-id]');
|
|
414
|
+
if (!control || !this.refs.list.contains(control)) return;
|
|
415
|
+
const item = findItem(this._items, control.getAttribute('data-app-nav-id'));
|
|
416
|
+
if (!item || item.disabled) {
|
|
417
|
+
event.preventDefault();
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (item.children?.length) this.toggleBranch(item.id);
|
|
421
|
+
else this._select(item, event);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** @param {KeyboardEvent} event @returns {void} */
|
|
425
|
+
_onTreeKeydown(event) {
|
|
426
|
+
const current = /** @type {Element|null} */ (event.target)?.closest?.('[data-app-nav-id]');
|
|
427
|
+
if (!(current instanceof HTMLElement)) return;
|
|
428
|
+
const item = findItem(this._items, current.getAttribute('data-app-nav-id'));
|
|
429
|
+
const controls = visibleTreeControls(this.refs.list);
|
|
430
|
+
const index = controls.indexOf(current);
|
|
431
|
+
if (event.key === 'ArrowRight' && item?.children?.length) {
|
|
432
|
+
event.preventDefault();
|
|
433
|
+
if (!this._expanded.has(String(item.id))) this.toggleBranch(item.id);
|
|
434
|
+
else visibleTreeControls(current.parentElement?.querySelector('.zx-app-sidebar__sublist'))[0]?.focus();
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (event.key === 'ArrowLeft') {
|
|
438
|
+
if (item?.children?.length && this._expanded.has(String(item.id))) {
|
|
439
|
+
event.preventDefault();
|
|
440
|
+
this.toggleBranch(item.id);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
const parentList = current.closest('.zx-app-sidebar__sublist');
|
|
444
|
+
const parentControl = parentList?.parentElement?.querySelector(':scope > [data-app-nav-id]');
|
|
445
|
+
if (parentControl instanceof HTMLElement) {
|
|
446
|
+
event.preventDefault();
|
|
447
|
+
parentControl.focus();
|
|
448
|
+
}
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
let nextIndex;
|
|
452
|
+
if (event.key === 'Home') nextIndex = 0;
|
|
453
|
+
else if (event.key === 'End') nextIndex = controls.length - 1;
|
|
454
|
+
else if (event.key === 'ArrowDown') nextIndex = Math.min(controls.length - 1, index + 1);
|
|
455
|
+
else if (event.key === 'ArrowUp') nextIndex = Math.max(0, index - 1);
|
|
456
|
+
else return;
|
|
457
|
+
event.preventDefault();
|
|
458
|
+
controls[nextIndex]?.focus();
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** @param {AppNavItem} item @param {Event} event @returns {void} */
|
|
462
|
+
_select(item, event) {
|
|
463
|
+
if (item.disabled) {
|
|
464
|
+
event.preventDefault();
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
const selected = this.emit('select', {
|
|
468
|
+
item,
|
|
469
|
+
id: item.id,
|
|
470
|
+
value: item.value ?? item.id,
|
|
471
|
+
href: item.href ?? null,
|
|
472
|
+
event
|
|
473
|
+
});
|
|
474
|
+
if (selected.defaultPrevented) event.preventDefault();
|
|
475
|
+
else if (!item.href) {
|
|
476
|
+
event.preventDefault();
|
|
477
|
+
item.invoke?.();
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** @returns {void} */
|
|
482
|
+
_syncActive() {
|
|
483
|
+
if (!this.refs.list) return;
|
|
484
|
+
const activeId = this._active == null ? null : String(this._active);
|
|
485
|
+
for (const control of this.refs.list.querySelectorAll('[data-app-nav-id]')) {
|
|
486
|
+
const item = findItem(this._items, control.getAttribute('data-app-nav-id'));
|
|
487
|
+
const direct = activeId !== null && control.getAttribute('data-app-nav-id') === activeId;
|
|
488
|
+
const descendant = activeId !== null && item
|
|
489
|
+
? containsItemId(item.children ?? [], activeId) : false;
|
|
490
|
+
if (direct) control.setAttribute('aria-current', 'page');
|
|
491
|
+
else control.removeAttribute('aria-current');
|
|
492
|
+
if (descendant) control.dataset.activeDescendant = 'true';
|
|
493
|
+
else delete control.dataset.activeDescendant;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/** @returns {void} */
|
|
498
|
+
_reconcileExpanded() {
|
|
499
|
+
const branchIds = new Set();
|
|
500
|
+
walkItems(this._items, (item) => {
|
|
501
|
+
if (item.children?.length) branchIds.add(String(item.id));
|
|
502
|
+
});
|
|
503
|
+
for (const id of this._expanded) if (!branchIds.has(id)) this._expanded.delete(id);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/** @param {AppNavItem} item @param {boolean} disclosure @param {boolean} expanded @param {string|null} controls @param {Node|null} visual @returns {HTMLElement} */
|
|
508
|
+
function navControl(item, disclosure, expanded, controls, visual) {
|
|
509
|
+
const tag = item.href && !disclosure ? 'a' : 'button';
|
|
510
|
+
return /** @type {HTMLElement} */ (h(tag, {
|
|
511
|
+
class: 'zx-app-sidebar__item',
|
|
512
|
+
type: tag === 'button' ? 'button' : null,
|
|
513
|
+
href: tag === 'a' ? item.href : null,
|
|
514
|
+
target: tag === 'a' ? item.target ?? null : null,
|
|
515
|
+
ariaDisabled: item.disabled ? 'true' : null,
|
|
516
|
+
ariaExpanded: disclosure ? String(expanded) : null,
|
|
517
|
+
ariaControls: controls,
|
|
518
|
+
dataset: { appNavId: String(item.id) }
|
|
519
|
+
},
|
|
520
|
+
visual ? h('span', { class: 'zx-app-sidebar__item-icon', ariaHidden: 'true' }, visual) : null,
|
|
521
|
+
h('span', { class: 'zx-app-sidebar__item-label' }, item.label),
|
|
522
|
+
item.badge != null ? h('span', { class: 'zx-app-sidebar__item-badge' }, String(item.badge)) : null,
|
|
523
|
+
disclosure ? h('span', { class: 'zx-app-sidebar__item-disclosure', ariaHidden: 'true' },
|
|
524
|
+
icon(expanded ? 'chevron-down' : 'chevron-right')) : null));
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/** @param {Element|null|undefined} root @returns {HTMLElement[]} */
|
|
528
|
+
function visibleTreeControls(root) {
|
|
529
|
+
if (!root) return [];
|
|
530
|
+
return /** @type {HTMLElement[]} */ ([...root.querySelectorAll('[data-app-nav-id]')]
|
|
531
|
+
.filter((control) => !control.closest('.zx-app-sidebar__sublist[hidden]')));
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/** Returns a control or its nearest owning disclosure when inside a collapsed branch. @param {Element|null} control @returns {HTMLElement|null} */
|
|
535
|
+
function visibleTreeControl(control) {
|
|
536
|
+
let candidate = /** @type {HTMLElement|null} */ (control);
|
|
537
|
+
while (candidate) {
|
|
538
|
+
const hiddenList = candidate.closest('.zx-app-sidebar__sublist[hidden]');
|
|
539
|
+
if (!hiddenList) return candidate;
|
|
540
|
+
candidate = /** @type {HTMLElement|null} */ (hiddenList.parentElement?.firstElementChild ?? null);
|
|
541
|
+
}
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/** @param {AppNavItem[]} items @param {string|number|null} id @returns {AppNavItem|null} */
|
|
546
|
+
function findItem(items, id) {
|
|
547
|
+
for (const item of items) {
|
|
548
|
+
if (String(item.id) === String(id)) return item;
|
|
549
|
+
const child = findItem(item.children ?? [], id);
|
|
550
|
+
if (child) return child;
|
|
551
|
+
}
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** @param {AppNavItem[]} items @param {(item: AppNavItem) => void} visit @returns {void} */
|
|
556
|
+
function walkItems(items, visit) {
|
|
557
|
+
for (const item of items) {
|
|
558
|
+
visit(item);
|
|
559
|
+
walkItems(item.children ?? [], visit);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/** @param {AppNavItem[]} items @param {string} id @returns {boolean} */
|
|
564
|
+
function containsItemId(items, id) {
|
|
565
|
+
return items.some((item) => String(item.id) === id || containsItemId(item.children ?? [], id));
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/** @param {Element} target @param {unknown} content @returns {void} */
|
|
569
|
+
function appendSlot(target, content) {
|
|
570
|
+
if (content == null) return;
|
|
571
|
+
if (typeof content === 'string' || typeof content === 'number') {
|
|
572
|
+
target.append(document.createTextNode(String(content)));
|
|
573
|
+
} else if (typeof content === 'object' && typeof content.toElement === 'function') {
|
|
574
|
+
const element = content.toElement();
|
|
575
|
+
if (element) target.append(element);
|
|
576
|
+
} else if (typeof content === 'object' && typeof content.nodeType === 'number') target.append(content);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/** @param {'vertical'|'horizontal'} orientation @param {unknown} side @returns {'left'|'right'|'top'|'bottom'} */
|
|
580
|
+
function normalizeSide(orientation, side) {
|
|
581
|
+
if (orientation === 'horizontal') return side === 'bottom' ? 'bottom' : 'top';
|
|
582
|
+
return side === 'right' ? 'right' : 'left';
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/** Fired when a destination/action is activated. @event AppSidebar#select @type {CustomEvent<AppNavSelectDetail>} */
|
|
586
|
+
/** 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>}>} */
|
|
587
|
+
/** Fired when expanded/minimized presentation changes. @event AppSidebar#collapsechange @type {CustomEvent<{collapsed: boolean}>} */
|
|
588
|
+
/** Fired when a minimized flyout changes. @event AppSidebar#flyoutchange @type {CustomEvent<{item: AppNavItem, id: string|number, open: 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); }
|