@djangocfg/ui-tools 2.1.415 → 2.1.416

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.
Files changed (110) hide show
  1. package/dist/file-icon/index.d.cts +1 -1
  2. package/dist/file-icon/index.d.ts +1 -1
  3. package/dist/slots-ClRpIzoh.d.cts +88 -0
  4. package/dist/slots-ClRpIzoh.d.ts +88 -0
  5. package/dist/tree/index.cjs +1994 -276
  6. package/dist/tree/index.cjs.map +1 -1
  7. package/dist/tree/index.d.cts +717 -72
  8. package/dist/tree/index.d.ts +717 -72
  9. package/dist/tree/index.mjs +1984 -279
  10. package/dist/tree/index.mjs.map +1 -1
  11. package/package.json +10 -6
  12. package/src/tools/chat/README.md +111 -1
  13. package/src/tools/chat/composer/Composer.tsx +138 -17
  14. package/src/tools/chat/composer/ComposerRichTextarea.tsx +25 -0
  15. package/src/tools/chat/composer/index.ts +22 -0
  16. package/src/tools/chat/composer/slash/README.md +187 -0
  17. package/src/tools/chat/composer/slash/SlashHighlightTextarea.tsx +144 -0
  18. package/src/tools/chat/composer/slash/SlashMenu.tsx +142 -0
  19. package/src/tools/chat/composer/slash/SlashToken.tsx +57 -0
  20. package/src/tools/chat/composer/slash/index.ts +44 -0
  21. package/src/tools/chat/composer/slash/labels.ts +19 -0
  22. package/src/tools/chat/composer/slash/state.ts +168 -0
  23. package/src/tools/chat/composer/slash/types.ts +64 -0
  24. package/src/tools/chat/composer/slash/useSlashCommands.ts +204 -0
  25. package/src/tools/chat/composer/types.ts +8 -0
  26. package/src/tools/chat/shell/SuggestedPrompts.tsx +194 -0
  27. package/src/tools/chat/shell/index.ts +6 -0
  28. package/src/tools/data/Listbox/lazy.tsx +1 -1
  29. package/src/tools/data/Masonry/lazy.tsx +1 -1
  30. package/src/tools/data/Timeline/lazy.tsx +1 -1
  31. package/src/tools/data/Tree/FinderTree.tsx +42 -0
  32. package/src/tools/data/Tree/README.md +337 -208
  33. package/src/tools/data/Tree/TreeDndProvider.tsx +137 -0
  34. package/src/tools/data/Tree/TreeRoot.tsx +170 -55
  35. package/src/tools/data/Tree/__tests__/dnd.test.ts +160 -0
  36. package/src/tools/data/Tree/__tests__/keyboard.test.ts +137 -0
  37. package/src/tools/data/Tree/__tests__/renameUtils.test.ts +52 -0
  38. package/src/tools/data/Tree/__tests__/selection.test.ts +227 -0
  39. package/src/tools/data/Tree/components/TreeDropIndicator.tsx +65 -0
  40. package/src/tools/data/Tree/components/TreeEmptyArea.tsx +160 -0
  41. package/src/tools/data/Tree/components/TreeRenameInput.tsx +114 -0
  42. package/src/tools/data/Tree/components/TreeRow.tsx +92 -8
  43. package/src/tools/data/Tree/components/index.ts +6 -0
  44. package/src/tools/data/Tree/context/TreeContext.tsx +204 -363
  45. package/src/tools/data/Tree/context/TreeContextValue.ts +139 -0
  46. package/src/tools/data/Tree/context/async-children/collect-ids.ts +27 -0
  47. package/src/tools/data/Tree/context/async-children/index.ts +8 -0
  48. package/src/tools/data/Tree/context/async-children/use-async-children.ts +157 -0
  49. package/src/tools/data/Tree/context/clipboard/index.ts +4 -0
  50. package/src/tools/data/Tree/context/clipboard/use-clipboard.ts +115 -0
  51. package/src/tools/data/Tree/context/dnd/index.ts +8 -0
  52. package/src/tools/data/Tree/context/dnd/use-dnd.ts +194 -0
  53. package/src/tools/data/Tree/context/expansion/index.ts +4 -0
  54. package/src/tools/data/Tree/context/expansion/use-expansion.ts +55 -0
  55. package/src/tools/data/Tree/context/hooks.ts +68 -1
  56. package/src/tools/data/Tree/context/index.ts +3 -0
  57. package/src/tools/data/Tree/context/menu/builtin-actions.ts +357 -0
  58. package/src/tools/data/Tree/context/menu/index.ts +10 -0
  59. package/src/tools/data/Tree/context/menu/use-resolved-menu.ts +127 -0
  60. package/src/tools/data/Tree/context/persist/index.ts +4 -0
  61. package/src/tools/data/Tree/context/persist/use-persist-sync.ts +74 -0
  62. package/src/tools/data/Tree/context/rename/index.ts +4 -0
  63. package/src/tools/data/Tree/context/rename/use-rename.ts +113 -0
  64. package/src/tools/data/Tree/context/selection/index.ts +4 -0
  65. package/src/tools/data/Tree/context/selection/use-selection.ts +146 -0
  66. package/src/tools/data/Tree/context/state/index.ts +6 -0
  67. package/src/tools/data/Tree/context/state/initial.ts +41 -0
  68. package/src/tools/data/Tree/context/state/reducer.ts +76 -0
  69. package/src/tools/data/Tree/context/state/types.ts +46 -0
  70. package/src/tools/data/Tree/data/clipboard.ts +33 -0
  71. package/src/tools/data/Tree/data/dnd.ts +123 -0
  72. package/src/tools/data/Tree/data/finderShortcuts.ts +67 -0
  73. package/src/tools/data/Tree/data/index.ts +19 -0
  74. package/src/tools/data/Tree/data/renameUtils.ts +51 -0
  75. package/src/tools/data/Tree/data/selection.ts +157 -0
  76. package/src/tools/data/Tree/hooks/finder-hotkeys/build-ctx.ts +48 -0
  77. package/src/tools/data/Tree/hooks/finder-hotkeys/index.ts +8 -0
  78. package/src/tools/data/Tree/hooks/finder-hotkeys/use-tree-finder-hotkeys.ts +166 -0
  79. package/src/tools/data/Tree/hooks/index.ts +23 -4
  80. package/src/tools/data/Tree/hooks/keyboard/activation.ts +27 -0
  81. package/src/tools/data/Tree/hooks/keyboard/arrow-nav.ts +26 -0
  82. package/src/tools/data/Tree/hooks/keyboard/expand-collapse.ts +54 -0
  83. package/src/tools/data/Tree/hooks/keyboard/index.ts +10 -0
  84. package/src/tools/data/Tree/hooks/keyboard/types.ts +39 -0
  85. package/src/tools/data/Tree/hooks/keyboard/use-tree-keyboard.ts +196 -0
  86. package/src/tools/data/Tree/hooks/type-ahead/index.ts +5 -0
  87. package/src/tools/data/Tree/hooks/type-ahead/match-prefix.ts +42 -0
  88. package/src/tools/data/Tree/hooks/{useTreeTypeAhead.ts → type-ahead/use-tree-type-ahead.ts} +8 -19
  89. package/src/tools/data/Tree/index.tsx +25 -2
  90. package/src/tools/data/Tree/types/activation.ts +30 -0
  91. package/src/tools/data/Tree/types/adapter.ts +70 -0
  92. package/src/tools/data/Tree/types/index.ts +27 -0
  93. package/src/tools/data/Tree/types/labels.ts +97 -0
  94. package/src/tools/data/Tree/types/loader.ts +9 -0
  95. package/src/tools/data/Tree/types/node.ts +38 -0
  96. package/src/tools/data/Tree/types/root-props.ts +142 -0
  97. package/src/tools/data/Tree/types/selection.ts +3 -0
  98. package/src/tools/data/Tree/types/slots.ts +64 -0
  99. package/src/tools/forms/MarkdownEditor/MarkdownEditor.tsx +85 -0
  100. package/src/tools/forms/MarkdownEditor/index.ts +1 -0
  101. package/src/tools/forms/MarkdownEditor/lazy.tsx +6 -0
  102. package/src/tools/forms/MarkdownEditor/slash/SlashCommandNode.ts +162 -0
  103. package/src/tools/forms/MarkdownEditor/slash/index.ts +4 -0
  104. package/src/tools/forms/MarkdownEditor/slash/syncSlashNode.ts +97 -0
  105. package/src/tools/forms/MarkdownEditor/slash/types.ts +13 -0
  106. package/src/tools/forms/MarkdownEditor/styles.css +18 -0
  107. package/dist/types-j2vhn4Kv.d.cts +0 -241
  108. package/dist/types-j2vhn4Kv.d.ts +0 -241
  109. package/src/tools/data/Tree/hooks/useTreeKeyboard.ts +0 -171
  110. package/src/tools/data/Tree/types.ts +0 -217
