@exegia/corpora-ui 0.9.0 → 0.11.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/agents/ai-sidebar.d.ts +2 -0
- package/dist-lib/components/agents/index.d.ts +12 -0
- package/dist-lib/components/agents/marquee-label.d.ts +4 -0
- package/dist-lib/components/agents/sidebar-icon.d.ts +2 -0
- package/dist-lib/components/agents/sidebar-row.d.ts +2 -0
- package/dist-lib/components/agents/type.d.ts +77 -0
- package/dist-lib/components/agents/use-ai-sidebar.d.ts +31 -0
- package/dist-lib/components/agents/utils.d.ts +16 -0
- package/dist-lib/components/beste/piece/browser-frame.d.ts +2 -0
- package/dist-lib/components/beste/piece/type.d.ts +9 -0
- package/dist-lib/components/beste/piece/utils.d.ts +3 -0
- package/dist-lib/components/blocks/auth/linked-accounts-block.d.ts +7 -1
- package/dist-lib/components/blocks/auth/signup-block.d.ts +5 -5
- package/dist-lib/components/blocks/nav/sidebar-block.d.ts +3 -56
- package/dist-lib/components/blocks/nav/sidebar-nav-row.d.ts +3 -0
- package/dist-lib/components/blocks/nav/types.d.ts +60 -0
- package/dist-lib/components/blocks/nav/utils.d.ts +1 -0
- package/dist-lib/components/blocks/profile/profile-card-block.d.ts +6 -1
- package/dist-lib/components/blocks/shell/index.d.ts +1 -0
- package/dist-lib/components/blocks/shell/shell-layout.d.ts +3 -0
- package/dist-lib/components/blocks/shell/type.d.ts +26 -0
- package/dist-lib/components/blocks/shell/utils.d.ts +1 -0
- package/dist-lib/components/composed/social-providers.d.ts +9 -1
- package/dist-lib/components/docs/demo-controls.d.ts +4 -4
- package/dist-lib/components/motion/animated-sidebar.d.ts +12 -1
- package/dist-lib/components/motion/popover-morph.d.ts +35 -0
- package/dist-lib/components/motion/popover-position.d.ts +15 -0
- package/dist-lib/components/ui/badge.d.ts +7 -0
- package/dist-lib/components/ui/select.d.ts +30 -0
- package/dist-lib/index.d.ts +1 -0
- package/dist-lib/index.js +669 -549
- package/dist-lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/agents/ai-sidebar.tsx +116 -0
- package/src/components/agents/index.ts +14 -0
- package/src/components/agents/marquee-label.tsx +59 -0
- package/src/components/agents/sidebar-icon.tsx +14 -0
- package/src/components/agents/sidebar-row.tsx +271 -0
- package/src/components/agents/type.ts +89 -0
- package/src/components/agents/use-ai-sidebar.ts +386 -0
- package/src/components/agents/utils.ts +142 -0
- package/src/components/beste/piece/browser-frame.tsx +55 -0
- package/src/components/beste/piece/type.ts +11 -0
- package/src/components/beste/piece/utils.ts +21 -0
- package/src/components/blocks/auth/__tests__/linked-accounts-block.test.tsx +18 -0
- package/src/components/blocks/auth/linked-accounts-block.tsx +63 -57
- package/src/components/blocks/auth/passkey-manager-block.tsx +88 -81
- package/src/components/blocks/auth/signup-block.tsx +6 -6
- package/src/components/blocks/nav/sidebar-block.tsx +25 -155
- package/src/components/blocks/nav/sidebar-nav-row.tsx +79 -0
- package/src/components/blocks/nav/types.ts +65 -0
- package/src/components/blocks/nav/utils.ts +4 -0
- package/src/components/blocks/profile/profile-card-block.tsx +17 -6
- package/src/components/blocks/shell/index.ts +1 -0
- package/src/components/blocks/shell/shell-layout.tsx +115 -0
- package/src/components/blocks/shell/type.ts +50 -0
- package/src/components/blocks/shell/utils.ts +2 -0
- package/src/components/composed/social-providers.tsx +83 -27
- package/src/components/docs/component-preview.tsx +13 -12
- package/src/components/docs/demo-controls.tsx +37 -21
- package/src/components/motion/animated-sidebar.tsx +392 -364
- package/src/components/motion/popover-morph.tsx +292 -0
- package/src/components/motion/popover-position.ts +86 -0
- package/src/components/ui/badge.tsx +52 -0
- package/src/components/ui/select.tsx +257 -0
- package/src/index.css +18 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type DragEvent,
|
|
5
|
+
type KeyboardEvent,
|
|
6
|
+
useCallback,
|
|
7
|
+
useEffect,
|
|
8
|
+
useId,
|
|
9
|
+
useMemo,
|
|
10
|
+
useRef,
|
|
11
|
+
useState,
|
|
12
|
+
} from "react";
|
|
13
|
+
import type {
|
|
14
|
+
AISidebarProps,
|
|
15
|
+
DropTarget,
|
|
16
|
+
FlatResource,
|
|
17
|
+
SidebarResource,
|
|
18
|
+
SidebarResourceMove,
|
|
19
|
+
} from "@/components/agents/type.ts"
|
|
20
|
+
import {
|
|
21
|
+
canContain,
|
|
22
|
+
containsResource,
|
|
23
|
+
findResource,
|
|
24
|
+
flattenResources,
|
|
25
|
+
moveResource,
|
|
26
|
+
renameResource,
|
|
27
|
+
} from "@/components/agents/utils.ts"
|
|
28
|
+
|
|
29
|
+
export type UseAISidebarOptions = Pick<
|
|
30
|
+
AISidebarProps,
|
|
31
|
+
| "items"
|
|
32
|
+
| "defaultItems"
|
|
33
|
+
| "onItemsChange"
|
|
34
|
+
| "onMove"
|
|
35
|
+
| "onMoveError"
|
|
36
|
+
| "onRename"
|
|
37
|
+
| "activeId"
|
|
38
|
+
| "defaultActiveId"
|
|
39
|
+
| "onActiveChange"
|
|
40
|
+
| "defaultExpandedIds"
|
|
41
|
+
>
|
|
42
|
+
|
|
43
|
+
export function useAISidebar({
|
|
44
|
+
items,
|
|
45
|
+
defaultItems = [],
|
|
46
|
+
onItemsChange,
|
|
47
|
+
onMove,
|
|
48
|
+
onMoveError,
|
|
49
|
+
onRename,
|
|
50
|
+
activeId,
|
|
51
|
+
defaultActiveId = null,
|
|
52
|
+
onActiveChange,
|
|
53
|
+
defaultExpandedIds = [],
|
|
54
|
+
}: UseAISidebarOptions) {
|
|
55
|
+
const [internalItems, setInternalItems] = useState(items ?? defaultItems);
|
|
56
|
+
const [internalActiveId, setInternalActiveId] = useState(defaultActiveId);
|
|
57
|
+
const [expandedIds, setExpandedIds] = useState(
|
|
58
|
+
() => new Set(defaultExpandedIds),
|
|
59
|
+
);
|
|
60
|
+
const [focusedId, setFocusedId] = useState<string | null>(
|
|
61
|
+
activeId ?? defaultActiveId,
|
|
62
|
+
);
|
|
63
|
+
const [draggingId, setDraggingId] = useState<string | null>(null);
|
|
64
|
+
const [dropTarget, setDropTarget] = useState<DropTarget | null>(null);
|
|
65
|
+
const [menuOpenId, setMenuOpenId] = useState<string | null>(null);
|
|
66
|
+
const [renamingId, setRenamingId] = useState<string | null>(null);
|
|
67
|
+
const [hoveredId, setHoveredId] = useState<string | null>(null);
|
|
68
|
+
const hoverLayoutId = useId();
|
|
69
|
+
const [announcement, setAnnouncement] = useState("");
|
|
70
|
+
const rowRefs = useRef(new Map<string, HTMLDivElement>());
|
|
71
|
+
const movePendingRef = useRef(false);
|
|
72
|
+
const renderedItems = internalItems;
|
|
73
|
+
const selectedId = activeId ?? internalActiveId;
|
|
74
|
+
|
|
75
|
+
const [prevItems, setPrevItems] = useState(items);
|
|
76
|
+
if (items !== prevItems) {
|
|
77
|
+
setPrevItems(items);
|
|
78
|
+
if (items) setInternalItems(items);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const flat = useMemo(
|
|
82
|
+
() => flattenResources(renderedItems, expandedIds),
|
|
83
|
+
[expandedIds, renderedItems],
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const isFocusedItemStillVisible =
|
|
87
|
+
focusedId !== null && flat.some((row) => row.item.id === focusedId);
|
|
88
|
+
if (!isFocusedItemStillVisible) {
|
|
89
|
+
const firstVisibleItemId = flat[0]?.item.id ?? null;
|
|
90
|
+
if (focusedId !== firstVisibleItemId) setFocusedId(firstVisibleItemId);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (!menuOpenId) return;
|
|
95
|
+
const frame = requestAnimationFrame(() => {
|
|
96
|
+
const menus = Array.from(
|
|
97
|
+
document.querySelectorAll<HTMLElement>("[data-sidebar-resource-menu]"),
|
|
98
|
+
);
|
|
99
|
+
menus
|
|
100
|
+
.find((menu) => menu.dataset.sidebarResourceMenu === menuOpenId)
|
|
101
|
+
?.querySelector<HTMLElement>("button, a[href]")
|
|
102
|
+
?.focus();
|
|
103
|
+
});
|
|
104
|
+
return () => cancelAnimationFrame(frame);
|
|
105
|
+
}, [menuOpenId]);
|
|
106
|
+
|
|
107
|
+
const updateItems = useCallback(
|
|
108
|
+
(next: SidebarResource[]) => {
|
|
109
|
+
setInternalItems(next);
|
|
110
|
+
onItemsChange?.(next);
|
|
111
|
+
},
|
|
112
|
+
[onItemsChange],
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const performMove = useCallback(
|
|
116
|
+
async (move: SidebarResourceMove) => {
|
|
117
|
+
if (movePendingRef.current) {
|
|
118
|
+
setAnnouncement("Wait for the current move to finish.");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const before = renderedItems;
|
|
122
|
+
const next = moveResource(before, move);
|
|
123
|
+
if (!next || next === before) return;
|
|
124
|
+
|
|
125
|
+
movePendingRef.current = true;
|
|
126
|
+
updateItems(next);
|
|
127
|
+
setDropTarget(null);
|
|
128
|
+
setDraggingId(null);
|
|
129
|
+
const moved = findResource(before, move.itemId);
|
|
130
|
+
const target = move.targetId ? findResource(before, move.targetId) : null;
|
|
131
|
+
setAnnouncement(
|
|
132
|
+
target
|
|
133
|
+
? `Moved ${moved?.label ?? "item"} ${move.position} ${target.label}.`
|
|
134
|
+
: `Moved ${moved?.label ?? "item"} to the top level.`,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
await onMove?.(move);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
updateItems(before);
|
|
141
|
+
setAnnouncement(`Move failed. ${moved?.label ?? "Item"} was restored.`);
|
|
142
|
+
onMoveError?.(error, move);
|
|
143
|
+
} finally {
|
|
144
|
+
movePendingRef.current = false;
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
[onMove, onMoveError, renderedItems, updateItems],
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const focusRow = useCallback((id: string) => {
|
|
151
|
+
setFocusedId(id);
|
|
152
|
+
requestAnimationFrame(() => rowRefs.current.get(id)?.focus());
|
|
153
|
+
}, []);
|
|
154
|
+
|
|
155
|
+
const select = useCallback(
|
|
156
|
+
(id: string) => {
|
|
157
|
+
if (activeId === undefined) setInternalActiveId(id);
|
|
158
|
+
onActiveChange?.(id);
|
|
159
|
+
},
|
|
160
|
+
[activeId, onActiveChange],
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const toggle = useCallback((id: string) => {
|
|
164
|
+
setExpandedIds((current) => {
|
|
165
|
+
const next = new Set(current);
|
|
166
|
+
if (next.has(id)) next.delete(id);
|
|
167
|
+
else next.add(id);
|
|
168
|
+
return next;
|
|
169
|
+
});
|
|
170
|
+
}, []);
|
|
171
|
+
|
|
172
|
+
const handleKeyDown = useCallback(
|
|
173
|
+
(event: KeyboardEvent<HTMLDivElement>, row: FlatResource) => {
|
|
174
|
+
const index = flat.findIndex(({ item }) => item.id === row.item.id);
|
|
175
|
+
const previous = flat[index - 1];
|
|
176
|
+
const next = flat[index + 1];
|
|
177
|
+
const moveModifier = event.altKey && event.shiftKey;
|
|
178
|
+
|
|
179
|
+
if (event.key === "ArrowDown" && !moveModifier && next) {
|
|
180
|
+
event.preventDefault();
|
|
181
|
+
focusRow(next.item.id);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (event.key === "ArrowUp" && !moveModifier && previous) {
|
|
185
|
+
event.preventDefault();
|
|
186
|
+
focusRow(previous.item.id);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (event.key === "Home" && flat[0]) {
|
|
190
|
+
event.preventDefault();
|
|
191
|
+
focusRow(flat[0].item.id);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (event.key === "End" && flat.at(-1)) {
|
|
195
|
+
event.preventDefault();
|
|
196
|
+
focusRow(flat.at(-1)?.item.id ?? row.item.id);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (row.item.disabled) {
|
|
201
|
+
if (event.key === "ArrowLeft" && row.parentId) {
|
|
202
|
+
event.preventDefault();
|
|
203
|
+
focusRow(row.parentId);
|
|
204
|
+
} else if (
|
|
205
|
+
moveModifier ||
|
|
206
|
+
["ArrowRight", "Enter", " ", "F2", "ContextMenu"].includes(
|
|
207
|
+
event.key,
|
|
208
|
+
) ||
|
|
209
|
+
(event.shiftKey && event.key === "F10")
|
|
210
|
+
) {
|
|
211
|
+
event.preventDefault();
|
|
212
|
+
}
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (moveModifier && event.key === "ArrowUp" && previous) {
|
|
217
|
+
event.preventDefault();
|
|
218
|
+
void performMove({ itemId: row.item.id, targetId: previous.item.id, position: "before" });
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (moveModifier && event.key === "ArrowDown" && next) {
|
|
222
|
+
event.preventDefault();
|
|
223
|
+
void performMove({ itemId: row.item.id, targetId: next.item.id, position: "after" });
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (moveModifier && event.key === "ArrowRight" && previous && canContain(previous.item)) {
|
|
227
|
+
event.preventDefault();
|
|
228
|
+
setExpandedIds((current) => new Set(current).add(previous.item.id));
|
|
229
|
+
void performMove({ itemId: row.item.id, targetId: previous.item.id, position: "inside" });
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (moveModifier && event.key === "ArrowLeft" && row.parentId) {
|
|
233
|
+
event.preventDefault();
|
|
234
|
+
void performMove({ itemId: row.item.id, targetId: row.parentId, position: "after" });
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (event.key === "ArrowRight" && canContain(row.item)) {
|
|
239
|
+
event.preventDefault();
|
|
240
|
+
if (!expandedIds.has(row.item.id)) toggle(row.item.id);
|
|
241
|
+
else if (next?.parentId === row.item.id) focusRow(next.item.id);
|
|
242
|
+
} else if (event.key === "ArrowLeft") {
|
|
243
|
+
event.preventDefault();
|
|
244
|
+
if (expandedIds.has(row.item.id)) toggle(row.item.id);
|
|
245
|
+
else if (row.parentId) focusRow(row.parentId);
|
|
246
|
+
} else if (event.key === "Enter" || event.key === " ") {
|
|
247
|
+
event.preventDefault();
|
|
248
|
+
if (canContain(row.item)) toggle(row.item.id);
|
|
249
|
+
else select(row.item.id);
|
|
250
|
+
} else if (event.key === "F2") {
|
|
251
|
+
event.preventDefault();
|
|
252
|
+
setRenamingId(row.item.id);
|
|
253
|
+
} else if (event.key === "ContextMenu" || (event.shiftKey && event.key === "F10")) {
|
|
254
|
+
event.preventDefault();
|
|
255
|
+
setMenuOpenId(row.item.id);
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
[expandedIds, flat, focusRow, performMove, select, toggle],
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
const handleRootDragOver = useCallback(
|
|
262
|
+
(event: DragEvent<HTMLDivElement>) => {
|
|
263
|
+
if (!draggingId || event.target !== event.currentTarget) return;
|
|
264
|
+
event.preventDefault();
|
|
265
|
+
setDropTarget({ id: null, position: "after" });
|
|
266
|
+
},
|
|
267
|
+
[draggingId],
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
const handleDrop = useCallback(
|
|
271
|
+
(event: DragEvent<HTMLDivElement>) => {
|
|
272
|
+
event.preventDefault();
|
|
273
|
+
event.stopPropagation();
|
|
274
|
+
if (draggingId && dropTarget) {
|
|
275
|
+
void performMove({
|
|
276
|
+
itemId: draggingId,
|
|
277
|
+
targetId: dropTarget.id,
|
|
278
|
+
position: dropTarget.position,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
[draggingId, dropTarget, performMove],
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
const handleRowDragStart = useCallback(
|
|
286
|
+
(event: DragEvent<HTMLDivElement>, id: string) => {
|
|
287
|
+
setDraggingId(id);
|
|
288
|
+
event.dataTransfer.effectAllowed = "move";
|
|
289
|
+
event.dataTransfer.setData("text/plain", id);
|
|
290
|
+
},
|
|
291
|
+
[],
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
const handleRowDragEnd = useCallback(() => {
|
|
295
|
+
setDraggingId(null);
|
|
296
|
+
setDropTarget(null);
|
|
297
|
+
}, []);
|
|
298
|
+
|
|
299
|
+
const handleRowDragOver = useCallback(
|
|
300
|
+
(event: DragEvent<HTMLDivElement>, targetRow: FlatResource) => {
|
|
301
|
+
if (!draggingId || draggingId === targetRow.item.id) return;
|
|
302
|
+
const source = findResource(renderedItems, draggingId);
|
|
303
|
+
if (source && containsResource(source, targetRow.item.id)) return;
|
|
304
|
+
event.preventDefault();
|
|
305
|
+
event.stopPropagation();
|
|
306
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
307
|
+
const ratio = (event.clientY - rect.top) / rect.height;
|
|
308
|
+
const position =
|
|
309
|
+
!targetRow.item.disabled &&
|
|
310
|
+
canContain(targetRow.item) &&
|
|
311
|
+
ratio >= 0.25 &&
|
|
312
|
+
ratio <= 0.75
|
|
313
|
+
? "inside"
|
|
314
|
+
: ratio < 0.5
|
|
315
|
+
? "before"
|
|
316
|
+
: "after";
|
|
317
|
+
setDropTarget({ id: targetRow.item.id, position });
|
|
318
|
+
},
|
|
319
|
+
[draggingId, renderedItems],
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
const commitRename = useCallback(
|
|
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) => {
|
|
346
|
+
setHoveredId((current) =>
|
|
347
|
+
hovered ? id : current === id ? null : current,
|
|
348
|
+
);
|
|
349
|
+
}, []);
|
|
350
|
+
|
|
351
|
+
const clearHover = useCallback(() => setHoveredId(null), []);
|
|
352
|
+
|
|
353
|
+
const setRowRef = useCallback((id: string, node: HTMLDivElement | null) => {
|
|
354
|
+
if (node) rowRefs.current.set(id, node);
|
|
355
|
+
else rowRefs.current.delete(id);
|
|
356
|
+
}, []);
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
flat,
|
|
360
|
+
selectedId,
|
|
361
|
+
expandedIds,
|
|
362
|
+
focusedId,
|
|
363
|
+
setFocusedId,
|
|
364
|
+
draggingId,
|
|
365
|
+
dropTarget,
|
|
366
|
+
hoveredId,
|
|
367
|
+
hoverLayoutId,
|
|
368
|
+
handleRowHover,
|
|
369
|
+
clearHover,
|
|
370
|
+
menuOpenId,
|
|
371
|
+
renamingId,
|
|
372
|
+
setRenamingId,
|
|
373
|
+
announcement,
|
|
374
|
+
select,
|
|
375
|
+
toggle,
|
|
376
|
+
handleKeyDown,
|
|
377
|
+
handleRootDragOver,
|
|
378
|
+
handleDrop,
|
|
379
|
+
handleRowDragStart,
|
|
380
|
+
handleRowDragEnd,
|
|
381
|
+
handleRowDragOver,
|
|
382
|
+
commitRename,
|
|
383
|
+
handleMenuOpenChange,
|
|
384
|
+
setRowRef,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { EASE_OUT } from "@/lib/ease.ts"
|
|
2
|
+
import type { FlatResource, SidebarResource, SidebarResourceDropPosition, SidebarResourceMove } from "@/components/agents/type.ts"
|
|
3
|
+
|
|
4
|
+
export const ROW_REVEAL = {
|
|
5
|
+
duration: 0.16,
|
|
6
|
+
ease: EASE_OUT,
|
|
7
|
+
} as const
|
|
8
|
+
|
|
9
|
+
export function canContain(item: SidebarResource) {
|
|
10
|
+
return item.kind === "folder" || item.kind === "project"
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function flattenResources(
|
|
14
|
+
items: SidebarResource[],
|
|
15
|
+
expanded: Set<string>,
|
|
16
|
+
depth = 0,
|
|
17
|
+
parentId: string | null = null
|
|
18
|
+
): FlatResource[] {
|
|
19
|
+
return items.flatMap((item) => {
|
|
20
|
+
const row = { item, depth, parentId }
|
|
21
|
+
if (!item.children?.length || !expanded.has(item.id)) return [row]
|
|
22
|
+
return [
|
|
23
|
+
row,
|
|
24
|
+
...flattenResources(item.children, expanded, depth + 1, item.id),
|
|
25
|
+
]
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function findResource(
|
|
30
|
+
items: SidebarResource[],
|
|
31
|
+
id: string
|
|
32
|
+
): SidebarResource | undefined {
|
|
33
|
+
for (const item of items) {
|
|
34
|
+
if (item.id === id) return item
|
|
35
|
+
const child = item.children ? findResource(item.children, id) : undefined
|
|
36
|
+
if (child) return child
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
export function containsResource(item: SidebarResource, id: string): boolean {
|
|
42
|
+
return (
|
|
43
|
+
item.id === id ||
|
|
44
|
+
item.children?.some((child) => containsResource(child, id)) === true
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function removeResource(
|
|
49
|
+
items: SidebarResource[],
|
|
50
|
+
id: string,
|
|
51
|
+
): { items: SidebarResource[]; removed?: SidebarResource } {
|
|
52
|
+
let removed: SidebarResource | undefined;
|
|
53
|
+
const next: SidebarResource[] = [];
|
|
54
|
+
|
|
55
|
+
for (const item of items) {
|
|
56
|
+
if (item.id === id) {
|
|
57
|
+
removed = item;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (item.children?.length) {
|
|
62
|
+
const childResult = removeResource(item.children, id);
|
|
63
|
+
if (childResult.removed) {
|
|
64
|
+
removed = childResult.removed;
|
|
65
|
+
next.push({ ...item, children: childResult.items });
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
next.push(item);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { items: next, removed };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function insertResource(
|
|
77
|
+
items: SidebarResource[],
|
|
78
|
+
resource: SidebarResource,
|
|
79
|
+
targetId: string | null,
|
|
80
|
+
position: SidebarResourceDropPosition,
|
|
81
|
+
): SidebarResource[] {
|
|
82
|
+
if (targetId === null) return [...items, resource];
|
|
83
|
+
|
|
84
|
+
const next: SidebarResource[] = [];
|
|
85
|
+
for (const item of items) {
|
|
86
|
+
if (item.id === targetId) {
|
|
87
|
+
if (position === "before") next.push(resource, item);
|
|
88
|
+
else if (position === "after") next.push(item, resource);
|
|
89
|
+
else next.push({ ...item, children: [...(item.children ?? []), resource] });
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (item.children?.length) {
|
|
94
|
+
next.push({
|
|
95
|
+
...item,
|
|
96
|
+
children: insertResource(item.children, resource, targetId, position),
|
|
97
|
+
});
|
|
98
|
+
} else {
|
|
99
|
+
next.push(item);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return next;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function moveResource(
|
|
106
|
+
items: SidebarResource[],
|
|
107
|
+
move: SidebarResourceMove,
|
|
108
|
+
): SidebarResource[] | null {
|
|
109
|
+
const source = findResource(items, move.itemId);
|
|
110
|
+
if (!source || source.disabled) return null;
|
|
111
|
+
if (move.targetId && containsResource(source, move.targetId)) return null;
|
|
112
|
+
|
|
113
|
+
const target = move.targetId ? findResource(items, move.targetId) : undefined;
|
|
114
|
+
if (
|
|
115
|
+
move.position === "inside" &&
|
|
116
|
+
(!target || target.disabled || !canContain(target))
|
|
117
|
+
)
|
|
118
|
+
return null;
|
|
119
|
+
|
|
120
|
+
const removed = removeResource(items, move.itemId);
|
|
121
|
+
if (!removed.removed) return null;
|
|
122
|
+
return insertResource(
|
|
123
|
+
removed.items,
|
|
124
|
+
removed.removed,
|
|
125
|
+
move.targetId,
|
|
126
|
+
move.position,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function renameResource(
|
|
131
|
+
items: SidebarResource[],
|
|
132
|
+
id: string,
|
|
133
|
+
label: string,
|
|
134
|
+
): SidebarResource[] {
|
|
135
|
+
return items.map((item) => ({
|
|
136
|
+
...item,
|
|
137
|
+
label: item.id === id ? label : item.label,
|
|
138
|
+
children: item.children
|
|
139
|
+
? renameResource(item.children, id, label)
|
|
140
|
+
: undefined,
|
|
141
|
+
}));
|
|
142
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Globe } from "lucide-react"
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
import type { IBrowserProps } from "./type"
|
|
6
|
+
import { titleBarStyle } from "./utils"
|
|
7
|
+
|
|
8
|
+
export function BrowserFrame({
|
|
9
|
+
url,
|
|
10
|
+
title,
|
|
11
|
+
className,
|
|
12
|
+
titleStyle,
|
|
13
|
+
children,
|
|
14
|
+
}: IBrowserProps) {
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div
|
|
20
|
+
className={cn(
|
|
21
|
+
"relative flex size-full items-center justify-center",
|
|
22
|
+
className
|
|
23
|
+
)}
|
|
24
|
+
>
|
|
25
|
+
<div className="flex min-h-96 w-full flex-col overflow-hidden rounded-lg border border-border bg-neutral-50 shadow-lg dark:bg-neutral-900 relative">
|
|
26
|
+
<div id="title-bar" className={cn("flex flex-1 w-full items-center gap-2 px-4 py-3 z-20", titleBarStyle(titleStyle, 'title'))}>
|
|
27
|
+
<div className="flex items-center gap-2" aria-hidden="true">
|
|
28
|
+
<span className="size-3 cursor-pointer rounded-full bg-rose-500 shadow-md outline outline-rose-800" />
|
|
29
|
+
<span className="size-3 cursor-pointer rounded-full bg-amber-500 shadow-md outline outline-amber-800" />
|
|
30
|
+
<span className="size-3 cursor-pointer rounded-full bg-emerald-500 shadow-md outline outline-emerald-800" />
|
|
31
|
+
</div>
|
|
32
|
+
{url && !title && (
|
|
33
|
+
<div className="flex max-w-1/3 flex-1 items-center gap-1.5 rounded-sm border border-border bg-card p-1">
|
|
34
|
+
<Globe
|
|
35
|
+
className="size-3 shrink-0 text-muted-foreground"
|
|
36
|
+
aria-hidden="true"
|
|
37
|
+
/>
|
|
38
|
+
<span className="truncate font-mono text-xs text-muted-foreground">
|
|
39
|
+
{url}
|
|
40
|
+
</span>
|
|
41
|
+
</div>
|
|
42
|
+
)}
|
|
43
|
+
{title && !url && (
|
|
44
|
+
<div className="flex w-full flex-1 justify-center select-none">
|
|
45
|
+
<span className="text-sm text-foreground ml-2">{title}</span>
|
|
46
|
+
</div>
|
|
47
|
+
)}
|
|
48
|
+
</div>
|
|
49
|
+
<div className={cn("flex flex-1 ", titleBarStyle(titleStyle, "frame"))}>
|
|
50
|
+
{children}
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ClassNameValue } from "tailwind-merge"
|
|
2
|
+
import type { TitleStyleType } from "./type"
|
|
3
|
+
|
|
4
|
+
export const titleBarStyle = (
|
|
5
|
+
style: TitleStyleType,
|
|
6
|
+
component: "title" | "frame"
|
|
7
|
+
): ClassNameValue => {
|
|
8
|
+
const titleBarClassName: Record<TitleStyleType, ClassNameValue> = {
|
|
9
|
+
titlebar: "",
|
|
10
|
+
hidden: "top-0 left-0 absolute",
|
|
11
|
+
expanded: "",
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const bodyClassName: Record<TitleStyleType, ClassNameValue> = {
|
|
15
|
+
titlebar: "",
|
|
16
|
+
hidden: "",
|
|
17
|
+
expanded: "",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return component == "title" ? titleBarClassName[style] : bodyClassName[style]
|
|
21
|
+
}
|
|
@@ -89,6 +89,24 @@ describe("LinkedAccountsBlock", () => {
|
|
|
89
89
|
expect(onUnlink).not.toHaveBeenCalled();
|
|
90
90
|
});
|
|
91
91
|
|
|
92
|
+
test("lifts the guard when another sign-in method exists off-list", async () => {
|
|
93
|
+
const user = userEvent.setup();
|
|
94
|
+
const onUnlink = mock(async () => {});
|
|
95
|
+
render(
|
|
96
|
+
<LinkedAccountsBlock
|
|
97
|
+
identities={[IDENTITIES[0]!]}
|
|
98
|
+
hasOtherSignInMethods
|
|
99
|
+
onUnlink={onUnlink}
|
|
100
|
+
/>,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const button = screen.getByRole("button", { name: "Disconnect Google" });
|
|
104
|
+
expect(button.hasAttribute("disabled")).toBe(false);
|
|
105
|
+
|
|
106
|
+
await user.click(button);
|
|
107
|
+
expect(onUnlink).toHaveBeenCalledWith("id-1");
|
|
108
|
+
});
|
|
109
|
+
|
|
92
110
|
test("surfaces a failing link as an error", async () => {
|
|
93
111
|
const user = userEvent.setup();
|
|
94
112
|
render(
|