@djangocfg/ui-tools 2.1.415 → 2.1.417

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 (140) hide show
  1. package/dist/audio-player/index.cjs +2099 -0
  2. package/dist/audio-player/index.cjs.map +1 -0
  3. package/dist/audio-player/index.css +65 -0
  4. package/dist/audio-player/index.css.map +1 -0
  5. package/dist/audio-player/index.d.cts +174 -0
  6. package/dist/audio-player/index.d.ts +174 -0
  7. package/dist/audio-player/index.mjs +2076 -0
  8. package/dist/audio-player/index.mjs.map +1 -0
  9. package/dist/composer-registry/index.cjs +45 -0
  10. package/dist/composer-registry/index.cjs.map +1 -0
  11. package/dist/composer-registry/index.d.cts +73 -0
  12. package/dist/composer-registry/index.d.ts +73 -0
  13. package/dist/composer-registry/index.mjs +39 -0
  14. package/dist/composer-registry/index.mjs.map +1 -0
  15. package/dist/file-icon/index.d.cts +1 -1
  16. package/dist/file-icon/index.d.ts +1 -1
  17. package/dist/slots-ClRpIzoh.d.cts +88 -0
  18. package/dist/slots-ClRpIzoh.d.ts +88 -0
  19. package/dist/tree/index.cjs +2019 -279
  20. package/dist/tree/index.cjs.map +1 -1
  21. package/dist/tree/index.d.cts +731 -72
  22. package/dist/tree/index.d.ts +731 -72
  23. package/dist/tree/index.mjs +2009 -282
  24. package/dist/tree/index.mjs.map +1 -1
  25. package/package.json +18 -9
  26. package/src/tools/chat/README.md +111 -1
  27. package/src/tools/chat/composer/Composer.tsx +146 -25
  28. package/src/tools/chat/composer/ComposerRichTextarea.tsx +25 -0
  29. package/src/tools/chat/composer/index.ts +22 -0
  30. package/src/tools/chat/composer/slash/README.md +187 -0
  31. package/src/tools/chat/composer/slash/SlashHighlightTextarea.tsx +144 -0
  32. package/src/tools/chat/composer/slash/SlashMenu.tsx +142 -0
  33. package/src/tools/chat/composer/slash/SlashToken.tsx +57 -0
  34. package/src/tools/chat/composer/slash/index.ts +44 -0
  35. package/src/tools/chat/composer/slash/labels.ts +19 -0
  36. package/src/tools/chat/composer/slash/state.ts +168 -0
  37. package/src/tools/chat/composer/slash/types.ts +64 -0
  38. package/src/tools/chat/composer/slash/useSlashCommands.ts +204 -0
  39. package/src/tools/chat/composer/types.ts +8 -0
  40. package/src/tools/chat/context/ChatProvider.tsx +13 -78
  41. package/src/tools/chat/hooks/useAutoFocusOnStreamEnd.ts +12 -15
  42. package/src/tools/chat/hooks/useFocusOnEmptyClick.ts +4 -5
  43. package/src/tools/chat/launcher/header/ChatHeader.tsx +14 -19
  44. package/src/tools/chat/launcher/header/ChatHeaderActionButton.tsx +8 -12
  45. package/src/tools/chat/shell/SuggestedPrompts.tsx +194 -0
  46. package/src/tools/chat/shell/index.ts +6 -0
  47. package/src/tools/data/Listbox/lazy.tsx +1 -1
  48. package/src/tools/data/Masonry/lazy.tsx +1 -1
  49. package/src/tools/data/Timeline/lazy.tsx +1 -1
  50. package/src/tools/data/Tree/FinderTree.tsx +42 -0
  51. package/src/tools/data/Tree/README.md +337 -208
  52. package/src/tools/data/Tree/TreeDndProvider.tsx +137 -0
  53. package/src/tools/data/Tree/TreeRoot.tsx +111 -72
  54. package/src/tools/data/Tree/__tests__/dnd.test.ts +160 -0
  55. package/src/tools/data/Tree/__tests__/keyboard.test.ts +137 -0
  56. package/src/tools/data/Tree/__tests__/renameUtils.test.ts +52 -0
  57. package/src/tools/data/Tree/__tests__/selection.test.ts +227 -0
  58. package/src/tools/data/Tree/components/TreeDropIndicator.tsx +65 -0
  59. package/src/tools/data/Tree/components/TreeEmptyArea.tsx +160 -0
  60. package/src/tools/data/Tree/components/TreeRenameInput.tsx +114 -0
  61. package/src/tools/data/Tree/components/TreeRow.tsx +103 -8
  62. package/src/tools/data/Tree/components/index.ts +6 -0
  63. package/src/tools/data/Tree/context/TreeContext.tsx +223 -363
  64. package/src/tools/data/Tree/context/TreeContextValue.ts +139 -0
  65. package/src/tools/data/Tree/context/async-children/collect-ids.ts +27 -0
  66. package/src/tools/data/Tree/context/async-children/index.ts +8 -0
  67. package/src/tools/data/Tree/context/async-children/use-async-children.ts +157 -0
  68. package/src/tools/data/Tree/context/clipboard/index.ts +4 -0
  69. package/src/tools/data/Tree/context/clipboard/use-clipboard.ts +115 -0
  70. package/src/tools/data/Tree/context/dnd/index.ts +8 -0
  71. package/src/tools/data/Tree/context/dnd/use-dnd.ts +194 -0
  72. package/src/tools/data/Tree/context/expansion/index.ts +4 -0
  73. package/src/tools/data/Tree/context/expansion/use-expansion.ts +55 -0
  74. package/src/tools/data/Tree/context/hooks.ts +68 -1
  75. package/src/tools/data/Tree/context/index.ts +3 -0
  76. package/src/tools/data/Tree/context/menu/builtin-actions.ts +357 -0
  77. package/src/tools/data/Tree/context/menu/index.ts +11 -0
  78. package/src/tools/data/Tree/context/menu/render.tsx +75 -0
  79. package/src/tools/data/Tree/context/menu/use-resolved-menu.ts +141 -0
  80. package/src/tools/data/Tree/context/persist/index.ts +4 -0
  81. package/src/tools/data/Tree/context/persist/use-persist-sync.ts +74 -0
  82. package/src/tools/data/Tree/context/rename/index.ts +4 -0
  83. package/src/tools/data/Tree/context/rename/use-rename.ts +113 -0
  84. package/src/tools/data/Tree/context/selection/index.ts +4 -0
  85. package/src/tools/data/Tree/context/selection/use-selection.ts +146 -0
  86. package/src/tools/data/Tree/context/state/index.ts +6 -0
  87. package/src/tools/data/Tree/context/state/initial.ts +41 -0
  88. package/src/tools/data/Tree/context/state/reducer.ts +76 -0
  89. package/src/tools/data/Tree/context/state/types.ts +46 -0
  90. package/src/tools/data/Tree/data/clipboard.ts +33 -0
  91. package/src/tools/data/Tree/data/dnd.ts +123 -0
  92. package/src/tools/data/Tree/data/finderShortcuts.ts +67 -0
  93. package/src/tools/data/Tree/data/index.ts +19 -0
  94. package/src/tools/data/Tree/data/renameUtils.ts +51 -0
  95. package/src/tools/data/Tree/data/selection.ts +157 -0
  96. package/src/tools/data/Tree/hooks/finder-hotkeys/build-ctx.ts +48 -0
  97. package/src/tools/data/Tree/hooks/finder-hotkeys/index.ts +8 -0
  98. package/src/tools/data/Tree/hooks/finder-hotkeys/use-tree-finder-hotkeys.ts +166 -0
  99. package/src/tools/data/Tree/hooks/index.ts +23 -4
  100. package/src/tools/data/Tree/hooks/keyboard/activation.ts +27 -0
  101. package/src/tools/data/Tree/hooks/keyboard/arrow-nav.ts +26 -0
  102. package/src/tools/data/Tree/hooks/keyboard/expand-collapse.ts +54 -0
  103. package/src/tools/data/Tree/hooks/keyboard/index.ts +10 -0
  104. package/src/tools/data/Tree/hooks/keyboard/types.ts +39 -0
  105. package/src/tools/data/Tree/hooks/keyboard/use-tree-keyboard.ts +196 -0
  106. package/src/tools/data/Tree/hooks/type-ahead/index.ts +5 -0
  107. package/src/tools/data/Tree/hooks/type-ahead/match-prefix.ts +42 -0
  108. package/src/tools/data/Tree/hooks/{useTreeTypeAhead.ts → type-ahead/use-tree-type-ahead.ts} +8 -19
  109. package/src/tools/data/Tree/index.tsx +26 -2
  110. package/src/tools/data/Tree/types/activation.ts +30 -0
  111. package/src/tools/data/Tree/types/adapter.ts +70 -0
  112. package/src/tools/data/Tree/types/index.ts +27 -0
  113. package/src/tools/data/Tree/types/labels.ts +97 -0
  114. package/src/tools/data/Tree/types/loader.ts +9 -0
  115. package/src/tools/data/Tree/types/node.ts +38 -0
  116. package/src/tools/data/Tree/types/root-props.ts +158 -0
  117. package/src/tools/data/Tree/types/selection.ts +3 -0
  118. package/src/tools/data/Tree/types/slots.ts +64 -0
  119. package/src/tools/dev/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/MetaActions.tsx +6 -9
  120. package/src/tools/dev/OpenapiViewer/components/DocsLayout/index.tsx +2 -4
  121. package/src/tools/forms/MarkdownEditor/MarkdownEditor.tsx +85 -0
  122. package/src/tools/forms/MarkdownEditor/index.ts +1 -0
  123. package/src/tools/forms/MarkdownEditor/lazy.tsx +6 -0
  124. package/src/tools/forms/MarkdownEditor/slash/SlashCommandNode.ts +162 -0
  125. package/src/tools/forms/MarkdownEditor/slash/index.ts +4 -0
  126. package/src/tools/forms/MarkdownEditor/slash/syncSlashNode.ts +97 -0
  127. package/src/tools/forms/MarkdownEditor/slash/types.ts +13 -0
  128. package/src/tools/forms/MarkdownEditor/styles.css +18 -0
  129. package/src/tools/input/SpeechRecognition/widgets/VoiceComposerSlot.tsx +11 -12
  130. package/src/tools/integration/ComposerRegistry/index.ts +105 -0
  131. package/src/tools/media/AudioPlayer/Player.tsx +2 -0
  132. package/src/tools/media/AudioPlayer/PlayerShell.tsx +37 -22
  133. package/src/tools/media/AudioPlayer/lazy.tsx +30 -42
  134. package/src/tools/media/AudioPlayer/parts/Controls/IconButton.tsx +10 -11
  135. package/src/tools/media/AudioPlayer/parts/Controls/VolumeControl.tsx +52 -115
  136. package/src/tools/media/AudioPlayer/types.ts +15 -0
  137. package/dist/types-j2vhn4Kv.d.cts +0 -241
  138. package/dist/types-j2vhn4Kv.d.ts +0 -241
  139. package/src/tools/data/Tree/hooks/useTreeKeyboard.ts +0 -171
  140. package/src/tools/data/Tree/types.ts +0 -217
