@vobs/ui 0.1.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 (48) hide show
  1. package/LICENSE +21 -0
  2. package/dist/app-shell-dom.d.ts +24 -0
  3. package/dist/app-shell-dom.js +573 -0
  4. package/dist/app-shell.d.ts +199 -0
  5. package/dist/app-shell.js +370 -0
  6. package/dist/base.css +52 -0
  7. package/dist/calendar-picker.d.ts +45 -0
  8. package/dist/calendar-picker.js +217 -0
  9. package/dist/calendar.d.ts +95 -0
  10. package/dist/calendar.js +194 -0
  11. package/dist/complex-inputs.d.ts +300 -0
  12. package/dist/complex-inputs.js +862 -0
  13. package/dist/components.css +4568 -0
  14. package/dist/data-display.d.ts +1063 -0
  15. package/dist/data-display.js +2298 -0
  16. package/dist/feedback.d.ts +88 -0
  17. package/dist/feedback.js +281 -0
  18. package/dist/forms.d.ts +378 -0
  19. package/dist/forms.js +1015 -0
  20. package/dist/icons-config.d.ts +25 -0
  21. package/dist/icons-config.js +14 -0
  22. package/dist/icons.d.ts +41 -0
  23. package/dist/icons.js +185 -0
  24. package/dist/index.d.ts +22 -0
  25. package/dist/index.js +32 -0
  26. package/dist/locale/en-US.d.ts +31 -0
  27. package/dist/locale/en-US.js +20 -0
  28. package/dist/locale/zh-CN.d.ts +31 -0
  29. package/dist/locale/zh-CN.js +20 -0
  30. package/dist/navigation.d.ts +609 -0
  31. package/dist/navigation.js +1707 -0
  32. package/dist/overlay.d.ts +304 -0
  33. package/dist/overlay.js +969 -0
  34. package/dist/primitives.d.ts +268 -0
  35. package/dist/primitives.js +764 -0
  36. package/dist/styles.css +3 -0
  37. package/dist/theme-data.d.ts +363 -0
  38. package/dist/theme-data.js +374 -0
  39. package/dist/theme.d.ts +79 -0
  40. package/dist/theme.js +262 -0
  41. package/dist/tokens.css +449 -0
  42. package/dist/tree.d.ts +144 -0
  43. package/dist/tree.js +469 -0
  44. package/dist/virtual-list.d.ts +190 -0
  45. package/dist/virtual-list.js +521 -0
  46. package/dist/workbench.d.ts +136 -0
  47. package/dist/workbench.js +156 -0
  48. package/package.json +113 -0
