@exegia/corpora-ui 0.19.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-lib/components/blocks/nav/sidebar/__tests__/use-ai-sidebar.test.d.ts +1 -0
- package/dist-lib/components/blocks/nav/sidebar/ai-sidebar.d.ts +11 -2
- package/dist-lib/components/blocks/nav/sidebar/type.d.ts +83 -1
- package/dist-lib/components/blocks/nav/sidebar/use-ai-sidebar.d.ts +22 -31
- package/dist-lib/components/blocks/nav/sidebar/utils.d.ts +5 -0
- package/dist-lib/components/blocks/scaffold/constants.d.ts +5 -0
- package/dist-lib/components/blocks/scaffold/scaffold-canvas.d.ts +2 -0
- package/dist-lib/components/blocks/scaffold/scaffold-panel.d.ts +1 -1
- package/dist-lib/components/blocks/scaffold/scaffold-tab.d.ts +6 -1
- package/dist-lib/components/blocks/scaffold/type.d.ts +28 -0
- package/dist-lib/components/blocks/scaffold/use-panel-visibility.d.ts +19 -0
- package/dist-lib/components/blocks/scaffold/utils.d.ts +6 -0
- package/dist-lib/components/composed/tree/__tests__/tree.test.d.ts +1 -0
- package/dist-lib/components/composed/tree/__tests__/use-tree.test.d.ts +1 -0
- package/dist-lib/components/composed/tree/constants.d.ts +35 -0
- package/dist-lib/components/composed/tree/index.d.ts +4 -0
- package/dist-lib/components/composed/tree/tree-context.d.ts +4 -0
- package/dist-lib/components/composed/tree/tree-node.d.ts +7 -0
- package/dist-lib/components/composed/tree/tree.d.ts +19 -0
- package/dist-lib/components/composed/tree/type.d.ts +192 -0
- package/dist-lib/components/composed/tree/use-tree-dnd.d.ts +7 -0
- package/dist-lib/components/composed/tree/use-tree.d.ts +20 -0
- package/dist-lib/components/composed/tree/utils.d.ts +28 -0
- package/dist-lib/components/user-avatar/audio-wave.d.ts +2 -0
- package/dist-lib/components/user-avatar/component.d.ts +10 -0
- package/dist-lib/components/user-avatar/fallback.d.ts +5 -0
- package/dist-lib/components/user-avatar/index.d.ts +13 -0
- package/dist-lib/components/user-avatar/type.d.ts +26 -0
- package/dist-lib/components/user-avatar/utils.d.ts +2 -0
- package/dist-lib/index.d.ts +1 -0
- package/dist-lib/index.js +2141 -1139
- package/dist-lib/index.js.map +1 -1
- package/package.json +2 -1
- package/src/components/beste/piece/browser-frame.tsx +9 -6
- package/src/components/blocks/nav/sidebar/__tests__/use-ai-sidebar.test.tsx +310 -0
- package/src/components/blocks/nav/sidebar/ai-sidebar.tsx +90 -60
- package/src/components/blocks/nav/sidebar/type.ts +105 -1
- package/src/components/blocks/nav/sidebar/use-ai-sidebar.ts +293 -116
- package/src/components/blocks/nav/sidebar/utils.ts +28 -0
- package/src/components/blocks/scaffold/__tests__/scaffold.test.tsx +160 -1
- package/src/components/blocks/scaffold/constants.ts +7 -0
- package/src/components/blocks/scaffold/panel-menu-button.tsx +3 -0
- package/src/components/blocks/scaffold/scaffold-canvas.tsx +48 -5
- package/src/components/blocks/scaffold/scaffold-inspector.tsx +3 -2
- package/src/components/blocks/scaffold/scaffold-main.tsx +1 -1
- package/src/components/blocks/scaffold/scaffold-panel.tsx +41 -5
- package/src/components/blocks/scaffold/scaffold-root.tsx +18 -3
- package/src/components/blocks/scaffold/scaffold-sidebar.tsx +1 -1
- package/src/components/blocks/scaffold/scaffold-tab.tsx +45 -7
- package/src/components/blocks/scaffold/type.ts +28 -0
- package/src/components/blocks/scaffold/use-panel-visibility.ts +180 -0
- package/src/components/blocks/scaffold/utils.ts +19 -0
- package/src/components/composed/tree/__tests__/tree.test.tsx +400 -0
- package/src/components/composed/tree/__tests__/use-tree.test.tsx +332 -0
- package/src/components/composed/tree/constants.ts +60 -0
- package/src/components/composed/tree/index.ts +13 -0
- package/src/components/composed/tree/tree-context.ts +13 -0
- package/src/components/composed/tree/tree-node.tsx +451 -0
- package/src/components/composed/tree/tree.tsx +278 -0
- package/src/components/composed/tree/type.ts +212 -0
- package/src/components/composed/tree/use-tree-dnd.ts +148 -0
- package/src/components/composed/tree/use-tree.ts +306 -0
- package/src/components/composed/tree/utils.ts +172 -0
- package/src/components/composed/user-avatar.tsx +4 -1
- package/src/components/docs/component-preview.tsx +11 -11
- package/src/components/user-avatar/audio-wave.tsx +22 -0
- package/src/components/user-avatar/component.tsx +53 -0
- package/src/components/user-avatar/fallback.tsx +26 -0
- package/src/components/user-avatar/index.ts +15 -0
- package/src/components/user-avatar/type.ts +35 -0
- package/src/components/user-avatar/utils.ts +8 -0
- package/src/index.css +211 -153
- package/src/index.ts +1 -0
|
@@ -11,51 +11,63 @@ import {
|
|
|
11
11
|
useState,
|
|
12
12
|
} from "react";
|
|
13
13
|
import type {
|
|
14
|
-
|
|
14
|
+
AISidebarController,
|
|
15
15
|
DropTarget,
|
|
16
16
|
FlatResource,
|
|
17
17
|
SidebarResource,
|
|
18
18
|
SidebarResourceMove,
|
|
19
|
+
UseAISidebarOptions,
|
|
19
20
|
} from "./type.ts"
|
|
20
21
|
import {
|
|
22
|
+
ancestorIdsOf,
|
|
21
23
|
canContain,
|
|
22
24
|
containsResource,
|
|
25
|
+
expandableIdsOf,
|
|
23
26
|
findResource,
|
|
24
27
|
flattenResources,
|
|
25
28
|
moveResource,
|
|
26
29
|
renameResource,
|
|
27
30
|
} from "./utils.ts"
|
|
28
31
|
|
|
29
|
-
export type UseAISidebarOptions
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
32
|
+
export type { UseAISidebarOptions }
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The AI sidebar's headless controller — every behaviour the block
|
|
36
|
+
* performs, callable from outside it: select, expand/collapse (one row,
|
|
37
|
+
* every row, or just the ancestors of one), move focus, rename, open a
|
|
38
|
+
* row's action menu, reorder.
|
|
39
|
+
*
|
|
40
|
+
* `items`, `activeId` and `expandedIds` are each controlled when passed
|
|
41
|
+
* and hook-owned via their `default*` twin otherwise. Reorder and rename
|
|
42
|
+
* are applied to the data optimistically either way and rolled back if
|
|
43
|
+
* `onMove`/`onRename` rejects, so a consumer never reduces the tree
|
|
44
|
+
* itself — `moveResource` and `renameResource` are exported for the ones
|
|
45
|
+
* who keep their data elsewhere.
|
|
46
|
+
*
|
|
47
|
+
* ```tsx
|
|
48
|
+
* const sidebar = useAISidebar({ defaultItems: resources })
|
|
49
|
+
* <AISidebar controller={sidebar} />
|
|
50
|
+
* <Button onClick={sidebar.collapseAll}>Collapse all</Button>
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export function useAISidebar(options: UseAISidebarOptions): AISidebarController {
|
|
54
|
+
const {
|
|
55
|
+
items,
|
|
56
|
+
defaultItems = [],
|
|
57
|
+
onItemsChange,
|
|
58
|
+
onMove,
|
|
59
|
+
onMoveError,
|
|
60
|
+
onRename,
|
|
61
|
+
activeId,
|
|
62
|
+
defaultActiveId = null,
|
|
63
|
+
onActiveChange,
|
|
64
|
+
onExpandedChange,
|
|
65
|
+
} = options
|
|
66
|
+
|
|
55
67
|
const [internalItems, setInternalItems] = useState(items ?? defaultItems);
|
|
56
68
|
const [internalActiveId, setInternalActiveId] = useState(defaultActiveId);
|
|
57
|
-
const [
|
|
58
|
-
() => new Set(defaultExpandedIds),
|
|
69
|
+
const [ownExpandedIds, setOwnExpandedIds] = useState(
|
|
70
|
+
() => new Set(options.defaultExpandedIds ?? []),
|
|
59
71
|
);
|
|
60
72
|
const [focusedId, setFocusedId] = useState<string | null>(
|
|
61
73
|
activeId ?? defaultActiveId,
|
|
@@ -67,11 +79,20 @@ export function useAISidebar({
|
|
|
67
79
|
const [hoveredId, setHoveredId] = useState<string | null>(null);
|
|
68
80
|
const hoverLayoutId = useId();
|
|
69
81
|
const [announcement, setAnnouncement] = useState("");
|
|
82
|
+
// `movePending` is mirrored in a ref because the guard has to read it
|
|
83
|
+
// synchronously, before React has committed the state change.
|
|
84
|
+
const [movePending, setMovePending] = useState(false);
|
|
70
85
|
const rowRefs = useRef(new Map<string, HTMLDivElement>());
|
|
71
86
|
const movePendingRef = useRef(false);
|
|
72
87
|
const renderedItems = internalItems;
|
|
73
88
|
const selectedId = activeId ?? internalActiveId;
|
|
74
89
|
|
|
90
|
+
const controlledExpandedIds = options.expandedIds
|
|
91
|
+
const expandedIds = useMemo(
|
|
92
|
+
() => (controlledExpandedIds ? new Set(controlledExpandedIds) : ownExpandedIds),
|
|
93
|
+
[controlledExpandedIds, ownExpandedIds],
|
|
94
|
+
)
|
|
95
|
+
|
|
75
96
|
const [prevItems, setPrevItems] = useState(items);
|
|
76
97
|
if (items !== prevItems) {
|
|
77
98
|
setPrevItems(items);
|
|
@@ -112,7 +133,70 @@ export function useAISidebar({
|
|
|
112
133
|
[onItemsChange],
|
|
113
134
|
);
|
|
114
135
|
|
|
115
|
-
const
|
|
136
|
+
const commitExpanded = useCallback(
|
|
137
|
+
(next: Set<string>) => {
|
|
138
|
+
if (controlledExpandedIds === undefined) setOwnExpandedIds(next);
|
|
139
|
+
onExpandedChange?.([...next]);
|
|
140
|
+
},
|
|
141
|
+
[controlledExpandedIds, onExpandedChange],
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const getItem = useCallback(
|
|
145
|
+
(id: string) => findResource(renderedItems, id),
|
|
146
|
+
[renderedItems],
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const isExpanded = useCallback(
|
|
150
|
+
(id: string) => expandedIds.has(id),
|
|
151
|
+
[expandedIds],
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const expand = useCallback(
|
|
155
|
+
(id: string) => {
|
|
156
|
+
if (expandedIds.has(id)) return;
|
|
157
|
+
commitExpanded(new Set(expandedIds).add(id));
|
|
158
|
+
},
|
|
159
|
+
[commitExpanded, expandedIds],
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const collapse = useCallback(
|
|
163
|
+
(id: string) => {
|
|
164
|
+
if (!expandedIds.has(id)) return;
|
|
165
|
+
const next = new Set(expandedIds);
|
|
166
|
+
next.delete(id);
|
|
167
|
+
commitExpanded(next);
|
|
168
|
+
},
|
|
169
|
+
[commitExpanded, expandedIds],
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
const toggleExpanded = useCallback(
|
|
173
|
+
(id: string) => {
|
|
174
|
+
const next = new Set(expandedIds);
|
|
175
|
+
if (next.has(id)) next.delete(id);
|
|
176
|
+
else next.add(id);
|
|
177
|
+
commitExpanded(next);
|
|
178
|
+
},
|
|
179
|
+
[commitExpanded, expandedIds],
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
const expandAll = useCallback(() => {
|
|
183
|
+
commitExpanded(new Set(expandableIdsOf(renderedItems)));
|
|
184
|
+
}, [commitExpanded, renderedItems]);
|
|
185
|
+
|
|
186
|
+
const collapseAll = useCallback(() => {
|
|
187
|
+
commitExpanded(new Set());
|
|
188
|
+
}, [commitExpanded]);
|
|
189
|
+
|
|
190
|
+
const reveal = useCallback(
|
|
191
|
+
(id: string) => {
|
|
192
|
+
const ancestors = ancestorIdsOf(renderedItems, id);
|
|
193
|
+
if (!ancestors.some((ancestor) => !expandedIds.has(ancestor))) return;
|
|
194
|
+
commitExpanded(new Set([...expandedIds, ...ancestors]));
|
|
195
|
+
},
|
|
196
|
+
[commitExpanded, expandedIds, renderedItems],
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
const move = useCallback(
|
|
116
200
|
async (move: SidebarResourceMove) => {
|
|
117
201
|
if (movePendingRef.current) {
|
|
118
202
|
setAnnouncement("Wait for the current move to finish.");
|
|
@@ -123,6 +207,7 @@ export function useAISidebar({
|
|
|
123
207
|
if (!next || next === before) return;
|
|
124
208
|
|
|
125
209
|
movePendingRef.current = true;
|
|
210
|
+
setMovePending(true);
|
|
126
211
|
updateItems(next);
|
|
127
212
|
setDropTarget(null);
|
|
128
213
|
setDraggingId(null);
|
|
@@ -142,12 +227,13 @@ export function useAISidebar({
|
|
|
142
227
|
onMoveError?.(error, move);
|
|
143
228
|
} finally {
|
|
144
229
|
movePendingRef.current = false;
|
|
230
|
+
setMovePending(false);
|
|
145
231
|
}
|
|
146
232
|
},
|
|
147
233
|
[onMove, onMoveError, renderedItems, updateItems],
|
|
148
234
|
);
|
|
149
235
|
|
|
150
|
-
const
|
|
236
|
+
const focus = useCallback((id: string) => {
|
|
151
237
|
setFocusedId(id);
|
|
152
238
|
requestAnimationFrame(() => rowRefs.current.get(id)?.focus());
|
|
153
239
|
}, []);
|
|
@@ -160,16 +246,52 @@ export function useAISidebar({
|
|
|
160
246
|
[activeId, onActiveChange],
|
|
161
247
|
);
|
|
162
248
|
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
249
|
+
const startRename = useCallback(
|
|
250
|
+
(id: string) => {
|
|
251
|
+
// An id from elsewhere would arm rename mode against a row that
|
|
252
|
+
// never renders.
|
|
253
|
+
if (!findResource(renderedItems, id)) return;
|
|
254
|
+
setRenamingId(id);
|
|
255
|
+
},
|
|
256
|
+
[renderedItems],
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
const cancelRename = useCallback(() => setRenamingId(null), []);
|
|
260
|
+
|
|
261
|
+
const rename = useCallback(
|
|
262
|
+
(id: string, label: string) => {
|
|
263
|
+
const trimmed = label.trim();
|
|
264
|
+
setRenamingId(null);
|
|
265
|
+
const item = findResource(renderedItems, id);
|
|
266
|
+
if (!item || !trimmed || trimmed === item.label) return;
|
|
267
|
+
const before = renderedItems;
|
|
268
|
+
updateItems(renameResource(before, id, trimmed));
|
|
269
|
+
void Promise.resolve(onRename?.(item, trimmed)).catch(() => {
|
|
270
|
+
updateItems(before);
|
|
271
|
+
setAnnouncement(`Rename failed. ${item.label} was restored.`);
|
|
272
|
+
});
|
|
273
|
+
},
|
|
274
|
+
[onRename, renderedItems, updateItems],
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
const openMenu = useCallback(
|
|
278
|
+
(id: string) => {
|
|
279
|
+
if (!findResource(renderedItems, id)) return;
|
|
280
|
+
setMenuOpenId(id);
|
|
281
|
+
},
|
|
282
|
+
[renderedItems],
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
// Closing returns focus to the row that owned the menu — otherwise focus
|
|
286
|
+
// falls back to the body and the keyboard user loses their place.
|
|
287
|
+
const closeMenu = useCallback(() => {
|
|
288
|
+
setMenuOpenId((current) => {
|
|
289
|
+
if (current) focus(current);
|
|
290
|
+
return null;
|
|
169
291
|
});
|
|
170
|
-
}, []);
|
|
292
|
+
}, [focus]);
|
|
171
293
|
|
|
172
|
-
const
|
|
294
|
+
const onRowKeyDown = useCallback(
|
|
173
295
|
(event: KeyboardEvent<HTMLDivElement>, row: FlatResource) => {
|
|
174
296
|
const index = flat.findIndex(({ item }) => item.id === row.item.id);
|
|
175
297
|
const previous = flat[index - 1];
|
|
@@ -178,29 +300,29 @@ export function useAISidebar({
|
|
|
178
300
|
|
|
179
301
|
if (event.key === "ArrowDown" && !moveModifier && next) {
|
|
180
302
|
event.preventDefault();
|
|
181
|
-
|
|
303
|
+
focus(next.item.id);
|
|
182
304
|
return;
|
|
183
305
|
}
|
|
184
306
|
if (event.key === "ArrowUp" && !moveModifier && previous) {
|
|
185
307
|
event.preventDefault();
|
|
186
|
-
|
|
308
|
+
focus(previous.item.id);
|
|
187
309
|
return;
|
|
188
310
|
}
|
|
189
311
|
if (event.key === "Home" && flat[0]) {
|
|
190
312
|
event.preventDefault();
|
|
191
|
-
|
|
313
|
+
focus(flat[0].item.id);
|
|
192
314
|
return;
|
|
193
315
|
}
|
|
194
316
|
if (event.key === "End" && flat.at(-1)) {
|
|
195
317
|
event.preventDefault();
|
|
196
|
-
|
|
318
|
+
focus(flat.at(-1)?.item.id ?? row.item.id);
|
|
197
319
|
return;
|
|
198
320
|
}
|
|
199
321
|
|
|
200
322
|
if (row.item.disabled) {
|
|
201
323
|
if (event.key === "ArrowLeft" && row.parentId) {
|
|
202
324
|
event.preventDefault();
|
|
203
|
-
|
|
325
|
+
focus(row.parentId);
|
|
204
326
|
} else if (
|
|
205
327
|
moveModifier ||
|
|
206
328
|
["ArrowRight", "Enter", " ", "F2", "ContextMenu"].includes(
|
|
@@ -215,50 +337,61 @@ export function useAISidebar({
|
|
|
215
337
|
|
|
216
338
|
if (moveModifier && event.key === "ArrowUp" && previous) {
|
|
217
339
|
event.preventDefault();
|
|
218
|
-
void
|
|
340
|
+
void move({ itemId: row.item.id, targetId: previous.item.id, position: "before" });
|
|
219
341
|
return;
|
|
220
342
|
}
|
|
221
343
|
if (moveModifier && event.key === "ArrowDown" && next) {
|
|
222
344
|
event.preventDefault();
|
|
223
|
-
void
|
|
345
|
+
void move({ itemId: row.item.id, targetId: next.item.id, position: "after" });
|
|
224
346
|
return;
|
|
225
347
|
}
|
|
226
348
|
if (moveModifier && event.key === "ArrowRight" && previous && canContain(previous.item)) {
|
|
227
349
|
event.preventDefault();
|
|
228
|
-
|
|
229
|
-
void
|
|
350
|
+
expand(previous.item.id);
|
|
351
|
+
void move({ itemId: row.item.id, targetId: previous.item.id, position: "inside" });
|
|
230
352
|
return;
|
|
231
353
|
}
|
|
232
354
|
if (moveModifier && event.key === "ArrowLeft" && row.parentId) {
|
|
233
355
|
event.preventDefault();
|
|
234
|
-
void
|
|
356
|
+
void move({ itemId: row.item.id, targetId: row.parentId, position: "after" });
|
|
235
357
|
return;
|
|
236
358
|
}
|
|
237
359
|
|
|
238
360
|
if (event.key === "ArrowRight" && canContain(row.item)) {
|
|
239
361
|
event.preventDefault();
|
|
240
|
-
if (!expandedIds.has(row.item.id))
|
|
241
|
-
else if (next?.parentId === row.item.id)
|
|
362
|
+
if (!expandedIds.has(row.item.id)) expand(row.item.id);
|
|
363
|
+
else if (next?.parentId === row.item.id) focus(next.item.id);
|
|
242
364
|
} else if (event.key === "ArrowLeft") {
|
|
243
365
|
event.preventDefault();
|
|
244
|
-
if (expandedIds.has(row.item.id))
|
|
245
|
-
else if (row.parentId)
|
|
366
|
+
if (expandedIds.has(row.item.id)) collapse(row.item.id);
|
|
367
|
+
else if (row.parentId) focus(row.parentId);
|
|
246
368
|
} else if (event.key === "Enter" || event.key === " ") {
|
|
247
369
|
event.preventDefault();
|
|
248
|
-
if (canContain(row.item))
|
|
370
|
+
if (canContain(row.item)) toggleExpanded(row.item.id);
|
|
249
371
|
else select(row.item.id);
|
|
250
372
|
} else if (event.key === "F2") {
|
|
251
373
|
event.preventDefault();
|
|
252
|
-
|
|
374
|
+
startRename(row.item.id);
|
|
253
375
|
} else if (event.key === "ContextMenu" || (event.shiftKey && event.key === "F10")) {
|
|
254
376
|
event.preventDefault();
|
|
255
|
-
|
|
377
|
+
openMenu(row.item.id);
|
|
256
378
|
}
|
|
257
379
|
},
|
|
258
|
-
[
|
|
380
|
+
[
|
|
381
|
+
collapse,
|
|
382
|
+
expand,
|
|
383
|
+
expandedIds,
|
|
384
|
+
flat,
|
|
385
|
+
focus,
|
|
386
|
+
move,
|
|
387
|
+
openMenu,
|
|
388
|
+
select,
|
|
389
|
+
startRename,
|
|
390
|
+
toggleExpanded,
|
|
391
|
+
],
|
|
259
392
|
);
|
|
260
393
|
|
|
261
|
-
const
|
|
394
|
+
const onRootDragOver = useCallback(
|
|
262
395
|
(event: DragEvent<HTMLDivElement>) => {
|
|
263
396
|
if (!draggingId || event.target !== event.currentTarget) return;
|
|
264
397
|
event.preventDefault();
|
|
@@ -267,22 +400,22 @@ export function useAISidebar({
|
|
|
267
400
|
[draggingId],
|
|
268
401
|
);
|
|
269
402
|
|
|
270
|
-
const
|
|
403
|
+
const onDrop = useCallback(
|
|
271
404
|
(event: DragEvent<HTMLDivElement>) => {
|
|
272
405
|
event.preventDefault();
|
|
273
406
|
event.stopPropagation();
|
|
274
407
|
if (draggingId && dropTarget) {
|
|
275
|
-
void
|
|
408
|
+
void move({
|
|
276
409
|
itemId: draggingId,
|
|
277
410
|
targetId: dropTarget.id,
|
|
278
411
|
position: dropTarget.position,
|
|
279
412
|
});
|
|
280
413
|
}
|
|
281
414
|
},
|
|
282
|
-
[draggingId, dropTarget,
|
|
415
|
+
[draggingId, dropTarget, move],
|
|
283
416
|
);
|
|
284
417
|
|
|
285
|
-
const
|
|
418
|
+
const onRowDragStart = useCallback(
|
|
286
419
|
(event: DragEvent<HTMLDivElement>, id: string) => {
|
|
287
420
|
setDraggingId(id);
|
|
288
421
|
event.dataTransfer.effectAllowed = "move";
|
|
@@ -291,12 +424,12 @@ export function useAISidebar({
|
|
|
291
424
|
[],
|
|
292
425
|
);
|
|
293
426
|
|
|
294
|
-
const
|
|
427
|
+
const onRowDragEnd = useCallback(() => {
|
|
295
428
|
setDraggingId(null);
|
|
296
429
|
setDropTarget(null);
|
|
297
430
|
}, []);
|
|
298
431
|
|
|
299
|
-
const
|
|
432
|
+
const onRowDragOver = useCallback(
|
|
300
433
|
(event: DragEvent<HTMLDivElement>, targetRow: FlatResource) => {
|
|
301
434
|
if (!draggingId || draggingId === targetRow.item.id) return;
|
|
302
435
|
const source = findResource(renderedItems, draggingId);
|
|
@@ -319,30 +452,7 @@ export function useAISidebar({
|
|
|
319
452
|
[draggingId, renderedItems],
|
|
320
453
|
);
|
|
321
454
|
|
|
322
|
-
const
|
|
323
|
-
(row: FlatResource, label: string) => {
|
|
324
|
-
const trimmed = label.trim();
|
|
325
|
-
setRenamingId(null);
|
|
326
|
-
if (!trimmed || trimmed === row.item.label) return;
|
|
327
|
-
const before = renderedItems;
|
|
328
|
-
updateItems(renameResource(before, row.item.id, trimmed));
|
|
329
|
-
void Promise.resolve(onRename?.(row.item, trimmed)).catch(() => {
|
|
330
|
-
updateItems(before);
|
|
331
|
-
setAnnouncement(`Rename failed. ${row.item.label} was restored.`);
|
|
332
|
-
});
|
|
333
|
-
},
|
|
334
|
-
[onRename, renderedItems, updateItems],
|
|
335
|
-
);
|
|
336
|
-
|
|
337
|
-
const handleMenuOpenChange = useCallback(
|
|
338
|
-
(row: FlatResource, open: boolean) => {
|
|
339
|
-
setMenuOpenId(open ? row.item.id : null);
|
|
340
|
-
if (!open) focusRow(row.item.id);
|
|
341
|
-
},
|
|
342
|
-
[focusRow],
|
|
343
|
-
);
|
|
344
|
-
|
|
345
|
-
const handleRowHover = useCallback((id: string, hovered: boolean) => {
|
|
455
|
+
const onRowHover = useCallback((id: string, hovered: boolean) => {
|
|
346
456
|
setHoveredId((current) =>
|
|
347
457
|
hovered ? id : current === id ? null : current,
|
|
348
458
|
);
|
|
@@ -355,32 +465,99 @@ export function useAISidebar({
|
|
|
355
465
|
else rowRefs.current.delete(id);
|
|
356
466
|
}, []);
|
|
357
467
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
468
|
+
const dnd = useMemo(
|
|
469
|
+
() => ({
|
|
470
|
+
draggingId,
|
|
471
|
+
dropTarget,
|
|
472
|
+
onRootDragOver,
|
|
473
|
+
onDrop,
|
|
474
|
+
onRowDragStart,
|
|
475
|
+
onRowDragEnd,
|
|
476
|
+
onRowDragOver,
|
|
477
|
+
}),
|
|
478
|
+
[
|
|
479
|
+
draggingId,
|
|
480
|
+
dropTarget,
|
|
481
|
+
onRootDragOver,
|
|
482
|
+
onDrop,
|
|
483
|
+
onRowDragStart,
|
|
484
|
+
onRowDragEnd,
|
|
485
|
+
onRowDragOver,
|
|
486
|
+
],
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
const hover = useMemo(
|
|
490
|
+
() => ({
|
|
491
|
+
hoveredId,
|
|
492
|
+
layoutId: hoverLayoutId,
|
|
493
|
+
onRowHover,
|
|
494
|
+
clear: clearHover,
|
|
495
|
+
}),
|
|
496
|
+
[clearHover, hoverLayoutId, hoveredId, onRowHover],
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
return useMemo<AISidebarController>(
|
|
500
|
+
() => ({
|
|
501
|
+
items: renderedItems,
|
|
502
|
+
flat,
|
|
503
|
+
getItem,
|
|
504
|
+
selectedId,
|
|
505
|
+
select,
|
|
506
|
+
expandedIds,
|
|
507
|
+
isExpanded,
|
|
508
|
+
expand,
|
|
509
|
+
collapse,
|
|
510
|
+
toggleExpanded,
|
|
511
|
+
expandAll,
|
|
512
|
+
collapseAll,
|
|
513
|
+
reveal,
|
|
514
|
+
focusedId,
|
|
515
|
+
focus,
|
|
516
|
+
renamingId,
|
|
517
|
+
startRename,
|
|
518
|
+
cancelRename,
|
|
519
|
+
rename,
|
|
520
|
+
menuOpenId,
|
|
521
|
+
openMenu,
|
|
522
|
+
closeMenu,
|
|
523
|
+
move,
|
|
524
|
+
movePending,
|
|
525
|
+
announcement,
|
|
526
|
+
dnd,
|
|
527
|
+
hover,
|
|
528
|
+
onRowKeyDown,
|
|
529
|
+
setRowRef,
|
|
530
|
+
}),
|
|
531
|
+
[
|
|
532
|
+
renderedItems,
|
|
533
|
+
flat,
|
|
534
|
+
getItem,
|
|
535
|
+
selectedId,
|
|
536
|
+
select,
|
|
537
|
+
expandedIds,
|
|
538
|
+
isExpanded,
|
|
539
|
+
expand,
|
|
540
|
+
collapse,
|
|
541
|
+
toggleExpanded,
|
|
542
|
+
expandAll,
|
|
543
|
+
collapseAll,
|
|
544
|
+
reveal,
|
|
545
|
+
focusedId,
|
|
546
|
+
focus,
|
|
547
|
+
renamingId,
|
|
548
|
+
startRename,
|
|
549
|
+
cancelRename,
|
|
550
|
+
rename,
|
|
551
|
+
menuOpenId,
|
|
552
|
+
openMenu,
|
|
553
|
+
closeMenu,
|
|
554
|
+
move,
|
|
555
|
+
movePending,
|
|
556
|
+
announcement,
|
|
557
|
+
dnd,
|
|
558
|
+
hover,
|
|
559
|
+
onRowKeyDown,
|
|
560
|
+
setRowRef,
|
|
561
|
+
],
|
|
562
|
+
);
|
|
386
563
|
}
|
|
@@ -38,6 +38,34 @@ export function findResource(
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
|
|
41
|
+
/** Ids of every ancestor of `id` (nearest last); empty when absent. */
|
|
42
|
+
export function ancestorIdsOf(
|
|
43
|
+
items: SidebarResource[],
|
|
44
|
+
id: string
|
|
45
|
+
): string[] {
|
|
46
|
+
const walk = (nodes: SidebarResource[], trail: string[]): string[] | null => {
|
|
47
|
+
for (const node of nodes) {
|
|
48
|
+
if (node.id === id) return trail
|
|
49
|
+
if (node.children) {
|
|
50
|
+
const found = walk(node.children, [...trail, node.id])
|
|
51
|
+
if (found) return found
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return null
|
|
55
|
+
}
|
|
56
|
+
return walk(items, []) ?? []
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Ids of every row that can hold children and has some — the set
|
|
60
|
+
* `expandAll` opens. */
|
|
61
|
+
export function expandableIdsOf(items: SidebarResource[]): string[] {
|
|
62
|
+
return items.flatMap((item) =>
|
|
63
|
+
item.children?.length && canContain(item)
|
|
64
|
+
? [item.id, ...expandableIdsOf(item.children)]
|
|
65
|
+
: []
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
41
69
|
export function containsResource(item: SidebarResource, id: string): boolean {
|
|
42
70
|
return (
|
|
43
71
|
item.id === id ||
|