@@ -1,164 +1,44 @@
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,
12
+ type TreeActivateOptions,
16
13
  type TreeContextMenuSlot,
17
14
  type TreeItemId,
18
15
  type TreeLabels,
19
- type TreeLoadChildren,
20
16
  type TreeNode,
21
- type TreeActivateOptions,
22
- type TreeActivationMode,
23
17
  type TreeRootProps,
24
- type TreeRowSlot,
25
- type TreeSelectionMode,
26
18
  } 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
19
 
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
-
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);
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);
162
42
 
163
43
  export function useTreeContext<T>(): TreeContextValue<T> {
164
44
  const ctx = React.useContext(TreeContext);
@@ -169,7 +49,20 @@ export function useTreeContext<T>(): TreeContextValue<T> {
169
49
  }
170
50
 
171
51
  // =====================================================================
172
- // Provider
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.
173
66
  // =====================================================================
174
67
 
175
68
  export interface TreeProviderProps<T>
@@ -194,36 +87,19 @@ export interface TreeProviderProps<T>
194
87
  | 'renderLabel'
195
88
  | 'renderActions'
196
89
  | 'renderContextMenu'
90
+ | 'contextMenuActions'
197
91
  | 'labels'
198
92
  | 'persistKey'
199
93
  | 'persistSelection'
94
+ | 'adapter'
95
+ | 'defaultMenuItems'
96
+ | 'enableInlineRename'
97
+ | 'enableDnD'
98
+ | 'canDrop'
200
99
  > {
201
100
  children: React.ReactNode;
202
101
  }
203
102
 
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
103
  export function TreeProvider<T>(props: TreeProviderProps<T>) {
228
104
  const {
229
105
  data,
@@ -245,13 +121,21 @@ export function TreeProvider<T>(props: TreeProviderProps<T>) {
245
121
  renderLabel,
246
122
  renderActions,
247
123
  renderContextMenu,
124
+ contextMenuActions,
248
125
  labels: labelsOverride,
249
126
  persistKey,
250
127
  persistSelection = false,
128
+ adapter,
129
+ defaultMenuItems,
130
+ enableInlineRename = false,
131
+ enableDnD = false,
132
+ canDrop,
251
133
  children,
252
134
  } = props;
253
135
 
254
- const labels = useMemo(
136
+ // ---- Stable config ------------------------------------------------
137
+
138
+ const labels = useMemo<TreeLabels>(
255
139
  () => ({ ...DEFAULT_TREE_LABELS, ...labelsOverride }),
256
140
  [labelsOverride],
257
141
  );
@@ -261,103 +145,53 @@ export function TreeProvider<T>(props: TreeProviderProps<T>) {
261
145
  [appearance, indent],
262
146
  );
263
147
 
264
- // Persisted state, loaded once.
148
+ // ---- Reducer ------------------------------------------------------
149
+
265
150
  const persisted = useMemo(
266
151
  () => (persistKey ? loadTreeState(persistKey) : null),
267
152
  [persistKey],
268
153
  );
269
154
 
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],
155
+ const [state, dispatch] = useReducer(reducer, undefined, () =>
156
+ createInitialState({
157
+ persisted,
158
+ initialExpandedIds,
159
+ initialSelectedIds,
160
+ persistSelection,
161
+ }),
319
162
  );
320
163
 
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]);
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) --------------------
347
183
 