package/dist/tree.d.ts ADDED
@@ -0,0 +1,144 @@
1
+ /** @license MIT
2
+ * Copyright (c) 2026 vobsjs
3
+ * @vobs/ui
4
+ */
5
+ import { type UiAttributes, type UiDomPlan } from './primitives.js';
6
+ export type TreeNodeKind = 'branch' | 'leaf';
7
+ export type TreeLazyLoadState = 'empty' | 'error' | 'loading' | 'partial' | 'ready';
8
+ export type TreeNodeLazyState = 'error' | 'idle' | 'loaded' | 'loading';
9
+ export type TreeNodeActionAvailability = 'available' | 'disabled';
10
+ export type TreePermission = 'allowed' | 'denied';
11
+ export type TreePermissionState = 'empty' | 'permission-limited' | 'ready';
12
+ export type TreeNodeTone = 'css' | 'default' | 'folder' | 'json' | 'ts';
13
+ export interface TreeNodeItem {
14
+ readonly id: string;
15
+ readonly label: string;
16
+ readonly depth?: number;
17
+ readonly kind?: TreeNodeKind;
18
+ readonly expanded?: boolean;
19
+ readonly disabled?: boolean;
20
+ readonly value?: string;
21
+ readonly tone?: TreeNodeTone;
22
+ readonly lazy?: boolean;
23
+ readonly loading?: boolean;
24
+ readonly loaded?: boolean;
25
+ readonly loadError?: string;
26
+ readonly retryable?: boolean;
27
+ readonly permission?: TreePermission;
28
+ readonly disabledReason?: string;
29
+ readonly actionable?: boolean;
30
+ readonly actionDisabled?: boolean;
31
+ readonly actionDisabledReason?: string;
32
+ }
33
+ export interface TreePlanOptions {
34
+ readonly id: string;
35
+ readonly nodes: readonly TreeNodeItem[];
36
+ readonly activeId?: string;
37
+ readonly selectedId?: string;
38
+ readonly multiselect?: boolean;
39
+ readonly disabled?: boolean;
40
+ readonly labelId?: string;
41
+ readonly describedBy?: string;
42
+ readonly className?: string;
43
+ readonly attrs?: UiAttributes;
44
+ }
45
+ export interface TreePlans {
46
+ readonly root: UiDomPlan<'div'>;
47
+ readonly rows: readonly UiDomPlan<'div'>[];
48
+ readonly chevrons: readonly UiDomPlan<'span'>[];
49
+ readonly icons: readonly UiDomPlan<'span'>[];
50
+ readonly labels: readonly UiDomPlan<'span'>[];
51
+ }
52
+ export interface TreeSelectPlanOptions extends TreePlanOptions {
53
+ readonly open?: boolean;
54
+ readonly placeholder?: string;
55
+ }
56
+ export interface TreeSelectPlans {
57
+ readonly root: UiDomPlan<'div'>;
58
+ readonly trigger: UiDomPlan<'button'>;
59
+ readonly popup: UiDomPlan<'div'>;
60
+ readonly tree: TreePlans;
61
+ }
62
+ export interface TreeLazyLoadStateContractOptions {
63
+ readonly nodes: readonly TreeNodeItem[];
64
+ readonly loadingIds?: ReadonlySet<string> | readonly string[];
65
+ readonly loadedIds?: ReadonlySet<string> | readonly string[];
66
+ readonly errorIds?: ReadonlySet<string> | readonly string[];
67
+ readonly errors?: Readonly<Record<string, string | undefined>>;
68
+ readonly retryableIds?: ReadonlySet<string> | readonly string[];
69
+ readonly disabled?: boolean;
70
+ }
71
+ export interface TreeLazyLoadNodeContract {
72
+ readonly id: string;
73
+ readonly state: TreeNodeLazyState;
74
+ readonly lazy: boolean;
75
+ readonly loading: boolean;
76
+ readonly loaded: boolean;
77
+ readonly error: string;
78
+ readonly retryable: boolean;
79
+ readonly canRetry: boolean;
80
+ readonly attrs: UiAttributes;
81
+ }
82
+ export interface TreeLazyLoadStateContract {
83
+ readonly state: TreeLazyLoadState;
84
+ readonly disabled: boolean;
85
+ readonly nodeCount: number;
86
+ readonly loadableCount: number;
87
+ readonly loadingCount: number;
88
+ readonly loadedCount: number;
89
+ readonly errorCount: number;
90
+ readonly retryableCount: number;
91
+ readonly loadingIds: readonly string[];
92
+ readonly loadedIds: readonly string[];
93
+ readonly errorIds: readonly string[];
94
+ readonly retryableIds: readonly string[];
95
+ readonly nodes: readonly TreeNodeItem[];
96
+ readonly nodeContracts: readonly TreeLazyLoadNodeContract[];
97
+ readonly attrs: UiAttributes;
98
+ }
99
+ export interface TreePermissionStateContractOptions {
100
+ readonly nodes: readonly TreeNodeItem[];
101
+ readonly allowedIds?: ReadonlySet<string> | readonly string[];
102
+ readonly deniedIds?: ReadonlySet<string> | readonly string[];
103
+ readonly disabledReasons?: Readonly<Record<string, string | undefined>>;
104
+ readonly actionableIds?: ReadonlySet<string> | readonly string[];
105
+ readonly actionDisabledIds?: ReadonlySet<string> | readonly string[];
106
+ readonly actionDisabledReasons?: Readonly<Record<string, string | undefined>>;
107
+ readonly disabled?: boolean;
108
+ }
109
+ export interface TreePermissionNodeContract {
110
+ readonly id: string;
111
+ readonly permission: TreePermission;
112
+ readonly disabled: boolean;
113
+ readonly disabledReason: string;
114
+ readonly actionable: boolean;
115
+ readonly actionAvailability: TreeNodeActionAvailability;
116
+ readonly actionDisabled: boolean;
117
+ readonly actionDisabledReason: string;
118
+ readonly attrs: UiAttributes;
119
+ }
120
+ export interface TreePermissionStateContract {
121
+ readonly state: TreePermissionState;
122
+ readonly disabled: boolean;
123
+ readonly nodeCount: number;
124
+ readonly allowedCount: number;
125
+ readonly deniedCount: number;
126
+ readonly disabledReasonCount: number;
127
+ readonly actionableCount: number;
128
+ readonly actionDisabledCount: number;
129
+ readonly actionDisabledReasonCount: number;
130
+ readonly allowedIds: readonly string[];
131
+ readonly deniedIds: readonly string[];
132
+ readonly actionableIds: readonly string[];
133
+ readonly actionDisabledIds: readonly string[];
134
+ readonly nodes: readonly TreeNodeItem[];
135
+ readonly nodeContracts: readonly TreePermissionNodeContract[];
136
+ readonly attrs: UiAttributes;
137
+ }
138
+ export declare function createTreePlans(options: TreePlanOptions): TreePlans;
139
+ export declare function createTreeLazyLoadStateContract(options: TreeLazyLoadStateContractOptions): TreeLazyLoadStateContract;
140
+ export declare function createTreePermissionStateContract(options: TreePermissionStateContractOptions): TreePermissionStateContract;
141
+ export declare function createTreeSelectPlans(options: TreeSelectPlanOptions): TreeSelectPlans;
142
+ export declare function getVisibleTreeNodeIds(nodes: readonly TreeNodeItem[]): readonly string[];
143
+ export declare function getNextTreeNodeId(nodes: readonly TreeNodeItem[], activeId: string | undefined, key: string): string | undefined;
144
+ export declare function shouldToggleTreeNodeForKey(key: string): boolean;
package/dist/tree.js ADDED
@@ -0,0 +1,469 @@
1
+ /** @license MIT
2
+ * Copyright (c) 2026 vobsjs
3
+ * @vobs/ui
4
+ */
5
+ import { createUiDomPlan } from './primitives.js';
6
+ export function createTreePlans(options) {
7
+ const visibleNodes = visibleTreeNodes(options.nodes);
8
+ const activeId = visibleNodes.some((node) => node.id === options.activeId)
9
+ ? options.activeId
10
+ : undefined;
11
+ const disabled = options.disabled === true;
12
+ return {
13
+ root: createUiDomPlan('div', 'tree', {
14
+ ...(options.className === undefined ? {} : { className: options.className }),
15
+ disabled,
16
+ state: disabled ? 'disabled' : visibleNodes.length === 0 ? 'empty' : 'idle',
17
+ attrs: {
18
+ id: options.id,
19
+ role: 'tree',
20
+ 'aria-labelledby': options.labelId,
21
+ 'aria-describedby': options.describedBy,
22
+ 'aria-multiselectable': options.multiselect === true ? 'true' : undefined,
23
+ ...options.attrs,
24
+ },
25
+ }),
26
+ rows: visibleNodes.map((node, index) => {
27
+ const nodeDisabled = disabled || node.disabled === true;
28
+ const selected = node.id === options.selectedId;
29
+ const active = node.id === activeId || (activeId === undefined && index === 0);
30
+ const branch = treeNodeKind(node) === 'branch';
31
+ return createUiDomPlan('div', 'tree-row', {
32
+ disabled: nodeDisabled,
33
+ state: nodeDisabled ? 'disabled' : active ? 'active' : selected ? 'selected' : 'idle',
34
+ ...(selected ? { className: 'is-active' } : {}),
35
+ attrs: {
36
+ id: treeNodeDomId(options.id, node.id),
37
+ role: 'treeitem',
38
+ tabindex: active && !nodeDisabled ? 0 : -1,
39
+ 'aria-level': treeNodeDepth(node) + 1,
40
+ 'aria-expanded': branch ? (node.expanded === true ? 'true' : 'false') : undefined,
41
+ 'aria-selected': selected ? 'true' : 'false',
42
+ 'data-node-id': node.id,
43
+ 'data-value': node.value,
44
+ 'data-depth': treeNodeDepth(node),
45
+ 'data-kind': treeNodeKind(node),
46
+ 'data-lazy-state': treeNodeLazyStateFromItem(node),
47
+ 'data-lazy': node.lazy === true ? 'true' : undefined,
48
+ 'data-loading': node.loading === true ? 'true' : undefined,
49
+ 'data-loaded': node.loaded === true ? 'true' : undefined,
50
+ 'data-load-error': node.loadError,
51
+ 'data-retryable': node.retryable === true ? 'true' : undefined,
52
+ 'data-can-retry': node.loadError !== undefined && node.retryable === true && !nodeDisabled
53
+ ? 'true'
54
+ : undefined,
55
+ 'data-permission': node.permission,
56
+ 'data-disabled-reason': node.disabledReason,
57
+ 'data-actionable': node.actionable === true ? 'true' : undefined,
58
+ 'data-action-availability': node.actionable === true
59
+ ? node.actionDisabled === true || nodeDisabled
60
+ ? 'disabled'
61
+ : 'available'
62
+ : undefined,
63
+ 'data-action-disabled': node.actionable === true && (node.actionDisabled === true || nodeDisabled)
64
+ ? 'true'
65
+ : undefined,
66
+ 'data-action-disabled-reason': node.actionDisabledReason,
67
+ 'aria-disabled': nodeDisabled ? 'true' : undefined,
68
+ 'aria-busy': node.loading === true ? 'true' : undefined,
69
+ },
70
+ });
71
+ }),
72
+ chevrons: visibleNodes.map((node) => {
73
+ const branch = treeNodeKind(node) === 'branch';
74
+ return createUiDomPlan('span', 'tree-chevron', {
75
+ state: branch ? (node.expanded === true ? 'expanded' : 'collapsed') : 'leaf',
76
+ ...(branch ? {} : { className: 'kui-tree-chevron--leaf' }),
77
+ attrs: {
78
+ 'aria-hidden': 'true',
79
+ 'data-expanded': branch ? (node.expanded === true ? 'true' : 'false') : undefined,
80
+ },
81
+ });
82
+ }),
83
+ icons: visibleNodes.map((node) => createUiDomPlan('span', 'tree-icon', {
84
+ state: treeNodeKind(node),
85
+ className: `kui-tree-icon--${node.tone ?? (treeNodeKind(node) === 'branch' ? 'folder' : 'default')}`,
86
+ attrs: {
87
+ 'aria-hidden': 'true',
88
+ 'data-tone': node.tone ?? (treeNodeKind(node) === 'branch' ? 'folder' : 'default'),
89
+ },
90
+ })),
91
+ labels: visibleNodes.map((node) => createUiDomPlan('span', 'tree-label', {
92
+ attrs: {
93
+ 'data-label': node.label,
94
+ },
95
+ })),
96
+ };
97
+ }
98
+ export function createTreeLazyLoadStateContract(options) {
99
+ const disabled = options.disabled === true;
100
+ const knownIds = new Set(options.nodes.map((node) => node.id));
101
+ const loadingIds = normalizeTreeIds(options.loadingIds, knownIds);
102
+ const loadedIds = normalizeTreeIds(options.loadedIds, knownIds);
103
+ const errorIds = normalizeTreeIds(options.errorIds, knownIds);
104
+ const retryableIds = normalizeTreeIds(options.retryableIds, knownIds);
105
+ const nodeContracts = options.nodes.map((node) => treeLazyLoadNodeContract(node, {
106
+ disabled,
107
+ error: options.errors?.[node.id],
108
+ loading: loadingIds.includes(node.id),
109
+ loaded: loadedIds.includes(node.id),
110
+ errored: errorIds.includes(node.id),
111
+ retryable: retryableIds.includes(node.id),
112
+ }));
113
+ const nodes = options.nodes.map((node, index) => treeNodeWithLazyContract(node, nodeContracts[index]));
114
+ const loadableCount = nodeContracts.filter((node) => node.lazy).length;
115
+ const loadingCount = nodeContracts.filter((node) => node.loading).length;
116
+ const loadedCount = nodeContracts.filter((node) => node.loaded).length;
117
+ const errorCount = nodeContracts.filter((node) => node.state === 'error').length;
118
+ const retryableCount = nodeContracts.filter((node) => node.canRetry).length;
119
+ const state = treeLazyLoadState({
120
+ errorCount,
121
+ loadableCount,
122
+ loadedCount,
123
+ loadingCount,
124
+ nodeCount: options.nodes.length,
125
+ });
126
+ return {
127
+ state,
128
+ disabled,
129
+ nodeCount: options.nodes.length,
130
+ loadableCount,
131
+ loadingCount,
132
+ loadedCount,
133
+ errorCount,
134
+ retryableCount,
135
+ loadingIds,
136
+ loadedIds,
137
+ errorIds,
138
+ retryableIds,
139
+ nodes,
140
+ nodeContracts,
141
+ attrs: {
142
+ 'data-tree-lazy-load-contract': 'true',
143
+ 'data-state': state,
144
+ 'data-disabled': disabled ? 'true' : undefined,
145
+ 'data-node-count': options.nodes.length,
146
+ 'data-loadable-count': loadableCount,
147
+ 'data-loading-count': loadingCount,
148
+ 'data-loaded-count': loadedCount,
149
+ 'data-error-count': errorCount,
150
+ 'data-retryable-count': retryableCount,
151
+ 'data-loading-ids': loadingIds.join(','),
152
+ 'data-loaded-ids': loadedIds.join(','),
153
+ 'data-error-ids': errorIds.join(','),
154
+ 'data-retryable-ids': retryableIds.join(','),
155
+ 'aria-busy': loadingCount > 0 ? 'true' : undefined,
156
+ },
157
+ };
158
+ }
159
+ export function createTreePermissionStateContract(options) {
160
+ const disabled = options.disabled === true;
161
+ const knownIds = new Set(options.nodes.map((node) => node.id));
162
+ const allowedIds = normalizeTreeIds(options.allowedIds, knownIds);
163
+ const deniedIds = normalizeTreeIds(options.deniedIds, knownIds);
164
+ const actionableIds = normalizeTreeIds(options.actionableIds, knownIds);
165
+ const actionDisabledIds = normalizeTreeIds(options.actionDisabledIds, knownIds);
166
+ const allowedSet = new Set(allowedIds);
167
+ const deniedSet = new Set(deniedIds);
168
+ const actionableSet = new Set(actionableIds);
169
+ const actionDisabledSet = new Set(actionDisabledIds);
170
+ const hasAllowedIds = options.allowedIds !== undefined;
171
+ const nodeContracts = options.nodes.map((node) => treePermissionNodeContract(node, {
172
+ actionDisabled: actionDisabledSet.has(node.id),
173
+ actionDisabledReason: normalizedTreeReason(options.actionDisabledReasons?.[node.id]),
174
+ actionable: actionableSet.has(node.id),
175
+ allowed: allowedSet.has(node.id),
176
+ denied: deniedSet.has(node.id),
177
+ disabled,
178
+ disabledReason: normalizedTreeReason(options.disabledReasons?.[node.id]),
179
+ hasAllowedIds,
180
+ }));
181
+ const nodes = options.nodes.map((node, index) => treeNodeWithPermissionContract(node, nodeContracts[index]));
182
+ const allowedNodeIds = nodeContracts
183
+ .filter((node) => node.permission === 'allowed')
184
+ .map((node) => node.id);
185
+ const deniedNodeIds = nodeContracts
186
+ .filter((node) => node.permission === 'denied')
187
+ .map((node) => node.id);
188
+ const actionableNodeIds = nodeContracts.filter((node) => node.actionable).map((node) => node.id);
189
+ const actionDisabledNodeIds = nodeContracts
190
+ .filter((node) => node.actionDisabled)
191
+ .map((node) => node.id);
192
+ const disabledReasonCount = nodeContracts.filter((node) => node.disabledReason !== '').length;
193
+ const actionDisabledReasonCount = nodeContracts.filter((node) => node.actionDisabledReason !== '').length;
194
+ const state = treePermissionState({
195
+ actionDisabledCount: actionDisabledNodeIds.length,
196
+ deniedCount: deniedNodeIds.length,
197
+ nodeCount: options.nodes.length,
198
+ });
199
+ return {
200
+ state,
201
+ disabled,
202
+ nodeCount: options.nodes.length,
203
+ allowedCount: allowedNodeIds.length,
204
+ deniedCount: deniedNodeIds.length,
205
+ disabledReasonCount,
206
+ actionableCount: actionableNodeIds.length,
207
+ actionDisabledCount: actionDisabledNodeIds.length,
208
+ actionDisabledReasonCount,
209
+ allowedIds: allowedNodeIds,
210
+ deniedIds: deniedNodeIds,
211
+ actionableIds: actionableNodeIds,
212
+ actionDisabledIds: actionDisabledNodeIds,
213
+ nodes,
214
+ nodeContracts,
215
+ attrs: {
216
+ 'data-tree-permission-contract': 'true',
217
+ 'data-state': state,
218
+ 'data-disabled': disabled ? 'true' : undefined,
219
+ 'data-node-count': options.nodes.length,
220
+ 'data-allowed-count': allowedNodeIds.length,
221
+ 'data-denied-count': deniedNodeIds.length,
222
+ 'data-disabled-reason-count': disabledReasonCount,
223
+ 'data-actionable-count': actionableNodeIds.length,
224
+ 'data-action-disabled-count': actionDisabledNodeIds.length,
225
+ 'data-action-disabled-reason-count': actionDisabledReasonCount,
226
+ 'data-allowed-ids': allowedNodeIds.join(','),
227
+ 'data-denied-ids': deniedNodeIds.join(','),
228
+ 'data-actionable-ids': actionableNodeIds.join(','),
229
+ 'data-action-disabled-ids': actionDisabledNodeIds.join(','),
230
+ },
231
+ };
232
+ }
233
+ export function createTreeSelectPlans(options) {
234
+ const open = options.open === true;
235
+ const selected = options.nodes.find((node) => node.id === options.selectedId);
236
+ const tree = createTreePlans({
237
+ id: `${options.id}-tree`,
238
+ nodes: options.nodes,
239
+ ...(options.activeId === undefined ? {} : { activeId: options.activeId }),
240
+ ...(options.selectedId === undefined ? {} : { selectedId: options.selectedId }),
241
+ ...(options.multiselect === undefined ? {} : { multiselect: options.multiselect }),
242
+ ...(options.disabled === undefined ? {} : { disabled: options.disabled }),
243
+ ...(options.labelId === undefined ? {} : { labelId: options.labelId }),
244
+ ...(options.describedBy === undefined ? {} : { describedBy: options.describedBy }),
245
+ });
246
+ return {
247
+ root: createUiDomPlan('div', 'tree-select', {
248
+ ...(options.className === undefined ? {} : { className: options.className }),
249
+ ...(options.disabled === undefined ? {} : { disabled: options.disabled }),
250
+ state: options.disabled === true ? 'disabled' : open ? 'open' : 'closed',
251
+ attrs: {
252
+ id: options.id,
253
+ },
254
+ }),
255
+ trigger: createUiDomPlan('button', 'tree-select-trigger', {
256
+ ...(options.disabled === undefined ? {} : { disabled: options.disabled }),
257
+ state: options.disabled === true ? 'disabled' : open ? 'open' : 'closed',
258
+ attrs: {
259
+ id: `${options.id}-trigger`,
260
+ type: 'button',
261
+ role: 'combobox',
262
+ 'aria-haspopup': 'tree',
263
+ 'aria-expanded': open ? 'true' : 'false',
264
+ 'aria-controls': `${options.id}-popup`,
265
+ 'aria-labelledby': options.labelId,
266
+ 'aria-describedby': options.describedBy,
267
+ 'data-value': selected?.value ?? selected?.id,
268
+ 'data-label': selected?.label ?? options.placeholder,
269
+ },
270
+ }),
271
+ popup: createUiDomPlan('div', 'tree-select-popup', {
272
+ state: open ? 'open' : 'closed',
273
+ attrs: {
274
+ id: `${options.id}-popup`,
275
+ hidden: open ? undefined : true,
276
+ },
277
+ }),
278
+ tree,
279
+ };
280
+ }
281
+ export function getVisibleTreeNodeIds(nodes) {
282
+ return visibleTreeNodes(nodes).map((node) => node.id);
283
+ }
284
+ export function getNextTreeNodeId(nodes, activeId, key) {
285
+ const enabled = visibleTreeNodes(nodes).filter((node) => node.disabled !== true);
286
+ if (enabled.length === 0)
287
+ return activeId;
288
+ const currentIndex = Math.max(-1, enabled.findIndex((node) => node.id === activeId));
289
+ if (key === 'Home')
290
+ return enabled[0]?.id;
291
+ if (key === 'End')
292
+ return enabled.at(-1)?.id;
293
+ if (key === 'ArrowUp')
294
+ return enabled.at(currentIndex - 1)?.id ?? enabled.at(-1)?.id;
295
+ if (key === 'ArrowDown')
296
+ return enabled[(currentIndex + 1) % enabled.length]?.id;
297
+ return activeId;
298
+ }
299
+ export function shouldToggleTreeNodeForKey(key) {
300
+ return key === 'Enter' || key === ' ' || key === 'ArrowLeft' || key === 'ArrowRight';
301
+ }
302
+ function treeNodeDomId(treeId, nodeId) {
303
+ return `${treeId}-${nodeId}-treeitem`;
304
+ }
305
+ function treeLazyLoadNodeContract(node, options) {
306
+ const lazy = node.lazy === true || treeNodeKind(node) === 'branch';
307
+ const error = (options.error ?? node.loadError ?? '').trim();
308
+ const loading = options.loading || node.loading === true;
309
+ const loaded = !loading && (options.loaded || node.loaded === true);
310
+ const errored = !loading && (options.errored || error !== '');
311
+ const state = loading
312
+ ? 'loading'
313
+ : errored
314
+ ? 'error'
315
+ : loaded
316
+ ? 'loaded'
317
+ : 'idle';
318
+ const retryable = options.retryable || node.retryable === true;
319
+ const canRetry = !options.disabled && state === 'error' && retryable;
320
+ return {
321
+ id: node.id,
322
+ state,
323
+ lazy,
324
+ loading,
325
+ loaded,
326
+ error,
327
+ retryable,
328
+ canRetry,
329
+ attrs: {
330
+ 'data-node-id': node.id,
331
+ 'data-lazy-state': state,
332
+ 'data-lazy': lazy ? 'true' : undefined,
333
+ 'data-loading': loading ? 'true' : undefined,
334
+ 'data-loaded': loaded ? 'true' : undefined,
335
+ 'data-load-error': error === '' ? undefined : error,
336
+ 'data-retryable': retryable ? 'true' : undefined,
337
+ 'data-can-retry': canRetry ? 'true' : 'false',
338
+ 'aria-busy': loading ? 'true' : undefined,
339
+ },
340
+ };
341
+ }
342
+ function treeNodeWithLazyContract(node, contract) {
343
+ if (contract === undefined)
344
+ return node;
345
+ return {
346
+ ...node,
347
+ lazy: contract.lazy,
348
+ loading: contract.loading,
349
+ loaded: contract.loaded,
350
+ ...(contract.error === '' ? {} : { loadError: contract.error }),
351
+ retryable: contract.retryable,
352
+ };
353
+ }
354
+ function treePermissionNodeContract(node, options) {
355
+ const permission = options.denied || (options.hasAllowedIds && !options.allowed) ? 'denied' : 'allowed';
356
+ const disabled = options.disabled || node.disabled === true || permission === 'denied';
357
+ const nodeDisabledReason = options.disabledReason || normalizedTreeReason(node.disabledReason);
358
+ const disabledReason = permission === 'denied' && nodeDisabledReason === ''
359
+ ? 'Permission required'
360
+ : nodeDisabledReason;
361
+ const actionable = options.actionable || node.actionable === true || options.actionDisabled;
362
+ const actionDisabled = actionable && (options.actionDisabled || node.actionDisabled === true);
363
+ const nodeActionDisabledReason = options.actionDisabledReason || normalizedTreeReason(node.actionDisabledReason);
364
+ const actionDisabledReason = actionDisabled && nodeActionDisabledReason === ''
365
+ ? 'Action unavailable'
366
+ : nodeActionDisabledReason;
367
+ const actionAvailability = actionDisabled || disabled ? 'disabled' : 'available';
368
+ return {
369
+ id: node.id,
370
+ permission,
371
+ disabled,
372
+ disabledReason,
373
+ actionable,
374
+ actionAvailability,
375
+ actionDisabled,
376
+ actionDisabledReason,
377
+ attrs: {
378
+ 'data-node-id': node.id,
379
+ 'data-permission': permission,
380
+ 'data-disabled-reason': disabledReason,
381
+ 'data-actionable': actionable ? 'true' : undefined,
382
+ 'data-action-availability': actionable ? actionAvailability : undefined,
383
+ 'data-action-disabled': actionable && actionAvailability === 'disabled' ? 'true' : undefined,
384
+ 'data-action-disabled-reason': actionDisabledReason,
385
+ 'aria-disabled': disabled ? 'true' : undefined,
386
+ },
387
+ };
388
+ }
389
+ function treeNodeWithPermissionContract(node, contract) {
390
+ if (contract === undefined)
391
+ return node;
392
+ return {
393
+ ...node,
394
+ disabled: contract.disabled,
395
+ permission: contract.permission,
396
+ ...(contract.disabledReason === '' ? {} : { disabledReason: contract.disabledReason }),
397
+ ...(contract.actionable ? { actionable: true } : {}),
398
+ ...(contract.actionDisabled ? { actionDisabled: true } : {}),
399
+ ...(contract.actionDisabledReason === ''
400
+ ? {}
401
+ : { actionDisabledReason: contract.actionDisabledReason }),
402
+ };
403
+ }
404
+ function normalizeTreeIds(ids, knownIds) {
405
+ if (ids === undefined)
406
+ return [];
407
+ const result = [];
408
+ for (const id of ids) {
409
+ if (!knownIds.has(id) || result.includes(id))
410
+ continue;
411
+ result.push(id);
412
+ }
413
+ return result;
414
+ }
415
+ function treeLazyLoadState(options) {
416
+ if (options.nodeCount === 0)
417
+ return 'empty';
418
+ if (options.loadingCount > 0)
419
+ return 'loading';
420
+ if (options.errorCount > 0)
421
+ return 'error';
422
+ if (options.loadableCount > 0 && options.loadedCount >= options.loadableCount)
423
+ return 'ready';
424
+ if (options.loadedCount > 0)
425
+ return 'partial';
426
+ return 'ready';
427
+ }
428
+ function treePermissionState(options) {
429
+ if (options.nodeCount === 0)
430
+ return 'empty';
431
+ if (options.deniedCount > 0 || options.actionDisabledCount > 0)
432
+ return 'permission-limited';
433
+ return 'ready';
434
+ }
435
+ function normalizedTreeReason(reason) {
436
+ return reason?.trim() ?? '';
437
+ }
438
+ function visibleTreeNodes(nodes) {
439
+ const visible = [];
440
+ let collapsedDepth;
441
+ for (const node of nodes) {
442
+ const depth = treeNodeDepth(node);
443
+ if (collapsedDepth !== undefined) {
444
+ if (depth > collapsedDepth)
445
+ continue;
446
+ collapsedDepth = undefined;
447
+ }
448
+ visible.push(node);
449
+ if (treeNodeKind(node) === 'branch' && node.expanded !== true) {
450
+ collapsedDepth = depth;
451
+ }
452
+ }
453
+ return visible;
454
+ }
455
+ function treeNodeDepth(node) {
456
+ return Math.max(0, Math.trunc(node.depth ?? 0));
457
+ }
458
+ function treeNodeKind(node) {
459
+ return node.kind ?? 'leaf';
460
+ }
461
+ function treeNodeLazyStateFromItem(node) {
462
+ if (node.loading === true)
463
+ return 'loading';
464
+ if (node.loadError !== undefined && node.loadError.trim() !== '')
465
+ return 'error';
466
+ if (node.loaded === true)
467
+ return 'loaded';
468
+ return node.lazy === true ? 'idle' : undefined;
469
+ }