@@ -1,164 +1,43 @@
1
1
  'use client';
2
2
 
3
3
  import * as React from 'react';
4
- import {
5
- createContext,
6
- useCallback,
7
- useEffect,
8
- useMemo,
9
- useReducer,
10
- useRef,
11
- } from 'react';
4
+ import { createContext, useCallback, useMemo, useReducer, useRef } from 'react';
12
5
 
6
+ import { flattenTree } from '../data/flatten';
7
+ import { loadTreeState } from '../data/persist';
8
+ import { resolveAppearance } from '../data/appearance';
13
9
  import {
14
10
  DEFAULT_TREE_LABELS,
15
11
  type FlatRow,
16
- type TreeContextMenuSlot,
12
+ type TreeActivateOptions,
17
13
  type TreeItemId,
18
14
  type TreeLabels,
19
- type TreeLoadChildren,
20
15
  type TreeNode,
21
- type TreeActivateOptions,
22
- type TreeActivationMode,
23
16
  type TreeRootProps,
24
- type TreeRowSlot,
25
- type TreeSelectionMode,
26
17
  } from '../types';
27
- import {
28
- createChildCache,
29
- type ChildCache,
30
- type ChildEntry,
31
- } from '../data/childCache';
32
- import { flattenTree } from '../data/flatten';
33
- import { loadTreeState, saveTreeState } from '../data/persist';
34
- import {
35
- resolveAppearance,
36
- type ResolvedAppearance,
37
- type TreeAppearance,
38
- } from '../data/appearance';
39
-
40
- // =====================================================================
41
- // Reducer
42
- // =====================================================================
43
-
44
- interface State<T> {
45
- expanded: Set<TreeItemId>;
46
- selected: Set<TreeItemId>;
47
- focused: TreeItemId | null;
48
- query: string;
49
- /** Bumped on every cache mutation so memos see a fresh dep. */
50
- cacheTick: number;
51
- }
52
-
53
- type Action<T> =
54
- | { type: 'expand'; id: TreeItemId }
55
- | { type: 'collapse'; id: TreeItemId }
56
- | { type: 'toggle'; id: TreeItemId }
57
- | { type: 'set-expanded'; ids: TreeItemId[] }
58
- | { type: 'select'; id: TreeItemId; mode: TreeSelectionMode }
59
- | { type: 'select-many'; ids: TreeItemId[] }
60
- | { type: 'clear-selection' }
61
- | { type: 'focus'; id: TreeItemId | null }
62
- | { type: 'set-query'; q: string }
63
- | { type: 'cache-tick' };
64
-
65
- const reducer = <T,>(state: State<T>, action: Action<T>): State<T> => {
66
- switch (action.type) {
67
- case 'expand': {
68
- if (state.expanded.has(action.id)) return state;
69
- const next = new Set(state.expanded);
70
- next.add(action.id);
71
- return { ...state, expanded: next };
72
- }
73
- case 'collapse': {
74
- if (!state.expanded.has(action.id)) return state;
75
- const next = new Set(state.expanded);
76
- next.delete(action.id);
77
- return { ...state, expanded: next };
78
- }
79
- case 'toggle': {
80
- const next = new Set(state.expanded);
81
- if (next.has(action.id)) next.delete(action.id);
82
- else next.add(action.id);
83
- return { ...state, expanded: next };
84
- }
85
- case 'set-expanded':
86
- return { ...state, expanded: new Set(action.ids) };
87
- case 'select': {
88
- if (action.mode === 'none') return state;
89
- if (action.mode === 'single') {
90
- return { ...state, selected: new Set([action.id]), focused: action.id };
91
- }
92
- const next = new Set(state.selected);
93
- if (next.has(action.id)) next.delete(action.id);
94
- else next.add(action.id);
95
- return { ...state, selected: next, focused: action.id };
96
- }
97
- case 'select-many':
98
- return { ...state, selected: new Set(action.ids) };
99
- case 'clear-selection':
100
- return { ...state, selected: new Set() };
101
- case 'focus':
102
- return { ...state, focused: action.id };
103
- case 'set-query':
104
- return { ...state, query: action.q };
105
- case 'cache-tick':
106
- return { ...state, cacheTick: state.cacheTick + 1 };
107
- default:
108
- return state;
109
- }
110
- };
111
18
 
