sh-ui-cli 0.116.0 → 0.118.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 (28) hide show
  1. package/data/changelog/versions.json +24 -0
  2. package/data/registry/flutter/registry.json +9 -0
  3. package/data/registry/flutter/widgets/sh_ui_tree.dart +428 -0
  4. package/data/registry/react/components/rich-text-editor/index.module.tsx +98 -8
  5. package/data/registry/react/components/rich-text-editor/index.tailwind.tsx +87 -4
  6. package/data/registry/react/components/rich-text-editor/index.tsx +98 -8
  7. package/data/registry/react/components/rich-text-editor/rich-text-editor.test.tsx +283 -0
  8. package/data/registry/react/components/table/index.module.tsx +69 -0
  9. package/data/registry/react/components/table/index.tailwind.tsx +68 -0
  10. package/data/registry/react/components/table/index.tsx +69 -0
  11. package/data/registry/react/components/table/styles.css +57 -0
  12. package/data/registry/react/components/table/styles.module.css +57 -0
  13. package/data/registry/react/components/table/table.test.tsx +59 -0
  14. package/data/registry/react/components/tree/flatten.test.ts +72 -0
  15. package/data/registry/react/components/tree/flatten.ts +69 -0
  16. package/data/registry/react/components/tree/index.module.tsx +215 -0
  17. package/data/registry/react/components/tree/index.tailwind.tsx +224 -0
  18. package/data/registry/react/components/tree/index.tsx +215 -0
  19. package/data/registry/react/components/tree/styles.css +69 -0
  20. package/data/registry/react/components/tree/styles.module.css +69 -0
  21. package/data/registry/react/components/tree/tree.test.tsx +110 -0
  22. package/data/registry/react/components/tree/types.ts +36 -0
  23. package/data/registry/react/peer-versions.json +9 -1
  24. package/data/registry/react/registry.json +78 -0
  25. package/data/registry/react/tokens-used.json +76 -1
  26. package/data/summaries/flutter.json +2 -1
  27. package/data/summaries/react.json +3 -1
  28. package/package.json +3 -3
