@djangocfg/widget-data 0.1.1
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 +55 -0
- package/package.json +122 -0
- package/src/DataGrid/README.md +48 -0
- package/src/DataGrid/context/DataGridContext.tsx +442 -0
- package/src/DataGrid/context/index.ts +4 -0
- package/src/DataGrid/index.ts +34 -0
- package/src/DataGrid/lazy.tsx +23 -0
- package/src/DataGrid/parts/DataGrid.tsx +125 -0
- package/src/DataGrid/parts/DataGridCell.tsx +30 -0
- package/src/DataGrid/parts/DataGridHeader.tsx +111 -0
- package/src/DataGrid/parts/DataGridPagination.tsx +105 -0
- package/src/DataGrid/parts/DataGridRow.tsx +84 -0
- package/src/DataGrid/parts/index.ts +7 -0
- package/src/DataGrid/types.ts +95 -0
- package/src/DataTable/README.md +42 -0
- package/src/DataTable/context/DataTableContext.tsx +434 -0
- package/src/DataTable/context/index.ts +4 -0
- package/src/DataTable/index.ts +33 -0
- package/src/DataTable/lazy.tsx +23 -0
- package/src/DataTable/parts/DataTable.tsx +142 -0
- package/src/DataTable/parts/DataTableHeader.tsx +103 -0
- package/src/DataTable/parts/DataTablePagination.tsx +115 -0
- package/src/DataTable/parts/DataTableRow.tsx +96 -0
- package/src/DataTable/parts/index.ts +6 -0
- package/src/DataTable/types.ts +88 -0
- package/src/FileIcon/FileIcon.tsx +87 -0
- package/src/FileIcon/get-file-icon.ts +73 -0
- package/src/FileIcon/icons/icon-data.ts +399 -0
- package/src/FileIcon/index.ts +19 -0
- package/src/FileIcon/loader.ts +29 -0
- package/src/FileIcon/specialFolders.ts +110 -0
- package/src/FileIcon/treeAdapter.tsx +49 -0
- package/src/JsonTree/JsonViewer.tsx +720 -0
- package/src/JsonTree/README.md +142 -0
- package/src/JsonTree/index.tsx +11 -0
- package/src/JsonTree/lazy.tsx +22 -0
- package/src/JsonTree/types.ts +91 -0
- package/src/Kanban/context/KanbanContext.tsx +165 -0
- package/src/Kanban/context/index.ts +4 -0
- package/src/Kanban/index.ts +31 -0
- package/src/Kanban/lazy.tsx +23 -0
- package/src/Kanban/parts/Kanban.tsx +77 -0
- package/src/Kanban/parts/KanbanCard.tsx +76 -0
- package/src/Kanban/parts/KanbanColumn.tsx +79 -0
- package/src/Kanban/parts/KanbanDragOverlay.tsx +46 -0
- package/src/Kanban/parts/index.ts +6 -0
- package/src/Kanban/types.ts +45 -0
- package/src/Listbox/context/ListboxProvider.tsx +616 -0
- package/src/Listbox/context/index.ts +8 -0
- package/src/Listbox/index.ts +28 -0
- package/src/Listbox/lazy.tsx +31 -0
- package/src/Listbox/parts/ListboxGroup.tsx +71 -0
- package/src/Listbox/parts/ListboxItem.tsx +151 -0
- package/src/Listbox/parts/index.ts +12 -0
- package/src/Listbox/types.ts +98 -0
- package/src/Masonry/README.md +79 -0
- package/src/Masonry/context/MasonryProvider.tsx +1205 -0
- package/src/Masonry/context/index.ts +8 -0
- package/src/Masonry/index.ts +18 -0
- package/src/Masonry/lazy.tsx +20 -0
- package/src/Masonry/parts/index.ts +2 -0
- package/src/Masonry/types.ts +44 -0
- package/src/Timeline/context/TimelineProvider.tsx +474 -0
- package/src/Timeline/context/index.ts +16 -0
- package/src/Timeline/index.ts +29 -0
- package/src/Timeline/lazy.tsx +27 -0
- package/src/Timeline/parts/index.ts +2 -0
- package/src/Timeline/types.ts +25 -0
- package/src/Tree/FinderTree.tsx +42 -0
- package/src/Tree/README.md +542 -0
- package/src/Tree/TreeDndProvider.tsx +137 -0
- package/src/Tree/TreeRoot.tsx +290 -0
- package/src/Tree/components/TreeChevron.tsx +48 -0
- package/src/Tree/components/TreeContent.tsx +70 -0
- package/src/Tree/components/TreeDropIndicator.tsx +65 -0
- package/src/Tree/components/TreeEmpty.tsx +22 -0
- package/src/Tree/components/TreeEmptyArea.tsx +160 -0
- package/src/Tree/components/TreeError.tsx +24 -0
- package/src/Tree/components/TreeIcon.tsx +39 -0
- package/src/Tree/components/TreeIndentGuides.tsx +33 -0
- package/src/Tree/components/TreeLabel.tsx +39 -0
- package/src/Tree/components/TreeRenameInput.tsx +114 -0
- package/src/Tree/components/TreeRow.tsx +300 -0
- package/src/Tree/components/TreeSearchInput.tsx +50 -0
- package/src/Tree/components/TreeSkeleton.tsx +22 -0
- package/src/Tree/components/index.ts +28 -0
- package/src/Tree/context/TreeContext.tsx +409 -0
- package/src/Tree/context/TreeContextValue.ts +139 -0
- package/src/Tree/context/async-children/collect-ids.ts +27 -0
- package/src/Tree/context/async-children/index.ts +8 -0
- package/src/Tree/context/async-children/use-async-children.ts +157 -0
- package/src/Tree/context/clipboard/index.ts +4 -0
- package/src/Tree/context/clipboard/use-clipboard.ts +115 -0
- package/src/Tree/context/dnd/index.ts +8 -0
- package/src/Tree/context/dnd/use-dnd.ts +194 -0
- package/src/Tree/context/expansion/index.ts +4 -0
- package/src/Tree/context/expansion/use-expansion.ts +55 -0
- package/src/Tree/context/hooks.ts +177 -0
- package/src/Tree/context/index.ts +16 -0
- package/src/Tree/context/menu/builtin-actions.ts +358 -0
- package/src/Tree/context/menu/index.ts +11 -0
- package/src/Tree/context/menu/render.tsx +104 -0
- package/src/Tree/context/menu/use-resolved-menu.ts +141 -0
- package/src/Tree/context/persist/index.ts +4 -0
- package/src/Tree/context/persist/use-persist-sync.ts +74 -0
- package/src/Tree/context/rename/index.ts +4 -0
- package/src/Tree/context/rename/use-rename.ts +113 -0
- package/src/Tree/context/selection/index.ts +4 -0
- package/src/Tree/context/selection/use-selection.ts +146 -0
- package/src/Tree/context/state/index.ts +6 -0
- package/src/Tree/context/state/initial.ts +41 -0
- package/src/Tree/context/state/reducer.ts +76 -0
- package/src/Tree/context/state/types.ts +46 -0
- package/src/Tree/data/appearance.ts +237 -0
- package/src/Tree/data/childCache.ts +43 -0
- package/src/Tree/data/clipboard.ts +33 -0
- package/src/Tree/data/createDemoTree.ts +42 -0
- package/src/Tree/data/dnd.ts +123 -0
- package/src/Tree/data/finderShortcuts.ts +67 -0
- package/src/Tree/data/flatten.ts +66 -0
- package/src/Tree/data/index.ts +45 -0
- package/src/Tree/data/persist.ts +62 -0
- package/src/Tree/data/renameUtils.ts +51 -0
- package/src/Tree/data/selection.ts +161 -0
- package/src/Tree/hooks/finder-hotkeys/build-ctx.ts +48 -0
- package/src/Tree/hooks/finder-hotkeys/index.ts +8 -0
- package/src/Tree/hooks/finder-hotkeys/use-tree-finder-hotkeys.ts +166 -0
- package/src/Tree/hooks/index.ts +25 -0
- package/src/Tree/hooks/keyboard/activation.ts +27 -0
- package/src/Tree/hooks/keyboard/arrow-nav.ts +26 -0
- package/src/Tree/hooks/keyboard/expand-collapse.ts +54 -0
- package/src/Tree/hooks/keyboard/index.ts +10 -0
- package/src/Tree/hooks/keyboard/types.ts +39 -0
- package/src/Tree/hooks/keyboard/use-tree-keyboard.ts +196 -0
- package/src/Tree/hooks/type-ahead/index.ts +5 -0
- package/src/Tree/hooks/type-ahead/match-prefix.ts +42 -0
- package/src/Tree/hooks/type-ahead/use-tree-type-ahead.ts +89 -0
- package/src/Tree/index.tsx +135 -0
- package/src/Tree/lazy.tsx +14 -0
- package/src/Tree/types/activation.ts +30 -0
- package/src/Tree/types/adapter.ts +70 -0
- package/src/Tree/types/index.ts +27 -0
- package/src/Tree/types/labels.ts +97 -0
- package/src/Tree/types/loader.ts +9 -0
- package/src/Tree/types/node.ts +38 -0
- package/src/Tree/types/root-props.ts +158 -0
- package/src/Tree/types/selection.ts +3 -0
- package/src/Tree/types/slots.ts +64 -0
- package/src/index.ts +21 -0
- package/src/stories/_fixtures/categories.ts +51 -0
- package/src/stories/_fixtures/finder-store.ts +196 -0
- package/src/stories/_fixtures/fs.ts +83 -0
- package/src/stories/_fixtures/index.ts +5 -0
- package/src/stories/_fixtures/org.ts +46 -0
- package/src/stories/_fixtures/status.ts +21 -0
- package/src/styles/index.css +10 -0
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { createContext, useCallback, useMemo, useReducer, useRef } from 'react';
|
|
5
|
+
|
|
6
|
+
import { flattenTree } from '../data/flatten';
|
|
7
|
+
import { loadTreeState } from '../data/persist';
|
|
8
|
+
import { resolveAppearance } from '../data/appearance';
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_TREE_LABELS,
|
|
11
|
+
type FlatRow,
|
|
12
|
+
type TreeActivateOptions,
|
|
13
|
+
type TreeContextMenuSlot,
|
|
14
|
+
type TreeItemId,
|
|
15
|
+
type TreeLabels,
|
|
16
|
+
type TreeNode,
|
|
17
|
+
type TreeRootProps,
|
|
18
|
+
} from '../types';
|
|
19
|
+
|
|
20
|
+
import { reducer, createInitialState } from './state';
|
|
21
|
+
import { useAsyncChildren } from './async-children';
|
|
22
|
+
import { useExpansion } from './expansion';
|
|
23
|
+
import { useSelection } from './selection';
|
|
24
|
+
import { useRename } from './rename';
|
|
25
|
+
import { useClipboard } from './clipboard';
|
|
26
|
+
import { useResolvedMenu, renderItemsAsContextMenu, tidyMenuItems } from './menu';
|
|
27
|
+
import { useDnd, type UseDndReturn } from './dnd';
|
|
28
|
+
import { usePersistSync } from './persist';
|
|
29
|
+
import type { TreeContextValue } from './TreeContextValue';
|
|
30
|
+
|
|
31
|
+
// Re-exported from this module: the value interface (so consumers
|
|
32
|
+
// continue to `import type { TreeContextValue }` from `./context`).
|
|
33
|
+
export type { TreeContextValue } from './TreeContextValue';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Internal context object. Exported so `TreeRoot` can wrap it with an
|
|
37
|
+
* override-provider that injects a slot-form `renderContextMenu`
|
|
38
|
+
* derived from the declarative resolver. Consumers should use
|
|
39
|
+
* `useTreeContext()` instead of touching this directly.
|
|
40
|
+
*/
|
|
41
|
+
export const TreeContext = createContext<TreeContextValue<unknown> | null>(null);
|
|
42
|
+
|
|
43
|
+
export function useTreeContext<T>(): TreeContextValue<T> {
|
|
44
|
+
const ctx = React.useContext(TreeContext);
|
|
45
|
+
if (!ctx) {
|
|
46
|
+
throw new Error('useTreeContext must be used inside <TreeProvider>');
|
|
47
|
+
}
|
|
48
|
+
return ctx as TreeContextValue<T>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// =====================================================================
|
|
52
|
+
// Provider — thin assembly. The real work lives in:
|
|
53
|
+
//
|
|
54
|
+
// state/ reducer + initial state
|
|
55
|
+
// async-children/ child cache, nodeById, fetchChildren, refresh, refreshAll
|
|
56
|
+
// expansion/ expand / collapse / toggle / expandAll / collapseAll
|
|
57
|
+
// selection/ click / move / select-all + plain select / clear
|
|
58
|
+
// rename/ start / cancel / commit
|
|
59
|
+
// clipboard/ cut / copy / paste / clear
|
|
60
|
+
// menu/ built-in actions registry + merged declarative resolver
|
|
61
|
+
// persist/ localStorage + onSelectionChange/onExpansionChange
|
|
62
|
+
//
|
|
63
|
+
// This file only stitches them together and shapes the final
|
|
64
|
+
// `TreeContextValue`. If a feature grows, add a folder above — don't
|
|
65
|
+
// extend this file.
|
|
66
|
+
// =====================================================================
|
|
67
|
+
|
|
68
|
+
export interface TreeProviderProps<T>
|
|
69
|
+
extends Pick<
|
|
70
|
+
TreeRootProps<T>,
|
|
71
|
+
| 'data'
|
|
72
|
+
| 'getItemName'
|
|
73
|
+
| 'loadChildren'
|
|
74
|
+
| 'selectionMode'
|
|
75
|
+
| 'activationMode'
|
|
76
|
+
| 'initialExpandedIds'
|
|
77
|
+
| 'initialSelectedIds'
|
|
78
|
+
| 'indent'
|
|
79
|
+
| 'appearance'
|
|
80
|
+
| 'onSelectionChange'
|
|
81
|
+
| 'onExpansionChange'
|
|
82
|
+
| 'onActivate'
|
|
83
|
+
| 'filterNode'
|
|
84
|
+
| 'enableSearch'
|
|
85
|
+
| 'showIndentGuides'
|
|
86
|
+
| 'renderIcon'
|
|
87
|
+
| 'renderLabel'
|
|
88
|
+
| 'renderActions'
|
|
89
|
+
| 'renderContextMenu'
|
|
90
|
+
| 'contextMenuActions'
|
|
91
|
+
| 'labels'
|
|
92
|
+
| 'persistKey'
|
|
93
|
+
| 'persistSelection'
|
|
94
|
+
| 'adapter'
|
|
95
|
+
| 'defaultMenuItems'
|
|
96
|
+
| 'enableInlineRename'
|
|
97
|
+
| 'enableDnD'
|
|
98
|
+
| 'canDrop'
|
|
99
|
+
> {
|
|
100
|
+
children: React.ReactNode;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function TreeProvider<T>(props: TreeProviderProps<T>) {
|
|
104
|
+
const {
|
|
105
|
+
data,
|
|
106
|
+
getItemName,
|
|
107
|
+
loadChildren,
|
|
108
|
+
selectionMode = 'single',
|
|
109
|
+
activationMode = 'single-click',
|
|
110
|
+
initialExpandedIds,
|
|
111
|
+
initialSelectedIds,
|
|
112
|
+
indent,
|
|
113
|
+
appearance,
|
|
114
|
+
onSelectionChange,
|
|
115
|
+
onExpansionChange,
|
|
116
|
+
onActivate,
|
|
117
|
+
filterNode,
|
|
118
|
+
enableSearch = false,
|
|
119
|
+
showIndentGuides = false,
|
|
120
|
+
renderIcon,
|
|
121
|
+
renderLabel,
|
|
122
|
+
renderActions,
|
|
123
|
+
renderContextMenu,
|
|
124
|
+
contextMenuActions,
|
|
125
|
+
labels: labelsOverride,
|
|
126
|
+
persistKey,
|
|
127
|
+
persistSelection = false,
|
|
128
|
+
adapter,
|
|
129
|
+
defaultMenuItems,
|
|
130
|
+
enableInlineRename = false,
|
|
131
|
+
enableDnD = false,
|
|
132
|
+
canDrop,
|
|
133
|
+
children,
|
|
134
|
+
} = props;
|
|
135
|
+
|
|
136
|
+
// ---- Stable config ------------------------------------------------
|
|
137
|
+
|
|
138
|
+
const labels = useMemo<TreeLabels>(
|
|
139
|
+
() => ({ ...DEFAULT_TREE_LABELS, ...labelsOverride }),
|
|
140
|
+
[labelsOverride],
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const resolvedAppearance = useMemo(
|
|
144
|
+
() => resolveAppearance(appearance, indent),
|
|
145
|
+
[appearance, indent],
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
// ---- Reducer ------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
const persisted = useMemo(
|
|
151
|
+
() => (persistKey ? loadTreeState(persistKey) : null),
|
|
152
|
+
[persistKey],
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const [state, dispatch] = useReducer(reducer, undefined, () =>
|
|
156
|
+
createInitialState({
|
|
157
|
+
persisted,
|
|
158
|
+
initialExpandedIds,
|
|
159
|
+
initialSelectedIds,
|
|
160
|
+
persistSelection,
|
|
161
|
+
}),
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const bumpCacheTick = useCallback(() => dispatch({ type: 'cache-tick' }), []);
|
|
165
|
+
|
|
166
|
+
// ---- Async children (cache + nodeById + refresh) ------------------
|
|
167
|
+
|
|
168
|
+
const {
|
|
169
|
+
nodeById,
|
|
170
|
+
refresh,
|
|
171
|
+
refreshAll,
|
|
172
|
+
collectFolderIds,
|
|
173
|
+
cache,
|
|
174
|
+
} = useAsyncChildren<T>({
|
|
175
|
+
data,
|
|
176
|
+
loadChildren,
|
|
177
|
+
expanded: state.expanded,
|
|
178
|
+
cacheTick: state.cacheTick,
|
|
179
|
+
bumpCacheTick,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// ---- Flat rows (depend on cache via cacheTick) --------------------
|
|
183
|
+
|
|
184
|
+
const flatRows = useMemo<FlatRow<T>[]>(
|
|
185
|
+
() =>
|
|
186
|
+
flattenTree<T>({
|
|
187
|
+
roots: data,
|
|
188
|
+
expandedIds: state.expanded,
|
|
189
|
+
cache,
|
|
190
|
+
filterNode,
|
|
191
|
+
}),
|
|
192
|
+
[data, state.expanded, state.cacheTick, cache, filterNode],
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
const matchingIds = useMemo(() => {
|
|
196
|
+
const set = new Set<TreeItemId>();
|
|
197
|
+
if (!enableSearch || state.query.trim() === '') return set;
|
|
198
|
+
const q = state.query.trim().toLowerCase();
|
|
199
|
+
for (const row of flatRows) {
|
|
200
|
+
if (getItemName(row.node).toLowerCase().includes(q)) {
|
|
201
|
+
set.add(row.node.id);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return set;
|
|
205
|
+
}, [enableSearch, state.query, flatRows, getItemName]);
|
|
206
|
+
|
|
207
|
+
// ---- Feature hooks ------------------------------------------------
|
|
208
|
+
|
|
209
|
+
const expansion = useExpansion({ dispatch, collectFolderIds });
|
|
210
|
+
const selection = useSelection<T>({
|
|
211
|
+
dispatch,
|
|
212
|
+
selectionMode,
|
|
213
|
+
flatRows,
|
|
214
|
+
selected: state.selected,
|
|
215
|
+
anchor: state.anchor,
|
|
216
|
+
focused: state.focused,
|
|
217
|
+
});
|
|
218
|
+
const rename = useRename<T>({
|
|
219
|
+
dispatch,
|
|
220
|
+
adapter,
|
|
221
|
+
enableInlineRename,
|
|
222
|
+
nodeById,
|
|
223
|
+
getItemName,
|
|
224
|
+
labels,
|
|
225
|
+
});
|
|
226
|
+
const clipboard = useClipboard<T>({
|
|
227
|
+
dispatch,
|
|
228
|
+
clipboard: state.clipboard,
|
|
229
|
+
adapter,
|
|
230
|
+
nodeById,
|
|
231
|
+
labels,
|
|
232
|
+
});
|
|
233
|
+
const dnd = useDnd<T>({
|
|
234
|
+
enabled: enableDnD,
|
|
235
|
+
adapter,
|
|
236
|
+
nodeById,
|
|
237
|
+
selected: state.selected,
|
|
238
|
+
labels,
|
|
239
|
+
canDrop,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// ---- Activation ---------------------------------------------------
|
|
243
|
+
|
|
244
|
+
const onActivateRef = useRef(onActivate);
|
|
245
|
+
onActivateRef.current = onActivate;
|
|
246
|
+
const activate = useCallback(
|
|
247
|
+
(node: TreeNode<T>, opts: TreeActivateOptions = { preview: false }) =>
|
|
248
|
+
onActivateRef.current?.(node, opts),
|
|
249
|
+
[],
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
const setQuery = useCallback(
|
|
253
|
+
(q: string) => dispatch({ type: 'set-query', q }),
|
|
254
|
+
[],
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
// ---- Persist + notify callbacks -----------------------------------
|
|
258
|
+
|
|
259
|
+
usePersistSync({
|
|
260
|
+
expanded: state.expanded,
|
|
261
|
+
selected: state.selected,
|
|
262
|
+
persistKey,
|
|
263
|
+
persistSelection,
|
|
264
|
+
onSelectionChange,
|
|
265
|
+
onExpansionChange,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// ---- Resolved context-menu resolver -------------------------------
|
|
269
|
+
|
|
270
|
+
const resolvedContextMenuActions = useResolvedMenu<T>({
|
|
271
|
+
adapter,
|
|
272
|
+
contextMenuActions,
|
|
273
|
+
defaultMenuItems,
|
|
274
|
+
labels,
|
|
275
|
+
selected: state.selected,
|
|
276
|
+
clipboard: state.clipboard,
|
|
277
|
+
nodeById,
|
|
278
|
+
getItemName,
|
|
279
|
+
enableInlineRename,
|
|
280
|
+
startRename: rename.startRename,
|
|
281
|
+
cutToClipboard: clipboard.cutToClipboard,
|
|
282
|
+
copyToClipboard: clipboard.copyToClipboard,
|
|
283
|
+
pasteFromClipboard: clipboard.pasteFromClipboard,
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// Translate the declarative resolver into a slot-form
|
|
287
|
+
// `renderContextMenu` so <TreeRow> doesn't need to know about it.
|
|
288
|
+
// Explicit slot prop wins (escape-hatch for full custom menus).
|
|
289
|
+
const finalRenderContextMenu = useMemo<TreeContextMenuSlot<T> | undefined>(
|
|
290
|
+
() => {
|
|
291
|
+
if (renderContextMenu) return renderContextMenu;
|
|
292
|
+
const resolve = resolvedContextMenuActions;
|
|
293
|
+
if (!resolve) return undefined;
|
|
294
|
+
return (rowProps, trigger) => {
|
|
295
|
+
const items = resolve(rowProps);
|
|
296
|
+
const cleaned = items ? tidyMenuItems(items) : null;
|
|
297
|
+
if (!cleaned || cleaned.length === 0) return trigger;
|
|
298
|
+
return renderItemsAsContextMenu(rowProps, cleaned, trigger);
|
|
299
|
+
};
|
|
300
|
+
},
|
|
301
|
+
[renderContextMenu, resolvedContextMenuActions],
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
// ---- Final value --------------------------------------------------
|
|
305
|
+
|
|
306
|
+
const value = useMemo<TreeContextValue<T>>(
|
|
307
|
+
() => ({
|
|
308
|
+
// state
|
|
309
|
+
expanded: state.expanded,
|
|
310
|
+
selected: state.selected,
|
|
311
|
+
anchor: state.anchor,
|
|
312
|
+
focused: state.focused,
|
|
313
|
+
query: state.query,
|
|
314
|
+
renamingId: state.renaming,
|
|
315
|
+
inlineRenameEnabled: rename.enabled,
|
|
316
|
+
clipboard: state.clipboard,
|
|
317
|
+
flatRows,
|
|
318
|
+
matchingIds,
|
|
319
|
+
|
|
320
|
+
// expansion
|
|
321
|
+
...expansion,
|
|
322
|
+
|
|
323
|
+
// selection (note: `select` is from selection hook; expansion exports no `select`)
|
|
324
|
+
...selection,
|
|
325
|
+
|
|
326
|
+
setQuery,
|
|
327
|
+
|
|
328
|
+
// clipboard
|
|
329
|
+
...clipboard,
|
|
330
|
+
|
|
331
|
+
// rename
|
|
332
|
+
startRename: rename.startRename,
|
|
333
|
+
cancelRename: rename.cancelRename,
|
|
334
|
+
commitRename: rename.commitRename,
|
|
335
|
+
|
|
336
|
+
// async
|
|
337
|
+
refresh,
|
|
338
|
+
refreshAll,
|
|
339
|
+
activate,
|
|
340
|
+
|
|
341
|
+
// config
|
|
342
|
+
labels,
|
|
343
|
+
appearance: resolvedAppearance,
|
|
344
|
+
indent: resolvedAppearance.indent,
|
|
345
|
+
selectionMode,
|
|
346
|
+
activationMode,
|
|
347
|
+
enableSearch,
|
|
348
|
+
showIndentGuides,
|
|
349
|
+
getItemName,
|
|
350
|
+
|
|
351
|
+
// slots
|
|
352
|
+
renderIcon,
|
|
353
|
+
renderLabel,
|
|
354
|
+
renderActions,
|
|
355
|
+
renderContextMenu: finalRenderContextMenu,
|
|
356
|
+
|
|
357
|
+
adapter,
|
|
358
|
+
resolvedContextMenuActions,
|
|
359
|
+
getNodeById: (id: TreeItemId) => nodeById.get(id),
|
|
360
|
+
dnd,
|
|
361
|
+
}),
|
|
362
|
+
[
|
|
363
|
+
state.expanded,
|
|
364
|
+
state.selected,
|
|
365
|
+
state.anchor,
|
|
366
|
+
state.focused,
|
|
367
|
+
state.query,
|
|
368
|
+
state.renaming,
|
|
369
|
+
state.clipboard,
|
|
370
|
+
rename.enabled,
|
|
371
|
+
rename.startRename,
|
|
372
|
+
rename.cancelRename,
|
|
373
|
+
rename.commitRename,
|
|
374
|
+
flatRows,
|
|
375
|
+
matchingIds,
|
|
376
|
+
expansion,
|
|
377
|
+
selection,
|
|
378
|
+
setQuery,
|
|
379
|
+
clipboard,
|
|
380
|
+
refresh,
|
|
381
|
+
refreshAll,
|
|
382
|
+
activate,
|
|
383
|
+
labels,
|
|
384
|
+
resolvedAppearance,
|
|
385
|
+
selectionMode,
|
|
386
|
+
activationMode,
|
|
387
|
+
enableSearch,
|
|
388
|
+
showIndentGuides,
|
|
389
|
+
getItemName,
|
|
390
|
+
renderIcon,
|
|
391
|
+
renderLabel,
|
|
392
|
+
renderActions,
|
|
393
|
+
finalRenderContextMenu,
|
|
394
|
+
adapter,
|
|
395
|
+
resolvedContextMenuActions,
|
|
396
|
+
nodeById,
|
|
397
|
+
dnd,
|
|
398
|
+
],
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
return (
|
|
402
|
+
<TreeContext.Provider value={value as TreeContextValue<unknown>}>
|
|
403
|
+
{children}
|
|
404
|
+
</TreeContext.Provider>
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Re-export internal types referenced by hook consumers.
|
|
409
|
+
export type { ChildCache, ChildEntry } from '../data/childCache';
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ClipboardState } from '../data/clipboard';
|
|
4
|
+
import type { UseDndReturn } from './dnd';
|
|
5
|
+
import type { ResolvedAppearance } from '../data/appearance';
|
|
6
|
+
import type { ClickModifiers } from '../data/selection';
|
|
7
|
+
import type {
|
|
8
|
+
FlatRow,
|
|
9
|
+
TreeActivateOptions,
|
|
10
|
+
TreeActivationMode,
|
|
11
|
+
TreeAdapter,
|
|
12
|
+
TreeContextMenuItem,
|
|
13
|
+
TreeContextMenuSlot,
|
|
14
|
+
TreeItemId,
|
|
15
|
+
TreeLabels,
|
|
16
|
+
TreeMovePosition,
|
|
17
|
+
TreeNode,
|
|
18
|
+
TreeRowRenderProps,
|
|
19
|
+
TreeRowSlot,
|
|
20
|
+
TreeSelectionMode,
|
|
21
|
+
} from '../types';
|
|
22
|
+
|
|
23
|
+
export interface TreeContextValue<T> {
|
|
24
|
+
// State
|
|
25
|
+
expanded: ReadonlySet<TreeItemId>;
|
|
26
|
+
selected: ReadonlySet<TreeItemId>;
|
|
27
|
+
/** Anchor for shift-range. `null` when nothing has been clicked yet. */
|
|
28
|
+
anchor: TreeItemId | null;
|
|
29
|
+
focused: TreeItemId | null;
|
|
30
|
+
query: string;
|
|
31
|
+
/** Id of the row currently in inline-rename mode (or `null`). */
|
|
32
|
+
renamingId: TreeItemId | null;
|
|
33
|
+
/** Is inline rename allowed by the host? */
|
|
34
|
+
inlineRenameEnabled: boolean;
|
|
35
|
+
|
|
36
|
+
/** Tree-local clipboard (cut / copy) — P5. `null` when empty. */
|
|
37
|
+
clipboard: ClipboardState;
|
|
38
|
+
|
|
39
|
+
// Flattened render rows (visible items only)
|
|
40
|
+
flatRows: FlatRow<T>[];
|
|
41
|
+
/** Search-matching node ids (subset of all flatRows). */
|
|
42
|
+
matchingIds: ReadonlySet<TreeItemId>;
|
|
43
|
+
|
|
44
|
+
// Imperative actions
|
|
45
|
+
expand: (id: TreeItemId) => void;
|
|
46
|
+
collapse: (id: TreeItemId) => void;
|
|
47
|
+
toggle: (id: TreeItemId) => void;
|
|
48
|
+
expandAll: () => void;
|
|
49
|
+
collapseAll: () => void;
|
|
50
|
+
select: (id: TreeItemId) => void;
|
|
51
|
+
setSelectedIds: (ids: TreeItemId[]) => void;
|
|
52
|
+
clearSelection: () => void;
|
|
53
|
+
/**
|
|
54
|
+
* Finder/Explorer-style click selection. Reads modifier keys from the
|
|
55
|
+
* passed event and decides between replace / toggle / range / union.
|
|
56
|
+
*/
|
|
57
|
+
clickSelect: (id: TreeItemId, mods: ClickModifiers) => void;
|
|
58
|
+
/**
|
|
59
|
+
* Move-focus with optional range-extend (shift+arrows).
|
|
60
|
+
*/
|
|
61
|
+
moveSelect: (id: TreeItemId, opts: { extend: boolean }) => void;
|
|
62
|
+
/** Select every currently visible row (mod+a). */
|
|
63
|
+
selectAll: () => void;
|
|
64
|
+
setFocus: (id: TreeItemId | null) => void;
|
|
65
|
+
setQuery: (q: string) => void;
|
|
66
|
+
|
|
67
|
+
/** Put the given ids on Tree's clipboard as `cut`. */
|
|
68
|
+
cutToClipboard: (ids: TreeItemId[]) => void;
|
|
69
|
+
/** Put the given ids on Tree's clipboard as `copy`. */
|
|
70
|
+
copyToClipboard: (ids: TreeItemId[]) => void;
|
|
71
|
+
/**
|
|
72
|
+
* Apply clipboard onto `target` (a row, or `null` for the root). Cut →
|
|
73
|
+
* `adapter.move`; copy → `adapter.copy`. After a successful cut+paste
|
|
74
|
+
* the clipboard is cleared. No-op when clipboard is empty or the
|
|
75
|
+
* matching adapter method is undefined.
|
|
76
|
+
*/
|
|
77
|
+
pasteFromClipboard: (
|
|
78
|
+
target: TreeNode<T> | null,
|
|
79
|
+
position?: TreeMovePosition,
|
|
80
|
+
) => Promise<void>;
|
|
81
|
+
/** Clear the clipboard without pasting. */
|
|
82
|
+
clearClipboard: () => void;
|
|
83
|
+
|
|
84
|
+
/** Begin inline rename on the given row. */
|
|
85
|
+
startRename: (id: TreeItemId) => void;
|
|
86
|
+
/** Cancel inline rename without committing. */
|
|
87
|
+
cancelRename: () => void;
|
|
88
|
+
/**
|
|
89
|
+
* Commit inline rename. Tree calls `adapter.rename` and then clears
|
|
90
|
+
* the renaming state. On validation failure surfaces an error via
|
|
91
|
+
* `window.dialog.alert` and keeps the input open.
|
|
92
|
+
*/
|
|
93
|
+
commitRename: (id: TreeItemId, nextName: string) => Promise<boolean>;
|
|
94
|
+
|
|
95
|
+
refresh: (id: TreeItemId) => Promise<void>;
|
|
96
|
+
refreshAll: () => Promise<void>;
|
|
97
|
+
activate: (node: TreeNode<T>, opts?: TreeActivateOptions) => void;
|
|
98
|
+
|
|
99
|
+
// Config / slots
|
|
100
|
+
labels: TreeLabels;
|
|
101
|
+
/** Resolved cosmetic config — never null. */
|
|
102
|
+
appearance: ResolvedAppearance;
|
|
103
|
+
/** Convenience alias for `appearance.indent`. */
|
|
104
|
+
indent: number;
|
|
105
|
+
selectionMode: TreeSelectionMode;
|
|
106
|
+
activationMode: TreeActivationMode;
|
|
107
|
+
enableSearch: boolean;
|
|
108
|
+
showIndentGuides: boolean;
|
|
109
|
+
getItemName: (node: TreeNode<T>) => string;
|
|
110
|
+
|
|
111
|
+
renderIcon?: TreeRowSlot<T>;
|
|
112
|
+
renderLabel?: TreeRowSlot<T>;
|
|
113
|
+
renderActions?: TreeRowSlot<T>;
|
|
114
|
+
renderContextMenu?: TreeContextMenuSlot<T>;
|
|
115
|
+
|
|
116
|
+
/** CRUD adapter (P2). May be undefined — Tree still renders normally. */
|
|
117
|
+
adapter?: TreeAdapter<T>;
|
|
118
|
+
/**
|
|
119
|
+
* Final, merged declarative menu resolver. Combines built-in adapter
|
|
120
|
+
* actions (filtered by `defaultMenuItems`) with the consumer's
|
|
121
|
+
* `contextMenuActions` resolver, and injects the current
|
|
122
|
+
* `selectedNodes` before delegating.
|
|
123
|
+
*/
|
|
124
|
+
resolvedContextMenuActions?: (
|
|
125
|
+
row: TreeRowRenderProps<T>,
|
|
126
|
+
) => TreeContextMenuItem<T>[] | null | undefined;
|
|
127
|
+
/**
|
|
128
|
+
* Imperative lookup for any node currently known to Tree (root +
|
|
129
|
+
* cached async children).
|
|
130
|
+
*/
|
|
131
|
+
getNodeById: (id: TreeItemId) => TreeNode<T> | undefined;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Drag-and-drop state and handlers (P6). `dnd.active` is `false`
|
|
135
|
+
* when the host didn't enable DnD or `adapter.move` is missing — in
|
|
136
|
+
* that case `TreeRow` skips all drag setup.
|
|
137
|
+
*/
|
|
138
|
+
dnd: UseDndReturn<T>;
|
|
139
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ChildCache } from '../../data/childCache';
|
|
4
|
+
import type { TreeItemId, TreeNode } from '../../types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Walk `roots` + cached async children and push every *folder* id into
|
|
8
|
+
* `out`. Used by `expandAll`.
|
|
9
|
+
*
|
|
10
|
+
* Leaves are intentionally not collected — expanding a leaf is a no-op.
|
|
11
|
+
*/
|
|
12
|
+
export function collectAllFolderIds<T>(
|
|
13
|
+
roots: TreeNode<T>[],
|
|
14
|
+
cache: ChildCache<T>,
|
|
15
|
+
out: TreeItemId[],
|
|
16
|
+
): void {
|
|
17
|
+
for (const node of roots) {
|
|
18
|
+
if (Array.isArray(node.children)) {
|
|
19
|
+
out.push(node.id);
|
|
20
|
+
collectAllFolderIds(node.children, cache, out);
|
|
21
|
+
} else if (node.isFolder) {
|
|
22
|
+
out.push(node.id);
|
|
23
|
+
const entry = cache.get(node.id);
|
|
24
|
+
if (entry?.children) collectAllFolderIds(entry.children, cache, out);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|