@kamod-ch/ui 1.2.0 → 1.3.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.
@@ -0,0 +1,921 @@
1
+ import { useHeightDisclosureContent } from './chunk-ISISDJ3Z.js';
2
+ import { cn } from './chunk-XIANSPQO.js';
3
+ import { useContext, useRef, useState, useMemo, useLayoutEffect } from 'preact/hooks';
4
+ import { signal } from '@preact/signals';
5
+ import { createContext, toChildArray } from 'preact';
6
+ import { tv } from 'tailwind-variants';
7
+ import { jsx, jsxs } from 'preact/jsx-runtime';
8
+
9
+ var TreeRegistry = class {
10
+ nodes = /* @__PURE__ */ new Map();
11
+ childrenByParent = /* @__PURE__ */ new Map();
12
+ orderCounter = 0;
13
+ version = signal(0);
14
+ bump() {
15
+ this.version.value += 1;
16
+ }
17
+ register(node) {
18
+ const existing = this.nodes.get(node.nodeId);
19
+ if (existing) {
20
+ existing.parentId = node.parentId;
21
+ existing.disabled = node.disabled;
22
+ existing.hasChildren = node.hasChildren;
23
+ existing.data = node.data;
24
+ existing.element = node.element;
25
+ existing.level = node.level;
26
+ this.rebuildChildren(node.parentId);
27
+ this.bump();
28
+ return;
29
+ }
30
+ this.nodes.set(node.nodeId, {
31
+ ...node,
32
+ order: this.orderCounter++,
33
+ isLast: false
34
+ });
35
+ this.rebuildChildren(node.parentId);
36
+ this.bump();
37
+ }
38
+ unregister(nodeId) {
39
+ const node = this.nodes.get(nodeId);
40
+ if (!node) return;
41
+ const parentId = node.parentId;
42
+ this.nodes.delete(nodeId);
43
+ this.rebuildChildren(parentId);
44
+ this.bump();
45
+ }
46
+ setElement(nodeId, element) {
47
+ const node = this.nodes.get(nodeId);
48
+ if (!node || node.element === element) return;
49
+ node.element = element;
50
+ this.bump();
51
+ }
52
+ getNode(nodeId) {
53
+ return this.nodes.get(nodeId);
54
+ }
55
+ getChildren(parentId) {
56
+ return this.childrenByParent.get(parentId) ?? [];
57
+ }
58
+ getParent(nodeId) {
59
+ const node = this.nodes.get(nodeId);
60
+ if (!node?.parentId) return null;
61
+ return this.nodes.get(node.parentId) ?? null;
62
+ }
63
+ isVisible(nodeId, expandedIds) {
64
+ let current = this.nodes.get(nodeId);
65
+ if (!current) return false;
66
+ while (current.parentId) {
67
+ if (!expandedIds.has(current.parentId)) return false;
68
+ current = this.nodes.get(current.parentId);
69
+ if (!current) return false;
70
+ }
71
+ return true;
72
+ }
73
+ getVisibleActivatableIds(expandedIds) {
74
+ const rootIds = this.getChildren(null);
75
+ const result = [];
76
+ const walk = (ids) => {
77
+ for (const id of ids) {
78
+ const node = this.nodes.get(id);
79
+ if (!node) continue;
80
+ if (!this.isVisible(id, expandedIds)) continue;
81
+ if (!node.disabled) result.push(id);
82
+ if (node.hasChildren && expandedIds.has(id)) {
83
+ walk(this.getChildren(id));
84
+ }
85
+ }
86
+ };
87
+ walk(rootIds);
88
+ return result;
89
+ }
90
+ rebuildChildren(parentId) {
91
+ const childIds = [...this.nodes.values()].filter((node) => node.parentId === parentId).sort((a, b) => a.order - b.order).map((node) => node.nodeId);
92
+ this.childrenByParent.set(parentId, childIds);
93
+ childIds.forEach((childId, index) => {
94
+ const node = this.nodes.get(childId);
95
+ if (node) node.isLast = index === childIds.length - 1;
96
+ });
97
+ }
98
+ };
99
+ var treeVariants = tv({
100
+ base: "w-full rounded-lg border border-border bg-background",
101
+ variants: {
102
+ variant: {
103
+ default: "",
104
+ outline: "border-2",
105
+ ghost: "border-transparent bg-transparent"
106
+ },
107
+ size: {
108
+ sm: "text-sm",
109
+ default: "",
110
+ lg: "text-lg"
111
+ }
112
+ },
113
+ defaultVariants: {
114
+ variant: "default",
115
+ size: "default"
116
+ }
117
+ });
118
+ var treeItemVariants = tv({
119
+ base: [
120
+ "group/tree-item relative flex min-w-0 cursor-default select-none items-center rounded-md outline-none transition-colors",
121
+ "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
122
+ "motion-reduce:transition-none"
123
+ ],
124
+ variants: {
125
+ variant: {
126
+ default: "hover:bg-accent/60 data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground",
127
+ outline: "hover:bg-accent/40 data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground",
128
+ ghost: "hover:bg-accent/50 data-[selected=true]:bg-accent/70 data-[selected=true]:text-accent-foreground"
129
+ },
130
+ size: {
131
+ sm: "min-h-7 gap-1.5 px-2 py-1 text-sm",
132
+ default: "min-h-8 gap-2 px-2.5 py-1.5 text-sm",
133
+ lg: "min-h-10 gap-2.5 px-3 py-2 text-base"
134
+ }
135
+ },
136
+ defaultVariants: {
137
+ variant: "default",
138
+ size: "default"
139
+ }
140
+ });
141
+ var TreeProviderContext = createContext(null);
142
+ var useTreeProvider = () => {
143
+ const context = useContext(TreeProviderContext);
144
+ if (!context) throw new Error("Tree components must be used within TreeProvider");
145
+ return context;
146
+ };
147
+ var TreeItemContext = createContext(null);
148
+ var useTreeItemContext = () => useContext(TreeItemContext);
149
+ var TreeProvider = ({
150
+ class: className,
151
+ variant = "default",
152
+ size = "default",
153
+ expandedIds: expandedIdsProp,
154
+ defaultExpandedIds = [],
155
+ onExpandedChange,
156
+ selectedIds: selectedIdsProp,
157
+ defaultSelectedIds = [],
158
+ onSelectionChange,
159
+ selectionMode = "none",
160
+ showLines = true,
161
+ showIcons = true,
162
+ animateExpand = true,
163
+ indent = 20,
164
+ icons,
165
+ children,
166
+ ...rest
167
+ }) => {
168
+ const registryRef = useRef(null);
169
+ if (!registryRef.current) registryRef.current = new TreeRegistry();
170
+ const registry = registryRef.current;
171
+ const isExpandedControlled = expandedIdsProp !== void 0;
172
+ const isSelectedControlled = selectedIdsProp !== void 0;
173
+ const [internalExpanded, setInternalExpanded] = useState(() => [...defaultExpandedIds]);
174
+ const [internalSelected, setInternalSelected] = useState(() => [...defaultSelectedIds]);
175
+ const expandedArray = isExpandedControlled ? [...expandedIdsProp] : internalExpanded;
176
+ const selectedArray = isSelectedControlled ? [...selectedIdsProp] : internalSelected;
177
+ const expandedIds = useMemo(() => signal(new Set(expandedArray)), []);
178
+ expandedIds.value = new Set(expandedArray);
179
+ const selectedIds = useMemo(() => signal(new Set(selectedArray)), []);
180
+ selectedIds.value = new Set(selectedArray);
181
+ const focusedId = useMemo(() => signal(null), []);
182
+ const tabbableId = useMemo(() => signal(null), []);
183
+ const setExpandedIds = (ids) => {
184
+ if (!isExpandedControlled) setInternalExpanded(ids);
185
+ expandedIds.value = new Set(ids);
186
+ onExpandedChange?.(ids);
187
+ };
188
+ const setSelectedIds = (ids) => {
189
+ if (!isSelectedControlled) setInternalSelected(ids);
190
+ selectedIds.value = new Set(ids);
191
+ onSelectionChange?.(ids);
192
+ };
193
+ const focusNode = (nodeId, options) => {
194
+ const node = registry.getNode(nodeId);
195
+ if (!node || node.disabled) return;
196
+ focusedId.value = nodeId;
197
+ tabbableId.value = nodeId;
198
+ if (options?.moveDOMFocus !== false && node.element) {
199
+ node.element.focus();
200
+ }
201
+ };
202
+ const collapseFocusCorrection = (branchId) => {
203
+ const focused = focusedId.value;
204
+ if (!focused || focused === branchId) return;
205
+ let current = registry.getNode(focused);
206
+ while (current) {
207
+ if (current.nodeId === branchId) {
208
+ focusNode(branchId);
209
+ return;
210
+ }
211
+ current = current.parentId ? registry.getNode(current.parentId) : void 0;
212
+ }
213
+ };
214
+ const toggleExpanded = (nodeId) => {
215
+ const next = new Set(expandedIds.value);
216
+ if (next.has(nodeId)) {
217
+ next.delete(nodeId);
218
+ setExpandedIds([...next]);
219
+ collapseFocusCorrection(nodeId);
220
+ } else {
221
+ next.add(nodeId);
222
+ setExpandedIds([...next]);
223
+ }
224
+ };
225
+ const selectNode = (nodeId, options) => {
226
+ if (selectionMode === "none") return;
227
+ const node = registry.getNode(nodeId);
228
+ if (!node || node.disabled) return;
229
+ const current = new Set(selectedIds.value);
230
+ const isSelected = current.has(nodeId);
231
+ if (selectionMode === "single") {
232
+ if (options?.toggle && isSelected) {
233
+ setSelectedIds([]);
234
+ return;
235
+ }
236
+ setSelectedIds([nodeId]);
237
+ return;
238
+ }
239
+ if (options?.additive) {
240
+ const next = new Set(current);
241
+ if (isSelected) next.delete(nodeId);
242
+ else next.add(nodeId);
243
+ setSelectedIds([...next]);
244
+ return;
245
+ }
246
+ if (options?.toggle && isSelected) {
247
+ current.delete(nodeId);
248
+ setSelectedIds([...current]);
249
+ return;
250
+ }
251
+ setSelectedIds([nodeId]);
252
+ };
253
+ const prevExpandedRef = useRef(expandedArray);
254
+ useLayoutEffect(() => {
255
+ const prev = new Set(prevExpandedRef.current);
256
+ const next = new Set(expandedArray);
257
+ for (const id of prev) {
258
+ if (!next.has(id)) collapseFocusCorrection(id);
259
+ }
260
+ prevExpandedRef.current = expandedArray;
261
+ });
262
+ const contextValue = {
263
+ registry,
264
+ expandedIds,
265
+ selectedIds,
266
+ selectionMode,
267
+ showLines,
268
+ showIcons,
269
+ animateExpand,
270
+ indent,
271
+ icons,
272
+ variant: variant ?? "default",
273
+ size: size ?? "default",
274
+ focusedId,
275
+ tabbableId,
276
+ isExpandedControlled,
277
+ isSelectedControlled,
278
+ setExpandedIds,
279
+ setSelectedIds,
280
+ toggleExpanded,
281
+ selectNode,
282
+ focusNode,
283
+ getNodeData: (nodeId) => registry.getNode(nodeId)?.data
284
+ };
285
+ return /* @__PURE__ */ jsx(TreeProviderContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(
286
+ "div",
287
+ {
288
+ "data-slot": "tree-provider",
289
+ "data-variant": variant ?? "default",
290
+ "data-size": size ?? "default",
291
+ class: cn("w-full", className),
292
+ ...rest,
293
+ children
294
+ }
295
+ ) });
296
+ };
297
+
298
+ // src/components/tree/tree-keyboard.ts
299
+ var isMetaLike = (event) => event.ctrlKey || event.metaKey;
300
+ var handleTreeKeyDown = (event, tree) => {
301
+ if (event.defaultPrevented) return;
302
+ const target = event.target;
303
+ const itemEl = target?.closest('[role="treeitem"]');
304
+ if (!itemEl) return;
305
+ const nodeId = itemEl.getAttribute("data-node-id");
306
+ if (!nodeId) return;
307
+ const visible = tree.registry.getVisibleActivatableIds(tree.expandedIds.value);
308
+ const index = visible.indexOf(nodeId);
309
+ if (index === -1) return;
310
+ const node = tree.registry.getNode(nodeId);
311
+ if (!node) return;
312
+ const moveFocus = (nextId) => {
313
+ event.preventDefault();
314
+ tree.focusNode(nextId);
315
+ };
316
+ switch (event.key) {
317
+ case "ArrowDown": {
318
+ if (index < visible.length - 1) moveFocus(visible[index + 1]);
319
+ break;
320
+ }
321
+ case "ArrowUp": {
322
+ if (index > 0) moveFocus(visible[index - 1]);
323
+ break;
324
+ }
325
+ case "Home": {
326
+ if (visible.length > 0) moveFocus(visible[0]);
327
+ break;
328
+ }
329
+ case "End": {
330
+ if (visible.length > 0) moveFocus(visible[visible.length - 1]);
331
+ break;
332
+ }
333
+ case "ArrowRight": {
334
+ event.preventDefault();
335
+ if (node.hasChildren) {
336
+ if (!tree.expandedIds.value.has(nodeId)) {
337
+ tree.toggleExpanded(nodeId);
338
+ } else {
339
+ const children = tree.registry.getChildren(nodeId).filter((id) => {
340
+ const child = tree.registry.getNode(id);
341
+ return child && !child.disabled && tree.registry.isVisible(id, tree.expandedIds.value);
342
+ });
343
+ if (children[0]) tree.focusNode(children[0]);
344
+ }
345
+ }
346
+ break;
347
+ }
348
+ case "ArrowLeft": {
349
+ event.preventDefault();
350
+ if (node.hasChildren && tree.expandedIds.value.has(nodeId)) {
351
+ tree.toggleExpanded(nodeId);
352
+ } else if (node.parentId) {
353
+ tree.focusNode(node.parentId);
354
+ }
355
+ break;
356
+ }
357
+ case "Enter":
358
+ case " ": {
359
+ if (event.key === " " && isMetaLike(event)) {
360
+ event.preventDefault();
361
+ tree.selectNode(nodeId, { additive: true, toggle: true });
362
+ break;
363
+ }
364
+ event.preventDefault();
365
+ if (node.hasChildren && !tree.expandedIds.value.has(nodeId)) {
366
+ tree.toggleExpanded(nodeId);
367
+ }
368
+ if (tree.selectionMode !== "none") {
369
+ tree.selectNode(nodeId, {
370
+ toggle: tree.selectionMode === "multiple"
371
+ });
372
+ }
373
+ break;
374
+ }
375
+ case "a":
376
+ case "A": {
377
+ if (!isMetaLike(event) || tree.selectionMode !== "multiple") break;
378
+ event.preventDefault();
379
+ tree.setSelectedIds(visible);
380
+ break;
381
+ }
382
+ }
383
+ };
384
+ var Tree = ({
385
+ class: className,
386
+ children,
387
+ "aria-label": ariaLabel,
388
+ "aria-labelledby": ariaLabelledBy,
389
+ ...rest
390
+ }) => {
391
+ const tree = useTreeProvider();
392
+ const rootRef = useRef(null);
393
+ if (!ariaLabel && !ariaLabelledBy) {
394
+ const env = globalThis.process?.env?.NODE_ENV;
395
+ if (env !== "production") {
396
+ console.warn(
397
+ "[kamod-ui] <Tree> requires an accessible name via aria-label or aria-labelledby."
398
+ );
399
+ }
400
+ }
401
+ const multiselectable = tree.selectionMode === "multiple";
402
+ return /* @__PURE__ */ jsx(
403
+ "div",
404
+ {
405
+ ref: rootRef,
406
+ role: "tree",
407
+ "aria-multiselectable": multiselectable ? true : void 0,
408
+ "aria-label": ariaLabel,
409
+ "aria-labelledby": ariaLabelledBy,
410
+ "data-slot": "tree",
411
+ "data-variant": tree.variant,
412
+ "data-size": tree.size,
413
+ class: cn(treeVariants({ variant: tree.variant, size: tree.size }), className),
414
+ onKeyDown: (event) => {
415
+ handleTreeKeyDown(event, tree);
416
+ },
417
+ onFocus: (event) => {
418
+ if (event.target !== event.currentTarget) return;
419
+ const visible = tree.registry.getVisibleActivatableIds(tree.expandedIds.value);
420
+ if (visible.length === 0) return;
421
+ const current = tree.tabbableId.value;
422
+ const target = current && visible.includes(current) ? current : visible[0];
423
+ tree.focusNode(target);
424
+ },
425
+ ...rest,
426
+ children
427
+ }
428
+ );
429
+ };
430
+ var indentCSSValue = (indent) => typeof indent === "number" ? `${indent}px` : indent;
431
+ var TreeLines = ({
432
+ level,
433
+ parentPath,
434
+ isLast,
435
+ indent,
436
+ class: className
437
+ }) => {
438
+ if (level <= 0) return null;
439
+ const indentPx = indentCSSValue(indent);
440
+ return /* @__PURE__ */ jsxs(
441
+ "div",
442
+ {
443
+ "aria-hidden": "true",
444
+ "data-slot": "tree-lines",
445
+ class: cn("pointer-events-none absolute inset-y-0 start-0", className),
446
+ children: [
447
+ parentPath.map((pathIsLast, pathIndex) => {
448
+ if (pathIsLast) return null;
449
+ return /* @__PURE__ */ jsx(
450
+ "span",
451
+ {
452
+ class: "absolute top-0 bottom-0 w-px bg-border",
453
+ style: { insetInlineStart: `calc(${pathIndex + 1} * ${indentPx})` }
454
+ },
455
+ `line-${pathIndex}`
456
+ );
457
+ }),
458
+ /* @__PURE__ */ jsx(
459
+ "span",
460
+ {
461
+ class: "absolute top-1/2 h-px bg-border",
462
+ style: {
463
+ insetInlineStart: `calc(${level - 1} * ${indentPx} + ${indentPx} / 2)`,
464
+ width: `calc(${indentPx} / 2)`
465
+ }
466
+ }
467
+ ),
468
+ !isLast ? /* @__PURE__ */ jsx(
469
+ "span",
470
+ {
471
+ class: "absolute top-0 bottom-0 w-px bg-border",
472
+ style: { insetInlineStart: `calc(${level} * ${indentPx})` }
473
+ }
474
+ ) : null
475
+ ]
476
+ }
477
+ );
478
+ };
479
+ var DefaultChevron = ({ expanded }) => /* @__PURE__ */ jsx(
480
+ "svg",
481
+ {
482
+ viewBox: "0 0 16 16",
483
+ width: "16",
484
+ height: "16",
485
+ fill: "none",
486
+ "aria-hidden": "true",
487
+ focusable: "false",
488
+ class: cn(
489
+ "size-4 shrink-0 origin-center text-muted-foreground transition-transform duration-200 motion-reduce:transition-none",
490
+ expanded && "rotate-90 rtl:-rotate-90"
491
+ ),
492
+ children: /* @__PURE__ */ jsx(
493
+ "path",
494
+ {
495
+ d: "M6 4l4 4-4 4",
496
+ stroke: "currentColor",
497
+ strokeWidth: "1.5",
498
+ strokeLinecap: "round",
499
+ strokeLinejoin: "round"
500
+ }
501
+ )
502
+ }
503
+ );
504
+ var DefaultBranchIcon = () => /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 16 16", width: "16", height: "16", fill: "none", "aria-hidden": "true", focusable: "false", children: [
505
+ /* @__PURE__ */ jsx(
506
+ "path",
507
+ {
508
+ d: "M2 4.5h8a2 2 0 012 2V12",
509
+ stroke: "currentColor",
510
+ strokeWidth: "1.5",
511
+ strokeLinecap: "round"
512
+ }
513
+ ),
514
+ /* @__PURE__ */ jsx("path", { d: "M12 12h2", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" })
515
+ ] });
516
+ var DefaultLeafIcon = () => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 16 16", width: "16", height: "16", fill: "none", "aria-hidden": "true", focusable: "false", children: /* @__PURE__ */ jsx("path", { d: "M4 8h8M8 4v8", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" }) });
517
+ var renderIconProp = (icon, state) => {
518
+ if (icon == null) return null;
519
+ if (typeof icon === "function") return icon(state);
520
+ return icon;
521
+ };
522
+ var resolveTreeIcon = ({
523
+ state,
524
+ icon,
525
+ expandedIcon,
526
+ selectedIcon,
527
+ disabledIcon,
528
+ providerIcons,
529
+ showFallback = true
530
+ }) => {
531
+ if (state.disabled && disabledIcon != null) return disabledIcon;
532
+ if (state.selected && selectedIcon != null) return selectedIcon;
533
+ if (state.branch && state.expanded && expandedIcon != null) return expandedIcon;
534
+ const nodeIcon = renderIconProp(icon, state);
535
+ if (nodeIcon != null) return nodeIcon;
536
+ if (providerIcons) {
537
+ if (state.branch) {
538
+ if (state.expanded && providerIcons.branchExpanded != null)
539
+ return providerIcons.branchExpanded;
540
+ if (providerIcons.branch != null) return providerIcons.branch;
541
+ } else if (providerIcons.leaf != null) {
542
+ return providerIcons.leaf;
543
+ }
544
+ }
545
+ if (!showFallback) return null;
546
+ return state.branch ? /* @__PURE__ */ jsx(DefaultBranchIcon, {}) : /* @__PURE__ */ jsx(DefaultLeafIcon, {});
547
+ };
548
+ var resolveTreeExpanderIcon = (expanded, providerIcons) => {
549
+ if (expanded && providerIcons?.expanderExpanded != null) return providerIcons.expanderExpanded;
550
+ if (!expanded && providerIcons?.expander != null) return providerIcons.expander;
551
+ return /* @__PURE__ */ jsx(DefaultChevron, { expanded });
552
+ };
553
+ var TreeNodeContext = createContext(null);
554
+ var useTreeNodeContext = () => {
555
+ const context = useContext(TreeNodeContext);
556
+ if (!context) throw new Error("Tree node subcomponents must be used within TreeNode");
557
+ return context;
558
+ };
559
+ var TreeNode = ({
560
+ nodeId,
561
+ disabled = false,
562
+ data,
563
+ icon,
564
+ expandedIcon,
565
+ selectedIcon,
566
+ disabledIcon,
567
+ children
568
+ }) => {
569
+ const tree = useTreeProvider();
570
+ const parentItem = useTreeItemContext();
571
+ const itemRef = useRef(null);
572
+ const childArray = toChildArray(children);
573
+ const hasChildren = childArray.some(
574
+ (child) => child != null && typeof child === "object" && "type" in child && typeof child.type === "function" && child.type.__kamodTreeContent === true
575
+ );
576
+ const level = parentItem ? parentItem.level + 1 : 1;
577
+ const parentPath = parentItem ? [...parentItem.parentPath, parentItem.isLast] : [];
578
+ tree.registry.version.value;
579
+ const nodeRecord = tree.registry.getNode(nodeId);
580
+ const isLast = nodeRecord?.isLast ?? false;
581
+ useLayoutEffect(() => {
582
+ const env = globalThis.process?.env?.NODE_ENV;
583
+ const existing = tree.registry.getNode(nodeId);
584
+ if (env !== "production" && existing && existing.element && existing.element !== itemRef.current) {
585
+ console.warn(`[kamod-ui] Duplicate Tree nodeId "${nodeId}" detected.`);
586
+ }
587
+ tree.registry.register({
588
+ nodeId,
589
+ parentId: parentItem?.nodeId ?? null,
590
+ disabled,
591
+ hasChildren,
592
+ data,
593
+ element: itemRef.current,
594
+ level
595
+ });
596
+ return () => {
597
+ tree.registry.unregister(nodeId);
598
+ };
599
+ }, [nodeId, parentItem?.nodeId, disabled, hasChildren, data, level, tree.registry]);
600
+ useLayoutEffect(() => {
601
+ const visible = tree.registry.getVisibleActivatableIds(tree.expandedIds.value);
602
+ if (visible.length === 0) {
603
+ tree.tabbableId.value = null;
604
+ return;
605
+ }
606
+ if (!tree.tabbableId.value || !visible.includes(tree.tabbableId.value)) {
607
+ tree.tabbableId.value = visible[0];
608
+ }
609
+ }, [tree.registry.version.value, tree.expandedIds.value, tree]);
610
+ const contextValue = {
611
+ nodeId,
612
+ disabled,
613
+ hasChildren,
614
+ data,
615
+ icon,
616
+ expandedIcon,
617
+ selectedIcon,
618
+ disabledIcon,
619
+ level,
620
+ parentPath,
621
+ isLast,
622
+ itemRef
623
+ };
624
+ return /* @__PURE__ */ jsx(TreeNodeContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx("div", { "data-slot": "tree-node", "data-node-id": nodeId, class: "relative min-w-0", children }) });
625
+ };
626
+ var TreeNodeTrigger = ({
627
+ class: className,
628
+ children,
629
+ onClick,
630
+ onFocus,
631
+ ...rest
632
+ }) => {
633
+ const tree = useTreeProvider();
634
+ const node = useTreeNodeContext();
635
+ useLayoutEffect(() => {
636
+ tree.registry.setElement(node.nodeId, node.itemRef.current);
637
+ });
638
+ const isExpanded = tree.expandedIds.value.has(node.nodeId);
639
+ const isSelected = tree.selectedIds.value.has(node.nodeId);
640
+ const isTabbable = tree.tabbableId.value === node.nodeId;
641
+ const handleItemClick = (event) => {
642
+ if (node.disabled) return;
643
+ tree.focusNode(node.nodeId);
644
+ if (tree.selectionMode !== "none") {
645
+ tree.selectNode(node.nodeId, {
646
+ toggle: tree.selectionMode === "multiple" && (event.metaKey || event.ctrlKey),
647
+ additive: tree.selectionMode === "multiple" && (event.metaKey || event.ctrlKey)
648
+ });
649
+ }
650
+ onClick?.(event);
651
+ };
652
+ return /* @__PURE__ */ jsxs(
653
+ "div",
654
+ {
655
+ ref: node.itemRef,
656
+ role: "treeitem",
657
+ "data-node-id": node.nodeId,
658
+ "data-slot": "tree-node-trigger",
659
+ "data-state": node.hasChildren ? isExpanded ? "open" : "closed" : "leaf",
660
+ "data-selected": isSelected ? "true" : void 0,
661
+ "data-disabled": node.disabled ? "" : void 0,
662
+ "data-level": node.level,
663
+ "aria-level": node.level,
664
+ "aria-expanded": node.hasChildren ? isExpanded : void 0,
665
+ "aria-selected": tree.selectionMode !== "none" ? isSelected : void 0,
666
+ "aria-disabled": node.disabled || void 0,
667
+ tabIndex: node.disabled ? -1 : isTabbable ? 0 : -1,
668
+ class: cn(
669
+ treeItemVariants({ variant: tree.variant, size: tree.size }),
670
+ "relative",
671
+ node.disabled && "pointer-events-none opacity-50",
672
+ className
673
+ ),
674
+ style: { paddingInlineStart: `calc(${node.level} * ${indentCSSValue(tree.indent)})` },
675
+ onClick: handleItemClick,
676
+ onFocus: (event) => {
677
+ tree.focusedId.value = node.nodeId;
678
+ tree.tabbableId.value = node.nodeId;
679
+ onFocus?.(event);
680
+ },
681
+ ...rest,
682
+ children: [
683
+ tree.showLines ? /* @__PURE__ */ jsx(
684
+ TreeLines,
685
+ {
686
+ level: node.level,
687
+ parentPath: node.parentPath,
688
+ isLast: node.isLast,
689
+ indent: tree.indent
690
+ }
691
+ ) : null,
692
+ children
693
+ ]
694
+ }
695
+ );
696
+ };
697
+ var TreeExpander = ({ class: className, ...rest }) => {
698
+ const tree = useTreeProvider();
699
+ const node = useTreeNodeContext();
700
+ const isExpanded = tree.expandedIds.value.has(node.nodeId);
701
+ const handleExpandToggle = (event) => {
702
+ event.stopPropagation();
703
+ if (node.disabled || !node.hasChildren) return;
704
+ tree.toggleExpanded(node.nodeId);
705
+ };
706
+ if (!node.hasChildren) {
707
+ return /* @__PURE__ */ jsx("span", { class: cn("inline-flex size-4 shrink-0", className), "aria-hidden": "true" });
708
+ }
709
+ return /* @__PURE__ */ jsx(
710
+ "span",
711
+ {
712
+ "data-slot": "tree-expander",
713
+ "data-expanded": isExpanded ? "true" : "false",
714
+ "data-disabled": node.disabled ? "" : void 0,
715
+ class: cn(
716
+ "relative z-[1] inline-flex size-4 shrink-0 cursor-pointer items-center justify-center",
717
+ className
718
+ ),
719
+ "aria-hidden": "true",
720
+ onClick: handleExpandToggle,
721
+ onKeyDown: (event) => {
722
+ if (event.key === "Enter" || event.key === " ") {
723
+ event.preventDefault();
724
+ event.stopPropagation();
725
+ handleExpandToggle(event);
726
+ }
727
+ rest.onKeyDown?.(event);
728
+ },
729
+ ...rest,
730
+ children: resolveTreeExpanderIcon(isExpanded, tree.icons)
731
+ }
732
+ );
733
+ };
734
+ var TreeIcon = ({
735
+ class: className,
736
+ icon: iconProp,
737
+ expandedIcon: expandedIconProp,
738
+ selectedIcon: selectedIconProp,
739
+ disabledIcon: disabledIconProp,
740
+ ...rest
741
+ }) => {
742
+ const tree = useTreeProvider();
743
+ const node = useTreeNodeContext();
744
+ if (!tree.showIcons) return null;
745
+ const isExpanded = tree.expandedIds.value.has(node.nodeId);
746
+ const isSelected = tree.selectedIds.value.has(node.nodeId);
747
+ const iconState = {
748
+ nodeId: node.nodeId,
749
+ branch: node.hasChildren,
750
+ expanded: isExpanded,
751
+ selected: isSelected,
752
+ disabled: node.disabled,
753
+ level: node.level
754
+ };
755
+ const hasIconSlot = iconProp != null || node.icon != null || node.expandedIcon != null || node.selectedIcon != null || node.disabledIcon != null || expandedIconProp != null || selectedIconProp != null || disabledIconProp != null || tree.icons != null;
756
+ if (!hasIconSlot) return null;
757
+ const content = resolveTreeIcon({
758
+ state: iconState,
759
+ icon: iconProp ?? node.icon,
760
+ expandedIcon: expandedIconProp ?? node.expandedIcon,
761
+ selectedIcon: selectedIconProp ?? node.selectedIcon,
762
+ disabledIcon: disabledIconProp ?? node.disabledIcon,
763
+ providerIcons: tree.icons,
764
+ showFallback: tree.icons != null
765
+ });
766
+ return /* @__PURE__ */ jsx(
767
+ "span",
768
+ {
769
+ "data-slot": "tree-icon",
770
+ "data-expanded": isExpanded ? "true" : "false",
771
+ "data-selected": isSelected ? "true" : void 0,
772
+ "data-disabled": node.disabled ? "" : void 0,
773
+ class: cn(
774
+ "relative z-[1] inline-flex size-4 shrink-0 items-center justify-center [&_svg]:size-4",
775
+ className
776
+ ),
777
+ "aria-hidden": "true",
778
+ ...rest,
779
+ children: content
780
+ }
781
+ );
782
+ };
783
+ var TreeLabel = ({ class: className, children, ...rest }) => /* @__PURE__ */ jsx(
784
+ "span",
785
+ {
786
+ "data-slot": "tree-label",
787
+ class: cn("relative z-[1] min-w-0 flex-1 truncate text-start", className),
788
+ ...rest,
789
+ children
790
+ }
791
+ );
792
+ var TreeNodeActions = ({
793
+ class: className,
794
+ children,
795
+ onClick,
796
+ onKeyDown,
797
+ ...rest
798
+ }) => /* @__PURE__ */ jsx(
799
+ "span",
800
+ {
801
+ "data-slot": "tree-node-actions",
802
+ class: cn("relative z-[1] ms-auto inline-flex shrink-0 items-center gap-1", className),
803
+ onClick: (event) => {
804
+ event.stopPropagation();
805
+ onClick?.(event);
806
+ },
807
+ onKeyDown: (event) => {
808
+ event.stopPropagation();
809
+ onKeyDown?.(event);
810
+ },
811
+ ...rest,
812
+ children
813
+ }
814
+ );
815
+ var TreeNodeContent = ({ children, class: className }) => {
816
+ const tree = useTreeProvider();
817
+ const node = useTreeNodeContext();
818
+ const isExpanded = tree.expandedIds.value.has(node.nodeId);
819
+ const {
820
+ allowRender,
821
+ isAnimatingClose,
822
+ isAnimatingOpen,
823
+ outerRef,
824
+ innerRef,
825
+ handleTransitionEnd
826
+ } = useHeightDisclosureContent({
827
+ open: isExpanded,
828
+ forceMount: !tree.animateExpand
829
+ });
830
+ const showBranch = node.hasChildren && (tree.animateExpand ? allowRender : isExpanded);
831
+ if (!node.hasChildren) return null;
832
+ const itemContextValue = {
833
+ nodeId: node.nodeId,
834
+ level: node.level,
835
+ parentPath: node.parentPath,
836
+ isLast: node.isLast
837
+ };
838
+ return /* @__PURE__ */ jsx(
839
+ "div",
840
+ {
841
+ role: "group",
842
+ "data-slot": "tree-node-content",
843
+ class: className,
844
+ hidden: !showBranch && !isAnimatingClose,
845
+ "aria-hidden": !isExpanded && !isAnimatingClose,
846
+ children: tree.animateExpand ? /* @__PURE__ */ jsx(
847
+ "div",
848
+ {
849
+ ref: outerRef,
850
+ "data-slot": "tree-node-content-outer",
851
+ "data-state": isExpanded ? "open" : "closed",
852
+ onTransitionEnd: handleTransitionEnd,
853
+ class: cn(
854
+ "min-h-0 overflow-hidden",
855
+ (isAnimatingOpen || isAnimatingClose) && "will-change-[height]"
856
+ ),
857
+ inert: !isExpanded || void 0,
858
+ children: /* @__PURE__ */ jsx("div", { ref: innerRef, "data-slot": "tree-node-content-inner", children: /* @__PURE__ */ jsx(TreeItemContext.Provider, { value: itemContextValue, children }) })
859
+ }
860
+ ) : isExpanded ? /* @__PURE__ */ jsx(TreeItemContext.Provider, { value: itemContextValue, children }) : null
861
+ }
862
+ );
863
+ };
864
+ TreeNodeContent.__kamodTreeContent = true;
865
+ var TreeItem = ({
866
+ nodeId,
867
+ label,
868
+ icon,
869
+ expandedIcon,
870
+ selectedIcon,
871
+ disabledIcon,
872
+ endContent,
873
+ data,
874
+ disabled = false,
875
+ class: className,
876
+ children,
877
+ ...rest
878
+ }) => {
879
+ const childArray = toChildArray(children);
880
+ const hasChildren = childArray.length > 0;
881
+ return /* @__PURE__ */ jsxs(
882
+ TreeNode,
883
+ {
884
+ nodeId,
885
+ disabled,
886
+ data,
887
+ icon,
888
+ expandedIcon,
889
+ selectedIcon,
890
+ disabledIcon,
891
+ children: [
892
+ /* @__PURE__ */ jsxs(TreeNodeTrigger, { class: className, ...rest, children: [
893
+ /* @__PURE__ */ jsx(TreeExpander, {}),
894
+ /* @__PURE__ */ jsx(TreeIcon, {}),
895
+ /* @__PURE__ */ jsx(TreeLabel, { children: label }),
896
+ endContent ? /* @__PURE__ */ jsx(TreeNodeActions, { children: endContent }) : null
897
+ ] }),
898
+ hasChildren ? /* @__PURE__ */ jsx(TreeNodeContent, { children }) : null
899
+ ]
900
+ }
901
+ );
902
+ };
903
+
904
+ // src/components/tree/index.ts
905
+ var tree_default = {
906
+ Provider: TreeProvider,
907
+ Root: Tree,
908
+ Item: TreeItem,
909
+ Node: TreeNode,
910
+ NodeTrigger: TreeNodeTrigger,
911
+ Expander: TreeExpander,
912
+ Icon: TreeIcon,
913
+ Label: TreeLabel,
914
+ NodeActions: TreeNodeActions,
915
+ NodeContent: TreeNodeContent,
916
+ Lines: TreeLines
917
+ };
918
+
919
+ export { Tree, TreeExpander, TreeIcon, TreeItem, TreeLabel, TreeLines, TreeNode, TreeNodeActions, TreeNodeContent, TreeNodeTrigger, TreeProvider, treeItemVariants, treeVariants, tree_default, useTreeItemContext, useTreeNodeContext, useTreeProvider };
920
+ //# sourceMappingURL=chunk-YMKC3H55.js.map
921
+ //# sourceMappingURL=chunk-YMKC3H55.js.map