@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.
- package/dist/audio-player/index.cjs +2099 -0
- package/dist/audio-player/index.cjs.map +1 -0
- package/dist/audio-player/index.css +65 -0
- package/dist/audio-player/index.css.map +1 -0
- package/dist/audio-player/index.d.cts +174 -0
- package/dist/audio-player/index.d.ts +174 -0
- package/dist/audio-player/index.mjs +2076 -0
- package/dist/audio-player/index.mjs.map +1 -0
- package/dist/composer-registry/index.cjs +45 -0
- package/dist/composer-registry/index.cjs.map +1 -0
- package/dist/composer-registry/index.d.cts +73 -0
- package/dist/composer-registry/index.d.ts +73 -0
- package/dist/composer-registry/index.mjs +39 -0
- package/dist/composer-registry/index.mjs.map +1 -0
- package/dist/file-icon/index.d.cts +1 -1
- package/dist/file-icon/index.d.ts +1 -1
- package/dist/slots-ClRpIzoh.d.cts +88 -0
- package/dist/slots-ClRpIzoh.d.ts +88 -0
- package/dist/tree/index.cjs +2019 -279
- package/dist/tree/index.cjs.map +1 -1
- package/dist/tree/index.d.cts +731 -72
- package/dist/tree/index.d.ts +731 -72
- package/dist/tree/index.mjs +2009 -282
- package/dist/tree/index.mjs.map +1 -1
- package/package.json +18 -9
- package/src/tools/chat/README.md +111 -1
- package/src/tools/chat/composer/Composer.tsx +146 -25
- package/src/tools/chat/composer/ComposerRichTextarea.tsx +25 -0
- package/src/tools/chat/composer/index.ts +22 -0
- package/src/tools/chat/composer/slash/README.md +187 -0
- package/src/tools/chat/composer/slash/SlashHighlightTextarea.tsx +144 -0
- package/src/tools/chat/composer/slash/SlashMenu.tsx +142 -0
- package/src/tools/chat/composer/slash/SlashToken.tsx +57 -0
- package/src/tools/chat/composer/slash/index.ts +44 -0
- package/src/tools/chat/composer/slash/labels.ts +19 -0
- package/src/tools/chat/composer/slash/state.ts +168 -0
- package/src/tools/chat/composer/slash/types.ts +64 -0
- package/src/tools/chat/composer/slash/useSlashCommands.ts +204 -0
- package/src/tools/chat/composer/types.ts +8 -0
- package/src/tools/chat/context/ChatProvider.tsx +13 -78
- package/src/tools/chat/hooks/useAutoFocusOnStreamEnd.ts +12 -15
- package/src/tools/chat/hooks/useFocusOnEmptyClick.ts +4 -5
- package/src/tools/chat/launcher/header/ChatHeader.tsx +14 -19
- package/src/tools/chat/launcher/header/ChatHeaderActionButton.tsx +8 -12
- package/src/tools/chat/shell/SuggestedPrompts.tsx +194 -0
- package/src/tools/chat/shell/index.ts +6 -0
- package/src/tools/data/Listbox/lazy.tsx +1 -1
- package/src/tools/data/Masonry/lazy.tsx +1 -1
- package/src/tools/data/Timeline/lazy.tsx +1 -1
- package/src/tools/data/Tree/FinderTree.tsx +42 -0
- package/src/tools/data/Tree/README.md +337 -208
- package/src/tools/data/Tree/TreeDndProvider.tsx +137 -0
- package/src/tools/data/Tree/TreeRoot.tsx +111 -72
- package/src/tools/data/Tree/__tests__/dnd.test.ts +160 -0
- package/src/tools/data/Tree/__tests__/keyboard.test.ts +137 -0
- package/src/tools/data/Tree/__tests__/renameUtils.test.ts +52 -0
- package/src/tools/data/Tree/__tests__/selection.test.ts +227 -0
- package/src/tools/data/Tree/components/TreeDropIndicator.tsx +65 -0
- package/src/tools/data/Tree/components/TreeEmptyArea.tsx +160 -0
- package/src/tools/data/Tree/components/TreeRenameInput.tsx +114 -0
- package/src/tools/data/Tree/components/TreeRow.tsx +103 -8
- package/src/tools/data/Tree/components/index.ts +6 -0
- package/src/tools/data/Tree/context/TreeContext.tsx +223 -363
- package/src/tools/data/Tree/context/TreeContextValue.ts +139 -0
- package/src/tools/data/Tree/context/async-children/collect-ids.ts +27 -0
- package/src/tools/data/Tree/context/async-children/index.ts +8 -0
- package/src/tools/data/Tree/context/async-children/use-async-children.ts +157 -0
- package/src/tools/data/Tree/context/clipboard/index.ts +4 -0
- package/src/tools/data/Tree/context/clipboard/use-clipboard.ts +115 -0
- package/src/tools/data/Tree/context/dnd/index.ts +8 -0
- package/src/tools/data/Tree/context/dnd/use-dnd.ts +194 -0
- package/src/tools/data/Tree/context/expansion/index.ts +4 -0
- package/src/tools/data/Tree/context/expansion/use-expansion.ts +55 -0
- package/src/tools/data/Tree/context/hooks.ts +68 -1
- package/src/tools/data/Tree/context/index.ts +3 -0
- package/src/tools/data/Tree/context/menu/builtin-actions.ts +357 -0
- package/src/tools/data/Tree/context/menu/index.ts +11 -0
- package/src/tools/data/Tree/context/menu/render.tsx +75 -0
- package/src/tools/data/Tree/context/menu/use-resolved-menu.ts +141 -0
- package/src/tools/data/Tree/context/persist/index.ts +4 -0
- package/src/tools/data/Tree/context/persist/use-persist-sync.ts +74 -0
- package/src/tools/data/Tree/context/rename/index.ts +4 -0
- package/src/tools/data/Tree/context/rename/use-rename.ts +113 -0
- package/src/tools/data/Tree/context/selection/index.ts +4 -0
- package/src/tools/data/Tree/context/selection/use-selection.ts +146 -0
- package/src/tools/data/Tree/context/state/index.ts +6 -0
- package/src/tools/data/Tree/context/state/initial.ts +41 -0
- package/src/tools/data/Tree/context/state/reducer.ts +76 -0
- package/src/tools/data/Tree/context/state/types.ts +46 -0
- package/src/tools/data/Tree/data/clipboard.ts +33 -0
- package/src/tools/data/Tree/data/dnd.ts +123 -0
- package/src/tools/data/Tree/data/finderShortcuts.ts +67 -0
- package/src/tools/data/Tree/data/index.ts +19 -0
- package/src/tools/data/Tree/data/renameUtils.ts +51 -0
- package/src/tools/data/Tree/data/selection.ts +157 -0
- package/src/tools/data/Tree/hooks/finder-hotkeys/build-ctx.ts +48 -0
- package/src/tools/data/Tree/hooks/finder-hotkeys/index.ts +8 -0
- package/src/tools/data/Tree/hooks/finder-hotkeys/use-tree-finder-hotkeys.ts +166 -0
- package/src/tools/data/Tree/hooks/index.ts +23 -4
- package/src/tools/data/Tree/hooks/keyboard/activation.ts +27 -0
- package/src/tools/data/Tree/hooks/keyboard/arrow-nav.ts +26 -0
- package/src/tools/data/Tree/hooks/keyboard/expand-collapse.ts +54 -0
- package/src/tools/data/Tree/hooks/keyboard/index.ts +10 -0
- package/src/tools/data/Tree/hooks/keyboard/types.ts +39 -0
- package/src/tools/data/Tree/hooks/keyboard/use-tree-keyboard.ts +196 -0
- package/src/tools/data/Tree/hooks/type-ahead/index.ts +5 -0
- package/src/tools/data/Tree/hooks/type-ahead/match-prefix.ts +42 -0
- package/src/tools/data/Tree/hooks/{useTreeTypeAhead.ts → type-ahead/use-tree-type-ahead.ts} +8 -19
- package/src/tools/data/Tree/index.tsx +26 -2
- package/src/tools/data/Tree/types/activation.ts +30 -0
- package/src/tools/data/Tree/types/adapter.ts +70 -0
- package/src/tools/data/Tree/types/index.ts +27 -0
- package/src/tools/data/Tree/types/labels.ts +97 -0
- package/src/tools/data/Tree/types/loader.ts +9 -0
- package/src/tools/data/Tree/types/node.ts +38 -0
- package/src/tools/data/Tree/types/root-props.ts +158 -0
- package/src/tools/data/Tree/types/selection.ts +3 -0
- package/src/tools/data/Tree/types/slots.ts +64 -0
- package/src/tools/dev/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/MetaActions.tsx +6 -9
- package/src/tools/dev/OpenapiViewer/components/DocsLayout/index.tsx +2 -4
- package/src/tools/forms/MarkdownEditor/MarkdownEditor.tsx +85 -0
- package/src/tools/forms/MarkdownEditor/index.ts +1 -0
- package/src/tools/forms/MarkdownEditor/lazy.tsx +6 -0
- package/src/tools/forms/MarkdownEditor/slash/SlashCommandNode.ts +162 -0
- package/src/tools/forms/MarkdownEditor/slash/index.ts +4 -0
- package/src/tools/forms/MarkdownEditor/slash/syncSlashNode.ts +97 -0
- package/src/tools/forms/MarkdownEditor/slash/types.ts +13 -0
- package/src/tools/forms/MarkdownEditor/styles.css +18 -0
- package/src/tools/input/SpeechRecognition/widgets/VoiceComposerSlot.tsx +11 -12
- package/src/tools/integration/ComposerRegistry/index.ts +105 -0
- package/src/tools/media/AudioPlayer/Player.tsx +2 -0
- package/src/tools/media/AudioPlayer/PlayerShell.tsx +37 -22
- package/src/tools/media/AudioPlayer/lazy.tsx +30 -42
- package/src/tools/media/AudioPlayer/parts/Controls/IconButton.tsx +10 -11
- package/src/tools/media/AudioPlayer/parts/Controls/VolumeControl.tsx +52 -115
- package/src/tools/media/AudioPlayer/types.ts +15 -0
- package/dist/types-j2vhn4Kv.d.cts +0 -241
- package/dist/types-j2vhn4Kv.d.ts +0 -241
- package/src/tools/data/Tree/hooks/useTreeKeyboard.ts +0 -171
- 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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
type
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
374
|
-
|
|
375
|
-
const
|
|
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
|
-
|
|
382
|
-
|
|
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
|
|
430
|
-
(
|
|
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
|
-
|
|
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
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
-
|
|
515
|
-
|
|
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
|
-
|
|
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';
|