348
- // Flatten on relevant changes.
349
- const flatRows = useMemo(
184
+ const flatRows = useMemo<FlatRow<T>[]>(
350
185
  () =>
351
186
  flattenTree<T>({
352
187
  roots: data,
353
188
  expandedIds: state.expanded,
354
- cache: cacheRef.current,
189
+ cache,
355
190
  filterNode,
356
191
  }),
357
- [data, state.expanded, state.cacheTick, filterNode],
192
+ [data, state.expanded, state.cacheTick, cache, filterNode],
358
193
  );
359
194
 
360
- // Search matches (case-insensitive substring on getItemName).
361
195
  const matchingIds = useMemo(() => {
362
196
  const set = new Set<TreeItemId>();
363
197
  if (!enableSearch || state.query.trim() === '') return set;
@@ -370,127 +204,141 @@ export function TreeProvider<T>(props: TreeProviderProps<T>) {
370
204
  return set;
371
205
  }, [enableSearch, state.query, flatRows, getItemName]);
372
206
 
373
- // External callbacks via stable refs.
374
- const onSelectionChangeRef = useRef(onSelectionChange);
375
- const onExpansionChangeRef = useRef(onExpansionChange);
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
+
376
244
  const onActivateRef = useRef(onActivate);
377
- onSelectionChangeRef.current = onSelectionChange;
378
- onExpansionChangeRef.current = onExpansionChange;
379
245
  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: [] }),
