ink-tree-view 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/LICENSE +21 -0
- package/README.md +383 -0
- package/dist/index.d.ts +258 -0
- package/dist/index.js +878 -0
- package/package.json +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-2026 John Costa
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
# ink-tree-view
|
|
2
|
+
|
|
3
|
+
[](https://github.com/costajohnt/ink-tree-view/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/ink-tree-view)
|
|
5
|
+
[](https://github.com/costajohnt/ink-tree-view/blob/main/LICENSE)
|
|
6
|
+
|
|
7
|
+
A tree view component for [Ink](https://github.com/vadimdemedes/ink) (React for CLIs). Display hierarchical data with expand/collapse, keyboard navigation, selection modes, custom rendering, virtual scrolling, and async lazy loading.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- Hierarchical data display with expand/collapse
|
|
12
|
+
- Full keyboard navigation (arrows, Home/End, Enter, Space)
|
|
13
|
+
- Selection modes: none, single, and multiple (with checkboxes)
|
|
14
|
+
- Custom node rendering via `renderNode` prop
|
|
15
|
+
- Virtual scrolling for large trees (`visibleNodeCount`)
|
|
16
|
+
- Async/lazy-loaded children via `loadChildren` + `isParent`
|
|
17
|
+
- Error handling for failed async loads via `onLoadError`
|
|
18
|
+
- Headless hooks (`useTreeViewState`, `useTreeView`) for full control
|
|
19
|
+
- TypeScript-first with complete type exports
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
npm install ink-tree-view
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Peer dependencies: `ink` (>=5.0.0), `react` (>=18.0.0)
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
import {render} from 'ink';
|
|
33
|
+
import {TreeView} from 'ink-tree-view';
|
|
34
|
+
|
|
35
|
+
const data = [
|
|
36
|
+
{
|
|
37
|
+
id: 'src',
|
|
38
|
+
label: 'src',
|
|
39
|
+
children: [
|
|
40
|
+
{id: 'index', label: 'index.ts'},
|
|
41
|
+
{
|
|
42
|
+
id: 'components',
|
|
43
|
+
label: 'components',
|
|
44
|
+
children: [
|
|
45
|
+
{id: 'button', label: 'button.tsx'},
|
|
46
|
+
{id: 'input', label: 'input.tsx'},
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
{id: 'readme', label: 'README.md'},
|
|
52
|
+
{id: 'package', label: 'package.json'},
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
render(<TreeView data={data} />);
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Use arrow keys to navigate, Right to expand, Left to collapse, and Enter to toggle.
|
|
59
|
+
|
|
60
|
+
## Data Model
|
|
61
|
+
|
|
62
|
+
Tree data is an array of `TreeNode<T>` objects. Each node must have a unique `id` across the entire tree.
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
type TreeNode<T = Record<string, unknown>> = {
|
|
66
|
+
/** Unique identifier. Must be unique across the entire tree. */
|
|
67
|
+
id: string;
|
|
68
|
+
/** Display label used by the default renderer. */
|
|
69
|
+
label: string;
|
|
70
|
+
/** Arbitrary user data attached to this node. */
|
|
71
|
+
data?: T;
|
|
72
|
+
/** Child nodes. Undefined or empty array means leaf node. */
|
|
73
|
+
children?: Array<TreeNode<T>>;
|
|
74
|
+
/** Mark as a parent whose children will be loaded via loadChildren. */
|
|
75
|
+
isParent?: boolean;
|
|
76
|
+
};
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Example with custom data
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
type FileInfo = {size: number; modified: string};
|
|
83
|
+
|
|
84
|
+
const data: TreeNode<FileInfo>[] = [
|
|
85
|
+
{
|
|
86
|
+
id: 'doc',
|
|
87
|
+
label: 'document.pdf',
|
|
88
|
+
data: {size: 1024, modified: '2025-01-15'},
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Props
|
|
94
|
+
|
|
95
|
+
| Prop | Type | Default | Description |
|
|
96
|
+
|------|------|---------|-------------|
|
|
97
|
+
| `data` | `TreeNode<T>[]` | *required* | Array of root-level tree nodes. |
|
|
98
|
+
| `selectionMode` | `'none' \| 'single' \| 'multiple'` | `'none'` | Selection behavior. `'single'` allows one selected node; `'multiple'` shows checkboxes. |
|
|
99
|
+
| `defaultExpanded` | `ReadonlySet<string> \| 'all'` | `undefined` | Node IDs expanded on mount, or `'all'` to expand everything. |
|
|
100
|
+
| `defaultSelected` | `ReadonlySet<string>` | `undefined` | Node IDs selected on mount (ignored in `'none'` mode). |
|
|
101
|
+
| `visibleNodeCount` | `number` | `Infinity` | Max visible rows. Enables virtual scrolling when finite. |
|
|
102
|
+
| `renderNode` | `(props: TreeNodeRendererProps<T>) => ReactNode` | `undefined` | Custom renderer for each node. Receives `{node, state}`. |
|
|
103
|
+
| `loadChildren` | `(node: TreeNode<T>) => Promise<TreeNode<T>[]>` | `undefined` | Async loader called when expanding an `isParent: true` node. |
|
|
104
|
+
| `onLoadError` | `(nodeId: string, error: Error) => void` | `undefined` | Called when `loadChildren` rejects. Loading state is cleared so the user can retry. |
|
|
105
|
+
| `onFocusChange` | `(nodeId: string) => void` | `undefined` | Called when the focused node changes (not on initial mount). |
|
|
106
|
+
| `onExpandChange` | `(expandedIds: ReadonlySet<string>) => void` | `undefined` | Called when the set of expanded nodes changes. |
|
|
107
|
+
| `onSelectChange` | `(selectedIds: ReadonlySet<string>) => void` | `undefined` | Called when the selection changes. |
|
|
108
|
+
| `isDisabled` | `boolean` | `false` | When true, all keyboard input is ignored. |
|
|
109
|
+
|
|
110
|
+
## Keyboard Shortcuts
|
|
111
|
+
|
|
112
|
+
| Key | Action |
|
|
113
|
+
|-----|--------|
|
|
114
|
+
| Up Arrow | Move focus to the previous visible node |
|
|
115
|
+
| Down Arrow | Move focus to the next visible node |
|
|
116
|
+
| Right Arrow | Expand focused node, or move to first child if already expanded. Triggers async load for `isParent` nodes. |
|
|
117
|
+
| Left Arrow | Collapse focused node, or move to parent if already collapsed |
|
|
118
|
+
| Enter | Toggle expand/collapse (`'none'` mode) or select (`'single'`/`'multiple'` mode) |
|
|
119
|
+
| Space | Toggle expand/collapse (`'none'`/`'single'` mode) or toggle selection (`'multiple'` mode) |
|
|
120
|
+
| Home | Jump to the first node |
|
|
121
|
+
| End | Jump to the last node |
|
|
122
|
+
|
|
123
|
+
## Custom Rendering
|
|
124
|
+
|
|
125
|
+
Use the `renderNode` prop to completely control how each node looks.
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
import {Box, Text} from 'ink';
|
|
129
|
+
import {TreeView, type TreeNodeRendererProps} from 'ink-tree-view';
|
|
130
|
+
|
|
131
|
+
type FileData = {size: number};
|
|
132
|
+
|
|
133
|
+
function CustomNode({node, state}: TreeNodeRendererProps<FileData>) {
|
|
134
|
+
const prefix = state.hasChildren
|
|
135
|
+
? state.isExpanded ? 'v ' : '> '
|
|
136
|
+
: ' ';
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<Box>
|
|
140
|
+
<Text dimColor={!state.isFocused}>
|
|
141
|
+
{' '.repeat(state.depth)}
|
|
142
|
+
{prefix}
|
|
143
|
+
{node.label}
|
|
144
|
+
</Text>
|
|
145
|
+
{node.data && (
|
|
146
|
+
<Text color="gray"> ({node.data.size} bytes)</Text>
|
|
147
|
+
)}
|
|
148
|
+
{state.isSelected && <Text color="green"> [selected]</Text>}
|
|
149
|
+
</Box>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
render(
|
|
154
|
+
<TreeView<FileData>
|
|
155
|
+
data={data}
|
|
156
|
+
selectionMode="single"
|
|
157
|
+
renderNode={CustomNode}
|
|
158
|
+
/>
|
|
159
|
+
);
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`TreeNodeRendererProps<T>` includes:
|
|
163
|
+
|
|
164
|
+
- `node` -- the `TreeNode<T>` data
|
|
165
|
+
- `state` -- a `TreeNodeState` with: `isFocused`, `isExpanded`, `isSelected`, `depth`, `hasChildren`, `isLoading`
|
|
166
|
+
|
|
167
|
+
## Selection Modes
|
|
168
|
+
|
|
169
|
+
### No selection (default)
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
<TreeView data={data} />
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Enter and Space toggle expand/collapse.
|
|
176
|
+
|
|
177
|
+
### Single selection
|
|
178
|
+
|
|
179
|
+
```tsx
|
|
180
|
+
<TreeView
|
|
181
|
+
data={data}
|
|
182
|
+
selectionMode="single"
|
|
183
|
+
onSelectChange={(selectedIds) => {
|
|
184
|
+
const selected = [...selectedIds][0];
|
|
185
|
+
console.log('Selected:', selected);
|
|
186
|
+
}}
|
|
187
|
+
/>
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Enter selects the focused node. Only one node can be selected at a time.
|
|
191
|
+
|
|
192
|
+
### Multiple selection
|
|
193
|
+
|
|
194
|
+
```tsx
|
|
195
|
+
<TreeView
|
|
196
|
+
data={data}
|
|
197
|
+
selectionMode="multiple"
|
|
198
|
+
defaultSelected={new Set(['node-1', 'node-3'])}
|
|
199
|
+
onSelectChange={(selectedIds) => {
|
|
200
|
+
console.log('Selected:', [...selectedIds]);
|
|
201
|
+
}}
|
|
202
|
+
/>
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Enter and Space toggle selection on the focused node. Checkboxes appear next to each node.
|
|
206
|
+
|
|
207
|
+
## Virtual Scrolling
|
|
208
|
+
|
|
209
|
+
For large trees, set `visibleNodeCount` to limit the number of visible rows. The viewport scrolls to keep the focused node in view, and scroll indicators appear when content extends beyond the viewport.
|
|
210
|
+
|
|
211
|
+
```tsx
|
|
212
|
+
<TreeView
|
|
213
|
+
data={largeTree}
|
|
214
|
+
defaultExpanded="all"
|
|
215
|
+
visibleNodeCount={15}
|
|
216
|
+
/>
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Async Children
|
|
220
|
+
|
|
221
|
+
Use `loadChildren` to lazily load children when a node is first expanded. Mark on-demand nodes with `isParent: true`. A loading indicator is shown while the request is in progress.
|
|
222
|
+
|
|
223
|
+
```tsx
|
|
224
|
+
async function fetchChildren(node) {
|
|
225
|
+
const response = await fetch(`/api/tree/${node.id}/children`);
|
|
226
|
+
return response.json();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const data = [
|
|
230
|
+
{id: 'root', label: 'Root', isParent: true},
|
|
231
|
+
{id: 'leaf', label: 'Leaf'},
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
render(
|
|
235
|
+
<TreeView
|
|
236
|
+
data={data}
|
|
237
|
+
loadChildren={fetchChildren}
|
|
238
|
+
onLoadError={(nodeId, error) => {
|
|
239
|
+
console.error(`Failed to load children for ${nodeId}:`, error.message);
|
|
240
|
+
}}
|
|
241
|
+
/>
|
|
242
|
+
);
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
When `loadChildren` rejects, `onLoadError` fires and the loading state is cleared so the user can retry by pressing Right Arrow again.
|
|
246
|
+
|
|
247
|
+
## Hooks API
|
|
248
|
+
|
|
249
|
+
For headless/custom usage, two hooks are exported directly.
|
|
250
|
+
|
|
251
|
+
### `useTreeViewState<T>(props)`
|
|
252
|
+
|
|
253
|
+
Manages all tree state: focus, expansion, selection, viewport scrolling, and loading.
|
|
254
|
+
|
|
255
|
+
```tsx
|
|
256
|
+
import {useTreeViewState} from 'ink-tree-view';
|
|
257
|
+
|
|
258
|
+
const state = useTreeViewState({
|
|
259
|
+
data,
|
|
260
|
+
selectionMode: 'multiple',
|
|
261
|
+
defaultExpanded: new Set(['root']),
|
|
262
|
+
visibleNodeCount: 10,
|
|
263
|
+
onFocusChange: (id) => { /* ... */ },
|
|
264
|
+
onExpandChange: (ids) => { /* ... */ },
|
|
265
|
+
onSelectChange: (ids) => { /* ... */ },
|
|
266
|
+
});
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
**Returned state:**
|
|
270
|
+
|
|
271
|
+
| Property | Type | Description |
|
|
272
|
+
|----------|------|-------------|
|
|
273
|
+
| `focusedId` | `string \| undefined` | Currently focused node ID |
|
|
274
|
+
| `expandedIds` | `ReadonlySet<string>` | Set of expanded node IDs |
|
|
275
|
+
| `selectedIds` | `ReadonlySet<string>` | Set of selected node IDs |
|
|
276
|
+
| `viewportNodes` | `Array<{node, state}>` | Nodes in current viewport |
|
|
277
|
+
| `visibleCount` | `number` | Total visible node count |
|
|
278
|
+
| `hasScrollUp` | `boolean` | Nodes exist above the viewport |
|
|
279
|
+
| `hasScrollDown` | `boolean` | Nodes exist below the viewport |
|
|
280
|
+
| `loadingIds` | `ReadonlySet<string>` | Currently loading node IDs |
|
|
281
|
+
| `nodeMap` | `TreeNodeMap<T>` | Underlying data structure |
|
|
282
|
+
|
|
283
|
+
**Actions:**
|
|
284
|
+
|
|
285
|
+
| Method | Description |
|
|
286
|
+
|--------|-------------|
|
|
287
|
+
| `focusNext()` | Move focus down |
|
|
288
|
+
| `focusPrevious()` | Move focus up |
|
|
289
|
+
| `focusFirst()` | Jump to first node |
|
|
290
|
+
| `focusLast()` | Jump to last node |
|
|
291
|
+
| `expand()` | Expand focused node |
|
|
292
|
+
| `expandNode(id)` | Expand a specific node |
|
|
293
|
+
| `collapse()` | Collapse focused node |
|
|
294
|
+
| `collapseNode(id)` | Collapse a specific node |
|
|
295
|
+
| `toggleExpanded()` | Toggle expand/collapse on focused node |
|
|
296
|
+
| `expandAll()` | Expand all nodes |
|
|
297
|
+
| `collapseAll()` | Collapse all nodes |
|
|
298
|
+
| `select()` | Select/deselect focused node |
|
|
299
|
+
| `focusParent()` | Move focus to parent |
|
|
300
|
+
| `focusFirstChild()` | Move focus to first child |
|
|
301
|
+
| `setLoading(id, bool)` | Mark a node as loading |
|
|
302
|
+
| `setChildrenError(id)` | Clear loading state after failure |
|
|
303
|
+
| `insertChildren(parentId, children)` | Insert children under a parent |
|
|
304
|
+
|
|
305
|
+
### `useTreeView<T>(props)`
|
|
306
|
+
|
|
307
|
+
Wires keyboard input to a `TreeViewState` instance. Call this after `useTreeViewState` to enable keyboard navigation.
|
|
308
|
+
|
|
309
|
+
```tsx
|
|
310
|
+
import {Box, Text} from 'ink';
|
|
311
|
+
import {useTreeViewState, useTreeView} from 'ink-tree-view';
|
|
312
|
+
|
|
313
|
+
function MyTree({data}) {
|
|
314
|
+
const state = useTreeViewState({data});
|
|
315
|
+
|
|
316
|
+
useTreeView({
|
|
317
|
+
state,
|
|
318
|
+
selectionMode: 'none',
|
|
319
|
+
loadChildren: async (node) => {
|
|
320
|
+
// fetch children...
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
return (
|
|
325
|
+
<Box flexDirection="column">
|
|
326
|
+
{state.viewportNodes.map(({node, state: ns}) => (
|
|
327
|
+
<Text key={node.id} bold={ns.isFocused}>
|
|
328
|
+
{' '.repeat(ns.depth)}{node.label}
|
|
329
|
+
</Text>
|
|
330
|
+
))}
|
|
331
|
+
</Box>
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
## TypeScript
|
|
337
|
+
|
|
338
|
+
All types are exported from the package entry point:
|
|
339
|
+
|
|
340
|
+
```ts
|
|
341
|
+
import type {
|
|
342
|
+
TreeNode,
|
|
343
|
+
TreeNodeState,
|
|
344
|
+
SelectionMode,
|
|
345
|
+
AsyncChildrenFn,
|
|
346
|
+
TreeViewProps,
|
|
347
|
+
TreeNodeRendererProps,
|
|
348
|
+
TreeViewState,
|
|
349
|
+
UseTreeViewStateProps,
|
|
350
|
+
UseTreeViewProps,
|
|
351
|
+
TreeViewTheme,
|
|
352
|
+
FlatNode,
|
|
353
|
+
} from 'ink-tree-view';
|
|
354
|
+
|
|
355
|
+
import {
|
|
356
|
+
TreeView,
|
|
357
|
+
useTreeViewState,
|
|
358
|
+
useTreeView,
|
|
359
|
+
treeViewTheme,
|
|
360
|
+
TreeNodeMap,
|
|
361
|
+
} from 'ink-tree-view';
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
## Contributing
|
|
365
|
+
|
|
366
|
+
Contributions are welcome. Please open an issue to discuss your idea before submitting a PR.
|
|
367
|
+
|
|
368
|
+
```bash
|
|
369
|
+
git clone https://github.com/costajohnt/ink-tree-view.git
|
|
370
|
+
cd ink-tree-view
|
|
371
|
+
npm install
|
|
372
|
+
npm test
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
Run `npm run build` to compile and `npm run typecheck` to verify types.
|
|
376
|
+
|
|
377
|
+
## Changelog
|
|
378
|
+
|
|
379
|
+
See [GitHub Releases](https://github.com/costajohnt/ink-tree-view/releases) for a list of changes.
|
|
380
|
+
|
|
381
|
+
## License
|
|
382
|
+
|
|
383
|
+
[MIT](LICENSE) -- Copyright (c) 2024-2026 John Costa
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { BoxProps, TextProps } from 'ink';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A single node in the tree. Generic over the user's data shape.
|
|
7
|
+
* The `id` field MUST be unique across the entire tree.
|
|
8
|
+
*/
|
|
9
|
+
type TreeNode<T = Record<string, unknown>> = {
|
|
10
|
+
/** Unique identifier for this node. Used as the key for all lookups. */
|
|
11
|
+
id: string;
|
|
12
|
+
/** Display label. Used by the default renderer. */
|
|
13
|
+
label: string;
|
|
14
|
+
/** Arbitrary user data attached to this node. */
|
|
15
|
+
data?: T;
|
|
16
|
+
/** Child nodes. Undefined or empty array means leaf node. */
|
|
17
|
+
children?: Array<TreeNode<T>>;
|
|
18
|
+
/**
|
|
19
|
+
* Explicitly marks this node as a parent that can have children loaded
|
|
20
|
+
* asynchronously via `loadChildren`. When true, the node shows an expand
|
|
21
|
+
* indicator even if `children` is empty or undefined, and expanding it
|
|
22
|
+
* triggers the `loadChildren` callback.
|
|
23
|
+
*/
|
|
24
|
+
isParent?: boolean;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* For async/lazy loading: a function that resolves children on demand.
|
|
28
|
+
*/
|
|
29
|
+
type AsyncChildrenFn<T = Record<string, unknown>> = (node: TreeNode<T>) => Promise<Array<TreeNode<T>>>;
|
|
30
|
+
/**
|
|
31
|
+
* State passed to custom renderers and theme functions.
|
|
32
|
+
*/
|
|
33
|
+
type TreeNodeState = {
|
|
34
|
+
/** Whether this node is currently focused (cursor is on it). */
|
|
35
|
+
isFocused: boolean;
|
|
36
|
+
/** Whether this node is expanded (children visible). */
|
|
37
|
+
isExpanded: boolean;
|
|
38
|
+
/** Whether this node is selected (in select/multi-select mode). */
|
|
39
|
+
isSelected: boolean;
|
|
40
|
+
/** Depth of this node in the tree (root = 0). */
|
|
41
|
+
depth: number;
|
|
42
|
+
/** Whether this node has children (or a lazy loader). */
|
|
43
|
+
hasChildren: boolean;
|
|
44
|
+
/** Whether children are currently loading (async mode). */
|
|
45
|
+
isLoading: boolean;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Props received by a custom node renderer.
|
|
49
|
+
*/
|
|
50
|
+
type TreeNodeRendererProps<T = Record<string, unknown>> = {
|
|
51
|
+
/** The tree node data. */
|
|
52
|
+
node: TreeNode<T>;
|
|
53
|
+
/** Current state of this node. */
|
|
54
|
+
state: TreeNodeState;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Selection mode for the tree view.
|
|
58
|
+
*/
|
|
59
|
+
type SelectionMode = 'none' | 'single' | 'multiple';
|
|
60
|
+
/**
|
|
61
|
+
* Props for the TreeView component.
|
|
62
|
+
*/
|
|
63
|
+
type TreeViewProps<T = Record<string, unknown>> = {
|
|
64
|
+
/** The tree data. Array of root-level nodes. */
|
|
65
|
+
readonly data: Array<TreeNode<T>>;
|
|
66
|
+
/**
|
|
67
|
+
* Selection mode.
|
|
68
|
+
* - 'none': No selection behavior (default).
|
|
69
|
+
* - 'single': One node can be selected at a time.
|
|
70
|
+
* - 'multiple': Multiple nodes can be selected (checkboxes shown).
|
|
71
|
+
* @default 'none'
|
|
72
|
+
*/
|
|
73
|
+
readonly selectionMode?: SelectionMode;
|
|
74
|
+
/**
|
|
75
|
+
* Set of node IDs that are expanded by default.
|
|
76
|
+
* If not provided, all nodes start collapsed.
|
|
77
|
+
*/
|
|
78
|
+
readonly defaultExpanded?: ReadonlySet<string> | 'all';
|
|
79
|
+
/**
|
|
80
|
+
* Set of node IDs that are selected by default.
|
|
81
|
+
*/
|
|
82
|
+
readonly defaultSelected?: ReadonlySet<string>;
|
|
83
|
+
/**
|
|
84
|
+
* Number of visible nodes in the viewport (for virtualization).
|
|
85
|
+
* @default Infinity (no virtualization)
|
|
86
|
+
*/
|
|
87
|
+
readonly visibleNodeCount?: number;
|
|
88
|
+
/**
|
|
89
|
+
* Custom node renderer. Receives node + state, returns Ink JSX.
|
|
90
|
+
*/
|
|
91
|
+
readonly renderNode?: (props: TreeNodeRendererProps<T>) => ReactNode;
|
|
92
|
+
/**
|
|
93
|
+
* Async function to load children on demand.
|
|
94
|
+
*/
|
|
95
|
+
readonly loadChildren?: AsyncChildrenFn<T>;
|
|
96
|
+
/**
|
|
97
|
+
* Called when `loadChildren` rejects. Receives the node ID and error.
|
|
98
|
+
* The node's loading state is automatically cleared so the user can retry.
|
|
99
|
+
*/
|
|
100
|
+
readonly onLoadError?: (nodeId: string, error: Error) => void;
|
|
101
|
+
/** Called when the focused node changes. */
|
|
102
|
+
readonly onFocusChange?: (nodeId: string) => void;
|
|
103
|
+
/** Called when expanded set changes. */
|
|
104
|
+
readonly onExpandChange?: (expandedIds: ReadonlySet<string>) => void;
|
|
105
|
+
/** Called when selection changes. */
|
|
106
|
+
readonly onSelectChange?: (selectedIds: ReadonlySet<string>) => void;
|
|
107
|
+
/**
|
|
108
|
+
* When disabled, user input is ignored.
|
|
109
|
+
* @default false
|
|
110
|
+
*/
|
|
111
|
+
readonly isDisabled?: boolean;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
declare function TreeView<T = Record<string, unknown>>({ data, selectionMode, defaultExpanded, defaultSelected, visibleNodeCount, renderNode, loadChildren, onLoadError, onFocusChange, onExpandChange, onSelectChange, isDisabled, }: TreeViewProps<T>): react_jsx_runtime.JSX.Element;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* A flattened representation of a tree node with navigation links.
|
|
118
|
+
*/
|
|
119
|
+
type FlatNode<T = Record<string, unknown>> = {
|
|
120
|
+
/** The original tree node. */
|
|
121
|
+
node: TreeNode<T>;
|
|
122
|
+
/** Depth in the tree (0 for roots). */
|
|
123
|
+
depth: number;
|
|
124
|
+
/** Index in the flattened DFS order (across ALL nodes, not just visible). */
|
|
125
|
+
flatIndex: number;
|
|
126
|
+
/** Parent's id, or undefined if root. */
|
|
127
|
+
parentId: string | undefined;
|
|
128
|
+
/** Whether this node has children. */
|
|
129
|
+
hasChildren: boolean;
|
|
130
|
+
/** Ordered list of direct children IDs. */
|
|
131
|
+
childrenIds: string[];
|
|
132
|
+
/** Previous sibling's ID, or undefined. */
|
|
133
|
+
previousSiblingId: string | undefined;
|
|
134
|
+
/** Next sibling's ID, or undefined. */
|
|
135
|
+
nextSiblingId: string | undefined;
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* A flattened map of all tree nodes built via DFS traversal.
|
|
139
|
+
* Stores parent/child/sibling links for O(1) navigation.
|
|
140
|
+
*/
|
|
141
|
+
declare class TreeNodeMap<T = Record<string, unknown>> {
|
|
142
|
+
/** Map from node ID to FlatNode. */
|
|
143
|
+
private readonly map;
|
|
144
|
+
/** All node IDs in DFS order. */
|
|
145
|
+
readonly orderedIds: string[];
|
|
146
|
+
/** Root-level node IDs. */
|
|
147
|
+
readonly rootIds: string[];
|
|
148
|
+
constructor(data: Array<TreeNode<T>>);
|
|
149
|
+
private buildFromData;
|
|
150
|
+
/**
|
|
151
|
+
* Get a flat node by ID.
|
|
152
|
+
*/
|
|
153
|
+
get(id: string): FlatNode<T> | undefined;
|
|
154
|
+
/**
|
|
155
|
+
* Total number of nodes in the tree.
|
|
156
|
+
*/
|
|
157
|
+
get size(): number;
|
|
158
|
+
/**
|
|
159
|
+
* Iterate over all entries.
|
|
160
|
+
*/
|
|
161
|
+
entries(): IterableIterator<[string, FlatNode<T>]>;
|
|
162
|
+
/**
|
|
163
|
+
* Check if a node is a descendant of another node.
|
|
164
|
+
*/
|
|
165
|
+
isDescendantOf(nodeId: string, ancestorId: string): boolean;
|
|
166
|
+
/**
|
|
167
|
+
* Given a set of expanded node IDs, return the ordered list of
|
|
168
|
+
* VISIBLE node IDs (i.e., a node is visible if all its ancestors
|
|
169
|
+
* are expanded).
|
|
170
|
+
*
|
|
171
|
+
* Uses iterative DFS, skipping collapsed subtrees.
|
|
172
|
+
*/
|
|
173
|
+
getVisibleIds(expandedIds: ReadonlySet<string>): string[];
|
|
174
|
+
/**
|
|
175
|
+
* Insert dynamically-loaded children under a parent node.
|
|
176
|
+
* Returns a new TreeNodeMap (immutable operation).
|
|
177
|
+
*/
|
|
178
|
+
withChildren(parentId: string, children: Array<TreeNode<T>>): TreeNodeMap<T>;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
type UseTreeViewStateProps<T = Record<string, unknown>> = {
|
|
182
|
+
data: Array<TreeNode<T>>;
|
|
183
|
+
selectionMode?: SelectionMode;
|
|
184
|
+
defaultExpanded?: ReadonlySet<string> | 'all';
|
|
185
|
+
defaultSelected?: ReadonlySet<string>;
|
|
186
|
+
visibleNodeCount?: number;
|
|
187
|
+
onFocusChange?: (nodeId: string) => void;
|
|
188
|
+
onExpandChange?: (expandedIds: ReadonlySet<string>) => void;
|
|
189
|
+
onSelectChange?: (selectedIds: ReadonlySet<string>) => void;
|
|
190
|
+
};
|
|
191
|
+
type TreeViewState<T = Record<string, unknown>> = {
|
|
192
|
+
focusedId: string | undefined;
|
|
193
|
+
expandedIds: ReadonlySet<string>;
|
|
194
|
+
selectedIds: ReadonlySet<string>;
|
|
195
|
+
viewportNodes: Array<{
|
|
196
|
+
node: TreeNode<T>;
|
|
197
|
+
state: TreeNodeState;
|
|
198
|
+
}>;
|
|
199
|
+
visibleCount: number;
|
|
200
|
+
hasScrollUp: boolean;
|
|
201
|
+
hasScrollDown: boolean;
|
|
202
|
+
viewportFromIndex: number;
|
|
203
|
+
viewportToIndex: number;
|
|
204
|
+
loadingIds: ReadonlySet<string>;
|
|
205
|
+
nodeMap: TreeNodeMap<T>;
|
|
206
|
+
focusNext: () => void;
|
|
207
|
+
focusPrevious: () => void;
|
|
208
|
+
focusFirst: () => void;
|
|
209
|
+
focusLast: () => void;
|
|
210
|
+
expand: () => void;
|
|
211
|
+
expandNode: (nodeId: string) => void;
|
|
212
|
+
collapse: () => void;
|
|
213
|
+
collapseNode: (nodeId: string) => void;
|
|
214
|
+
toggleExpanded: () => void;
|
|
215
|
+
expandAll: () => void;
|
|
216
|
+
collapseAll: () => void;
|
|
217
|
+
select: () => void;
|
|
218
|
+
focusParent: () => void;
|
|
219
|
+
focusFirstChild: () => void;
|
|
220
|
+
setLoading: (nodeId: string, isLoading: boolean) => void;
|
|
221
|
+
setChildrenError: (nodeId: string) => void;
|
|
222
|
+
insertChildren: (parentId: string, children: Array<TreeNode<T>>) => void;
|
|
223
|
+
};
|
|
224
|
+
declare function useTreeViewState<T = Record<string, unknown>>({ data, selectionMode, defaultExpanded, defaultSelected, visibleNodeCount, onFocusChange, onExpandChange, onSelectChange, }: UseTreeViewStateProps<T>): TreeViewState<T>;
|
|
225
|
+
|
|
226
|
+
type UseTreeViewProps<T = Record<string, unknown>> = {
|
|
227
|
+
isDisabled?: boolean;
|
|
228
|
+
selectionMode: SelectionMode;
|
|
229
|
+
state: TreeViewState<T>;
|
|
230
|
+
loadChildren?: AsyncChildrenFn<T>;
|
|
231
|
+
onLoadError?: (nodeId: string, error: Error) => void;
|
|
232
|
+
};
|
|
233
|
+
declare function useTreeView<T>({ isDisabled, selectionMode, state, loadChildren, onLoadError, }: UseTreeViewProps<T>): void;
|
|
234
|
+
|
|
235
|
+
type StyleFnProps = {
|
|
236
|
+
isFocused?: boolean;
|
|
237
|
+
isExpanded?: boolean;
|
|
238
|
+
isSelected?: boolean;
|
|
239
|
+
depth?: number;
|
|
240
|
+
hasChildren?: boolean;
|
|
241
|
+
isLoading?: boolean;
|
|
242
|
+
};
|
|
243
|
+
declare const theme: {
|
|
244
|
+
styles: {
|
|
245
|
+
container: () => BoxProps;
|
|
246
|
+
node: ({ isFocused }: StyleFnProps) => BoxProps;
|
|
247
|
+
indent: ({ depth }: StyleFnProps) => BoxProps;
|
|
248
|
+
focusIndicator: () => TextProps;
|
|
249
|
+
expandIndicator: (_props: StyleFnProps) => TextProps;
|
|
250
|
+
label: ({ isFocused, isSelected }: StyleFnProps) => TextProps;
|
|
251
|
+
selectedIndicator: () => TextProps;
|
|
252
|
+
loadingIndicator: () => TextProps;
|
|
253
|
+
};
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
type Theme = typeof theme;
|
|
257
|
+
|
|
258
|
+
export { type AsyncChildrenFn, type FlatNode, type SelectionMode, type TreeNode, TreeNodeMap, type TreeNodeRendererProps, type TreeNodeState, TreeView, type TreeViewProps, type TreeViewState, type Theme as TreeViewTheme, type UseTreeViewProps, type UseTreeViewStateProps, theme as treeViewTheme, useTreeView, useTreeViewState };
|