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/dist/index.js ADDED
@@ -0,0 +1,878 @@
1
+ // src/components/tree-view/tree-view.tsx
2
+ import { Box as Box2, Text as Text2 } from "ink";
3
+
4
+ // src/components/tree-view/use-tree-view-state.ts
5
+ import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react";
6
+
7
+ // src/tree-node-map.ts
8
+ var TreeNodeMap = class _TreeNodeMap {
9
+ /** Map from node ID to FlatNode. */
10
+ map;
11
+ /** All node IDs in DFS order. */
12
+ orderedIds;
13
+ /** Root-level node IDs. */
14
+ rootIds;
15
+ constructor(data) {
16
+ this.map = /* @__PURE__ */ new Map();
17
+ this.orderedIds = [];
18
+ this.rootIds = [];
19
+ this.buildFromData(data);
20
+ }
21
+ buildFromData(data) {
22
+ const stack = [];
23
+ for (let i = data.length - 1; i >= 0; i--) {
24
+ stack.push({
25
+ node: data[i],
26
+ depth: 0,
27
+ parentId: void 0,
28
+ siblings: data,
29
+ siblingIndex: i
30
+ });
31
+ }
32
+ for (const node of data) {
33
+ this.rootIds.push(node.id);
34
+ }
35
+ let flatIndex = 0;
36
+ while (stack.length > 0) {
37
+ const entry = stack.pop();
38
+ const { node, depth, parentId, siblings, siblingIndex } = entry;
39
+ if (this.map.has(node.id)) {
40
+ throw new Error(
41
+ `TreeView: Duplicate node id '${node.id}' found. All node ids must be unique.`
42
+ );
43
+ }
44
+ const children = node.children ?? [];
45
+ const hasChildren = children.length > 0 || node.isParent === true;
46
+ const childrenIds = children.map((c) => c.id);
47
+ const previousSiblingId = siblingIndex > 0 ? siblings[siblingIndex - 1].id : void 0;
48
+ const nextSiblingId = siblingIndex < siblings.length - 1 ? siblings[siblingIndex + 1].id : void 0;
49
+ const flatNode = {
50
+ node,
51
+ depth,
52
+ flatIndex,
53
+ parentId,
54
+ hasChildren,
55
+ childrenIds,
56
+ previousSiblingId,
57
+ nextSiblingId
58
+ };
59
+ this.map.set(node.id, flatNode);
60
+ this.orderedIds.push(node.id);
61
+ flatIndex++;
62
+ if (hasChildren) {
63
+ for (let i = children.length - 1; i >= 0; i--) {
64
+ stack.push({
65
+ node: children[i],
66
+ depth: depth + 1,
67
+ parentId: node.id,
68
+ siblings: children,
69
+ siblingIndex: i
70
+ });
71
+ }
72
+ }
73
+ }
74
+ }
75
+ /**
76
+ * Get a flat node by ID.
77
+ */
78
+ get(id) {
79
+ return this.map.get(id);
80
+ }
81
+ /**
82
+ * Total number of nodes in the tree.
83
+ */
84
+ get size() {
85
+ return this.map.size;
86
+ }
87
+ /**
88
+ * Iterate over all entries.
89
+ */
90
+ entries() {
91
+ return this.map.entries();
92
+ }
93
+ /**
94
+ * Check if a node is a descendant of another node.
95
+ */
96
+ isDescendantOf(nodeId, ancestorId) {
97
+ let currentId = nodeId;
98
+ while (currentId !== void 0) {
99
+ const flat = this.map.get(currentId);
100
+ if (!flat) return false;
101
+ if (flat.parentId === ancestorId) return true;
102
+ currentId = flat.parentId;
103
+ }
104
+ return false;
105
+ }
106
+ /**
107
+ * Given a set of expanded node IDs, return the ordered list of
108
+ * VISIBLE node IDs (i.e., a node is visible if all its ancestors
109
+ * are expanded).
110
+ *
111
+ * Uses iterative DFS, skipping collapsed subtrees.
112
+ */
113
+ getVisibleIds(expandedIds) {
114
+ const result = [];
115
+ const stack = [];
116
+ for (let i = this.rootIds.length - 1; i >= 0; i--) {
117
+ stack.push(this.rootIds[i]);
118
+ }
119
+ while (stack.length > 0) {
120
+ const id = stack.pop();
121
+ result.push(id);
122
+ const flatNode = this.map.get(id);
123
+ if (flatNode && expandedIds.has(id) && flatNode.childrenIds.length > 0) {
124
+ for (let i = flatNode.childrenIds.length - 1; i >= 0; i--) {
125
+ stack.push(flatNode.childrenIds[i]);
126
+ }
127
+ }
128
+ }
129
+ return result;
130
+ }
131
+ /**
132
+ * Insert dynamically-loaded children under a parent node.
133
+ * Returns a new TreeNodeMap (immutable operation).
134
+ */
135
+ withChildren(parentId, children) {
136
+ const parentFlat = this.map.get(parentId);
137
+ if (!parentFlat) return this;
138
+ const rebuildNode = (node) => {
139
+ if (node.id === parentId) {
140
+ return { ...node, children };
141
+ }
142
+ if (node.children) {
143
+ return { ...node, children: node.children.map(rebuildNode) };
144
+ }
145
+ return node;
146
+ };
147
+ const rootData = [];
148
+ for (const rootId of this.rootIds) {
149
+ const rootFlat = this.map.get(rootId);
150
+ if (rootFlat) {
151
+ rootData.push(rebuildNode(rootFlat.node));
152
+ }
153
+ }
154
+ return new _TreeNodeMap(rootData);
155
+ }
156
+ };
157
+
158
+ // src/components/tree-view/use-tree-view-state.ts
159
+ function buildVisibleIdIndex(visibleIds) {
160
+ const map = /* @__PURE__ */ new Map();
161
+ for (let i = 0; i < visibleIds.length; i++) {
162
+ map.set(visibleIds[i], i);
163
+ }
164
+ return map;
165
+ }
166
+ function adjustViewport(state, targetIndex) {
167
+ if (state.visibleNodeCount >= state.visibleIds.length) {
168
+ return { viewportFromIndex: 0, viewportToIndex: state.visibleIds.length };
169
+ }
170
+ let from = state.viewportFromIndex;
171
+ let to = state.viewportToIndex;
172
+ if (targetIndex >= to) {
173
+ to = targetIndex + 1;
174
+ from = to - state.visibleNodeCount;
175
+ }
176
+ if (targetIndex < from) {
177
+ from = targetIndex;
178
+ to = from + state.visibleNodeCount;
179
+ }
180
+ return {
181
+ viewportFromIndex: Math.max(0, from),
182
+ viewportToIndex: Math.min(state.visibleIds.length, to)
183
+ };
184
+ }
185
+ function adjustViewportForNewVisible(state, newVisible) {
186
+ if (state.visibleNodeCount >= newVisible.length) {
187
+ return { viewportFromIndex: 0, viewportToIndex: newVisible.length };
188
+ }
189
+ const focusedIdx = state.focusedId ? newVisible.indexOf(state.focusedId) : -1;
190
+ if (focusedIdx < 0) {
191
+ return {
192
+ viewportFromIndex: 0,
193
+ viewportToIndex: Math.min(state.visibleNodeCount, newVisible.length)
194
+ };
195
+ }
196
+ let from = state.viewportFromIndex;
197
+ let to = state.viewportToIndex;
198
+ to = Math.min(to, newVisible.length);
199
+ from = Math.max(0, to - state.visibleNodeCount);
200
+ if (focusedIdx >= to) {
201
+ to = focusedIdx + 1;
202
+ from = to - state.visibleNodeCount;
203
+ }
204
+ if (focusedIdx < from) {
205
+ from = focusedIdx;
206
+ to = from + state.visibleNodeCount;
207
+ }
208
+ return {
209
+ viewportFromIndex: Math.max(0, from),
210
+ viewportToIndex: Math.min(newVisible.length, to)
211
+ };
212
+ }
213
+ function reducer(state, action) {
214
+ switch (action.type) {
215
+ case "focus-next": {
216
+ if (!state.focusedId) return state;
217
+ const idx = state.visibleIdIndex.get(state.focusedId) ?? -1;
218
+ if (idx < 0 || idx >= state.visibleIds.length - 1) return state;
219
+ const nextId = state.visibleIds[idx + 1];
220
+ return {
221
+ ...state,
222
+ focusedId: nextId,
223
+ ...adjustViewport(state, idx + 1)
224
+ };
225
+ }
226
+ case "focus-previous": {
227
+ if (!state.focusedId) return state;
228
+ const idx = state.visibleIdIndex.get(state.focusedId) ?? -1;
229
+ if (idx <= 0) return state;
230
+ const prevId = state.visibleIds[idx - 1];
231
+ return {
232
+ ...state,
233
+ focusedId: prevId,
234
+ ...adjustViewport(state, idx - 1)
235
+ };
236
+ }
237
+ case "focus-first": {
238
+ if (state.visibleIds.length === 0) return state;
239
+ return {
240
+ ...state,
241
+ focusedId: state.visibleIds[0],
242
+ viewportFromIndex: 0,
243
+ viewportToIndex: Math.min(
244
+ state.visibleNodeCount,
245
+ state.visibleIds.length
246
+ )
247
+ };
248
+ }
249
+ case "focus-last": {
250
+ if (state.visibleIds.length === 0) return state;
251
+ const lastIdx = state.visibleIds.length - 1;
252
+ return {
253
+ ...state,
254
+ focusedId: state.visibleIds[lastIdx],
255
+ ...adjustViewport(state, lastIdx)
256
+ };
257
+ }
258
+ case "expand": {
259
+ if (!state.focusedId) return state;
260
+ return reducer(state, { type: "expand-node", nodeId: state.focusedId });
261
+ }
262
+ case "expand-node": {
263
+ const { nodeId } = action;
264
+ const flat = state.nodeMap.get(nodeId);
265
+ if (!flat || !flat.hasChildren) return state;
266
+ if (state.expandedIds.has(nodeId)) return state;
267
+ const newExpanded = new Set(state.expandedIds);
268
+ newExpanded.add(nodeId);
269
+ const newVisible = state.nodeMap.getVisibleIds(newExpanded);
270
+ return {
271
+ ...state,
272
+ previousExpandedIds: state.expandedIds,
273
+ expandedIds: newExpanded,
274
+ visibleIds: newVisible,
275
+ visibleIdIndex: buildVisibleIdIndex(newVisible),
276
+ ...adjustViewportForNewVisible(
277
+ { ...state, visibleIds: newVisible },
278
+ newVisible
279
+ )
280
+ };
281
+ }
282
+ case "collapse": {
283
+ if (!state.focusedId) return state;
284
+ return reducer(state, { type: "collapse-node", nodeId: state.focusedId });
285
+ }
286
+ case "collapse-node": {
287
+ const { nodeId } = action;
288
+ if (!state.expandedIds.has(nodeId)) return state;
289
+ const newExpanded = new Set(state.expandedIds);
290
+ newExpanded.delete(nodeId);
291
+ const newVisible = state.nodeMap.getVisibleIds(newExpanded);
292
+ let newFocusedId = state.focusedId;
293
+ if (newFocusedId !== void 0 && newFocusedId !== nodeId && state.nodeMap.isDescendantOf(newFocusedId, nodeId)) {
294
+ newFocusedId = nodeId;
295
+ }
296
+ return {
297
+ ...state,
298
+ focusedId: newFocusedId,
299
+ previousExpandedIds: state.expandedIds,
300
+ expandedIds: newExpanded,
301
+ visibleIds: newVisible,
302
+ visibleIdIndex: buildVisibleIdIndex(newVisible),
303
+ ...adjustViewportForNewVisible(
304
+ { ...state, focusedId: newFocusedId, visibleIds: newVisible },
305
+ newVisible
306
+ )
307
+ };
308
+ }
309
+ case "toggle-expanded": {
310
+ if (!state.focusedId) return state;
311
+ if (state.expandedIds.has(state.focusedId)) {
312
+ return reducer(state, { type: "collapse" });
313
+ }
314
+ return reducer(state, { type: "expand" });
315
+ }
316
+ case "expand-all": {
317
+ const allParentIds = /* @__PURE__ */ new Set();
318
+ for (const [id, flat] of state.nodeMap.entries()) {
319
+ if (flat.hasChildren) allParentIds.add(id);
320
+ }
321
+ const newVisible = state.nodeMap.getVisibleIds(allParentIds);
322
+ return {
323
+ ...state,
324
+ previousExpandedIds: state.expandedIds,
325
+ expandedIds: allParentIds,
326
+ visibleIds: newVisible,
327
+ visibleIdIndex: buildVisibleIdIndex(newVisible),
328
+ ...adjustViewportForNewVisible(
329
+ { ...state, visibleIds: newVisible },
330
+ newVisible
331
+ )
332
+ };
333
+ }
334
+ case "collapse-all": {
335
+ const newExpanded = /* @__PURE__ */ new Set();
336
+ const newVisible = state.nodeMap.getVisibleIds(newExpanded);
337
+ return {
338
+ ...state,
339
+ previousExpandedIds: state.expandedIds,
340
+ expandedIds: newExpanded,
341
+ visibleIds: newVisible,
342
+ visibleIdIndex: buildVisibleIdIndex(newVisible),
343
+ focusedId: newVisible[0],
344
+ viewportFromIndex: 0,
345
+ viewportToIndex: Math.min(
346
+ state.visibleNodeCount,
347
+ newVisible.length
348
+ )
349
+ };
350
+ }
351
+ case "select": {
352
+ if (!state.focusedId) return state;
353
+ if (state.selectionMode === "none") return state;
354
+ if (state.selectionMode === "single") {
355
+ const newSelected2 = /* @__PURE__ */ new Set([state.focusedId]);
356
+ return {
357
+ ...state,
358
+ previousSelectedIds: state.selectedIds,
359
+ selectedIds: newSelected2
360
+ };
361
+ }
362
+ const newSelected = new Set(state.selectedIds);
363
+ if (newSelected.has(state.focusedId)) {
364
+ newSelected.delete(state.focusedId);
365
+ } else {
366
+ newSelected.add(state.focusedId);
367
+ }
368
+ return {
369
+ ...state,
370
+ previousSelectedIds: state.selectedIds,
371
+ selectedIds: newSelected
372
+ };
373
+ }
374
+ case "focus-parent": {
375
+ if (!state.focusedId) return state;
376
+ const flat = state.nodeMap.get(state.focusedId);
377
+ if (!flat?.parentId) return state;
378
+ const parentIdx = state.visibleIdIndex.get(flat.parentId) ?? -1;
379
+ if (parentIdx < 0) return state;
380
+ return {
381
+ ...state,
382
+ focusedId: flat.parentId,
383
+ ...adjustViewport(state, parentIdx)
384
+ };
385
+ }
386
+ case "focus-first-child": {
387
+ if (!state.focusedId) return state;
388
+ const flat = state.nodeMap.get(state.focusedId);
389
+ if (!flat || flat.childrenIds.length === 0) return state;
390
+ if (!state.expandedIds.has(state.focusedId)) return state;
391
+ const firstChildId = flat.childrenIds[0];
392
+ const childIdx = state.visibleIdIndex.get(firstChildId) ?? -1;
393
+ if (childIdx < 0) return state;
394
+ return {
395
+ ...state,
396
+ focusedId: firstChildId,
397
+ ...adjustViewport(state, childIdx)
398
+ };
399
+ }
400
+ case "set-loading": {
401
+ const newLoading = new Set(state.loadingIds);
402
+ if (action.isLoading) {
403
+ newLoading.add(action.nodeId);
404
+ } else {
405
+ newLoading.delete(action.nodeId);
406
+ }
407
+ return { ...state, loadingIds: newLoading };
408
+ }
409
+ case "set-children-error": {
410
+ const newLoading = new Set(state.loadingIds);
411
+ newLoading.delete(action.nodeId);
412
+ return { ...state, loadingIds: newLoading };
413
+ }
414
+ case "insert-children": {
415
+ const parentFlat = state.nodeMap.get(action.parentId);
416
+ if (!parentFlat) return state;
417
+ const newNodeMap = state.nodeMap.withChildren(
418
+ action.parentId,
419
+ action.children
420
+ );
421
+ const newVisible = newNodeMap.getVisibleIds(state.expandedIds);
422
+ return {
423
+ ...state,
424
+ nodeMap: newNodeMap,
425
+ visibleIds: newVisible,
426
+ visibleIdIndex: buildVisibleIdIndex(newVisible)
427
+ };
428
+ }
429
+ case "reset": {
430
+ return action.state;
431
+ }
432
+ default: {
433
+ return state;
434
+ }
435
+ }
436
+ }
437
+ function createDefaultState({
438
+ data,
439
+ selectionMode,
440
+ defaultExpanded,
441
+ defaultSelected,
442
+ visibleNodeCount
443
+ }) {
444
+ const nodeMap = new TreeNodeMap(data);
445
+ let expandedIds;
446
+ if (defaultExpanded === "all") {
447
+ expandedIds = /* @__PURE__ */ new Set();
448
+ for (const [id, flat] of nodeMap.entries()) {
449
+ if (flat.hasChildren) expandedIds.add(id);
450
+ }
451
+ } else if (defaultExpanded) {
452
+ expandedIds = new Set(defaultExpanded);
453
+ } else {
454
+ expandedIds = /* @__PURE__ */ new Set();
455
+ }
456
+ const visibleIds = nodeMap.getVisibleIds(expandedIds);
457
+ const selectedIds = selectionMode !== "none" && defaultSelected ? new Set(defaultSelected) : /* @__PURE__ */ new Set();
458
+ const nodeCount = Math.min(visibleNodeCount, visibleIds.length);
459
+ return {
460
+ nodeMap,
461
+ expandedIds,
462
+ visibleIds,
463
+ visibleIdIndex: buildVisibleIdIndex(visibleIds),
464
+ focusedId: visibleIds[0],
465
+ visibleNodeCount,
466
+ viewportFromIndex: 0,
467
+ viewportToIndex: nodeCount,
468
+ selectionMode,
469
+ selectedIds,
470
+ previousSelectedIds: selectedIds,
471
+ previousExpandedIds: expandedIds,
472
+ loadingIds: /* @__PURE__ */ new Set()
473
+ };
474
+ }
475
+ function useTreeViewState({
476
+ data,
477
+ selectionMode = "none",
478
+ defaultExpanded,
479
+ defaultSelected,
480
+ visibleNodeCount = Infinity,
481
+ onFocusChange,
482
+ onExpandChange,
483
+ onSelectChange
484
+ }) {
485
+ const [state, dispatch] = useReducer(
486
+ reducer,
487
+ { data, selectionMode, defaultExpanded, defaultSelected, visibleNodeCount },
488
+ createDefaultState
489
+ );
490
+ const [lastData, setLastData] = useState(data);
491
+ if (data !== lastData) {
492
+ dispatch({
493
+ type: "reset",
494
+ state: createDefaultState({
495
+ data,
496
+ selectionMode,
497
+ defaultExpanded,
498
+ defaultSelected,
499
+ visibleNodeCount
500
+ })
501
+ });
502
+ setLastData(data);
503
+ }
504
+ const isInitialMount = useRef(true);
505
+ useEffect(() => {
506
+ if (isInitialMount.current) {
507
+ isInitialMount.current = false;
508
+ return;
509
+ }
510
+ if (state.focusedId) onFocusChange?.(state.focusedId);
511
+ }, [state.focusedId, onFocusChange]);
512
+ useEffect(() => {
513
+ if (state.expandedIds !== state.previousExpandedIds) {
514
+ onExpandChange?.(state.expandedIds);
515
+ }
516
+ }, [state.expandedIds, state.previousExpandedIds, onExpandChange]);
517
+ useEffect(() => {
518
+ if (state.selectedIds !== state.previousSelectedIds) {
519
+ onSelectChange?.(state.selectedIds);
520
+ }
521
+ }, [state.selectedIds, state.previousSelectedIds, onSelectChange]);
522
+ const viewportNodes = useMemo(() => {
523
+ return state.visibleIds.slice(state.viewportFromIndex, state.viewportToIndex).map((id) => {
524
+ const flat = state.nodeMap.get(id);
525
+ return {
526
+ node: flat.node,
527
+ state: {
528
+ isFocused: id === state.focusedId,
529
+ isExpanded: state.expandedIds.has(id),
530
+ isSelected: state.selectedIds.has(id),
531
+ depth: flat.depth,
532
+ hasChildren: flat.hasChildren,
533
+ isLoading: state.loadingIds.has(id)
534
+ }
535
+ };
536
+ });
537
+ }, [
538
+ state.visibleIds,
539
+ state.viewportFromIndex,
540
+ state.viewportToIndex,
541
+ state.focusedId,
542
+ state.expandedIds,
543
+ state.selectedIds,
544
+ state.nodeMap,
545
+ state.loadingIds
546
+ ]);
547
+ const focusNext = useCallback(
548
+ () => dispatch({ type: "focus-next" }),
549
+ []
550
+ );
551
+ const focusPrevious = useCallback(
552
+ () => dispatch({ type: "focus-previous" }),
553
+ []
554
+ );
555
+ const focusFirst = useCallback(
556
+ () => dispatch({ type: "focus-first" }),
557
+ []
558
+ );
559
+ const focusLast = useCallback(
560
+ () => dispatch({ type: "focus-last" }),
561
+ []
562
+ );
563
+ const expand = useCallback(() => dispatch({ type: "expand" }), []);
564
+ const expandNode = useCallback(
565
+ (nodeId) => dispatch({ type: "expand-node", nodeId }),
566
+ []
567
+ );
568
+ const collapse = useCallback(
569
+ () => dispatch({ type: "collapse" }),
570
+ []
571
+ );
572
+ const collapseNode = useCallback(
573
+ (nodeId) => dispatch({ type: "collapse-node", nodeId }),
574
+ []
575
+ );
576
+ const toggleExpanded = useCallback(
577
+ () => dispatch({ type: "toggle-expanded" }),
578
+ []
579
+ );
580
+ const expandAll = useCallback(
581
+ () => dispatch({ type: "expand-all" }),
582
+ []
583
+ );
584
+ const collapseAll = useCallback(
585
+ () => dispatch({ type: "collapse-all" }),
586
+ []
587
+ );
588
+ const select = useCallback(() => dispatch({ type: "select" }), []);
589
+ const focusParent = useCallback(
590
+ () => dispatch({ type: "focus-parent" }),
591
+ []
592
+ );
593
+ const focusFirstChild = useCallback(
594
+ () => dispatch({ type: "focus-first-child" }),
595
+ []
596
+ );
597
+ const setLoading = useCallback(
598
+ (nodeId, isLoading) => dispatch({ type: "set-loading", nodeId, isLoading }),
599
+ []
600
+ );
601
+ const setChildrenError = useCallback(
602
+ (nodeId) => dispatch({ type: "set-children-error", nodeId }),
603
+ []
604
+ );
605
+ const insertChildren = useCallback(
606
+ (parentId, children) => dispatch({ type: "insert-children", parentId, children }),
607
+ []
608
+ );
609
+ return {
610
+ focusedId: state.focusedId,
611
+ expandedIds: state.expandedIds,
612
+ selectedIds: state.selectedIds,
613
+ viewportNodes,
614
+ visibleCount: state.visibleIds.length,
615
+ hasScrollUp: state.viewportFromIndex > 0,
616
+ hasScrollDown: state.viewportToIndex < state.visibleIds.length,
617
+ viewportFromIndex: state.viewportFromIndex,
618
+ viewportToIndex: state.viewportToIndex,
619
+ loadingIds: state.loadingIds,
620
+ nodeMap: state.nodeMap,
621
+ focusNext,
622
+ focusPrevious,
623
+ focusFirst,
624
+ focusLast,
625
+ expand,
626
+ expandNode,
627
+ collapse,
628
+ collapseNode,
629
+ toggleExpanded,
630
+ expandAll,
631
+ collapseAll,
632
+ select,
633
+ focusParent,
634
+ focusFirstChild,
635
+ setLoading,
636
+ setChildrenError,
637
+ insertChildren
638
+ };
639
+ }
640
+
641
+ // src/components/tree-view/use-tree-view.ts
642
+ import { useInput } from "ink";
643
+ import { useRef as useRef2 } from "react";
644
+ function useTreeView({
645
+ isDisabled = false,
646
+ selectionMode,
647
+ state,
648
+ loadChildren,
649
+ onLoadError
650
+ }) {
651
+ const loadingRef = useRef2(/* @__PURE__ */ new Set());
652
+ const stateRef = useRef2(state);
653
+ stateRef.current = state;
654
+ const loadChildrenRef = useRef2(loadChildren);
655
+ loadChildrenRef.current = loadChildren;
656
+ const onLoadErrorRef = useRef2(onLoadError);
657
+ onLoadErrorRef.current = onLoadError;
658
+ const triggerLoadRef = useRef2(async (nodeId) => {
659
+ if (loadingRef.current.has(nodeId)) return;
660
+ const currentLoadChildren = loadChildrenRef.current;
661
+ if (!currentLoadChildren) return;
662
+ const flat = stateRef.current.nodeMap.get(nodeId);
663
+ if (!flat || flat.childrenIds.length > 0) return;
664
+ loadingRef.current.add(nodeId);
665
+ stateRef.current.setLoading(nodeId, true);
666
+ try {
667
+ const children = await currentLoadChildren(flat.node);
668
+ stateRef.current.insertChildren(nodeId, children);
669
+ stateRef.current.expandNode(nodeId);
670
+ } catch (error) {
671
+ const err = error instanceof Error ? error : new Error(String(error));
672
+ stateRef.current.setChildrenError(nodeId);
673
+ onLoadErrorRef.current?.(nodeId, err);
674
+ } finally {
675
+ stateRef.current.setLoading(nodeId, false);
676
+ loadingRef.current.delete(nodeId);
677
+ }
678
+ });
679
+ const triggerLoad = triggerLoadRef.current;
680
+ useInput(
681
+ (input, key) => {
682
+ if (key.downArrow) {
683
+ state.focusNext();
684
+ return;
685
+ }
686
+ if (key.upArrow) {
687
+ state.focusPrevious();
688
+ return;
689
+ }
690
+ if (key.rightArrow) {
691
+ if (state.focusedId && state.expandedIds.has(state.focusedId)) {
692
+ state.focusFirstChild();
693
+ } else if (state.focusedId) {
694
+ if (loadChildren) {
695
+ const flat = state.nodeMap.get(state.focusedId);
696
+ if (flat && flat.hasChildren && flat.childrenIds.length === 0) {
697
+ void triggerLoad(state.focusedId);
698
+ return;
699
+ }
700
+ }
701
+ state.expand();
702
+ }
703
+ return;
704
+ }
705
+ if (key.leftArrow) {
706
+ if (state.focusedId && state.expandedIds.has(state.focusedId)) {
707
+ state.collapse();
708
+ } else {
709
+ state.focusParent();
710
+ }
711
+ return;
712
+ }
713
+ if (key.return) {
714
+ if (selectionMode !== "none") {
715
+ state.select();
716
+ } else {
717
+ state.toggleExpanded();
718
+ }
719
+ return;
720
+ }
721
+ if (input === " ") {
722
+ if (selectionMode === "multiple") {
723
+ state.select();
724
+ } else {
725
+ state.toggleExpanded();
726
+ }
727
+ return;
728
+ }
729
+ if (input === "\x1B[H" || input === "\x1B[1~" || input === "\x1BOH") {
730
+ state.focusFirst();
731
+ return;
732
+ }
733
+ if (input === "\x1B[F" || input === "\x1B[4~" || input === "\x1BOF") {
734
+ state.focusLast();
735
+ return;
736
+ }
737
+ },
738
+ { isActive: !isDisabled }
739
+ );
740
+ }
741
+
742
+ // src/components/tree-view/tree-view-node.tsx
743
+ import { Box, Text } from "ink";
744
+ import figures from "figures";
745
+ import { jsx, jsxs } from "react/jsx-runtime";
746
+ function TreeViewNode({
747
+ node,
748
+ nodeState,
749
+ selectionMode,
750
+ styles
751
+ }) {
752
+ const { isFocused, isExpanded, isSelected, depth, hasChildren, isLoading } = nodeState;
753
+ let expandChar = " ";
754
+ if (isLoading) {
755
+ expandChar = "\u27F3";
756
+ } else if (hasChildren) {
757
+ expandChar = isExpanded ? figures.triangleDown : figures.triangleRight;
758
+ }
759
+ return /* @__PURE__ */ jsxs(Box, { ...styles.node({ isFocused }), children: [
760
+ isFocused && /* @__PURE__ */ jsx(Text, { ...styles.focusIndicator(), children: figures.pointer }),
761
+ depth > 0 && /* @__PURE__ */ jsx(Box, { ...styles.indent({ depth }) }),
762
+ selectionMode === "multiple" && /* @__PURE__ */ jsx(Text, { ...styles.selectedIndicator(), children: isSelected ? figures.checkboxOn : figures.checkboxOff }),
763
+ /* @__PURE__ */ jsx(Text, { ...isLoading ? styles.loadingIndicator() : styles.expandIndicator({ isExpanded }), children: expandChar }),
764
+ /* @__PURE__ */ jsx(Text, { ...styles.label({ isFocused, isSelected }), children: node.label }),
765
+ selectionMode === "single" && isSelected && /* @__PURE__ */ jsxs(Text, { ...styles.selectedIndicator(), children: [
766
+ " ",
767
+ figures.tick
768
+ ] })
769
+ ] });
770
+ }
771
+
772
+ // src/components/tree-view/theme.ts
773
+ var theme = {
774
+ styles: {
775
+ container: () => ({
776
+ flexDirection: "column"
777
+ }),
778
+ node: ({ isFocused }) => ({
779
+ gap: 1,
780
+ paddingLeft: isFocused ? 0 : 2
781
+ }),
782
+ indent: ({ depth }) => ({
783
+ width: (depth ?? 0) * 2
784
+ }),
785
+ focusIndicator: () => ({
786
+ color: "blue"
787
+ }),
788
+ expandIndicator: (_props) => ({
789
+ color: "gray"
790
+ }),
791
+ label: ({ isFocused, isSelected }) => {
792
+ let color;
793
+ if (isSelected) color = "green";
794
+ if (isFocused) color = "blue";
795
+ return { color };
796
+ },
797
+ selectedIndicator: () => ({
798
+ color: "green"
799
+ }),
800
+ loadingIndicator: () => ({
801
+ color: "yellow"
802
+ })
803
+ }
804
+ };
805
+ var theme_default = theme;
806
+
807
+ // src/components/tree-view/tree-view.tsx
808
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
809
+ function TreeView({
810
+ data,
811
+ selectionMode = "none",
812
+ defaultExpanded,
813
+ defaultSelected,
814
+ visibleNodeCount,
815
+ renderNode,
816
+ loadChildren,
817
+ onLoadError,
818
+ onFocusChange,
819
+ onExpandChange,
820
+ onSelectChange,
821
+ isDisabled = false
822
+ }) {
823
+ const state = useTreeViewState({
824
+ data,
825
+ selectionMode,
826
+ defaultExpanded,
827
+ defaultSelected,
828
+ visibleNodeCount,
829
+ onFocusChange,
830
+ onExpandChange,
831
+ onSelectChange
832
+ });
833
+ useTreeView({
834
+ isDisabled,
835
+ selectionMode,
836
+ state,
837
+ loadChildren,
838
+ onLoadError
839
+ });
840
+ const styles = theme_default.styles;
841
+ return /* @__PURE__ */ jsxs2(Box2, { ...styles.container(), children: [
842
+ state.hasScrollUp && /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
843
+ " ",
844
+ "\\u2191 ",
845
+ state.viewportFromIndex,
846
+ " more above"
847
+ ] }),
848
+ state.viewportNodes.map(({ node, state: nodeState }) => {
849
+ if (renderNode) {
850
+ return /* @__PURE__ */ jsx2(Box2, { children: renderNode({ node, state: nodeState }) }, node.id);
851
+ }
852
+ return /* @__PURE__ */ jsx2(
853
+ TreeViewNode,
854
+ {
855
+ node,
856
+ nodeState,
857
+ selectionMode,
858
+ styles
859
+ },
860
+ node.id
861
+ );
862
+ }),
863
+ state.hasScrollDown && /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
864
+ " ",
865
+ "\\u2193 ",
866
+ state.visibleCount - state.viewportToIndex,
867
+ " ",
868
+ "more below"
869
+ ] })
870
+ ] });
871
+ }
872
+ export {
873
+ TreeNodeMap,
874
+ TreeView,
875
+ theme_default as treeViewTheme,
876
+ useTreeView,
877
+ useTreeViewState
878
+ };