shadcn-treeview 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.
package/README.md ADDED
@@ -0,0 +1,174 @@
1
+ # shadcn-treeview
2
+
3
+ A lightweight, accessible, and customizable tree view component for React. Built on top of `react-accessible-treeview` with Shadcn UI styling.
4
+
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ ## Features
8
+
9
+ - **Accessible** - Built on `react-accessible-treeview` with full keyboard navigation and ARIA support
10
+ - **Customizable** - Flexible styling with Tailwind CSS classes
11
+ - **Lightweight** - Minimal dependencies
12
+ - **TypeScript** - Full TypeScript support with exported types
13
+ - **Inline Editing** - Built-in support for renaming items
14
+ - **Multi-select** - Support for single and multi-selection modes
15
+
16
+ ## Installation
17
+
18
+ ### Shadcn CLI (Recommended)
19
+
20
+ ```bash
21
+ npx shadcn@latest add https://shadcn-treeview.achromatic.dev/registry/tree-view.json
22
+ ```
23
+
24
+ ### Package Manager
25
+
26
+ ```bash
27
+ npm install shadcn-treeview
28
+ ```
29
+
30
+ ## Quick Start
31
+
32
+ ### 1. Import the components
33
+
34
+ ```tsx
35
+ import { TreeView, TreeViewItem, flattenTree } from "shadcn-treeview";
36
+ ```
37
+
38
+ ### 2. Create your tree data
39
+
40
+ ```tsx
41
+ const data = flattenTree({
42
+ name: "Project",
43
+ children: [
44
+ {
45
+ name: "src",
46
+ children: [
47
+ { name: "components", children: [{ name: "tree-view.tsx" }] },
48
+ { name: "app.tsx" },
49
+ { name: "index.tsx" }
50
+ ]
51
+ },
52
+ { name: "package.json" },
53
+ { name: "README.md" }
54
+ ]
55
+ });
56
+ ```
57
+
58
+ ### 3. Render the tree
59
+
60
+ ```tsx
61
+ <TreeView
62
+ data={data}
63
+ aria-label="File tree"
64
+ className="w-64 rounded border p-2"
65
+ nodeRenderer={({
66
+ element,
67
+ isBranch,
68
+ isExpanded,
69
+ isSelected,
70
+ getNodeProps,
71
+ level
72
+ }) => (
73
+ <TreeViewItem
74
+ {...getNodeProps()}
75
+ name={element.name}
76
+ isBranch={isBranch}
77
+ isExpanded={isExpanded}
78
+ isSelected={isSelected}
79
+ level={level}
80
+ indentation={16}
81
+ icon={isBranch ? <FolderIcon /> : <FileIcon />}
82
+ />
83
+ )}
84
+ />
85
+ ```
86
+
87
+ ## API Reference
88
+
89
+ ### TreeView
90
+
91
+ The main container component. Accepts all props from `react-accessible-treeview`.
92
+
93
+ | Prop | Type | Description |
94
+ |------|------|-------------|
95
+ | `data` | `INode[]` | Flattened tree data (use `flattenTree` helper) |
96
+ | `nodeRenderer` | `(props: INodeRendererProps) => ReactNode` | Render function for each node |
97
+ | `multiSelect` | `boolean` | Enable multi-selection |
98
+ | `togglableSelect` | `boolean` | Allow toggling selection |
99
+ | `clickAction` | `"EXCLUSIVE_SELECT" \| "SELECT" \| "FOCUS"` | Action on click |
100
+
101
+ ### TreeViewItem
102
+
103
+ The component for rendering individual tree items.
104
+
105
+ | Prop | Type | Default | Description |
106
+ |------|------|---------|-------------|
107
+ | `name` | `string` | Required | Display name of the item |
108
+ | `level` | `number` | Required | Nesting level (1-based) |
109
+ | `isBranch` | `boolean` | `false` | Whether item has children |
110
+ | `isExpanded` | `boolean` | `false` | Whether item is expanded |
111
+ | `isSelected` | `boolean` | `false` | Whether item is selected |
112
+ | `indentation` | `number` | `16` | Base indentation in pixels |
113
+ | `levelIndentation` | `number` | `48` | Indentation per level |
114
+ | `icon` | `ReactNode` | - | Icon to display |
115
+ | `isEditing` | `boolean` | `false` | Enable edit mode |
116
+ | `onEditSubmit` | `(value: string) => void` | - | Edit submit callback |
117
+ | `isLoading` | `boolean` | `false` | Show loading state |
118
+ | `expandIcon` | `ReactNode` | - | Custom expand icon |
119
+ | `loadingIcon` | `ReactNode` | - | Custom loading icon |
120
+
121
+ ## Examples
122
+
123
+ ### Multi-select
124
+
125
+ ```tsx
126
+ <TreeView
127
+ data={data}
128
+ multiSelect
129
+ togglableSelect
130
+ clickAction="EXCLUSIVE_SELECT"
131
+ onSelect={(props) => console.log("Selected:", props)}
132
+ nodeRenderer={/* ... */}
133
+ />
134
+ ```
135
+
136
+ ### With Context Menu
137
+
138
+ ```tsx
139
+ <ContextMenu>
140
+ <ContextMenuTrigger asChild>
141
+ <TreeViewItem {...props} />
142
+ </ContextMenuTrigger>
143
+ <ContextMenuContent>
144
+ <ContextMenuItem>Rename</ContextMenuItem>
145
+ <ContextMenuItem>Delete</ContextMenuItem>
146
+ </ContextMenuContent>
147
+ </ContextMenu>
148
+ ```
149
+
150
+ ### Inline Editing
151
+
152
+ ```tsx
153
+ <TreeViewItem
154
+ name={element.name}
155
+ isEditing={element.metadata?.isEditing}
156
+ onEditSubmit={(newName) => {
157
+ // Update tree data with new name
158
+ updateNodeName(element.id, newName);
159
+ }}
160
+ {...otherProps}
161
+ />
162
+ ```
163
+
164
+ ## Documentation
165
+
166
+ For full documentation, visit [shadcn-treeview.achromatic.dev](https://shadcn-treeview.achromatic.dev).
167
+
168
+ ## License
169
+
170
+ MIT
171
+
172
+ ---
173
+
174
+ Maintained by [Achromatic](https://achromatic.dev)
package/dist/index.cjs ADDED
@@ -0,0 +1,184 @@
1
+ 'use strict';
2
+
3
+ var TreeViewPrimitive = require('react-accessible-treeview');
4
+ var React = require('react');
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+
7
+ function _interopNamespace(e) {
8
+ if (e && e.__esModule) return e;
9
+ var n = Object.create(null);
10
+ if (e) {
11
+ Object.keys(e).forEach(function (k) {
12
+ if (k !== 'default') {
13
+ var d = Object.getOwnPropertyDescriptor(e, k);
14
+ Object.defineProperty(n, k, d.get ? d : {
15
+ enumerable: true,
16
+ get: function () { return e[k]; }
17
+ });
18
+ }
19
+ });
20
+ }
21
+ n.default = e;
22
+ return Object.freeze(n);
23
+ }
24
+
25
+ var TreeViewPrimitive__namespace = /*#__PURE__*/_interopNamespace(TreeViewPrimitive);
26
+ var React__namespace = /*#__PURE__*/_interopNamespace(React);
27
+
28
+ // src/utils.ts
29
+ function cn(...inputs) {
30
+ return inputs.flat().filter((x) => typeof x === "string" && x.length > 0).join(" ");
31
+ }
32
+ var TreeView = TreeViewPrimitive__namespace.default;
33
+ var TreeViewItem = React__namespace.forwardRef(
34
+ ({
35
+ level = 1,
36
+ isExpanded = false,
37
+ isBranch = false,
38
+ isSelected = false,
39
+ isLoading = false,
40
+ indentation = 16,
41
+ levelIndentation = 48,
42
+ name = "",
43
+ icon,
44
+ isEditing = false,
45
+ onEditSubmit,
46
+ expandIcon,
47
+ loadingIcon,
48
+ className,
49
+ style,
50
+ ...props
51
+ }, ref) => {
52
+ const [localValueState, setLocalValueState] = React__namespace.useState(name);
53
+ const inputRef = React__namespace.useRef(null);
54
+ React__namespace.useEffect(() => {
55
+ const handleClickOutside = (event) => {
56
+ if (!inputRef.current?.contains(event.target)) {
57
+ onEditSubmit?.(localValueState);
58
+ }
59
+ };
60
+ if (isEditing) {
61
+ document.addEventListener("mousedown", handleClickOutside);
62
+ }
63
+ return () => {
64
+ if (isEditing) {
65
+ document.removeEventListener("mousedown", handleClickOutside);
66
+ }
67
+ };
68
+ }, [isEditing, localValueState, onEditSubmit]);
69
+ React__namespace.useEffect(() => {
70
+ if (isEditing) {
71
+ inputRef.current?.focus();
72
+ }
73
+ }, [isEditing]);
74
+ const handleSubmit = (e) => {
75
+ e.preventDefault();
76
+ onEditSubmit?.(localValueState);
77
+ };
78
+ const paddingLeft = level === 1 && !isBranch ? indentation : levelIndentation * (level - 1) + indentation;
79
+ const defaultExpandIcon = /* @__PURE__ */ jsxRuntime.jsx(
80
+ "svg",
81
+ {
82
+ "aria-hidden": "true",
83
+ className: "text-muted-foreground transition-transform duration-200 group-aria-expanded:rotate-90",
84
+ width: "14",
85
+ height: "14",
86
+ viewBox: "0 0 24 24",
87
+ fill: "none",
88
+ stroke: "currentColor",
89
+ strokeWidth: "2",
90
+ strokeLinecap: "round",
91
+ strokeLinejoin: "round",
92
+ children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "m9 18 6-6-6-6" })
93
+ }
94
+ );
95
+ const defaultLoadingIcon = /* @__PURE__ */ jsxRuntime.jsx(
96
+ "svg",
97
+ {
98
+ "aria-hidden": "true",
99
+ className: "animate-spin text-muted-foreground",
100
+ width: "14",
101
+ height: "14",
102
+ viewBox: "0 0 24 24",
103
+ fill: "none",
104
+ stroke: "currentColor",
105
+ strokeWidth: "2",
106
+ strokeLinecap: "round",
107
+ strokeLinejoin: "round",
108
+ children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M21 12a9 9 0 1 1-6.219-8.56" })
109
+ }
110
+ );
111
+ return /* @__PURE__ */ jsxRuntime.jsxs(
112
+ "div",
113
+ {
114
+ ref,
115
+ "aria-expanded": !isEditing && isExpanded,
116
+ className: cn(
117
+ "group relative flex h-8 cursor-pointer select-none items-center gap-3 text-sm text-muted-foreground transition-colors hover:bg-muted",
118
+ isSelected && "!bg-muted text-foreground",
119
+ className
120
+ ),
121
+ style: {
122
+ paddingLeft,
123
+ ...style
124
+ },
125
+ "data-treeview-is-branch": isBranch,
126
+ "data-treeview-level": level,
127
+ ...props,
128
+ children: [
129
+ level > 1 && /* @__PURE__ */ jsxRuntime.jsx(
130
+ "div",
131
+ {
132
+ style: {
133
+ left: (levelIndentation / 2 + 4) * (level - 1) + indentation
134
+ },
135
+ className: "absolute h-full w-px group-data-[treeview-is-branch=false]:border"
136
+ }
137
+ ),
138
+ isSelected && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute left-0 h-full w-0.5 bg-foreground" }),
139
+ isBranch && (isLoading ? loadingIcon ?? defaultLoadingIcon : expandIcon ?? defaultExpandIcon),
140
+ icon,
141
+ /* @__PURE__ */ jsxRuntime.jsx(
142
+ "span",
143
+ {
144
+ className: cn("truncate text-sm", isEditing && "hidden"),
145
+ title: name,
146
+ children: name
147
+ }
148
+ ),
149
+ isEditing && /* @__PURE__ */ jsxRuntime.jsx("form", { onSubmit: handleSubmit, children: /* @__PURE__ */ jsxRuntime.jsx(
150
+ "input",
151
+ {
152
+ ref: inputRef,
153
+ onChange: (e) => {
154
+ setLocalValueState(e.target.value);
155
+ },
156
+ onKeyDownCapture: (e) => {
157
+ if (e.key === "Enter") {
158
+ onEditSubmit?.(localValueState);
159
+ } else if (e.key === "Escape") {
160
+ setLocalValueState(name);
161
+ onEditSubmit?.(name);
162
+ } else {
163
+ e.stopPropagation();
164
+ }
165
+ },
166
+ className: "block h-7 w-full rounded border bg-background px-2 py-1 text-sm focus:outline-none focus:ring-1 focus:ring-ring",
167
+ value: localValueState
168
+ }
169
+ ) })
170
+ ]
171
+ }
172
+ );
173
+ }
174
+ );
175
+ TreeViewItem.displayName = "TreeViewItem";
176
+
177
+ Object.defineProperty(exports, "flattenTree", {
178
+ enumerable: true,
179
+ get: function () { return TreeViewPrimitive.flattenTree; }
180
+ });
181
+ exports.TreeView = TreeView;
182
+ exports.TreeViewItem = TreeViewItem;
183
+ //# sourceMappingURL=index.cjs.map
184
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils.ts","../src/tree-view.tsx"],"names":["TreeViewPrimitive","React","jsx","jsxs"],"mappings":"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","file":"index.cjs","sourcesContent":["import type { ClassValue } from \"clsx\";\n\n/**\n * Utility function to merge class names\n * This is a minimal implementation that doesn't require clsx/tailwind-merge as dependencies\n */\nexport function cn(...inputs: ClassValue[]): string {\n\treturn inputs\n\t\t.flat()\n\t\t.filter((x) => typeof x === \"string\" && x.length > 0)\n\t\t.join(\" \");\n}\n","\"use client\";\n\nimport * as React from \"react\";\nimport * as TreeViewPrimitive from \"react-accessible-treeview\";\n\nimport { cn } from \"./utils\";\n\n/**\n * TreeView component props - extends the underlying react-accessible-treeview props\n */\nexport type TreeViewProps = React.ComponentPropsWithoutRef<\n\ttypeof TreeViewPrimitive.default\n>;\n\n/**\n * TreeView component - a wrapper around react-accessible-treeview\n *\n * @example\n * ```tsx\n * import { TreeView, TreeViewItem, flattenTree } from \"shadcn-treeview\";\n *\n * const data = flattenTree({\n * name: \"Root\",\n * children: [{ name: \"Child 1\" }, { name: \"Child 2\" }]\n * });\n *\n * <TreeView\n * data={data}\n * nodeRenderer={({ element, getNodeProps, level, isBranch, isExpanded, isSelected }) => (\n * <TreeViewItem\n * {...getNodeProps()}\n * name={element.name}\n * level={level}\n * isBranch={isBranch}\n * isExpanded={isExpanded}\n * isSelected={isSelected}\n * indentation={16}\n * />\n * )}\n * />\n * ```\n */\nconst TreeView = TreeViewPrimitive.default;\n\n/**\n * Props for the TreeViewItem component\n */\nexport type TreeViewItemProps = React.ComponentPropsWithoutRef<\"div\"> & {\n\t/** The nesting level of the item (1-based) */\n\tlevel: number;\n\t/** Whether this item is expanded (only applicable for branches) */\n\tisExpanded?: boolean;\n\t/** Whether this item is a branch (has children) */\n\tisBranch?: boolean;\n\t/** Whether this item is currently selected */\n\tisSelected?: boolean;\n\t/** Base indentation in pixels */\n\tindentation?: number;\n\t/** Indentation per level in pixels */\n\tlevelIndentation?: number;\n\t/** The display name of the item */\n\tname: string;\n\t/** Optional icon to display before the name */\n\ticon?: React.ReactNode;\n\t/** Whether the item is in editing mode */\n\tisEditing?: boolean;\n\t/** Callback when editing is submitted */\n\tonEditSubmit?: (value: string) => void;\n\t/** Whether the item is in a loading state */\n\tisLoading?: boolean;\n\t/** Custom expand/collapse icon */\n\texpandIcon?: React.ReactNode;\n\t/** Custom loading icon */\n\tloadingIcon?: React.ReactNode;\n};\n\n/**\n * TreeViewItem component - renders a single item in the tree\n *\n * @example\n * ```tsx\n * <TreeViewItem\n * name=\"My File\"\n * level={1}\n * isBranch={false}\n * isSelected={false}\n * indentation={16}\n * icon={<FileIcon className=\"h-4 w-4\" />}\n * />\n * ```\n */\nconst TreeViewItem = React.forwardRef<HTMLDivElement, TreeViewItemProps>(\n\t(\n\t\t{\n\t\t\tlevel = 1,\n\t\t\tisExpanded = false,\n\t\t\tisBranch = false,\n\t\t\tisSelected = false,\n\t\t\tisLoading = false,\n\t\t\tindentation = 16,\n\t\t\tlevelIndentation = 48,\n\t\t\tname = \"\",\n\t\t\ticon,\n\t\t\tisEditing = false,\n\t\t\tonEditSubmit,\n\t\t\texpandIcon,\n\t\t\tloadingIcon,\n\t\t\tclassName,\n\t\t\tstyle,\n\t\t\t...props\n\t\t},\n\t\tref,\n\t) => {\n\t\tconst [localValueState, setLocalValueState] = React.useState(name);\n\t\tconst inputRef = React.useRef<HTMLInputElement>(null);\n\n\t\tReact.useEffect(() => {\n\t\t\tconst handleClickOutside = (event: MouseEvent) => {\n\t\t\t\tif (!inputRef.current?.contains(event.target as Node)) {\n\t\t\t\t\tonEditSubmit?.(localValueState);\n\t\t\t\t}\n\t\t\t};\n\t\t\tif (isEditing) {\n\t\t\t\tdocument.addEventListener(\"mousedown\", handleClickOutside);\n\t\t\t}\n\t\t\treturn () => {\n\t\t\t\tif (isEditing) {\n\t\t\t\t\tdocument.removeEventListener(\"mousedown\", handleClickOutside);\n\t\t\t\t}\n\t\t\t};\n\t\t}, [isEditing, localValueState, onEditSubmit]);\n\n\t\tReact.useEffect(() => {\n\t\t\tif (isEditing) {\n\t\t\t\tinputRef.current?.focus();\n\t\t\t}\n\t\t}, [isEditing]);\n\n\t\tconst handleSubmit = (e: React.FormEvent<HTMLFormElement>): void => {\n\t\t\te.preventDefault();\n\t\t\tonEditSubmit?.(localValueState);\n\t\t};\n\n\t\tconst paddingLeft =\n\t\t\tlevel === 1 && !isBranch\n\t\t\t\t? indentation\n\t\t\t\t: levelIndentation * (level - 1) + indentation;\n\n\t\tconst defaultExpandIcon = (\n\t\t\t<svg\n\t\t\t\taria-hidden=\"true\"\n\t\t\t\tclassName=\"text-muted-foreground transition-transform duration-200 group-aria-expanded:rotate-90\"\n\t\t\t\twidth=\"14\"\n\t\t\t\theight=\"14\"\n\t\t\t\tviewBox=\"0 0 24 24\"\n\t\t\t\tfill=\"none\"\n\t\t\t\tstroke=\"currentColor\"\n\t\t\t\tstrokeWidth=\"2\"\n\t\t\t\tstrokeLinecap=\"round\"\n\t\t\t\tstrokeLinejoin=\"round\"\n\t\t\t>\n\t\t\t\t<path d=\"m9 18 6-6-6-6\" />\n\t\t\t</svg>\n\t\t);\n\n\t\tconst defaultLoadingIcon = (\n\t\t\t<svg\n\t\t\t\taria-hidden=\"true\"\n\t\t\t\tclassName=\"animate-spin text-muted-foreground\"\n\t\t\t\twidth=\"14\"\n\t\t\t\theight=\"14\"\n\t\t\t\tviewBox=\"0 0 24 24\"\n\t\t\t\tfill=\"none\"\n\t\t\t\tstroke=\"currentColor\"\n\t\t\t\tstrokeWidth=\"2\"\n\t\t\t\tstrokeLinecap=\"round\"\n\t\t\t\tstrokeLinejoin=\"round\"\n\t\t\t>\n\t\t\t\t<path d=\"M21 12a9 9 0 1 1-6.219-8.56\" />\n\t\t\t</svg>\n\t\t);\n\n\t\treturn (\n\t\t\t<div\n\t\t\t\tref={ref}\n\t\t\t\taria-expanded={!isEditing && isExpanded}\n\t\t\t\tclassName={cn(\n\t\t\t\t\t\"group relative flex h-8 cursor-pointer select-none items-center gap-3 text-sm text-muted-foreground transition-colors hover:bg-muted\",\n\t\t\t\t\tisSelected && \"!bg-muted text-foreground\",\n\t\t\t\t\tclassName,\n\t\t\t\t)}\n\t\t\t\tstyle={{\n\t\t\t\t\tpaddingLeft,\n\t\t\t\t\t...style,\n\t\t\t\t}}\n\t\t\t\tdata-treeview-is-branch={isBranch}\n\t\t\t\tdata-treeview-level={level}\n\t\t\t\t{...props}\n\t\t\t>\n\t\t\t\t{level > 1 && (\n\t\t\t\t\t<div\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\tleft: (levelIndentation / 2 + 4) * (level - 1) + indentation,\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tclassName=\"absolute h-full w-px group-data-[treeview-is-branch=false]:border\"\n\t\t\t\t\t/>\n\t\t\t\t)}\n\t\t\t\t{isSelected && (\n\t\t\t\t\t<div className=\"absolute left-0 h-full w-0.5 bg-foreground\" />\n\t\t\t\t)}\n\t\t\t\t{isBranch &&\n\t\t\t\t\t(isLoading\n\t\t\t\t\t\t? (loadingIcon ?? defaultLoadingIcon)\n\t\t\t\t\t\t: (expandIcon ?? defaultExpandIcon))}\n\t\t\t\t{icon}\n\t\t\t\t<span\n\t\t\t\t\tclassName={cn(\"truncate text-sm\", isEditing && \"hidden\")}\n\t\t\t\t\ttitle={name}\n\t\t\t\t>\n\t\t\t\t\t{name}\n\t\t\t\t</span>\n\t\t\t\t{isEditing && (\n\t\t\t\t\t<form onSubmit={handleSubmit}>\n\t\t\t\t\t\t<input\n\t\t\t\t\t\t\tref={inputRef}\n\t\t\t\t\t\t\tonChange={(e) => {\n\t\t\t\t\t\t\t\tsetLocalValueState(e.target.value);\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\tonKeyDownCapture={(e) => {\n\t\t\t\t\t\t\t\tif (e.key === \"Enter\") {\n\t\t\t\t\t\t\t\t\tonEditSubmit?.(localValueState);\n\t\t\t\t\t\t\t\t} else if (e.key === \"Escape\") {\n\t\t\t\t\t\t\t\t\tsetLocalValueState(name);\n\t\t\t\t\t\t\t\t\tonEditSubmit?.(name);\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\te.stopPropagation();\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\tclassName=\"block h-7 w-full rounded border bg-background px-2 py-1 text-sm focus:outline-none focus:ring-1 focus:ring-ring\"\n\t\t\t\t\t\t\tvalue={localValueState}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</form>\n\t\t\t\t)}\n\t\t\t</div>\n\t\t);\n\t},\n);\nTreeViewItem.displayName = \"TreeViewItem\";\n\nexport { TreeView, TreeViewItem };\n"]}
@@ -0,0 +1,114 @@
1
+ import * as TreeViewPrimitive from 'react-accessible-treeview';
2
+ export { INode, INodeRendererProps, ITreeViewOnExpandProps, ITreeViewOnLoadDataProps, ITreeViewOnSelectProps, ITreeViewProps, flattenTree } from 'react-accessible-treeview';
3
+ import * as react_accessible_treeview_dist_TreeView_utils from 'react-accessible-treeview/dist/TreeView/utils';
4
+ import * as React from 'react';
5
+
6
+ /**
7
+ * TreeView component props - extends the underlying react-accessible-treeview props
8
+ */
9
+ type TreeViewProps = React.ComponentPropsWithoutRef<typeof TreeViewPrimitive.default>;
10
+ /**
11
+ * TreeView component - a wrapper around react-accessible-treeview
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * import { TreeView, TreeViewItem, flattenTree } from "shadcn-treeview";
16
+ *
17
+ * const data = flattenTree({
18
+ * name: "Root",
19
+ * children: [{ name: "Child 1" }, { name: "Child 2" }]
20
+ * });
21
+ *
22
+ * <TreeView
23
+ * data={data}
24
+ * nodeRenderer={({ element, getNodeProps, level, isBranch, isExpanded, isSelected }) => (
25
+ * <TreeViewItem
26
+ * {...getNodeProps()}
27
+ * name={element.name}
28
+ * level={level}
29
+ * isBranch={isBranch}
30
+ * isExpanded={isExpanded}
31
+ * isSelected={isSelected}
32
+ * indentation={16}
33
+ * />
34
+ * )}
35
+ * />
36
+ * ```
37
+ */
38
+ declare const TreeView: React.ForwardRefExoticComponent<TreeViewPrimitive.ITreeViewProps<react_accessible_treeview_dist_TreeView_utils.IFlatMetadata> & React.RefAttributes<HTMLUListElement>>;
39
+ /**
40
+ * Props for the TreeViewItem component
41
+ */
42
+ type TreeViewItemProps = React.ComponentPropsWithoutRef<"div"> & {
43
+ /** The nesting level of the item (1-based) */
44
+ level: number;
45
+ /** Whether this item is expanded (only applicable for branches) */
46
+ isExpanded?: boolean;
47
+ /** Whether this item is a branch (has children) */
48
+ isBranch?: boolean;
49
+ /** Whether this item is currently selected */
50
+ isSelected?: boolean;
51
+ /** Base indentation in pixels */
52
+ indentation?: number;
53
+ /** Indentation per level in pixels */
54
+ levelIndentation?: number;
55
+ /** The display name of the item */
56
+ name: string;
57
+ /** Optional icon to display before the name */
58
+ icon?: React.ReactNode;
59
+ /** Whether the item is in editing mode */
60
+ isEditing?: boolean;
61
+ /** Callback when editing is submitted */
62
+ onEditSubmit?: (value: string) => void;
63
+ /** Whether the item is in a loading state */
64
+ isLoading?: boolean;
65
+ /** Custom expand/collapse icon */
66
+ expandIcon?: React.ReactNode;
67
+ /** Custom loading icon */
68
+ loadingIcon?: React.ReactNode;
69
+ };
70
+ /**
71
+ * TreeViewItem component - renders a single item in the tree
72
+ *
73
+ * @example
74
+ * ```tsx
75
+ * <TreeViewItem
76
+ * name="My File"
77
+ * level={1}
78
+ * isBranch={false}
79
+ * isSelected={false}
80
+ * indentation={16}
81
+ * icon={<FileIcon className="h-4 w-4" />}
82
+ * />
83
+ * ```
84
+ */
85
+ declare const TreeViewItem: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & {
86
+ /** The nesting level of the item (1-based) */
87
+ level: number;
88
+ /** Whether this item is expanded (only applicable for branches) */
89
+ isExpanded?: boolean;
90
+ /** Whether this item is a branch (has children) */
91
+ isBranch?: boolean;
92
+ /** Whether this item is currently selected */
93
+ isSelected?: boolean;
94
+ /** Base indentation in pixels */
95
+ indentation?: number;
96
+ /** Indentation per level in pixels */
97
+ levelIndentation?: number;
98
+ /** The display name of the item */
99
+ name: string;
100
+ /** Optional icon to display before the name */
101
+ icon?: React.ReactNode;
102
+ /** Whether the item is in editing mode */
103
+ isEditing?: boolean;
104
+ /** Callback when editing is submitted */
105
+ onEditSubmit?: (value: string) => void;
106
+ /** Whether the item is in a loading state */
107
+ isLoading?: boolean;
108
+ /** Custom expand/collapse icon */
109
+ expandIcon?: React.ReactNode;
110
+ /** Custom loading icon */
111
+ loadingIcon?: React.ReactNode;
112
+ } & React.RefAttributes<HTMLDivElement>>;
113
+
114
+ export { TreeView, TreeViewItem, type TreeViewItemProps, type TreeViewProps };
@@ -0,0 +1,114 @@
1
+ import * as TreeViewPrimitive from 'react-accessible-treeview';
2
+ export { INode, INodeRendererProps, ITreeViewOnExpandProps, ITreeViewOnLoadDataProps, ITreeViewOnSelectProps, ITreeViewProps, flattenTree } from 'react-accessible-treeview';
3
+ import * as react_accessible_treeview_dist_TreeView_utils from 'react-accessible-treeview/dist/TreeView/utils';
4
+ import * as React from 'react';
5
+
6
+ /**
7
+ * TreeView component props - extends the underlying react-accessible-treeview props
8
+ */
9
+ type TreeViewProps = React.ComponentPropsWithoutRef<typeof TreeViewPrimitive.default>;
10
+ /**
11
+ * TreeView component - a wrapper around react-accessible-treeview
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * import { TreeView, TreeViewItem, flattenTree } from "shadcn-treeview";
16
+ *
17
+ * const data = flattenTree({
18
+ * name: "Root",
19
+ * children: [{ name: "Child 1" }, { name: "Child 2" }]
20
+ * });
21
+ *
22
+ * <TreeView
23
+ * data={data}
24
+ * nodeRenderer={({ element, getNodeProps, level, isBranch, isExpanded, isSelected }) => (
25
+ * <TreeViewItem
26
+ * {...getNodeProps()}
27
+ * name={element.name}
28
+ * level={level}
29
+ * isBranch={isBranch}
30
+ * isExpanded={isExpanded}
31
+ * isSelected={isSelected}
32
+ * indentation={16}
33
+ * />
34
+ * )}
35
+ * />
36
+ * ```
37
+ */
38
+ declare const TreeView: React.ForwardRefExoticComponent<TreeViewPrimitive.ITreeViewProps<react_accessible_treeview_dist_TreeView_utils.IFlatMetadata> & React.RefAttributes<HTMLUListElement>>;
39
+ /**
40
+ * Props for the TreeViewItem component
41
+ */
42
+ type TreeViewItemProps = React.ComponentPropsWithoutRef<"div"> & {
43
+ /** The nesting level of the item (1-based) */
44
+ level: number;
45
+ /** Whether this item is expanded (only applicable for branches) */
46
+ isExpanded?: boolean;
47
+ /** Whether this item is a branch (has children) */
48
+ isBranch?: boolean;
49
+ /** Whether this item is currently selected */
50
+ isSelected?: boolean;
51
+ /** Base indentation in pixels */
52
+ indentation?: number;
53
+ /** Indentation per level in pixels */
54
+ levelIndentation?: number;
55
+ /** The display name of the item */
56
+ name: string;
57
+ /** Optional icon to display before the name */
58
+ icon?: React.ReactNode;
59
+ /** Whether the item is in editing mode */
60
+ isEditing?: boolean;
61
+ /** Callback when editing is submitted */
62
+ onEditSubmit?: (value: string) => void;
63
+ /** Whether the item is in a loading state */
64
+ isLoading?: boolean;
65
+ /** Custom expand/collapse icon */
66
+ expandIcon?: React.ReactNode;
67
+ /** Custom loading icon */
68
+ loadingIcon?: React.ReactNode;
69
+ };
70
+ /**
71
+ * TreeViewItem component - renders a single item in the tree
72
+ *
73
+ * @example
74
+ * ```tsx
75
+ * <TreeViewItem
76
+ * name="My File"
77
+ * level={1}
78
+ * isBranch={false}
79
+ * isSelected={false}
80
+ * indentation={16}
81
+ * icon={<FileIcon className="h-4 w-4" />}
82
+ * />
83
+ * ```
84
+ */
85
+ declare const TreeViewItem: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & {
86
+ /** The nesting level of the item (1-based) */
87
+ level: number;
88
+ /** Whether this item is expanded (only applicable for branches) */
89
+ isExpanded?: boolean;
90
+ /** Whether this item is a branch (has children) */
91
+ isBranch?: boolean;
92
+ /** Whether this item is currently selected */
93
+ isSelected?: boolean;
94
+ /** Base indentation in pixels */
95
+ indentation?: number;
96
+ /** Indentation per level in pixels */
97
+ levelIndentation?: number;
98
+ /** The display name of the item */
99
+ name: string;
100
+ /** Optional icon to display before the name */
101
+ icon?: React.ReactNode;
102
+ /** Whether the item is in editing mode */
103
+ isEditing?: boolean;
104
+ /** Callback when editing is submitted */
105
+ onEditSubmit?: (value: string) => void;
106
+ /** Whether the item is in a loading state */
107
+ isLoading?: boolean;
108
+ /** Custom expand/collapse icon */
109
+ expandIcon?: React.ReactNode;
110
+ /** Custom loading icon */
111
+ loadingIcon?: React.ReactNode;
112
+ } & React.RefAttributes<HTMLDivElement>>;
113
+
114
+ export { TreeView, TreeViewItem, type TreeViewItemProps, type TreeViewProps };
package/dist/index.js ADDED
@@ -0,0 +1,157 @@
1
+ import * as TreeViewPrimitive from 'react-accessible-treeview';
2
+ export { flattenTree } from 'react-accessible-treeview';
3
+ import * as React from 'react';
4
+ import { jsxs, jsx } from 'react/jsx-runtime';
5
+
6
+ // src/utils.ts
7
+ function cn(...inputs) {
8
+ return inputs.flat().filter((x) => typeof x === "string" && x.length > 0).join(" ");
9
+ }
10
+ var TreeView = TreeViewPrimitive.default;
11
+ var TreeViewItem = React.forwardRef(
12
+ ({
13
+ level = 1,
14
+ isExpanded = false,
15
+ isBranch = false,
16
+ isSelected = false,
17
+ isLoading = false,
18
+ indentation = 16,
19
+ levelIndentation = 48,
20
+ name = "",
21
+ icon,
22
+ isEditing = false,
23
+ onEditSubmit,
24
+ expandIcon,
25
+ loadingIcon,
26
+ className,
27
+ style,
28
+ ...props
29
+ }, ref) => {
30
+ const [localValueState, setLocalValueState] = React.useState(name);
31
+ const inputRef = React.useRef(null);
32
+ React.useEffect(() => {
33
+ const handleClickOutside = (event) => {
34
+ if (!inputRef.current?.contains(event.target)) {
35
+ onEditSubmit?.(localValueState);
36
+ }
37
+ };
38
+ if (isEditing) {
39
+ document.addEventListener("mousedown", handleClickOutside);
40
+ }
41
+ return () => {
42
+ if (isEditing) {
43
+ document.removeEventListener("mousedown", handleClickOutside);
44
+ }
45
+ };
46
+ }, [isEditing, localValueState, onEditSubmit]);
47
+ React.useEffect(() => {
48
+ if (isEditing) {
49
+ inputRef.current?.focus();
50
+ }
51
+ }, [isEditing]);
52
+ const handleSubmit = (e) => {
53
+ e.preventDefault();
54
+ onEditSubmit?.(localValueState);
55
+ };
56
+ const paddingLeft = level === 1 && !isBranch ? indentation : levelIndentation * (level - 1) + indentation;
57
+ const defaultExpandIcon = /* @__PURE__ */ jsx(
58
+ "svg",
59
+ {
60
+ "aria-hidden": "true",
61
+ className: "text-muted-foreground transition-transform duration-200 group-aria-expanded:rotate-90",
62
+ width: "14",
63
+ height: "14",
64
+ viewBox: "0 0 24 24",
65
+ fill: "none",
66
+ stroke: "currentColor",
67
+ strokeWidth: "2",
68
+ strokeLinecap: "round",
69
+ strokeLinejoin: "round",
70
+ children: /* @__PURE__ */ jsx("path", { d: "m9 18 6-6-6-6" })
71
+ }
72
+ );
73
+ const defaultLoadingIcon = /* @__PURE__ */ jsx(
74
+ "svg",
75
+ {
76
+ "aria-hidden": "true",
77
+ className: "animate-spin text-muted-foreground",
78
+ width: "14",
79
+ height: "14",
80
+ viewBox: "0 0 24 24",
81
+ fill: "none",
82
+ stroke: "currentColor",
83
+ strokeWidth: "2",
84
+ strokeLinecap: "round",
85
+ strokeLinejoin: "round",
86
+ children: /* @__PURE__ */ jsx("path", { d: "M21 12a9 9 0 1 1-6.219-8.56" })
87
+ }
88
+ );
89
+ return /* @__PURE__ */ jsxs(
90
+ "div",
91
+ {
92
+ ref,
93
+ "aria-expanded": !isEditing && isExpanded,
94
+ className: cn(
95
+ "group relative flex h-8 cursor-pointer select-none items-center gap-3 text-sm text-muted-foreground transition-colors hover:bg-muted",
96
+ isSelected && "!bg-muted text-foreground",
97
+ className
98
+ ),
99
+ style: {
100
+ paddingLeft,
101
+ ...style
102
+ },
103
+ "data-treeview-is-branch": isBranch,
104
+ "data-treeview-level": level,
105
+ ...props,
106
+ children: [
107
+ level > 1 && /* @__PURE__ */ jsx(
108
+ "div",
109
+ {
110
+ style: {
111
+ left: (levelIndentation / 2 + 4) * (level - 1) + indentation
112
+ },
113
+ className: "absolute h-full w-px group-data-[treeview-is-branch=false]:border"
114
+ }
115
+ ),
116
+ isSelected && /* @__PURE__ */ jsx("div", { className: "absolute left-0 h-full w-0.5 bg-foreground" }),
117
+ isBranch && (isLoading ? loadingIcon ?? defaultLoadingIcon : expandIcon ?? defaultExpandIcon),
118
+ icon,
119
+ /* @__PURE__ */ jsx(
120
+ "span",
121
+ {
122
+ className: cn("truncate text-sm", isEditing && "hidden"),
123
+ title: name,
124
+ children: name
125
+ }
126
+ ),
127
+ isEditing && /* @__PURE__ */ jsx("form", { onSubmit: handleSubmit, children: /* @__PURE__ */ jsx(
128
+ "input",
129
+ {
130
+ ref: inputRef,
131
+ onChange: (e) => {
132
+ setLocalValueState(e.target.value);
133
+ },
134
+ onKeyDownCapture: (e) => {
135
+ if (e.key === "Enter") {
136
+ onEditSubmit?.(localValueState);
137
+ } else if (e.key === "Escape") {
138
+ setLocalValueState(name);
139
+ onEditSubmit?.(name);
140
+ } else {
141
+ e.stopPropagation();
142
+ }
143
+ },
144
+ className: "block h-7 w-full rounded border bg-background px-2 py-1 text-sm focus:outline-none focus:ring-1 focus:ring-ring",
145
+ value: localValueState
146
+ }
147
+ ) })
148
+ ]
149
+ }
150
+ );
151
+ }
152
+ );
153
+ TreeViewItem.displayName = "TreeViewItem";
154
+
155
+ export { TreeView, TreeViewItem };
156
+ //# sourceMappingURL=index.js.map
157
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils.ts","../src/tree-view.tsx"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { ClassValue } from \"clsx\";\n\n/**\n * Utility function to merge class names\n * This is a minimal implementation that doesn't require clsx/tailwind-merge as dependencies\n */\nexport function cn(...inputs: ClassValue[]): string {\n\treturn inputs\n\t\t.flat()\n\t\t.filter((x) => typeof x === \"string\" && x.length > 0)\n\t\t.join(\" \");\n}\n","\"use client\";\n\nimport * as React from \"react\";\nimport * as TreeViewPrimitive from \"react-accessible-treeview\";\n\nimport { cn } from \"./utils\";\n\n/**\n * TreeView component props - extends the underlying react-accessible-treeview props\n */\nexport type TreeViewProps = React.ComponentPropsWithoutRef<\n\ttypeof TreeViewPrimitive.default\n>;\n\n/**\n * TreeView component - a wrapper around react-accessible-treeview\n *\n * @example\n * ```tsx\n * import { TreeView, TreeViewItem, flattenTree } from \"shadcn-treeview\";\n *\n * const data = flattenTree({\n * name: \"Root\",\n * children: [{ name: \"Child 1\" }, { name: \"Child 2\" }]\n * });\n *\n * <TreeView\n * data={data}\n * nodeRenderer={({ element, getNodeProps, level, isBranch, isExpanded, isSelected }) => (\n * <TreeViewItem\n * {...getNodeProps()}\n * name={element.name}\n * level={level}\n * isBranch={isBranch}\n * isExpanded={isExpanded}\n * isSelected={isSelected}\n * indentation={16}\n * />\n * )}\n * />\n * ```\n */\nconst TreeView = TreeViewPrimitive.default;\n\n/**\n * Props for the TreeViewItem component\n */\nexport type TreeViewItemProps = React.ComponentPropsWithoutRef<\"div\"> & {\n\t/** The nesting level of the item (1-based) */\n\tlevel: number;\n\t/** Whether this item is expanded (only applicable for branches) */\n\tisExpanded?: boolean;\n\t/** Whether this item is a branch (has children) */\n\tisBranch?: boolean;\n\t/** Whether this item is currently selected */\n\tisSelected?: boolean;\n\t/** Base indentation in pixels */\n\tindentation?: number;\n\t/** Indentation per level in pixels */\n\tlevelIndentation?: number;\n\t/** The display name of the item */\n\tname: string;\n\t/** Optional icon to display before the name */\n\ticon?: React.ReactNode;\n\t/** Whether the item is in editing mode */\n\tisEditing?: boolean;\n\t/** Callback when editing is submitted */\n\tonEditSubmit?: (value: string) => void;\n\t/** Whether the item is in a loading state */\n\tisLoading?: boolean;\n\t/** Custom expand/collapse icon */\n\texpandIcon?: React.ReactNode;\n\t/** Custom loading icon */\n\tloadingIcon?: React.ReactNode;\n};\n\n/**\n * TreeViewItem component - renders a single item in the tree\n *\n * @example\n * ```tsx\n * <TreeViewItem\n * name=\"My File\"\n * level={1}\n * isBranch={false}\n * isSelected={false}\n * indentation={16}\n * icon={<FileIcon className=\"h-4 w-4\" />}\n * />\n * ```\n */\nconst TreeViewItem = React.forwardRef<HTMLDivElement, TreeViewItemProps>(\n\t(\n\t\t{\n\t\t\tlevel = 1,\n\t\t\tisExpanded = false,\n\t\t\tisBranch = false,\n\t\t\tisSelected = false,\n\t\t\tisLoading = false,\n\t\t\tindentation = 16,\n\t\t\tlevelIndentation = 48,\n\t\t\tname = \"\",\n\t\t\ticon,\n\t\t\tisEditing = false,\n\t\t\tonEditSubmit,\n\t\t\texpandIcon,\n\t\t\tloadingIcon,\n\t\t\tclassName,\n\t\t\tstyle,\n\t\t\t...props\n\t\t},\n\t\tref,\n\t) => {\n\t\tconst [localValueState, setLocalValueState] = React.useState(name);\n\t\tconst inputRef = React.useRef<HTMLInputElement>(null);\n\n\t\tReact.useEffect(() => {\n\t\t\tconst handleClickOutside = (event: MouseEvent) => {\n\t\t\t\tif (!inputRef.current?.contains(event.target as Node)) {\n\t\t\t\t\tonEditSubmit?.(localValueState);\n\t\t\t\t}\n\t\t\t};\n\t\t\tif (isEditing) {\n\t\t\t\tdocument.addEventListener(\"mousedown\", handleClickOutside);\n\t\t\t}\n\t\t\treturn () => {\n\t\t\t\tif (isEditing) {\n\t\t\t\t\tdocument.removeEventListener(\"mousedown\", handleClickOutside);\n\t\t\t\t}\n\t\t\t};\n\t\t}, [isEditing, localValueState, onEditSubmit]);\n\n\t\tReact.useEffect(() => {\n\t\t\tif (isEditing) {\n\t\t\t\tinputRef.current?.focus();\n\t\t\t}\n\t\t}, [isEditing]);\n\n\t\tconst handleSubmit = (e: React.FormEvent<HTMLFormElement>): void => {\n\t\t\te.preventDefault();\n\t\t\tonEditSubmit?.(localValueState);\n\t\t};\n\n\t\tconst paddingLeft =\n\t\t\tlevel === 1 && !isBranch\n\t\t\t\t? indentation\n\t\t\t\t: levelIndentation * (level - 1) + indentation;\n\n\t\tconst defaultExpandIcon = (\n\t\t\t<svg\n\t\t\t\taria-hidden=\"true\"\n\t\t\t\tclassName=\"text-muted-foreground transition-transform duration-200 group-aria-expanded:rotate-90\"\n\t\t\t\twidth=\"14\"\n\t\t\t\theight=\"14\"\n\t\t\t\tviewBox=\"0 0 24 24\"\n\t\t\t\tfill=\"none\"\n\t\t\t\tstroke=\"currentColor\"\n\t\t\t\tstrokeWidth=\"2\"\n\t\t\t\tstrokeLinecap=\"round\"\n\t\t\t\tstrokeLinejoin=\"round\"\n\t\t\t>\n\t\t\t\t<path d=\"m9 18 6-6-6-6\" />\n\t\t\t</svg>\n\t\t);\n\n\t\tconst defaultLoadingIcon = (\n\t\t\t<svg\n\t\t\t\taria-hidden=\"true\"\n\t\t\t\tclassName=\"animate-spin text-muted-foreground\"\n\t\t\t\twidth=\"14\"\n\t\t\t\theight=\"14\"\n\t\t\t\tviewBox=\"0 0 24 24\"\n\t\t\t\tfill=\"none\"\n\t\t\t\tstroke=\"currentColor\"\n\t\t\t\tstrokeWidth=\"2\"\n\t\t\t\tstrokeLinecap=\"round\"\n\t\t\t\tstrokeLinejoin=\"round\"\n\t\t\t>\n\t\t\t\t<path d=\"M21 12a9 9 0 1 1-6.219-8.56\" />\n\t\t\t</svg>\n\t\t);\n\n\t\treturn (\n\t\t\t<div\n\t\t\t\tref={ref}\n\t\t\t\taria-expanded={!isEditing && isExpanded}\n\t\t\t\tclassName={cn(\n\t\t\t\t\t\"group relative flex h-8 cursor-pointer select-none items-center gap-3 text-sm text-muted-foreground transition-colors hover:bg-muted\",\n\t\t\t\t\tisSelected && \"!bg-muted text-foreground\",\n\t\t\t\t\tclassName,\n\t\t\t\t)}\n\t\t\t\tstyle={{\n\t\t\t\t\tpaddingLeft,\n\t\t\t\t\t...style,\n\t\t\t\t}}\n\t\t\t\tdata-treeview-is-branch={isBranch}\n\t\t\t\tdata-treeview-level={level}\n\t\t\t\t{...props}\n\t\t\t>\n\t\t\t\t{level > 1 && (\n\t\t\t\t\t<div\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\tleft: (levelIndentation / 2 + 4) * (level - 1) + indentation,\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tclassName=\"absolute h-full w-px group-data-[treeview-is-branch=false]:border\"\n\t\t\t\t\t/>\n\t\t\t\t)}\n\t\t\t\t{isSelected && (\n\t\t\t\t\t<div className=\"absolute left-0 h-full w-0.5 bg-foreground\" />\n\t\t\t\t)}\n\t\t\t\t{isBranch &&\n\t\t\t\t\t(isLoading\n\t\t\t\t\t\t? (loadingIcon ?? defaultLoadingIcon)\n\t\t\t\t\t\t: (expandIcon ?? defaultExpandIcon))}\n\t\t\t\t{icon}\n\t\t\t\t<span\n\t\t\t\t\tclassName={cn(\"truncate text-sm\", isEditing && \"hidden\")}\n\t\t\t\t\ttitle={name}\n\t\t\t\t>\n\t\t\t\t\t{name}\n\t\t\t\t</span>\n\t\t\t\t{isEditing && (\n\t\t\t\t\t<form onSubmit={handleSubmit}>\n\t\t\t\t\t\t<input\n\t\t\t\t\t\t\tref={inputRef}\n\t\t\t\t\t\t\tonChange={(e) => {\n\t\t\t\t\t\t\t\tsetLocalValueState(e.target.value);\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\tonKeyDownCapture={(e) => {\n\t\t\t\t\t\t\t\tif (e.key === \"Enter\") {\n\t\t\t\t\t\t\t\t\tonEditSubmit?.(localValueState);\n\t\t\t\t\t\t\t\t} else if (e.key === \"Escape\") {\n\t\t\t\t\t\t\t\t\tsetLocalValueState(name);\n\t\t\t\t\t\t\t\t\tonEditSubmit?.(name);\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\te.stopPropagation();\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\tclassName=\"block h-7 w-full rounded border bg-background px-2 py-1 text-sm focus:outline-none focus:ring-1 focus:ring-ring\"\n\t\t\t\t\t\t\tvalue={localValueState}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</form>\n\t\t\t\t)}\n\t\t\t</div>\n\t\t);\n\t},\n);\nTreeViewItem.displayName = \"TreeViewItem\";\n\nexport { TreeView, TreeViewItem };\n"]}
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "shadcn-treeview",
3
+ "version": "0.1.0",
4
+ "description": "Accessible tree view component for React with Shadcn UI styling. Built on react-accessible-treeview.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "import": {
13
+ "types": "./dist/index.d.ts",
14
+ "default": "./dist/index.js"
15
+ },
16
+ "require": {
17
+ "types": "./dist/index.d.cts",
18
+ "default": "./dist/index.cjs"
19
+ }
20
+ }
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "README.md"
25
+ ],
26
+ "scripts": {
27
+ "build": "tsup",
28
+ "dev": "tsup --watch",
29
+ "typecheck": "tsc --noEmit",
30
+ "lint": "biome check",
31
+ "lint:fix": "biome check --fix",
32
+ "test": "vitest run",
33
+ "prepublishOnly": "npm run build"
34
+ },
35
+ "peerDependencies": {
36
+ "react": ">=18.0.0",
37
+ "react-dom": ">=18.0.0"
38
+ },
39
+ "dependencies": {
40
+ "react-accessible-treeview": "^2.10.0"
41
+ },
42
+ "devDependencies": {
43
+ "@biomejs/biome": "^2.3.11",
44
+ "@testing-library/jest-dom": "^6.9.1",
45
+ "@testing-library/react": "^16.3.0",
46
+ "@types/react": "^19.0.0",
47
+ "@types/react-dom": "^19.0.0",
48
+ "@vitest/coverage-v8": "^3.2.4",
49
+ "jsdom": "^27.4.0",
50
+ "react": "^19.0.0",
51
+ "react-dom": "^19.0.0",
52
+ "tsup": "^8.5.0",
53
+ "typescript": "^5.8.0",
54
+ "vitest": "^3.2.0"
55
+ },
56
+ "keywords": [
57
+ "react",
58
+ "treeview",
59
+ "tree",
60
+ "accessible",
61
+ "a11y",
62
+ "shadcn-ui",
63
+ "shadcn",
64
+ "file-tree",
65
+ "hierarchy",
66
+ "typescript",
67
+ "hooks"
68
+ ],
69
+ "repository": {
70
+ "type": "git",
71
+ "url": "git+https://github.com/achromaticlabs/shadcn-treeview.git"
72
+ },
73
+ "homepage": "https://shadcn-treeview.achromatic.dev",
74
+ "bugs": {
75
+ "url": "https://github.com/achromaticlabs/shadcn-treeview/issues"
76
+ }
77
+ }