@@ -0,0 +1,215 @@
1
+ import * as React from "react";
2
+ import styles from "./styles.module.css";
3
+ import { cn } from "@SH_UI_UTILS@";
4
+ import type { TreeNode, TreeProps } from "./types";
5
+ export type { TreeNode, TreeProps, TreeSize } from "./types";
6
+ import { flattenVisible, nextFocusable, prevFocusable, findByTypeahead } from "./flatten";
7
+
8
+ function useControllableSet(
9
+ controlled: string[] | undefined,
10
+ defaultValue: string[] | undefined,
11
+ onChange?: (ids: string[]) => void,
12
+ ) {
13
+ const [internal, setInternal] = React.useState<Set<string>>(
14
+ () => new Set(controlled ?? defaultValue ?? []),
15
+ );
16
+ const set = controlled ? new Set(controlled) : internal;
17
+ const update = (next: Set<string>) => {
18
+ if (!controlled) setInternal(next);
19
+ onChange?.([...next]);
20
+ };
21
+ return [set, update] as const;
22
+ }
23
+
24
+ export const Tree = React.forwardRef<HTMLDivElement, TreeProps>(function Tree(
25
+ {
26
+ nodes,
27
+ expandedIds,
28
+ defaultExpandedIds,
29
+ onExpandedChange,
30
+ selectedId,
31
+ defaultSelectedId,
32
+ onSelect,
33
+ renderLabel,
34
+ size = "md",
35
+ className,
36
+ },
37
+ ref,
38
+ ) {
39
+ const [expanded, setExpanded] = useControllableSet(
40
+ expandedIds,
41
+ defaultExpandedIds,
42
+ onExpandedChange,
43
+ );
44
+
45
+ const isSelectedControlled = selectedId !== undefined;
46
+ const [selInternal, setSelInternal] = React.useState<string | null>(
47
+ defaultSelectedId ?? null,
48
+ );
49
+ const selected = isSelectedControlled ? selectedId! : selInternal;
50
+ const selectNode = (id: string | null) => {
51
+ if (!isSelectedControlled) setSelInternal(id);
52
+ onSelect?.(id);
53
+ };
54
+
55
+ const visible = flattenVisible(nodes, expanded);
56
+ const visibleMap = React.useMemo(() => new Map(visible.map((v) => [v.id, v])), [visible]);
57
+ const [focusId, setFocusId] = React.useState<string | null>(visible[0]?.id ?? null);
58
+
59
+ React.useEffect(() => {
60
+ if (focusId && !visibleMap.has(focusId)) {
61
+ setFocusId(visible[0]?.id ?? null);
62
+ }
63
+ }, [visibleMap, focusId, visible]);
64
+
65
+ const toggle = (id: string) => {
66
+ const next = new Set(expanded);
67
+ if (next.has(id)) next.delete(id);
68
+ else next.add(id);
69
+ setExpanded(next);
70
+ };
71
+
72
+ const onItemClick = (n: TreeNode, hasChildren: boolean) => {
73
+ if (n.disabled) return;
74
+ setFocusId(n.id);
75
+ if (hasChildren) toggle(n.id);
76
+ selectNode(n.id);
77
+ };
78
+
79
+ const itemRefs = React.useRef<Map<string, HTMLDivElement>>(new Map());
80
+ const focusNode = (id: string) => {
81
+ setFocusId(id);
82
+ itemRefs.current.get(id)?.focus();
83
+ };
84
+
85
+ const onItemKeyDown = (e: React.KeyboardEvent, n: TreeNode, hasChildren: boolean) => {
86
+ switch (e.key) {
87
+ case "ArrowDown": {
88
+ e.preventDefault();
89
+ const nx = nextFocusable(visible, n.id);
90
+ if (nx) focusNode(nx.id);
91
+ break;
92
+ }
93
+ case "ArrowUp": {
94
+ e.preventDefault();
95
+ const pv = prevFocusable(visible, n.id);
96
+ if (pv) focusNode(pv.id);
97
+ break;
98
+ }
99
+ case "ArrowRight": {
100
+ e.preventDefault();
101
+ if (hasChildren && !expanded.has(n.id)) toggle(n.id);
102
+ else if (hasChildren) {
103
+ const first = n.children!.find((c) => !c.disabled);
104
+ if (first) focusNode(first.id);
105
+ }
106
+ break;
107
+ }
108
+ case "ArrowLeft": {
109
+ e.preventDefault();
110
+ if (hasChildren && expanded.has(n.id)) toggle(n.id);
111
+ else {
112
+ const meta = visibleMap.get(n.id);
113
+ if (meta?.parentId) focusNode(meta.parentId);
114
+ }
115
+ break;
116
+ }
117
+ case "Home": {
118
+ e.preventDefault();
119
+ const first = visible.find((v) => !v.disabled);
120
+ if (first) focusNode(first.id);
121
+ break;
122
+ }
123
+ case "End": {
124
+ e.preventDefault();
125
+ for (let i = visible.length - 1; i >= 0; i--)
126
+ if (!visible[i].disabled) {
127
+ focusNode(visible[i].id);
128
+ break;
129
+ }
130
+ break;
131
+ }
132
+ case "Enter":
133
+ case " ": {
134
+ e.preventDefault();
135
+ if (!n.disabled) selectNode(n.id);
136
+ break;
137
+ }
138
+ default: {
139
+ if (e.key.length === 1 && /\S/.test(e.key)) {
140
+ const hit = findByTypeahead(visible, e.key, n.id);
141
+ if (hit) focusNode(hit.id);
142
+ }
143
+ }
144
+ }
145
+ };
146
+
147
+ const renderNodes = (siblings: TreeNode[], level: number): React.ReactNode => (
148
+ <div role={level === 0 ? undefined : "group"} className={styles.tree__group}>
149
+ {siblings.map((n) => {
150
+ const hasChildren = !!n.children?.length;
151
+ const isExpanded = hasChildren && expanded.has(n.id);
152
+ const meta = visibleMap.get(n.id);
153
+ const depth = meta?.level ?? level;
154
+ return (
155
+ <div key={n.id}>
156
+ <div
157
+ role="treeitem"
158
+ ref={(el) => {
159
+ if (el) itemRefs.current.set(n.id, el);
160
+ else itemRefs.current.delete(n.id);
161
+ }}
162
+ aria-level={depth + 1}
163
+ aria-setsize={meta?.setSize}
164
+ aria-posinset={meta?.posInSet}
165
+ aria-expanded={hasChildren ? isExpanded : undefined}
166
+ aria-selected={selected === n.id}
167
+ aria-disabled={n.disabled || undefined}
168
+ aria-label={typeof n.label === "string" ? n.label : undefined}
169
+ tabIndex={focusId === n.id ? 0 : -1}
170
+ data-disabled={n.disabled || undefined}
171
+ className={cn(
172
+ styles.tree__item,
173
+ selected === n.id && styles["tree__item--selected"],
174
+ )}
175
+ onClick={() => onItemClick(n, hasChildren)}
176
+ onKeyDown={(e) => onItemKeyDown(e, n, hasChildren)}
177
+ >
178
+ <span
179
+ className={styles.tree__indent}
180
+ style={{ width: `calc(var(--space-4) * ${depth})` }}
181
+ aria-hidden
182
+ />
183
+ {hasChildren ? (
184
+ <span
185
+ className={styles.tree__chevron}
186
+ data-expanded={isExpanded || undefined}
187
+ aria-hidden
188
+ >
189
+
190
+ </span>
191
+ ) : (
192
+ <span className={cn(styles.tree__chevron, styles["tree__chevron--leaf"])} aria-hidden />
193
+ )}
194
+ {n.icon ? (
195
+ <span className={styles.tree__icon} aria-hidden>
196
+ {n.icon}
197
+ </span>
198
+ ) : null}
199
+ <span className={styles.tree__label}>
200
+ {renderLabel ? renderLabel(n) : n.label}
201
+ </span>
202
+ </div>
203
+ {isExpanded ? renderNodes(n.children!, level + 1) : null}
204
+ </div>
205
+ );
206
+ })}
207
+ </div>
208
+ );
209
+
210
+ return (
211
+ <div ref={ref} role="tree" data-size={size} className={cn(styles.tree, className)}>
212
+ {renderNodes(nodes, 0)}
213
+ </div>
214
+ );
215
+ });
@@ -0,0 +1,224 @@
1
+ import * as React from "react";
2
+ import { cn } from "@SH_UI_UTILS@";
3
+ import type { TreeNode, TreeProps } from "./types";
4
+ export type { TreeNode, TreeProps, TreeSize } from "./types";
5
+ import { flattenVisible, nextFocusable, prevFocusable, findByTypeahead } from "./flatten";
6
+
7
+ function useControllableSet(
8
+ controlled: string[] | undefined,
9
+ defaultValue: string[] | undefined,
10
+ onChange?: (ids: string[]) => void,
11
+ ) {
12
+ const [internal, setInternal] = React.useState<Set<string>>(
13
+ () => new Set(controlled ?? defaultValue ?? []),
14
+ );
15
+ const set = controlled ? new Set(controlled) : internal;
16
+ const update = (next: Set<string>) => {
17
+ if (!controlled) setInternal(next);
18
+ onChange?.([...next]);
19
+ };
20
+ return [set, update] as const;
21
+ }
22
+
23
+ export const Tree = React.forwardRef<HTMLDivElement, TreeProps>(function Tree(
24
+ {
25
+ nodes,
26
+ expandedIds,
27
+ defaultExpandedIds,
28
+ onExpandedChange,
29
+ selectedId,
30
+ defaultSelectedId,
31
+ onSelect,
32
+ renderLabel,
33
+ size = "md",
34
+ className,
35
+ },
36
+ ref,
37
+ ) {
38
+ const [expanded, setExpanded] = useControllableSet(
39
+ expandedIds,
40
+ defaultExpandedIds,
41
+ onExpandedChange,
42
+ );
43
+
44
+ const isSelectedControlled = selectedId !== undefined;
45
+ const [selInternal, setSelInternal] = React.useState<string | null>(
46
+ defaultSelectedId ?? null,
47
+ );
48
+ const selected = isSelectedControlled ? selectedId! : selInternal;
49
+ const selectNode = (id: string | null) => {
50
+ if (!isSelectedControlled) setSelInternal(id);
51
+ onSelect?.(id);
52
+ };
53
+
54
+ const visible = flattenVisible(nodes, expanded);
55
+ const visibleMap = React.useMemo(() => new Map(visible.map((v) => [v.id, v])), [visible]);
56
+ const [focusId, setFocusId] = React.useState<string | null>(visible[0]?.id ?? null);
57
+
58
+ React.useEffect(() => {
59
+ if (focusId && !visibleMap.has(focusId)) {
60
+ setFocusId(visible[0]?.id ?? null);
61
+ }
62
+ }, [visibleMap, focusId, visible]);
63
+
64
+ const toggle = (id: string) => {
65
+ const next = new Set(expanded);
66
+ if (next.has(id)) next.delete(id);
67
+ else next.add(id);
68
+ setExpanded(next);
69
+ };
70
+
71
+ const onItemClick = (n: TreeNode, hasChildren: boolean) => {
72
+ if (n.disabled) return;
73
+ setFocusId(n.id);
74
+ if (hasChildren) toggle(n.id);
75
+ selectNode(n.id);
76
+ };
77
+
78
+ const itemRefs = React.useRef<Map<string, HTMLDivElement>>(new Map());
79
+ const focusNode = (id: string) => {
80
+ setFocusId(id);
81
+ itemRefs.current.get(id)?.focus();
82
+ };
83
+
84
+ const onItemKeyDown = (e: React.KeyboardEvent, n: TreeNode, hasChildren: boolean) => {
85
+ switch (e.key) {
86
+ case "ArrowDown": {
87
+ e.preventDefault();
88
+ const nx = nextFocusable(visible, n.id);
89
+ if (nx) focusNode(nx.id);
90
+ break;
91
+ }
92
+ case "ArrowUp": {
93
+ e.preventDefault();
94
+ const pv = prevFocusable(visible, n.id);
95
+ if (pv) focusNode(pv.id);
96
+ break;
97
+ }
98
+ case "ArrowRight": {
99
+ e.preventDefault();
100
+ if (hasChildren && !expanded.has(n.id)) toggle(n.id);
101
+ else if (hasChildren) {
102
+ const first = n.children!.find((c) => !c.disabled);
103
+ if (first) focusNode(first.id);
104
+ }
105
+ break;
106
+ }
107
+ case "ArrowLeft": {
108
+ e.preventDefault();
109
+ if (hasChildren && expanded.has(n.id)) toggle(n.id);
110
+ else {
111
+ const meta = visibleMap.get(n.id);
112
+ if (meta?.parentId) focusNode(meta.parentId);
113
+ }
114
+ break;
115
+ }
116
+ case "Home": {
117
+ e.preventDefault();
118
+ const first = visible.find((v) => !v.disabled);
119
+ if (first) focusNode(first.id);
120
+ break;
121
+ }
122
+ case "End": {
123
+ e.preventDefault();
124
+ for (let i = visible.length - 1; i >= 0; i--)
125
+ if (!visible[i].disabled) {
126
+ focusNode(visible[i].id);
127
+ break;
128
+ }
129
+ break;
130
+ }
131
+ case "Enter":
132
+ case " ": {
133
+ e.preventDefault();
134
+ if (!n.disabled) selectNode(n.id);
135
+ break;
136
+ }
137
+ default: {
138
+ if (e.key.length === 1 && /\S/.test(e.key)) {
139
+ const hit = findByTypeahead(visible, e.key, n.id);
140
+ if (hit) focusNode(hit.id);
141
+ }
142
+ }
143
+ }
144
+ };
145
+
146
+ const renderNodes = (siblings: TreeNode[], level: number): React.ReactNode => (
147
+ <div role={level === 0 ? undefined : "group"} className="flex flex-col">
148
+ {siblings.map((n) => {
149
+ const hasChildren = !!n.children?.length;
150
+ const isExpanded = hasChildren && expanded.has(n.id);
151
+ const meta = visibleMap.get(n.id);
152
+ const depth = meta?.level ?? level;
153
+ return (
154
+ <div key={n.id}>
155
+ <div
156
+ role="treeitem"
157
+ ref={(el) => {
158
+ if (el) itemRefs.current.set(n.id, el);
159
+ else itemRefs.current.delete(n.id);
160
+ }}
161
+ aria-level={depth + 1}
162
+ aria-setsize={meta?.setSize}
163
+ aria-posinset={meta?.posInSet}
164
+ aria-expanded={hasChildren ? isExpanded : undefined}
165
+ aria-selected={selected === n.id}
166
+ aria-disabled={n.disabled || undefined}
167
+ aria-label={typeof n.label === "string" ? n.label : undefined}
168
+ tabIndex={focusId === n.id ? 0 : -1}
169
+ data-disabled={n.disabled || undefined}
170
+ className={cn(
171
+ "flex items-center gap-[var(--space-2)] px-[var(--space-2)] py-[var(--space-2)] rounded-[calc(var(--radius)-2px)] cursor-pointer select-none transition-[background-color] duration-[var(--duration-fast)] hover:not-data-[disabled]:bg-background-muted focus-visible:outline-[length:var(--border-width-strong)] focus-visible:outline-ring focus-visible:outline-offset-[-2px] data-[disabled]:text-foreground-muted data-[disabled]:cursor-not-allowed motion-reduce:transition-none",
172
+ selected === n.id && "bg-background-muted font-medium",
173
+ )}
174
+ onClick={() => onItemClick(n, hasChildren)}
175
+ onKeyDown={(e) => onItemKeyDown(e, n, hasChildren)}
176
+ >
177
+ <span
178
+ style={{ width: `calc(var(--space-4) * ${depth})` }}
179
+ aria-hidden
180
+ />
181
+ {hasChildren ? (
182
+ <span
183
+ className="inline-flex w-[var(--space-4)] justify-center transition-transform duration-[var(--duration-fast)] data-[expanded]:rotate-90 motion-reduce:transition-none"
184
+ data-expanded={isExpanded || undefined}
185
+ aria-hidden
186
+ >
187
+
188
+ </span>
189
+ ) : (
190
+ <span
191
+ className="inline-flex w-[var(--space-4)] justify-center transition-transform duration-[var(--duration-fast)] data-[expanded]:rotate-90 motion-reduce:transition-none invisible"
192
+ aria-hidden
193
+ />
194
+ )}
195
+ {n.icon ? (
196
+ <span className="inline-flex" aria-hidden>
197
+ {n.icon}
198
+ </span>
199
+ ) : null}
200
+ <span className="min-w-0 [overflow-wrap:anywhere]">
201
+ {renderLabel ? renderLabel(n) : n.label}
202
+ </span>
203
+ </div>
204
+ {isExpanded ? renderNodes(n.children!, level + 1) : null}
205
+ </div>
206
+ );
207
+ })}
208
+ </div>
209
+ );
210
+
211
+ return (
212
+ <div
213
+ ref={ref}
214
+ role="tree"
215
+ data-size={size}
216
+ className={cn(
217
+ "flex flex-col w-full text-foreground text-[0.9375rem] data-[size=sm]:text-[length:var(--text-xs)]",
218
+ className,
219
+ )}
220
+ >
221
+ {renderNodes(nodes, 0)}
222
+ </div>
223
+ );
224
+ });
@@ -0,0 +1,215 @@
1
+ import * as React from "react";
2
+ import "./styles.css";
3
+ import { cn } from "@SH_UI_UTILS@";
4
+ import type { TreeNode, TreeProps } from "./types";
5
+ export type { TreeNode, TreeProps, TreeSize } from "./types";
6
+ import { flattenVisible, nextFocusable, prevFocusable, findByTypeahead } from "./flatten";
7
+
8
+ function useControllableSet(
9
+ controlled: string[] | undefined,
10
+ defaultValue: string[] | undefined,
11
+ onChange?: (ids: string[]) => void,
12
+ ) {
13
+ const [internal, setInternal] = React.useState<Set<string>>(
14
+ () => new Set(controlled ?? defaultValue ?? []),
15
+ );
16
+ const set = controlled ? new Set(controlled) : internal;
17
+ const update = (next: Set<string>) => {
18
+ if (!controlled) setInternal(next);
19
+ onChange?.([...next]);
20
+ };
21
+ return [set, update] as const;
22
+ }
23
+
24
+ export const Tree = React.forwardRef<HTMLDivElement, TreeProps>(function Tree(
25
+ {
26
+ nodes,
27
+ expandedIds,
28
+ defaultExpandedIds,
29
+ onExpandedChange,
30
+ selectedId,
31
+ defaultSelectedId,
32
+ onSelect,
33
+ renderLabel,
34
+ size = "md",
35
+ className,
36
+ },
37
+ ref,
38
+ ) {
39
+ const [expanded, setExpanded] = useControllableSet(
40
+ expandedIds,
41
+ defaultExpandedIds,
42
+ onExpandedChange,
43
+ );
44
+
45
+ const isSelectedControlled = selectedId !== undefined;
46
+ const [selInternal, setSelInternal] = React.useState<string | null>(
47
+ defaultSelectedId ?? null,
48
+ );
49
+ const selected = isSelectedControlled ? selectedId! : selInternal;
50
+ const selectNode = (id: string | null) => {
51
+ if (!isSelectedControlled) setSelInternal(id);
52
+ onSelect?.(id);
53
+ };
54
+
55
+ const visible = flattenVisible(nodes, expanded);
56
+ const visibleMap = React.useMemo(() => new Map(visible.map((v) => [v.id, v])), [visible]);
57
+ const [focusId, setFocusId] = React.useState<string | null>(visible[0]?.id ?? null);
58
+
59
+ React.useEffect(() => {
60
+ if (focusId && !visibleMap.has(focusId)) {
61
+ setFocusId(visible[0]?.id ?? null);
62
+ }
63
+ }, [visibleMap, focusId, visible]);
64
+
65
+ const toggle = (id: string) => {
66
+ const next = new Set(expanded);
67
+ if (next.has(id)) next.delete(id);
68
+ else next.add(id);
69
+ setExpanded(next);
70
+ };
71
+
72
+ const onItemClick = (n: TreeNode, hasChildren: boolean) => {
73
+ if (n.disabled) return;
74
+ setFocusId(n.id);
75
+ if (hasChildren) toggle(n.id);
76
+ selectNode(n.id);
77
+ };
78
+
79
+ const itemRefs = React.useRef<Map<string, HTMLDivElement>>(new Map());
80
+ const focusNode = (id: string) => {
81
+ setFocusId(id);
82
+ itemRefs.current.get(id)?.focus();
83
+ };
84
+
85
+ const onItemKeyDown = (e: React.KeyboardEvent, n: TreeNode, hasChildren: boolean) => {
86
+ switch (e.key) {
87
+ case "ArrowDown": {
88
+ e.preventDefault();
89
+ const nx = nextFocusable(visible, n.id);
90
+ if (nx) focusNode(nx.id);
91
+ break;
92
+ }
93
+ case "ArrowUp": {
94
+ e.preventDefault();
95
+ const pv = prevFocusable(visible, n.id);
96
+ if (pv) focusNode(pv.id);
97
+ break;
98
+ }
99
+ case "ArrowRight": {
100
+ e.preventDefault();
101
+ if (hasChildren && !expanded.has(n.id)) toggle(n.id);
102
+ else if (hasChildren) {
103
+ const first = n.children!.find((c) => !c.disabled);
104
+ if (first) focusNode(first.id);
105
+ }
106
+ break;
107
+ }
108
+ case "ArrowLeft": {
109
+ e.preventDefault();
110
+ if (hasChildren && expanded.has(n.id)) toggle(n.id);
111
+ else {
112
+ const meta = visibleMap.get(n.id);
113
+ if (meta?.parentId) focusNode(meta.parentId);
114
+ }
115
+ break;
116
+ }
117
+ case "Home": {
118
+ e.preventDefault();
119
+ const first = visible.find((v) => !v.disabled);
120
+ if (first) focusNode(first.id);
121
+ break;
122
+ }
123
+ case "End": {
124
+ e.preventDefault();
125
+ for (let i = visible.length - 1; i >= 0; i--)
126
+ if (!visible[i].disabled) {
127
+ focusNode(visible[i].id);
128
+ break;
129
+ }
130
+ break;
131
+ }
132
+ case "Enter":
133
+ case " ": {
134
+ e.preventDefault();
135
+ if (!n.disabled) selectNode(n.id);
136
+ break;
137
+ }
138
+ default: {
139
+ if (e.key.length === 1 && /\S/.test(e.key)) {
140
+ const hit = findByTypeahead(visible, e.key, n.id);
141
+ if (hit) focusNode(hit.id);
142
+ }
143
+ }
144
+ }
145
+ };
146
+
147
+ const renderNodes = (siblings: TreeNode[], level: number): React.ReactNode => (
148
+ <div role={level === 0 ? undefined : "group"} className="sh-ui-tree__group">
149
+ {siblings.map((n) => {
150
+ const hasChildren = !!n.children?.length;
151
+ const isExpanded = hasChildren && expanded.has(n.id);
152
+ const meta = visibleMap.get(n.id);
153
+ const depth = meta?.level ?? level;
154
+ return (
155
+ <div key={n.id}>
156
+ <div
157
+ role="treeitem"
158
+ ref={(el) => {
159
+ if (el) itemRefs.current.set(n.id, el);
160
+ else itemRefs.current.delete(n.id);
161
+ }}
162
+ aria-level={depth + 1}
163
+ aria-setsize={meta?.setSize}
164
+ aria-posinset={meta?.posInSet}
165
+ aria-expanded={hasChildren ? isExpanded : undefined}
166
+ aria-selected={selected === n.id}
167
+ aria-disabled={n.disabled || undefined}
168
+ aria-label={typeof n.label === "string" ? n.label : undefined}
169
+ tabIndex={focusId === n.id ? 0 : -1}
170
+ data-disabled={n.disabled || undefined}
171
+ className={cn(
172
+ "sh-ui-tree__item",
173
+ selected === n.id && "sh-ui-tree__item--selected",
174
+ )}
175
+ onClick={() => onItemClick(n, hasChildren)}
176
+ onKeyDown={(e) => onItemKeyDown(e, n, hasChildren)}
177
+ >
178
+ <span
179
+ className="sh-ui-tree__indent"
180
+ style={{ width: `calc(var(--space-4) * ${depth})` }}
181
+ aria-hidden
182
+ />
183
+ {hasChildren ? (
184
+ <span
185
+ className="sh-ui-tree__chevron"
186
+ data-expanded={isExpanded || undefined}
187
+ aria-hidden
188
+ >
189
+
190
+ </span>
191
+ ) : (
192
+ <span className="sh-ui-tree__chevron sh-ui-tree__chevron--leaf" aria-hidden />
193
+ )}
194
+ {n.icon ? (
195
+ <span className="sh-ui-tree__icon" aria-hidden>
196
+ {n.icon}
197
+ </span>
198
+ ) : null}
199
+ <span className="sh-ui-tree__label">
200
+ {renderLabel ? renderLabel(n) : n.label}
201
+ </span>
202
+ </div>
203
+ {isExpanded ? renderNodes(n.children!, level + 1) : null}
204
+ </div>
205
+ );
206
+ })}
207
+ </div>
208
+ );
209
+
210
+ return (
211
+ <div ref={ref} role="tree" data-size={size} className={cn("sh-ui-tree", className)}>
212
+ {renderNodes(nodes, 0)}
213
+ </div>
214
+ );
215
+ });