112
- // =====================================================================
113
- // Context value
114
- // =====================================================================
115
-
116
- export interface TreeContextValue<T> {
117
- // State
118
- expanded: ReadonlySet<TreeItemId>;
119
- selected: ReadonlySet<TreeItemId>;
120
- focused: TreeItemId | null;
121
- query: string;
122
-
123
- // Flattened render rows (visible items only)
124
- flatRows: FlatRow<T>[];
125
- /** Search-matching node ids (subset of all flatRows). */
126
- matchingIds: ReadonlySet<TreeItemId>;
127
-
128
- // Imperative actions
129
- expand: (id: TreeItemId) => void;
130
- collapse: (id: TreeItemId) => void;
131
- toggle: (id: TreeItemId) => void;
132
- expandAll: () => void;
133
- collapseAll: () => void;
134
- select: (id: TreeItemId) => void;
135
- setSelectedIds: (ids: TreeItemId[]) => void;
136
- clearSelection: () => void;
137
- setFocus: (id: TreeItemId | null) => void;
138
- setQuery: (q: string) => void;
139
- refresh: (id: TreeItemId) => Promise<void>;
140
- refreshAll: () => Promise<void>;
141
- activate: (node: TreeNode<T>, opts?: TreeActivateOptions) => void;
142
-
143
- // Config / slots
144
- labels: TreeLabels;
145
- /** Resolved cosmetic config — never null. */
146
- appearance: ResolvedAppearance;
147
- /** Convenience alias for `appearance.indent`. */
148
- indent: number;
149
- selectionMode: TreeSelectionMode;
150
- activationMode: TreeActivationMode;
151
- enableSearch: boolean;
152
- showIndentGuides: boolean;
153
- getItemName: (node: TreeNode<T>) => string;
154
-
155
- renderIcon?: TreeRowSlot<T>;
156
- renderLabel?: TreeRowSlot<T>;
157
- renderActions?: TreeRowSlot<T>;
158
- renderContextMenu?: TreeContextMenuSlot<T>;
159
- }
160
-
161
- const TreeContext = createContext<TreeContextValue<unknown> | null>(null);
19
+ import { reducer, createInitialState } from './state';
20
+ import { useAsyncChildren } from './async-children';
21
+ import { useExpansion } from './expansion';
22
+ import { useSelection } from './selection';
23
+ import { useRename } from './rename';
24
+ import { useClipboard } from './clipboard';
25
+ import { useResolvedMenu } from './menu';
26
+ import { useDnd, type UseDndReturn } from './dnd';
27
+ import { usePersistSync } from './persist';
28
+ import type { TreeContextValue } from './TreeContextValue';
29
+
30
+ // Re-exported from this module: the value interface (so consumers
31
+ // continue to `import type { TreeContextValue }` from `./context`).
32
+ export type { TreeContextValue } from './TreeContextValue';
33
+
34
+ /**
35
+ * Internal context object. Exported so `TreeRoot` can wrap it with an
36
+ * override-provider that injects a slot-form `renderContextMenu`
37
+ * derived from the declarative resolver. Consumers should use
38
+ * `useTreeContext()` instead of touching this directly.
39
+ */
40
+ export const TreeContext = createContext<TreeContextValue<unknown> | null>(null);
162
41
 
