@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.
Files changed (67) hide show
  1. package/dist-lib/components/agents/ai-sidebar.d.ts +2 -0
  2. package/dist-lib/components/agents/index.d.ts +12 -0
  3. package/dist-lib/components/agents/marquee-label.d.ts +4 -0
  4. package/dist-lib/components/agents/sidebar-icon.d.ts +2 -0
  5. package/dist-lib/components/agents/sidebar-row.d.ts +2 -0
  6. package/dist-lib/components/agents/type.d.ts +77 -0
  7. package/dist-lib/components/agents/use-ai-sidebar.d.ts +31 -0
  8. package/dist-lib/components/agents/utils.d.ts +16 -0
  9. package/dist-lib/components/beste/piece/browser-frame.d.ts +2 -0
  10. package/dist-lib/components/beste/piece/type.d.ts +9 -0
  11. package/dist-lib/components/beste/piece/utils.d.ts +3 -0
  12. package/dist-lib/components/blocks/auth/linked-accounts-block.d.ts +7 -1
  13. package/dist-lib/components/blocks/auth/signup-block.d.ts +5 -5
  14. package/dist-lib/components/blocks/nav/sidebar-block.d.ts +3 -56
  15. package/dist-lib/components/blocks/nav/sidebar-nav-row.d.ts +3 -0
  16. package/dist-lib/components/blocks/nav/types.d.ts +60 -0
  17. package/dist-lib/components/blocks/nav/utils.d.ts +1 -0
  18. package/dist-lib/components/blocks/profile/profile-card-block.d.ts +6 -1
  19. package/dist-lib/components/blocks/shell/index.d.ts +1 -0
  20. package/dist-lib/components/blocks/shell/shell-layout.d.ts +3 -0
  21. package/dist-lib/components/blocks/shell/type.d.ts +26 -0
  22. package/dist-lib/components/blocks/shell/utils.d.ts +1 -0
  23. package/dist-lib/components/composed/social-providers.d.ts +9 -1
  24. package/dist-lib/components/docs/demo-controls.d.ts +4 -4
  25. package/dist-lib/components/motion/animated-sidebar.d.ts +12 -1
  26. package/dist-lib/components/motion/popover-morph.d.ts +35 -0
  27. package/dist-lib/components/motion/popover-position.d.ts +15 -0
  28. package/dist-lib/components/ui/badge.d.ts +7 -0
  29. package/dist-lib/components/ui/select.d.ts +30 -0
  30. package/dist-lib/index.d.ts +1 -0
  31. package/dist-lib/index.js +669 -549
  32. package/dist-lib/index.js.map +1 -1
  33. package/package.json +1 -1
  34. package/src/components/agents/ai-sidebar.tsx +116 -0
  35. package/src/components/agents/index.ts +14 -0
  36. package/src/components/agents/marquee-label.tsx +59 -0
  37. package/src/components/agents/sidebar-icon.tsx +14 -0
  38. package/src/components/agents/sidebar-row.tsx +271 -0
  39. package/src/components/agents/type.ts +89 -0
  40. package/src/components/agents/use-ai-sidebar.ts +386 -0
  41. package/src/components/agents/utils.ts +142 -0
  42. package/src/components/beste/piece/browser-frame.tsx +55 -0
  43. package/src/components/beste/piece/type.ts +11 -0
  44. package/src/components/beste/piece/utils.ts +21 -0
  45. package/src/components/blocks/auth/__tests__/linked-accounts-block.test.tsx +18 -0
  46. package/src/components/blocks/auth/linked-accounts-block.tsx +63 -57
  47. package/src/components/blocks/auth/passkey-manager-block.tsx +88 -81
  48. package/src/components/blocks/auth/signup-block.tsx +6 -6
  49. package/src/components/blocks/nav/sidebar-block.tsx +25 -155
  50. package/src/components/blocks/nav/sidebar-nav-row.tsx +79 -0
  51. package/src/components/blocks/nav/types.ts +65 -0
  52. package/src/components/blocks/nav/utils.ts +4 -0
  53. package/src/components/blocks/profile/profile-card-block.tsx +17 -6
  54. package/src/components/blocks/shell/index.ts +1 -0
  55. package/src/components/blocks/shell/shell-layout.tsx +115 -0
  56. package/src/components/blocks/shell/type.ts +50 -0
  57. package/src/components/blocks/shell/utils.ts +2 -0
  58. package/src/components/composed/social-providers.tsx +83 -27
  59. package/src/components/docs/component-preview.tsx +13 -12
  60. package/src/components/docs/demo-controls.tsx +37 -21
  61. package/src/components/motion/animated-sidebar.tsx +392 -364
  62. package/src/components/motion/popover-morph.tsx +292 -0
  63. package/src/components/motion/popover-position.ts +86 -0
  64. package/src/components/ui/badge.tsx +52 -0
  65. package/src/components/ui/select.tsx +257 -0
  66. package/src/index.css +18 -0
  67. 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,11 @@
1
+ import type { ReactNode } from "react"
2
+
3
+ export type TitleStyleType = "hidden" | "titlebar" | "expanded"
4
+
5
+ export interface IBrowserProps {
6
+ url?: string
7
+ title?: string
8
+ className?: string
9
+ children: ReactNode
10
+ titleStyle: TitleStyleType
11
+ }
@@ -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(