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/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
|
+
};
|