163
42
  export function useTreeContext<T>(): TreeContextValue<T> {
164
43
  const ctx = React.useContext(TreeContext);
@@ -169,7 +48,20 @@ export function useTreeContext<T>(): TreeContextValue<T> {
169
48
  }
170
49
 
171
50
  // =====================================================================
172
- // Provider
51
+ // Provider — thin assembly. The real work lives in:
52
+ //
53
+ // state/ reducer + initial state
54
+ // async-children/ child cache, nodeById, fetchChildren, refresh, refreshAll
55
+ // expansion/ expand / collapse / toggle / expandAll / collapseAll
56
+ // selection/ click / move / select-all + plain select / clear
57
+ // rename/ start / cancel / commit
58
+ // clipboard/ cut / copy / paste / clear
59
+ // menu/ built-in actions registry + merged declarative resolver
60
+ // persist/ localStorage + onSelectionChange/onExpansionChange
61
+ //
62
+ // This file only stitches them together and shapes the final
63
+ // `TreeContextValue`. If a feature grows, add a folder above — don't
64
+ // extend this file.
173
65
  // =====================================================================
174
66
 
175
67
  export interface TreeProviderProps<T>
@@ -194,36 +86,19 @@ export interface TreeProviderProps<T>
194
86
  | 'renderLabel'
195
87
  | 'renderActions'
196
88
  | 'renderContextMenu'
89
+ | 'contextMenuActions'
197
90
  | 'labels'
198
91
  | 'persistKey'
199
92
  | 'persistSelection'
93
+ | 'adapter'
94
+ | 'defaultMenuItems'
95
+ | 'enableInlineRename'
96
+ | 'enableDnD'
97
+ | 'canDrop'
200
98
  > {
201
99
  children: React.ReactNode;
202
100
  }
203
101
 
204
- const setEqualsArr = (set: ReadonlySet<string>, arr: readonly string[]) => {
205
- if (set.size !== arr.length) return false;
206
- for (const id of arr) if (!set.has(id)) return false;
207
- return true;
208
- };
209
-
210
- const collectAllIds = <T,>(
211
- roots: TreeNode<T>[],
212
- cache: ChildCache<T>,
213
- out: TreeItemId[],
214
- ) => {
215
- for (const node of roots) {
216
- if (Array.isArray(node.children)) {
217
- out.push(node.id);
218
- collectAllIds(node.children, cache, out);
219
- } else if (node.isFolder) {
220
- out.push(node.id);
221
- const entry = cache.get(node.id);
222
- if (entry?.children) collectAllIds(entry.children, cache, out);
223
- }
224
- }
225
- };
226
-
227
102
  export function TreeProvider<T>(props: TreeProviderProps<T>) {
228
103
  const {
229
104
  data,
@@ -245,13 +120,21 @@ export function TreeProvider<T>(props: TreeProviderProps<T>) {
245
120
  renderLabel,
246
121
  renderActions,
247
122
  renderContextMenu,
123
+ contextMenuActions,
248
124
  labels: labelsOverride,
249
125
  persistKey,
250
126
  persistSelection = false,
127
+ adapter,
128
+ defaultMenuItems,
129
+ enableInlineRename = false,
130
+ enableDnD = false,
131
+ canDrop,
251
132
  children,
252
133
  } = props;
253
134
 
254
- const labels = useMemo(
135
+ // ---- Stable config ------------------------------------------------
136
+
137
+ const labels = useMemo<TreeLabels>(
255
138
  () => ({ ...DEFAULT_TREE_LABELS, ...labelsOverride }),
256
139
  [labelsOverride],
257
140
  );
@@ -261,103 +144,53 @@ export function TreeProvider<T>(props: TreeProviderProps<T>) {
261
144
  [appearance, indent],
262
145
  );
263
146
 
264
- // Persisted state, loaded once.
147
+ // ---- Reducer ------------------------------------------------------
148
+
265
149
  const persisted = useMemo(
266
150
  () => (persistKey ? loadTreeState(persistKey) : null),
267
151
  [persistKey],
268
152
  );
269
153
 
270
- const [state, dispatch] = useReducer(reducer<T>, undefined, () => ({
271
- expanded: new Set(persisted?.expandedItems ?? initialExpandedIds ?? []),
272
- selected: new Set(
273
- (persistSelection ? persisted?.selectedItems : undefined) ??
274
- initialSelectedIds ??
275
- [],
276
- ),
277
- focused: null,
278
- query: '',
279
- cacheTick: 0,
280
- }));
281
-
282
- // Async cache survives provider re-renders, lives in a ref.
283
- const cacheRef = useRef<ChildCache<T>>(createChildCache<T>());
284
- const inflightRef = useRef<Map<TreeItemId, Promise<void>>>(new Map());
285
-
286
- // Trigger one fetch per (folder id) — concurrent expansions are deduped.
287
- const fetchChildren = useCallback(
288
- async (node: TreeNode<T>) => {
289
- if (!loadChildren) return;
290
- if (Array.isArray(node.children)) return;
291
- const existing = cacheRef.current.get(node.id);
292
- if (existing?.status === 'loaded' || existing?.status === 'loading') return;
293
- const inflight = inflightRef.current.get(node.id);
294
- if (inflight) return inflight;
295
-
296
- cacheRef.current.set(node.id, { status: 'loading', children: [] });
297
- dispatch({ type: 'cache-tick' });
298
-
299
- const promise = (async () => {
300
- try {
301
- const children = await loadChildren(node);
302
- cacheRef.current.set(node.id, { status: 'loaded', children });
303
- } catch (err) {
304
- cacheRef.current.set(node.id, {
305
- status: 'error',
306
- children: [],
307
- error: err instanceof Error ? err.message : String(err),
308
- });
309
- } finally {
310
- inflightRef.current.delete(node.id);
311
- dispatch({ type: 'cache-tick' });
312
- }
313
- })();
314
-
315
- inflightRef.current.set(node.id, promise);
316
- return promise;
317
- },
318
- [loadChildren],
154
+ const [state, dispatch] = useReducer(reducer, undefined, () =>
155
+ createInitialState({
156
+ persisted,
157
+ initialExpandedIds,
158
+ initialSelectedIds,
159
+ persistSelection,
160
+ }),
319
161
  );
320
162
 
321
- // Build a quick id node map for any visible node (incl. cached children).
322
- const nodeById = useMemo(() => {
323
- const map = new Map<TreeItemId, TreeNode<T>>();
324
- const walk = (nodes: TreeNode<T>[]) => {
325
- for (const n of nodes) {
326
- map.set(n.id, n);
327
- if (Array.isArray(n.children)) walk(n.children);
328
- else {
329
- const entry = cacheRef.current.get(n.id);
330
- if (entry?.children) walk(entry.children);
331
- }
332
- }
333
- };
334
- walk(data);
335
- return map;
336
- }, [data, state.cacheTick]);
337
-
338
- // On expand, kick async fetch (no-op if already cached or has inline children).
339
- useEffect(() => {
340
- if (!loadChildren) return;
341
- for (const id of state.expanded) {
342
- const node = nodeById.get(id);
343
- if (!node) continue;
344
- void fetchChildren(node);
345
- }
346
- }, [loadChildren, state.expanded, state.cacheTick, nodeById, fetchChildren]);
163
+ const bumpCacheTick = useCallback(() => dispatch({ type: 'cache-tick' }), []);
164
+
165
+ // ---- Async children (cache + nodeById + refresh) ------------------
166
+
167
+ const {
168
+ nodeById,
169
+ refresh,
170
+ refreshAll,
171
+ collectFolderIds,
172
+ cache,
173
+ } = useAsyncChildren<T>({
174
+ data,
175
+ loadChildren,
176
+ expanded: state.expanded,
177
+ cacheTick: state.cacheTick,
178
+ bumpCacheTick,
179
+ });
180
+
181
+ // ---- Flat rows (depend on cache via cacheTick) --------------------
347
182
 
348
- // Flatten on relevant changes.
349
- const flatRows = useMemo(
183
+ const flatRows = useMemo<FlatRow<T>[]>(
350
184
  () =>
351
185
  flattenTree<T>({
352
186
  roots: data,
353
187
  expandedIds: state.expanded,
354
- cache: cacheRef.current,
188
+ cache,
355
189
  filterNode,
356
190
  }),
357
- [data, state.expanded, state.cacheTick, filterNode],
191
+ [data, state.expanded, state.cacheTick, cache, filterNode],
358
192
  );
359
193
 
360
- // Search matches (case-insensitive substring on getItemName).
361
194
  const matchingIds = useMemo(() => {
362
195
  const set = new Set<TreeItemId>();
363
196
  if (!enableSearch || state.query.trim() === '') return set;
@@ -370,127 +203,123 @@ export function TreeProvider<T>(props: TreeProviderProps<T>) {
370
203
  return set;
371
204
  }, [enableSearch, state.query, flatRows, getItemName]);
372
205
 
373
- // External callbacks via stable refs.
374
- const onSelectionChangeRef = useRef(onSelectionChange);
375
- const onExpansionChangeRef = useRef(onExpansionChange);
206
+ // ---- Feature hooks ------------------------------------------------
207
+
208
+ const expansion = useExpansion({ dispatch, collectFolderIds });
209
+ const selection = useSelection<T>({
210
+ dispatch,
211
+ selectionMode,
212
+ flatRows,
213
+ selected: state.selected,
214
+ anchor: state.anchor,
215
+ focused: state.focused,
216
+ });
217
+ const rename = useRename<T>({
218
+ dispatch,
219
+ adapter,
220
+ enableInlineRename,
221
+ nodeById,
222
+ getItemName,
223
+ labels,
224
+ });
225
+ const clipboard = useClipboard<T>({
226
+ dispatch,
227
+ clipboard: state.clipboard,
228
+ adapter,
229
+ nodeById,
230
+ labels,
231
+ });
232
+ const dnd = useDnd<T>({
233
+ enabled: enableDnD,
234
+ adapter,
235
+ nodeById,
236
+ selected: state.selected,
237
+ labels,
238
+ canDrop,
239
+ });
240
+
241
+ // ---- Activation ---------------------------------------------------
242
+
376
243
  const onActivateRef = useRef(onActivate);
377
- onSelectionChangeRef.current = onSelectionChange;
378
- onExpansionChangeRef.current = onExpansionChange;
379
244
  onActivateRef.current = onActivate;
380
-
381
- // Notify on changes + persist.
382
- const lastSelectedArrRef = useRef<TreeItemId[]>([...state.selected]);
383
- const lastExpandedArrRef = useRef<TreeItemId[]>([...state.expanded]);
384
-
385
- useEffect(() => {
386
- const arr = [...state.expanded];
387
- if (!setEqualsArr(state.expanded, lastExpandedArrRef.current)) {
388
- lastExpandedArrRef.current = arr;
389
- onExpansionChangeRef.current?.(arr);
390
- if (persistKey) {
391
- saveTreeState(persistKey, {
392
- expandedItems: arr,
393
- selectedItems: persistSelection ? [...state.selected] : [],
394
- });
395
- }
396
- }
397
- }, [state.expanded, persistKey, persistSelection, state.selected]);
398
-
399
- useEffect(() => {
400
- const arr = [...state.selected];
401
- if (!setEqualsArr(state.selected, lastSelectedArrRef.current)) {
402
- lastSelectedArrRef.current = arr;
403
- onSelectionChangeRef.current?.(arr);
404
- if (persistKey && persistSelection) {
405
- saveTreeState(persistKey, {
406
- expandedItems: [...state.expanded],
407
- selectedItems: arr,
408
- });
409
- }
410
- }
411
- }, [state.selected, persistKey, persistSelection, state.expanded]);
412
-
413
- // Imperative actions.
414
- const expand = useCallback((id: TreeItemId) => dispatch({ type: 'expand', id }), []);
415
- const collapse = useCallback((id: TreeItemId) => dispatch({ type: 'collapse', id }), []);
416
- const toggle = useCallback((id: TreeItemId) => dispatch({ type: 'toggle', id }), []);
417
-
418
- const expandAll = useCallback(() => {
419
- const ids: TreeItemId[] = [];
420
- collectAllIds(data, cacheRef.current, ids);
421
- dispatch({ type: 'set-expanded', ids });
422
- }, [data]);
423
-
424
- const collapseAll = useCallback(
425
- () => dispatch({ type: 'set-expanded', ids: [] }),
245
+ const activate = useCallback(
246
+ (node: TreeNode<T>, opts: TreeActivateOptions = { preview: false }) =>
247
+ onActivateRef.current?.(node, opts),
426
248
  [],
427
249
  );
428
250
 
429
- const select = useCallback(
430
- (id: TreeItemId) => dispatch({ type: 'select', id, mode: selectionMode }),
431
- [selectionMode],
432
- );
433
- const setSelectedIds = useCallback(
434
- (ids: TreeItemId[]) => dispatch({ type: 'select-many', ids }),
251
+ const setQuery = useCallback(
252
+ (q: string) => dispatch({ type: 'set-query', q }),
435
253
  [],
436
254
  );
437
- const clearSelection = useCallback(() => dispatch({ type: 'clear-selection' }), []);
438
- const setFocus = useCallback(
439
- (id: TreeItemId | null) => dispatch({ type: 'focus', id }),
440
- [],
441
- );
442
- const setQuery = useCallback((q: string) => dispatch({ type: 'set-query', q }), []);
443
-
444
- const refresh = useCallback(
445
- async (id: TreeItemId) => {
446
- const node = nodeById.get(id);
447
- if (!node || !loadChildren) return;
448
- cacheRef.current.delete(id);
449
- dispatch({ type: 'cache-tick' });
450
- await fetchChildren(node);
451
- },
452
- [nodeById, loadChildren, fetchChildren],
453
- );
454
255
 
455
- const refreshAll = useCallback(async () => {
456
- cacheRef.current.clear();
457
- dispatch({ type: 'cache-tick' });
458
- if (!loadChildren) return;
459
- await Promise.all(
460
- [...state.expanded].map((id) => {
461
- const node = nodeById.get(id);
462
- return node ? fetchChildren(node) : undefined;
463
- }),
464
- );
465
- }, [loadChildren, state.expanded, nodeById, fetchChildren]);
256
+ // ---- Persist + notify callbacks -----------------------------------
466
257
 
467
- const activate = useCallback(
468
- (node: TreeNode<T>, opts: TreeActivateOptions = { preview: false }) =>
469
- onActivateRef.current?.(node, opts),
470
- [],
471
- );
258
+ usePersistSync({
259
+ expanded: state.expanded,
260
+ selected: state.selected,
261
+ persistKey,
262
+ persistSelection,
263
+ onSelectionChange,
264
+ onExpansionChange,
265
+ });
266
+
267
+ // ---- Resolved context-menu resolver -------------------------------
268
+
269
+ const resolvedContextMenuActions = useResolvedMenu<T>({
270
+ adapter,
271
+ contextMenuActions,
272
+ defaultMenuItems,
273
+ labels,
274
+ selected: state.selected,
275
+ clipboard: state.clipboard,
276
+ nodeById,
277
+ getItemName,
278
+ enableInlineRename,
279
+ startRename: rename.startRename,
280
+ cutToClipboard: clipboard.cutToClipboard,
281
+ copyToClipboard: clipboard.copyToClipboard,
282
+ pasteFromClipboard: clipboard.pasteFromClipboard,
283
+ });
284
+
285
+ // ---- Final value --------------------------------------------------
472
286
 
473
287
  const value = useMemo<TreeContextValue<T>>(
474
288
  () => ({
289
+ // state
475
290
  expanded: state.expanded,
476
291
  selected: state.selected,
292
+ anchor: state.anchor,
477
293
  focused: state.focused,
478
294
  query: state.query,
295
+ renamingId: state.renaming,
296
+ inlineRenameEnabled: rename.enabled,
297
+ clipboard: state.clipboard,
479
298
  flatRows,
480
299
  matchingIds,
481
- expand,
482
- collapse,
483
- toggle,
484
- expandAll,
485
- collapseAll,
486
- select,
487
- setSelectedIds,
488
- clearSelection,
489
- setFocus,
300
+
301
+ // expansion
302
+ ...expansion,
303
+
304
+ // selection (note: `select` is from selection hook; expansion exports no `select`)
305
+ ...selection,
306
+
490
307
  setQuery,
308
+
309
+ // clipboard
310
+ ...clipboard,
311
+
312
+ // rename
313
+ startRename: rename.startRename,
314
+ cancelRename: rename.cancelRename,
315
+ commitRename: rename.commitRename,
316
+
317
+ // async
491
318
  refresh,
492
319
  refreshAll,
493
320
  activate,
321
+
322
+ // config
494
323
  labels,
495
324
  appearance: resolvedAppearance,
496
325
  indent: resolvedAppearance.indent,
@@ -499,28 +328,36 @@ export function TreeProvider<T>(props: TreeProviderProps<T>) {
499
328
  enableSearch,
500
329
  showIndentGuides,
501
330
  getItemName,
331
+
332
+ // slots
502
333
  renderIcon,
503
334
  renderLabel,
504
335
  renderActions,
505
336
  renderContextMenu,
337
+
338
+ adapter,
339
+ resolvedContextMenuActions,
340
+ getNodeById: (id: TreeItemId) => nodeById.get(id),
341
+ dnd,
506
342
  }),
507
343
  [
508
344
  state.expanded,
509
345
  state.selected,
346
+ state.anchor,
510
347
  state.focused,
511
348
  state.query,
349
+ state.renaming,
350
+ state.clipboard,
351
+ rename.enabled,
352
+ rename.startRename,
353
+ rename.cancelRename,
354
+ rename.commitRename,
512
355
  flatRows,
513
356
  matchingIds,
514
- expand,
515
- collapse,
516
- toggle,
517
- expandAll,
518
- collapseAll,
519
- select,
520
- setSelectedIds,
521
- clearSelection,
522
- setFocus,
357
+ expansion,
358
+ selection,
523
359
  setQuery,
360
+ clipboard,
524
361
  refresh,
525
362
  refreshAll,
526
363
  activate,
@@ -535,6 +372,10 @@ export function TreeProvider<T>(props: TreeProviderProps<T>) {
535
372
  renderLabel,
536
373
  renderActions,
537
374
  renderContextMenu,
375
+ adapter,
376
+ resolvedContextMenuActions,
377
+ nodeById,
378
+ dnd,
538
379
  ],
539
380
  );
540
381
 
@@ -546,4 +387,4 @@ export function TreeProvider<T>(props: TreeProviderProps<T>) {
546
387
  }
547
388
 
548
389
  // Re-export internal types referenced by hook consumers.
549
- export type { ChildCache, ChildEntry };
390
+ export type { ChildCache, ChildEntry } from '../data/childCache';