@gridui/tree 0.0.0-stage → 0.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/README.md CHANGED
@@ -1,3 +1,31 @@
1
- # Temporary Holding Version
1
+ # @gridui/tree
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A single-selection hierarchical tree with keyboard navigation, controlled expansion, and searchable labels. Tree owns interaction and row presentation; applications own data loading, navigation, and persistence.
4
+
5
+ ```tsx
6
+ import "@gridui/theme/styles.css";
7
+ import "@gridui/tree/styles.css";
8
+ import { Tree } from "@gridui/tree";
9
+
10
+ <Tree
11
+ label="Project files"
12
+ items={[
13
+ { id: "docs", label: "Documents", children: [{ id: "brief", label: "Project brief" }] },
14
+ { id: "assets", label: "Assets" },
15
+ ]}
16
+ defaultExpandedIds={["docs"]}
17
+ onSelectedIdChange={setSelectedFile}
18
+ />;
19
+ ```
20
+
21
+ Every `TreeNode` has a stable, unique non-empty `id`, `label`, optional `children`, and optional `disabled`. A non-text label requires `textValue`, which supplies its accessible name and typeahead text. Labels contain presentation rather than nested interactive controls. Empty children are a leaf; this component does not infer asynchronous loading or children that have not been supplied.
22
+
23
+ `expandedIds`/`onExpandedIdsChange` and `selectedId`/`onSelectedIdChange` independently control expansion and selection. Their uncontrolled counterparts are `defaultExpandedIds` and `defaultSelectedId`. `null` means no selection. Controlled consumers can reject a change. Collapsing a branch preserves descendant expansion and selection. Only visible descendants render. `empty` supplies optional content when the tree has no items.
24
+
25
+ The tree has one tab stop. Up/Down navigate visible items; Home/End jump to the first/last. Right expands a closed branch or enters its first child; Left collapses it or focuses its parent. Horizontal commands reverse in inherited RTL. Enter/Space select without making arrow navigation select. `*` expands available sibling branches. Printable keys find a visible name; consecutive input forms a prefix for 700 ms, repeated letters cycle matches, and `locale` controls case/accent-insensitive matching. IME composition is left to the input method.
26
+
27
+ Click a row to select, the disclosure marker to expand/collapse, or double-click a branch row to toggle it. Disabled items remain keyboard discoverable but cannot be selected or toggled. `disabled` disables activation for the whole tree while preserving inspection. When a focused descendant collapses, focus returns to the nearest visible ancestor; when it is removed, focus falls back to the first visible item. Updates repair only focus owned by a removed tree item; they do not steal focus after the user leaves the tree, including clicking a nonfocusable area. When an empty, focused tree receives items, focus moves to its first item.
28
+
29
+ `label` is the tree's required accessible name. Each item reports level, position, set size, expansion, selection, and disabled state. Native ul props and `ref` reach the root. Tree emits no hidden form field; applications submit their selected identifier explicitly. The component does not introduce routes, drag-and-drop, multiselect, or virtualization policy.
30
+
31
+ See [release history](https://github.com/sudo-ui/GridUI-React-Monorepo/tree/main/packages-core/tree/.changeset/history).
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @author WMXPY
3
+ * @package Tree
4
+ * @namespace Root
5
+ * @description Index
6
+ */
7
+ export type { TreeProps } from "./tree/tree";
8
+ export { Tree } from "./tree/tree";
9
+ export type { TreeNode } from "./tree/types";
package/dist/index.js ADDED
@@ -0,0 +1,379 @@
1
+ // src/tree/model.ts
2
+ var treeModel = (items, expanded) => {
3
+ const entries = /* @__PURE__ */ new Map();
4
+ const visible = [];
5
+ const visit = (nodes, parent, level, shown) => {
6
+ nodes.forEach((node, index) => {
7
+ if (!node.id || entries.has(node.id)) {
8
+ throw new Error("Tree items require unique, non-empty IDs.");
9
+ }
10
+ entries.set(node.id, {
11
+ node,
12
+ parent,
13
+ level,
14
+ position: index + 1,
15
+ count: nodes.length
16
+ });
17
+ if (shown) {
18
+ visible.push(node.id);
19
+ }
20
+ visit(node.children ?? [], node.id, level + 1, shown && expanded.has(node.id));
21
+ });
22
+ };
23
+ visit(items, null, 1, true);
24
+ return {
25
+ entries,
26
+ visible
27
+ };
28
+ };
29
+ var visibleFocus = (id, model) => {
30
+ let candidate = id;
31
+ while (candidate !== null) {
32
+ if (model.visible.includes(candidate)) {
33
+ return candidate;
34
+ }
35
+ candidate = model.entries.get(candidate)?.parent ?? null;
36
+ }
37
+ return model.visible[0] ?? null;
38
+ };
39
+ var nodeText = (node) => {
40
+ return node.textValue ?? (typeof node.label === "string" ? node.label : "");
41
+ };
42
+ var typeaheadMatch = (query, active, model, locale) => {
43
+ const characters = Array.from(query);
44
+ const repeated = characters.length > 1 && characters.every((character) => {
45
+ return character === characters[0];
46
+ });
47
+ const term = repeated ? characters[0] ?? "" : query;
48
+ const collator = new Intl.Collator(locale, {
49
+ usage: "search",
50
+ sensitivity: "base"
51
+ });
52
+ const current = model.visible.indexOf(active);
53
+ const offset = term.length === 1 ? 1 : 0;
54
+ for (let index = offset; index < model.visible.length + offset; index++) {
55
+ const id = model.visible[(current + index + model.visible.length) % model.visible.length];
56
+ const node = id === void 0 ? void 0 : model.entries.get(id)?.node;
57
+ if (node && id && collator.compare(nodeText(node).slice(0, term.length), term) === 0) {
58
+ return id;
59
+ }
60
+ }
61
+ return null;
62
+ };
63
+ var treeKeyAction = (key, id, model, expanded, reverse) => {
64
+ const entry = model.entries.get(id);
65
+ if (!entry) {
66
+ return null;
67
+ }
68
+ const index = model.visible.indexOf(id);
69
+ let target;
70
+ if (key === "ArrowDown") {
71
+ target = model.visible[Math.min(index + 1, model.visible.length - 1)];
72
+ } else if (key === "ArrowUp") {
73
+ target = model.visible[Math.max(index - 1, 0)];
74
+ } else if (key === "Home") {
75
+ target = model.visible[0];
76
+ } else if (key === "End") {
77
+ target = model.visible.at(-1);
78
+ } else if (key === (reverse ? "ArrowLeft" : "ArrowRight")) {
79
+ if (entry.node.children?.length) {
80
+ if (!expanded.has(id)) {
81
+ return {
82
+ kind: "expand",
83
+ id
84
+ };
85
+ }
86
+ target = entry.node.children[0]?.id;
87
+ }
88
+ } else if (key === (reverse ? "ArrowRight" : "ArrowLeft")) {
89
+ if (entry.node.children?.length && expanded.has(id)) {
90
+ return {
91
+ kind: "collapse",
92
+ id
93
+ };
94
+ }
95
+ target = entry.parent ?? void 0;
96
+ } else if (key === "Enter" || key === " ") {
97
+ return {
98
+ kind: "select",
99
+ id
100
+ };
101
+ } else if (key === "*") {
102
+ return {
103
+ kind: "siblings",
104
+ id
105
+ };
106
+ } else {
107
+ return null;
108
+ }
109
+ return {
110
+ kind: "focus",
111
+ id: target ?? id
112
+ };
113
+ };
114
+
115
+ // src/tree/tree.tsx
116
+ import { isComposingKeyEvent } from "@gridui/interaction";
117
+ import { cn } from "@gridui/style-utils";
118
+ import { ChevronDown, ChevronRight } from "lucide-react";
119
+ import { useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
120
+ import { jsx, jsxs } from "react/jsx-runtime";
121
+ var Tree = (props) => {
122
+ const {
123
+ items,
124
+ label,
125
+ expandedIds,
126
+ defaultExpandedIds = [],
127
+ onExpandedIdsChange,
128
+ selectedId,
129
+ defaultSelectedId = null,
130
+ onSelectedIdChange,
131
+ disabled = false,
132
+ locale,
133
+ empty,
134
+ className,
135
+ onKeyDown,
136
+ ref,
137
+ ...restProps
138
+ } = props;
139
+ const [
140
+ localExpanded,
141
+ setLocalExpanded
142
+ ] = useState(defaultExpandedIds);
143
+ const [
144
+ localSelected,
145
+ setLocalSelected
146
+ ] = useState(defaultSelectedId);
147
+ const [
148
+ active,
149
+ setActive
150
+ ] = useState(selectedId ?? defaultSelectedId);
151
+ const root = useRef(null);
152
+ const rows = useRef(/* @__PURE__ */ new Map());
153
+ const removedFocus = useRef(null);
154
+ const search = useRef({
155
+ text: "",
156
+ time: 0
157
+ });
158
+ const expanded = new Set(expandedIds ?? localExpanded);
159
+ const selected = selectedId === void 0 ? localSelected : selectedId;
160
+ const model = treeModel(items, expanded);
161
+ const focused = visibleFocus(active, model);
162
+ useImperativeHandle(ref, () => {
163
+ return root.current;
164
+ });
165
+ useLayoutEffect(() => {
166
+ const previous = removedFocus.current;
167
+ removedFocus.current = null;
168
+ const element = root.current;
169
+ if (!element) {
170
+ return;
171
+ }
172
+ const document = element.ownerDocument;
173
+ const activeElement = document.activeElement;
174
+ const populatedFocusedRoot = activeElement === element && focused !== null;
175
+ const removedOwnedFocus = previous && !previous.isConnected && (activeElement === document.body || element.contains(activeElement));
176
+ if (populatedFocusedRoot || removedOwnedFocus) {
177
+ if (focused) {
178
+ rows.current.get(focused)?.focus({ preventScroll: true });
179
+ } else {
180
+ element.focus({ preventScroll: true });
181
+ }
182
+ }
183
+ });
184
+ const focus = (id) => {
185
+ rows.current.get(id)?.focus();
186
+ };
187
+ const changeExpanded = (next) => {
188
+ const values = [...next];
189
+ if (expandedIds === void 0) {
190
+ setLocalExpanded(values);
191
+ }
192
+ onExpandedIdsChange?.(values);
193
+ };
194
+ const toggle = (id, open) => {
195
+ if (disabled || model.entries.get(id)?.node.disabled || expanded.has(id) === open) {
196
+ return;
197
+ }
198
+ const next = new Set(expanded);
199
+ if (open) {
200
+ next.add(id);
201
+ } else {
202
+ next.delete(id);
203
+ }
204
+ changeExpanded(next);
205
+ };
206
+ const select = (id) => {
207
+ if (disabled || model.entries.get(id)?.node.disabled || selected === id) {
208
+ return;
209
+ }
210
+ if (selectedId === void 0) {
211
+ setLocalSelected(id);
212
+ }
213
+ onSelectedIdChange?.(id);
214
+ };
215
+ const renderItems = (nodes) => {
216
+ return nodes.map((node) => {
217
+ const entry = model.entries.get(node.id);
218
+ const branch = Boolean(node.children?.length);
219
+ const open = branch && expanded.has(node.id);
220
+ const unavailable = disabled || node.disabled;
221
+ return /* @__PURE__ */ jsxs(
222
+ "li",
223
+ {
224
+ role: "treeitem",
225
+ "aria-label": nodeText(node),
226
+ "aria-level": entry?.level,
227
+ "aria-posinset": entry?.position,
228
+ "aria-setsize": entry?.count,
229
+ "aria-expanded": branch ? open : void 0,
230
+ "aria-selected": selected === node.id,
231
+ "aria-disabled": unavailable || void 0,
232
+ tabIndex: focused === node.id ? 0 : -1,
233
+ className: "gridui-tree-item",
234
+ ref: (element) => {
235
+ if (element) {
236
+ rows.current.set(node.id, element);
237
+ } else {
238
+ const previous = rows.current.get(node.id);
239
+ if (previous && previous.ownerDocument.activeElement === previous) {
240
+ removedFocus.current = previous;
241
+ }
242
+ rows.current.delete(node.id);
243
+ }
244
+ },
245
+ onFocus: (event) => {
246
+ if (event.target === event.currentTarget) {
247
+ setActive(node.id);
248
+ }
249
+ },
250
+ children: [
251
+ /* @__PURE__ */ jsxs(
252
+ "div",
253
+ {
254
+ className: "gridui-tree-row",
255
+ onClick: () => {
256
+ focus(node.id);
257
+ select(node.id);
258
+ },
259
+ onDoubleClick: () => {
260
+ if (branch) {
261
+ toggle(node.id, !open);
262
+ }
263
+ },
264
+ children: [
265
+ /* @__PURE__ */ jsx(
266
+ "span",
267
+ {
268
+ "aria-hidden": "true",
269
+ className: "gridui-tree-disclosure",
270
+ onClick: (event) => {
271
+ if (branch) {
272
+ event.stopPropagation();
273
+ focus(node.id);
274
+ toggle(node.id, !open);
275
+ }
276
+ },
277
+ onDoubleClick: (event) => {
278
+ event.stopPropagation();
279
+ },
280
+ children: branch && (open ? /* @__PURE__ */ jsx(ChevronDown, {}) : /* @__PURE__ */ jsx(
281
+ ChevronRight,
282
+ {
283
+ className: "gridui-tree-chevron"
284
+ }
285
+ ))
286
+ }
287
+ ),
288
+ /* @__PURE__ */ jsx(
289
+ "span",
290
+ {
291
+ className: "gridui-tree-label",
292
+ children: node.label
293
+ }
294
+ )
295
+ ]
296
+ }
297
+ ),
298
+ open && /* @__PURE__ */ jsx(
299
+ "ul",
300
+ {
301
+ role: "group",
302
+ className: "gridui-tree-group",
303
+ children: renderItems(node.children ?? [])
304
+ }
305
+ )
306
+ ]
307
+ },
308
+ node.id
309
+ );
310
+ });
311
+ };
312
+ return /* @__PURE__ */ jsxs(
313
+ "ul",
314
+ {
315
+ ...restProps,
316
+ ref: root,
317
+ role: "tree",
318
+ "aria-label": label,
319
+ "aria-disabled": disabled || void 0,
320
+ tabIndex: focused === null ? 0 : -1,
321
+ className: cn("gridui-tree", className),
322
+ onKeyDown: (event) => {
323
+ onKeyDown?.(event);
324
+ if (event.defaultPrevented || isComposingKeyEvent(event.nativeEvent) || event.altKey || event.ctrlKey || event.metaKey || !focused) {
325
+ return;
326
+ }
327
+ const action = treeKeyAction(event.key, focused, model, expanded, getComputedStyle(event.currentTarget).direction === "rtl");
328
+ if (action) {
329
+ event.preventDefault();
330
+ search.current.text = "";
331
+ if (action.kind === "focus") {
332
+ focus(action.id);
333
+ } else if (action.kind === "select") {
334
+ select(action.id);
335
+ } else if (action.kind === "expand" || action.kind === "collapse") {
336
+ toggle(action.id, action.kind === "expand");
337
+ } else if (!disabled) {
338
+ const next = new Set(expanded);
339
+ const parent = model.entries.get(action.id)?.parent;
340
+ for (const [
341
+ id,
342
+ entry
343
+ ] of model.entries) {
344
+ if (entry.parent === parent && entry.node.children?.length && !entry.node.disabled) {
345
+ next.add(id);
346
+ }
347
+ }
348
+ if (next.size !== expanded.size) {
349
+ changeExpanded(next);
350
+ }
351
+ }
352
+ } else if (event.key.length === 1) {
353
+ const now = Date.now();
354
+ search.current.text = now - search.current.time > 700 ? event.key : search.current.text + event.key;
355
+ search.current.time = now;
356
+ const match = typeaheadMatch(search.current.text, focused, model, locale);
357
+ if (match) {
358
+ event.preventDefault();
359
+ focus(match);
360
+ }
361
+ }
362
+ },
363
+ children: [
364
+ renderItems(items),
365
+ items.length === 0 && empty !== void 0 && /* @__PURE__ */ jsx(
366
+ "li",
367
+ {
368
+ role: "none",
369
+ children: empty
370
+ }
371
+ )
372
+ ]
373
+ }
374
+ );
375
+ };
376
+ export {
377
+ Tree
378
+ };
379
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/tree/model.ts", "../src/tree/tree.tsx"],
4
+ "sourcesContent": ["/**\n * @author WMXPY\n * @package Tree\n * @namespace Tree\n * @description Model\n */\n\nimport type { TreeNode } from \"./types\";\n\nexport type TreeEntry = {\n readonly node: TreeNode;\n readonly parent: string | null;\n readonly level: number;\n readonly position: number;\n readonly count: number;\n};\nexport type TreeModel = {\n readonly entries: ReadonlyMap<string, TreeEntry>;\n readonly visible: readonly string[];\n};\nexport type TreeAction = {\n readonly kind: \"focus\" | \"expand\" | \"collapse\" | \"select\" | \"siblings\";\n readonly id: string;\n};\n\nexport const treeModel = (items: readonly TreeNode[], expanded: ReadonlySet<string>): TreeModel => {\n const entries = new Map<string, TreeEntry>();\n const visible: string[] = [];\n const visit = (nodes: readonly TreeNode[], parent: string | null, level: number, shown: boolean): void => {\n nodes.forEach((node, index) => {\n if (!node.id || entries.has(node.id)) {\n throw new Error(\"Tree items require unique, non-empty IDs.\");\n }\n entries.set(node.id, {\n node,\n parent,\n level,\n position: index + 1,\n count: nodes.length,\n });\n if (shown) {\n visible.push(node.id);\n }\n visit(node.children ?? [], node.id, level + 1, shown && expanded.has(node.id));\n });\n };\n visit(items, null, 1, true);\n return {\n entries,\n visible,\n };\n};\n\nexport const visibleFocus = (id: string | null, model: TreeModel): string | null => {\n let candidate = id;\n while (candidate !== null) {\n if (model.visible.includes(candidate)) {\n return candidate;\n }\n candidate = model.entries.get(candidate)?.parent ?? null;\n }\n return model.visible[0] ?? null;\n};\n\nexport const nodeText = (node: TreeNode): string => {\n return node.textValue ?? (typeof node.label === \"string\" ? node.label : \"\");\n};\n\nexport const typeaheadMatch = (query: string, active: string, model: TreeModel, locale?: string): string | null => {\n const characters = Array.from(query);\n const repeated = characters.length > 1 && characters.every((character) => {\n return character === characters[0];\n });\n const term = repeated ? characters[0] ?? \"\" : query;\n const collator = new Intl.Collator(locale, {\n usage: \"search\",\n sensitivity: \"base\",\n });\n const current = model.visible.indexOf(active);\n const offset = term.length === 1 ? 1 : 0;\n for (let index = offset; index < model.visible.length + offset; index++) {\n const id = model.visible[(current + index + model.visible.length) % model.visible.length];\n const node = id === undefined ? undefined : model.entries.get(id)?.node;\n if (node && id && collator.compare(nodeText(node).slice(0, term.length), term) === 0) {\n return id;\n }\n }\n return null;\n};\n\nexport const treeKeyAction = (key: string, id: string, model: TreeModel, expanded: ReadonlySet<string>, reverse: boolean): TreeAction | null => {\n const entry = model.entries.get(id);\n if (!entry) {\n return null;\n }\n const index = model.visible.indexOf(id);\n let target: string | undefined;\n if (key === \"ArrowDown\") {\n target = model.visible[Math.min(index + 1, model.visible.length - 1)];\n } else if (key === \"ArrowUp\") {\n target = model.visible[Math.max(index - 1, 0)];\n } else if (key === \"Home\") {\n target = model.visible[0];\n } else if (key === \"End\") {\n target = model.visible.at(-1);\n } else if (key === (reverse ? \"ArrowLeft\" : \"ArrowRight\")) {\n if (entry.node.children?.length) {\n if (!expanded.has(id)) {\n return {\n kind: \"expand\",\n id,\n };\n }\n target = entry.node.children[0]?.id;\n }\n } else if (key === (reverse ? \"ArrowRight\" : \"ArrowLeft\")) {\n if (entry.node.children?.length && expanded.has(id)) {\n return {\n kind: \"collapse\",\n id,\n };\n }\n target = entry.parent ?? undefined;\n } else if (key === \"Enter\" || key === \" \") {\n return {\n kind: \"select\",\n id,\n };\n } else if (key === \"*\") {\n return {\n kind: \"siblings\",\n id,\n };\n } else {\n return null;\n }\n return {\n kind: \"focus\",\n id: target ?? id,\n };\n};\n", "/**\n * @author WMXPY\n * @package Tree\n * @namespace Tree\n * @description Tree\n */\n\nimport { nodeText, treeKeyAction, treeModel, typeaheadMatch, visibleFocus } from \"./model\";\nimport type { TreeNode } from \"./types\";\nimport { isComposingKeyEvent } from \"@gridui/interaction\";\nimport { cn } from \"@gridui/style-utils\";\nimport { ChevronDown, ChevronRight } from \"lucide-react\";\nimport { type ComponentPropsWithRef, type FC, type ReactNode, useImperativeHandle, useLayoutEffect, useRef, useState } from \"react\";\n\nexport type TreeProps = Omit<ComponentPropsWithRef<\"ul\">, \"children\" | \"role\" | \"tabIndex\" | \"aria-label\"> & {\n readonly items: readonly TreeNode[];\n readonly label: string;\n readonly expandedIds?: readonly string[];\n readonly defaultExpandedIds?: readonly string[];\n readonly onExpandedIdsChange?: (ids: string[]) => void;\n readonly selectedId?: string | null;\n readonly defaultSelectedId?: string | null;\n readonly onSelectedIdChange?: (id: string | null) => void;\n readonly disabled?: boolean;\n readonly locale?: string;\n readonly empty?: ReactNode;\n};\n\nexport const Tree: FC<TreeProps> = (props: TreeProps) => {\n const { items, label, expandedIds, defaultExpandedIds = [], onExpandedIdsChange,\n selectedId, defaultSelectedId = null, onSelectedIdChange, disabled = false, locale, empty,\n className, onKeyDown, ref, ...restProps } = props;\n const [\n localExpanded,\n setLocalExpanded,\n ] = useState<readonly string[]>(defaultExpandedIds);\n const [\n localSelected,\n setLocalSelected,\n ] = useState<string | null>(defaultSelectedId);\n const [\n active,\n setActive,\n ] = useState<string | null>(selectedId ?? defaultSelectedId);\n const root = useRef<HTMLUListElement>(null);\n const rows = useRef(new Map<string, HTMLLIElement>());\n const removedFocus = useRef<HTMLLIElement | null>(null);\n const search = useRef({\n text: \"\",\n time: 0,\n });\n const expanded = new Set(expandedIds ?? localExpanded);\n const selected = selectedId === undefined ? localSelected : selectedId;\n const model = treeModel(items, expanded);\n const focused = visibleFocus(active, model);\n useImperativeHandle(ref, () => {\n return root.current as HTMLUListElement;\n });\n useLayoutEffect(() => {\n const previous = removedFocus.current;\n removedFocus.current = null;\n const element = root.current;\n if (!element) {\n return;\n }\n const document = element.ownerDocument;\n const activeElement = document.activeElement;\n const populatedFocusedRoot = activeElement === element && focused !== null;\n const removedOwnedFocus = previous && !previous.isConnected\n && (activeElement === document.body || element.contains(activeElement));\n if (populatedFocusedRoot || removedOwnedFocus) {\n if (focused) {\n rows.current.get(focused)?.focus({ preventScroll: true });\n } else {\n element.focus({ preventScroll: true });\n }\n }\n });\n const focus = (id: string) => {\n rows.current.get(id)?.focus();\n };\n const changeExpanded = (next: Set<string>) => {\n const values = [...next];\n if (expandedIds === undefined) {\n setLocalExpanded(values);\n }\n onExpandedIdsChange?.(values);\n };\n const toggle = (id: string, open: boolean) => {\n if (disabled || model.entries.get(id)?.node.disabled || expanded.has(id) === open) {\n return;\n }\n const next = new Set(expanded);\n if (open) {\n next.add(id);\n } else {\n next.delete(id);\n }\n changeExpanded(next);\n };\n const select = (id: string) => {\n if (disabled || model.entries.get(id)?.node.disabled || selected === id) {\n return;\n }\n if (selectedId === undefined) {\n setLocalSelected(id);\n }\n onSelectedIdChange?.(id);\n };\n const renderItems = (nodes: readonly TreeNode[]): ReactNode => {\n return nodes.map((node) => {\n const entry = model.entries.get(node.id);\n const branch = Boolean(node.children?.length);\n const open = branch && expanded.has(node.id);\n const unavailable = disabled || node.disabled;\n return (<li\n key={node.id}\n role=\"treeitem\"\n aria-label={nodeText(node)}\n aria-level={entry?.level}\n aria-posinset={entry?.position}\n aria-setsize={entry?.count}\n aria-expanded={branch ? open : undefined}\n aria-selected={selected === node.id}\n aria-disabled={unavailable || undefined}\n tabIndex={focused === node.id ? 0 : -1}\n className=\"gridui-tree-item\"\n ref={(element) => {\n if (element) {\n rows.current.set(node.id, element);\n } else {\n const previous = rows.current.get(node.id);\n if (previous && previous.ownerDocument.activeElement === previous) {\n removedFocus.current = previous;\n }\n rows.current.delete(node.id);\n }\n }}\n onFocus={(event) => {\n if (event.target === event.currentTarget) {\n setActive(node.id);\n }\n }}\n >\n <div\n className=\"gridui-tree-row\"\n onClick={() => {\n focus(node.id);\n select(node.id);\n }}\n onDoubleClick={() => {\n if (branch) {\n toggle(node.id, !open);\n }\n }}\n >\n <span\n aria-hidden=\"true\"\n className=\"gridui-tree-disclosure\"\n onClick={(event) => {\n if (branch) {\n event.stopPropagation();\n focus(node.id);\n toggle(node.id, !open);\n }\n }}\n onDoubleClick={(event) => {\n event.stopPropagation();\n }}\n >\n {branch && (open ? (<ChevronDown />) : (<ChevronRight\n className=\"gridui-tree-chevron\"\n />))}\n </span>\n <span\n className=\"gridui-tree-label\"\n >\n {node.label}\n </span>\n </div>\n {open && (<ul\n role=\"group\"\n className=\"gridui-tree-group\"\n >\n {renderItems(node.children ?? [])}\n </ul>)}\n </li>);\n });\n };\n return (<ul\n {...restProps}\n ref={root}\n role=\"tree\"\n aria-label={label}\n aria-disabled={disabled || undefined}\n tabIndex={focused === null ? 0 : -1}\n className={cn(\"gridui-tree\", className)}\n onKeyDown={(event) => {\n onKeyDown?.(event);\n if (event.defaultPrevented || isComposingKeyEvent(event.nativeEvent) || event.altKey || event.ctrlKey || event.metaKey || !focused) {\n return;\n }\n const action = treeKeyAction(event.key, focused, model, expanded, getComputedStyle(event.currentTarget).direction === \"rtl\");\n if (action) {\n event.preventDefault();\n search.current.text = \"\";\n if (action.kind === \"focus\") {\n focus(action.id);\n } else if (action.kind === \"select\") {\n select(action.id);\n } else if (action.kind === \"expand\" || action.kind === \"collapse\") {\n toggle(action.id, action.kind === \"expand\");\n } else if (!disabled) {\n const next = new Set(expanded);\n const parent = model.entries.get(action.id)?.parent;\n for (const [\n id,\n entry,\n ] of model.entries) {\n if (entry.parent === parent && entry.node.children?.length && !entry.node.disabled) {\n next.add(id);\n }\n }\n if (next.size !== expanded.size) {\n changeExpanded(next);\n }\n }\n } else if (event.key.length === 1) {\n const now = Date.now();\n search.current.text = now - search.current.time > 700 ? event.key : search.current.text + event.key;\n search.current.time = now;\n const match = typeaheadMatch(search.current.text, focused, model, locale);\n if (match) {\n event.preventDefault();\n focus(match);\n }\n }\n }}\n >\n {renderItems(items)}\n {items.length === 0 && empty !== undefined && (<li\n role=\"none\"\n >\n {empty}\n </li>)}\n </ul>);\n};\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,69 @@
1
+ /*! tailwindcss v4.3.4 | MIT License | https://tailwindcss.com */
2
+ @layer theme, base, components, utilities;
3
+ @layer utilities;
4
+ @layer components {
5
+ .gridui-tree, .gridui-tree-group {
6
+ margin: 0;
7
+ padding: 0;
8
+ list-style: none;
9
+ }
10
+ .gridui-tree {
11
+ min-inline-size: 0;
12
+ color: var(--gridui-foreground);
13
+ }
14
+ .gridui-tree-group {
15
+ padding-inline-start: var(--gridui-space-24);
16
+ }
17
+ .gridui-tree-item {
18
+ outline-offset: calc(-1 * var(--gridui-focus-width));
19
+ }
20
+ .gridui-tree-row {
21
+ display: flex;
22
+ align-items: center;
23
+ min-block-size: var(--gridui-control-min-height);
24
+ padding-inline-end: var(--gridui-space-8);
25
+ gap: var(--gridui-space-4);
26
+ cursor: pointer;
27
+ }
28
+ .gridui-tree-label {
29
+ min-inline-size: 0;
30
+ overflow-wrap: anywhere;
31
+ }
32
+ .gridui-tree-disclosure {
33
+ display: inline-flex;
34
+ flex: none;
35
+ align-self: stretch;
36
+ align-items: center;
37
+ justify-content: center;
38
+ inline-size: var(--gridui-space-24);
39
+ }
40
+ .gridui-tree-disclosure svg {
41
+ inline-size: var(--gridui-icon-size);
42
+ block-size: var(--gridui-icon-size);
43
+ }
44
+ .gridui-tree-chevron:dir(rtl) {
45
+ transform: rotate(180deg);
46
+ }
47
+ .gridui-tree-item[aria-selected="true"] > .gridui-tree-row {
48
+ background: var(--gridui-surface);
49
+ box-shadow: inset 0 -1px var(--gridui-border);
50
+ }
51
+ .gridui-tree-item[aria-disabled="true"] > .gridui-tree-row {
52
+ color: var(--gridui-muted);
53
+ cursor: default;
54
+ }
55
+ .gridui-tree-item:focus-visible {
56
+ outline: none;
57
+ }
58
+ .gridui-tree-item:focus-visible > .gridui-tree-row {
59
+ position: relative;
60
+ z-index: var(--gridui-z-interaction);
61
+ outline: var(--gridui-focus-width) solid var(--gridui-focus-color);
62
+ outline-offset: calc(-1 * var(--gridui-focus-width));
63
+ }
64
+ @media (hover: hover) and (pointer: fine) {
65
+ .gridui-tree-item:not([aria-disabled="true"]) > .gridui-tree-row:hover {
66
+ background: var(--gridui-surface-hover);
67
+ }
68
+ }
69
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * @author WMXPY
3
+ * @package Tree
4
+ * @namespace Tree
5
+ * @description Model
6
+ */
7
+ import type { TreeNode } from "./types";
8
+ export type TreeEntry = {
9
+ readonly node: TreeNode;
10
+ readonly parent: string | null;
11
+ readonly level: number;
12
+ readonly position: number;
13
+ readonly count: number;
14
+ };
15
+ export type TreeModel = {
16
+ readonly entries: ReadonlyMap<string, TreeEntry>;
17
+ readonly visible: readonly string[];
18
+ };
19
+ export type TreeAction = {
20
+ readonly kind: "focus" | "expand" | "collapse" | "select" | "siblings";
21
+ readonly id: string;
22
+ };
23
+ export declare const treeModel: (items: readonly TreeNode[], expanded: ReadonlySet<string>) => TreeModel;
24
+ export declare const visibleFocus: (id: string | null, model: TreeModel) => string | null;
25
+ export declare const nodeText: (node: TreeNode) => string;
26
+ export declare const typeaheadMatch: (query: string, active: string, model: TreeModel, locale?: string) => string | null;
27
+ export declare const treeKeyAction: (key: string, id: string, model: TreeModel, expanded: ReadonlySet<string>, reverse: boolean) => TreeAction | null;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @author WMXPY
3
+ * @package Tree
4
+ * @namespace Tree
5
+ * @description Tree
6
+ */
7
+ import type { TreeNode } from "./types";
8
+ import { type ComponentPropsWithRef, type FC, type ReactNode } from "react";
9
+ export type TreeProps = Omit<ComponentPropsWithRef<"ul">, "children" | "role" | "tabIndex" | "aria-label"> & {
10
+ readonly items: readonly TreeNode[];
11
+ readonly label: string;
12
+ readonly expandedIds?: readonly string[];
13
+ readonly defaultExpandedIds?: readonly string[];
14
+ readonly onExpandedIdsChange?: (ids: string[]) => void;
15
+ readonly selectedId?: string | null;
16
+ readonly defaultSelectedId?: string | null;
17
+ readonly onSelectedIdChange?: (id: string | null) => void;
18
+ readonly disabled?: boolean;
19
+ readonly locale?: string;
20
+ readonly empty?: ReactNode;
21
+ };
22
+ export declare const Tree: FC<TreeProps>;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @author WMXPY
3
+ * @package Tree
4
+ * @namespace Tree
5
+ * @description Types
6
+ */
7
+ import type { ReactNode } from "react";
8
+ export type TreeNode = {
9
+ readonly id: string;
10
+ readonly children?: readonly TreeNode[];
11
+ readonly disabled?: boolean;
12
+ } & ({
13
+ readonly label: string;
14
+ readonly textValue?: string;
15
+ } | {
16
+ readonly label: ReactNode;
17
+ readonly textValue: string;
18
+ });
@@ -0,0 +1,35 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "name": "@gridui/tree",
4
+ "version": "0.0.0",
5
+ "category": "core",
6
+ "description": "GridUI React tree",
7
+ "usage": "A single-selection hierarchical tree with keyboard navigation, controlled expansion, and searchable labels. Tree owns interaction and row presentation; applications own data loading, navigation, and persistence.",
8
+ "documentation": [
9
+ "README.md"
10
+ ],
11
+ "exports": [
12
+ {
13
+ "name": "Tree",
14
+ "kind": "component",
15
+ "importPath": "@gridui/tree"
16
+ },
17
+ {
18
+ "name": "TreeNode",
19
+ "kind": "type",
20
+ "importPath": "@gridui/tree"
21
+ },
22
+ {
23
+ "name": "TreeProps",
24
+ "kind": "type",
25
+ "importPath": "@gridui/tree"
26
+ }
27
+ ],
28
+ "styles": [
29
+ "@gridui/theme/styles.css",
30
+ "@gridui/tree/styles.css"
31
+ ],
32
+ "examples": [
33
+ "https://github.com/sudo-ui/GridUI-React-Monorepo/blob/main/packages-core/tree/stories/tree.stories.tsx"
34
+ ]
35
+ }
package/package.json CHANGED
@@ -1,6 +1,54 @@
1
1
  {
2
2
  "name": "@gridui/tree",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.0.0",
4
+ "description": "GridUI React tree",
5
+ "license": "UNLICENSED",
6
+ "type": "module",
7
+ "files": [
8
+ "dist",
9
+ "README.md",
10
+ "docs"
11
+ ],
12
+ "sideEffects": [
13
+ "**/*.css"
14
+ ],
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.js"
19
+ },
20
+ "./styles.css": "./dist/styles.css"
21
+ },
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "peerDependencies": {
26
+ "react": "^19.2.0",
27
+ "react-dom": "^19.2.0"
28
+ },
29
+ "dependencies": {
30
+ "@gridui/interaction": "^1.1.2",
31
+ "@gridui/style-utils": "^2.2.0",
32
+ "@gridui/theme": "^2.5.1",
33
+ "lucide-react": "1.68.0"
34
+ },
35
+ "devDependencies": {
36
+ "@gridui-internal/configuration": "0.0.0-internal",
37
+ "@jest/globals": "30.5.2",
38
+ "@storybook/react-vite": "10.6.5",
39
+ "@types/jest": "30.0.0",
40
+ "@types/node": "26.6.6",
41
+ "@types/react": "19.3.0",
42
+ "@types/react-dom": "19.3.0",
43
+ "jest": "30.5.2",
44
+ "react": "19.3.1",
45
+ "react-dom": "19.3.1",
46
+ "typescript": "6.0.3"
47
+ },
48
+ "scripts": {
49
+ "build": "node ../../scripts/compilation/package.ts",
50
+ "compile": "tsc --project tsconfig.json",
51
+ "test": "jest",
52
+ "coverage": "jest --coverage"
53
+ }
6
54
  }