246
+ const activate = useCallback(
247
+ (node: TreeNode<T>, opts: TreeActivateOptions = { preview: false }) =>
248
+ onActivateRef.current?.(node, opts),
426
249
  [],
427
250
  );
428
251
 
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 }),
252
+ const setQuery = useCallback(
253
+ (q: string) => dispatch({ type: 'set-query', q }),
435
254
  [],
436
255
  );
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
256
 
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]);
257
+ // ---- Persist + notify callbacks -----------------------------------
466
258
 
467
- const activate = useCallback(
468
- (node: TreeNode<T>, opts: TreeActivateOptions = { preview: false }) =>
469
- onActivateRef.current?.(node, opts),
470
- [],
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],
471
302
  );
472
303
 
304
+ // ---- Final value --------------------------------------------------
305
+
473
306
  const value = useMemo<TreeContextValue<T>>(
474
307
  () => ({
308
+ // state
475
309
  expanded: state.expanded,
476
310
  selected: state.selected,
311
+ anchor: state.anchor,
477
312
  focused: state.focused,
478
313
  query: state.query,
314
+ renamingId: state.renaming,
315
+ inlineRenameEnabled: rename.enabled,
316
+ clipboard: state.clipboard,
479
317
  flatRows,
480
318
  matchingIds,
481
- expand,
482
- collapse,
483
- toggle,
484
- expandAll,
485
- collapseAll,
486
- select,
487
- setSelectedIds,
488
- clearSelection,
489
- setFocus,
319
+
320
+ // expansion
321
+ ...expansion,
322
+
323
+ // selection (note: `select` is from selection hook; expansion exports no `select`)
324
+ ...selection,
325
+
490
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
491
337
  refresh,
492
338
  refreshAll,
493
339
  activate,
340
+
341
+ // config
494
342
  labels,
495
343
  appearance: resolvedAppearance,
496
344
  indent: resolvedAppearance.indent,
@@ -499,28 +347,36 @@ export function TreeProvider<T>(props: TreeProviderProps<T>) {
499
347
  enableSearch,
500
348
  showIndentGuides,
501
349
  getItemName,
350
+
351
+ // slots
502
352
  renderIcon,
503
353
  renderLabel,
504
354
  renderActions,
505
- renderContextMenu,
355
+ renderContextMenu: finalRenderContextMenu,
356
+
357
+ adapter,
358
+ resolvedContextMenuActions,
359
+ getNodeById: (id: TreeItemId) => nodeById.get(id),
360
+ dnd,
506
361
  }),
507
362
  [
508
363
  state.expanded,
509
364
  state.selected,
365
+ state.anchor,
510
366
  state.focused,
511
367
  state.query,
368
+ state.renaming,
369
+ state.clipboard,
370
+ rename.enabled,
371
+ rename.startRename,
372
+ rename.cancelRename,
373
+ rename.commitRename,
512
374
  flatRows,
513
375
  matchingIds,
514
- expand,
515
- collapse,
516
- toggle,
517
- expandAll,
518
- collapseAll,
519
- select,
520
- setSelectedIds,
521
- clearSelection,
522
- setFocus,
376
+ expansion,
377
+ selection,
523
378
  setQuery,
379
+ clipboard,
524
380
  refresh,
525
381
  refreshAll,
526
382
  activate,
@@ -534,7 +390,11 @@ export function TreeProvider<T>(props: TreeProviderProps<T>) {
534
390
  renderIcon,
535
391
  renderLabel,
536
392
  renderActions,
537
- renderContextMenu,
393
+ finalRenderContextMenu,
394
+ adapter,
395
+ resolvedContextMenuActions,
396
+ nodeById,
397
+ dnd,
538
398
  ],
539
399
  );
540
400
 
@@ -546,4 +406,4 @@ export function TreeProvider<T>(props: TreeProviderProps<T>) {
546
406
  }
547
407
 
548
408
  // Re-export internal types referenced by hook consumers.
549
- export type { ChildCache, ChildEntry };
409
+ export type { ChildCache, ChildEntry } from '../data/childCache';