@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.
Files changed (73) hide show
  1. package/dist-lib/components/blocks/nav/sidebar/__tests__/use-ai-sidebar.test.d.ts +1 -0
  2. package/dist-lib/components/blocks/nav/sidebar/ai-sidebar.d.ts +11 -2
  3. package/dist-lib/components/blocks/nav/sidebar/type.d.ts +83 -1
  4. package/dist-lib/components/blocks/nav/sidebar/use-ai-sidebar.d.ts +22 -31
  5. package/dist-lib/components/blocks/nav/sidebar/utils.d.ts +5 -0
  6. package/dist-lib/components/blocks/scaffold/constants.d.ts +5 -0
  7. package/dist-lib/components/blocks/scaffold/scaffold-canvas.d.ts +2 -0
  8. package/dist-lib/components/blocks/scaffold/scaffold-panel.d.ts +1 -1
  9. package/dist-lib/components/blocks/scaffold/scaffold-tab.d.ts +6 -1
  10. package/dist-lib/components/blocks/scaffold/type.d.ts +28 -0
  11. package/dist-lib/components/blocks/scaffold/use-panel-visibility.d.ts +19 -0
  12. package/dist-lib/components/blocks/scaffold/utils.d.ts +6 -0
  13. package/dist-lib/components/composed/tree/__tests__/tree.test.d.ts +1 -0
  14. package/dist-lib/components/composed/tree/__tests__/use-tree.test.d.ts +1 -0
  15. package/dist-lib/components/composed/tree/constants.d.ts +35 -0
  16. package/dist-lib/components/composed/tree/index.d.ts +4 -0
  17. package/dist-lib/components/composed/tree/tree-context.d.ts +4 -0
  18. package/dist-lib/components/composed/tree/tree-node.d.ts +7 -0
  19. package/dist-lib/components/composed/tree/tree.d.ts +19 -0
  20. package/dist-lib/components/composed/tree/type.d.ts +192 -0
  21. package/dist-lib/components/composed/tree/use-tree-dnd.d.ts +7 -0
  22. package/dist-lib/components/composed/tree/use-tree.d.ts +20 -0
  23. package/dist-lib/components/composed/tree/utils.d.ts +28 -0
  24. package/dist-lib/components/user-avatar/audio-wave.d.ts +2 -0
  25. package/dist-lib/components/user-avatar/component.d.ts +10 -0
  26. package/dist-lib/components/user-avatar/fallback.d.ts +5 -0
  27. package/dist-lib/components/user-avatar/index.d.ts +13 -0
  28. package/dist-lib/components/user-avatar/type.d.ts +26 -0
  29. package/dist-lib/components/user-avatar/utils.d.ts +2 -0
  30. package/dist-lib/index.d.ts +1 -0
  31. package/dist-lib/index.js +2141 -1139
  32. package/dist-lib/index.js.map +1 -1
  33. package/package.json +2 -1
  34. package/src/components/beste/piece/browser-frame.tsx +9 -6
  35. package/src/components/blocks/nav/sidebar/__tests__/use-ai-sidebar.test.tsx +310 -0
  36. package/src/components/blocks/nav/sidebar/ai-sidebar.tsx +90 -60
  37. package/src/components/blocks/nav/sidebar/type.ts +105 -1
  38. package/src/components/blocks/nav/sidebar/use-ai-sidebar.ts +293 -116
  39. package/src/components/blocks/nav/sidebar/utils.ts +28 -0
  40. package/src/components/blocks/scaffold/__tests__/scaffold.test.tsx +160 -1
  41. package/src/components/blocks/scaffold/constants.ts +7 -0
  42. package/src/components/blocks/scaffold/panel-menu-button.tsx +3 -0
  43. package/src/components/blocks/scaffold/scaffold-canvas.tsx +48 -5
  44. package/src/components/blocks/scaffold/scaffold-inspector.tsx +3 -2
  45. package/src/components/blocks/scaffold/scaffold-main.tsx +1 -1
  46. package/src/components/blocks/scaffold/scaffold-panel.tsx +41 -5
  47. package/src/components/blocks/scaffold/scaffold-root.tsx +18 -3
  48. package/src/components/blocks/scaffold/scaffold-sidebar.tsx +1 -1
  49. package/src/components/blocks/scaffold/scaffold-tab.tsx +45 -7
  50. package/src/components/blocks/scaffold/type.ts +28 -0
  51. package/src/components/blocks/scaffold/use-panel-visibility.ts +180 -0
  52. package/src/components/blocks/scaffold/utils.ts +19 -0
  53. package/src/components/composed/tree/__tests__/tree.test.tsx +400 -0
  54. package/src/components/composed/tree/__tests__/use-tree.test.tsx +332 -0
  55. package/src/components/composed/tree/constants.ts +60 -0
  56. package/src/components/composed/tree/index.ts +13 -0
  57. package/src/components/composed/tree/tree-context.ts +13 -0
  58. package/src/components/composed/tree/tree-node.tsx +451 -0
  59. package/src/components/composed/tree/tree.tsx +278 -0
  60. package/src/components/composed/tree/type.ts +212 -0
  61. package/src/components/composed/tree/use-tree-dnd.ts +148 -0
  62. package/src/components/composed/tree/use-tree.ts +306 -0
  63. package/src/components/composed/tree/utils.ts +172 -0
  64. package/src/components/composed/user-avatar.tsx +4 -1
  65. package/src/components/docs/component-preview.tsx +11 -11
  66. package/src/components/user-avatar/audio-wave.tsx +22 -0
  67. package/src/components/user-avatar/component.tsx +53 -0
  68. package/src/components/user-avatar/fallback.tsx +26 -0
  69. package/src/components/user-avatar/index.ts +15 -0
  70. package/src/components/user-avatar/type.ts +35 -0
  71. package/src/components/user-avatar/utils.ts +8 -0
  72. package/src/index.css +211 -153
  73. package/src/index.ts +1 -0
@@ -0,0 +1,451 @@
1
+ "use client"
2
+
3
+ import { LucideChevronLeft, LucideChevronRight } from "lucide-react"
4
+ import { AnimatePresence, motion, useReducedMotion } from "motion/react"
5
+ import * as React from "react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+ import {
9
+ Tooltip,
10
+ TooltipContent,
11
+ TooltipTrigger,
12
+ } from "@/components/ui/tooltip"
13
+ import {
14
+ TREE_BRANCH_EXIT,
15
+ TREE_BRANCH_VARIANTS,
16
+ TREE_COLLAPSE_DURATION,
17
+ TREE_LABEL_REVEAL_DELAY,
18
+ TREE_MICRO_TRANSITION,
19
+ TREE_ROW_VARIANTS,
20
+ } from "./constants"
21
+ import { useTreeContext } from "./tree-context"
22
+ import type { TreeNode } from "./type"
23
+ import { EASE_OUT } from "@/lib/ease.ts"
24
+
25
+ /** How a row behaves, resolved from variant + depth + shape. */
26
+ type RowKind =
27
+ | "section" // navigation depth 0 with 3-level data — heading, toggles
28
+ | "toggle" // expands/collapses on row press (nav parents, files folders)
29
+ | "link" // navigates; may still carry a chevron that toggles separately
30
+ export interface TreeRowProps {
31
+ node: TreeNode
32
+ depth: number
33
+ }
34
+
35
+ function rowKindOf(
36
+ node: TreeNode,
37
+ depth: number,
38
+ variant: string,
39
+ sectioned: boolean
40
+ ): RowKind {
41
+ if (variant === "sidebar") return "link"
42
+ const branch =
43
+ variant === "files" ? node.children !== undefined : !!node.children?.length
44
+ if (!branch) return "link"
45
+ if (variant === "navigation")
46
+ return sectioned && depth === 0 ? "section" : "toggle"
47
+ // toc parents navigate — their chevron is a separate toggle.
48
+ return variant === "toc" ? "link" : "toggle"
49
+ }
50
+
51
+ export function TreeRow({ node, depth }: TreeRowProps): React.ReactElement {
52
+ const { tree: ctx, renderTrailing } = useTreeContext()
53
+ const reduce = useReducedMotion()
54
+ const kind = rowKindOf(node, depth, ctx.variant, ctx.sectioned)
55
+
56
+ // The sidebar rail is single-level by contract — nested children are
57
+ // ignored rather than rendered somewhere misleading.
58
+ const children = ctx.variant === "sidebar" ? [] : (node.children ?? [])
59
+ const canExpand =
60
+ kind !== "link" || (ctx.variant === "toc" && children.length > 0)
61
+ const open = ctx.isExpanded(node.id)
62
+ const active = ctx.activeId === node.id
63
+ const renaming = ctx.renamingId === node.id
64
+
65
+ const { dnd } = ctx
66
+ const dragging = dnd.draggedId === node.id
67
+ const drop = dnd.dropTarget?.id === node.id ? dnd.dropTarget.position : null
68
+
69
+ // toc rows below the route level scroll — they anchor to the heading
70
+ // that shares their id (an explicit href always wins).
71
+ const href =
72
+ kind === "link" && !node.disabled
73
+ ? (node.href ??
74
+ (ctx.variant === "toc" && depth > 0 ? `#${node.id}` : undefined))
75
+ : undefined
76
+
77
+ const files = ctx.variant === "files"
78
+ const collapsed = ctx.collapsed
79
+
80
+ function handlePress(event: React.MouseEvent) {
81
+ if (node.disabled) {
82
+ event.preventDefault()
83
+ return
84
+ }
85
+ if (kind === "link") {
86
+ ctx.select(node.id)
87
+ return
88
+ }
89
+ ctx.toggleExpanded(node.id)
90
+ }
91
+
92
+ function startRename(event: React.MouseEvent) {
93
+ if (!ctx.canRename || node.disabled) return
94
+ event.preventDefault()
95
+ ctx.startRename(node.id)
96
+ }
97
+
98
+ const label = renaming ? (
99
+ <input
100
+ autoFocus
101
+ className={cn(
102
+ "h-5 min-w-0 flex-1 rounded-sm border border-input bg-background px-1",
103
+ "text-[length:inherit] text-foreground outline-none focus-visible:ring-1 focus-visible:ring-ring"
104
+ )}
105
+ data-slot="tree-rename-input"
106
+ defaultValue={node.label}
107
+ onBlur={(event) => ctx.rename(node.id, event.currentTarget.value)}
108
+ onClick={(event) => event.stopPropagation()}
109
+ onKeyDown={(event) => {
110
+ event.stopPropagation()
111
+ if (event.key === "Enter") ctx.rename(node.id, event.currentTarget.value)
112
+ if (event.key === "Escape") ctx.cancelRename()
113
+ }}
114
+ />
115
+ ) : ctx.variant === "sidebar" ? (
116
+ // The rail label folds away with the rail instead of unmounting rows.
117
+ <AnimatePresence initial={false} mode="wait">
118
+ <motion.span
119
+ initial={{ width: 0, opacity: 0, x: -8 }}
120
+ animate={{
121
+ width: collapsed ? 0 : "auto",
122
+ opacity: collapsed ? 0 : 1,
123
+ x: collapsed ? -8 : 0,
124
+ display: collapsed ? "none" : "flex",
125
+ transition: reduce
126
+ ? { duration: 0 }
127
+ : {
128
+ width: {
129
+ delay: TREE_LABEL_REVEAL_DELAY,
130
+ duration: TREE_COLLAPSE_DURATION,
131
+ ease: "easeInOut",
132
+ },
133
+ opacity: {
134
+ delay: TREE_LABEL_REVEAL_DELAY,
135
+ duration: TREE_COLLAPSE_DURATION,
136
+ ease: "easeInOut",
137
+ },
138
+ x: {
139
+ duration: TREE_COLLAPSE_DURATION,
140
+ ease: "easeInOut",
141
+ },
142
+ },
143
+ }}
144
+ exit={{
145
+ width: 0,
146
+ opacity: 0,
147
+ display: "none",
148
+ x: -8,
149
+ transition: reduce
150
+ ? { duration: 0 }
151
+ : {
152
+ display: {
153
+ duration: TREE_COLLAPSE_DURATION,
154
+ ease: "easeOut",
155
+ },
156
+ width: {
157
+ duration: TREE_COLLAPSE_DURATION * 3,
158
+ ease: EASE_OUT,
159
+ },
160
+ opacity: {
161
+ duration: TREE_COLLAPSE_DURATION,
162
+ ease: "easeOut",
163
+ },
164
+ x: {
165
+ duration: TREE_COLLAPSE_DURATION * 3,
166
+ ease: EASE_OUT,
167
+ },
168
+ },
169
+ }}
170
+ className={cn("min-w-0 overflow-clip whitespace-nowrap")}
171
+ data-slot="tree-row-label"
172
+ >
173
+ {node.label}
174
+ </motion.span>
175
+ </AnimatePresence>
176
+ ) : (
177
+ <span
178
+ className="min-w-0 flex-1 truncate"
179
+ onDoubleClick={ctx.canRename ? startRename : undefined}
180
+ >
181
+ {node.label}
182
+ </span>
183
+ )
184
+
185
+ const chevron = canExpand ? (
186
+ <motion.span
187
+ animate={{ rotate: open ? (ctx.variant === "navigation" ? -90 : 90) : 0 }}
188
+ aria-hidden
189
+ className="flex size-4 shrink-0 items-center justify-center text-muted-foreground/70"
190
+ transition={reduce ? { duration: 0 } : TREE_MICRO_TRANSITION}
191
+ >
192
+ {ctx.variant === "sidebar" && (
193
+ <LucideChevronLeft className="size-3.5 stroke-3" />
194
+ )}
195
+ {ctx.variant === "toc" && (
196
+ <LucideChevronRight className="size-3.5 stroke-3" />
197
+ )}
198
+ {ctx.variant === "navigation" && (
199
+ <LucideChevronLeft className="size-3.5 stroke-3" />
200
+ )}
201
+ </motion.span>
202
+ ) : null
203
+
204
+ // The trailing slot is consumer-rendered and usually holds buttons, so it
205
+ // cannot live inside the row — a files folder row is itself a <button>, and
206
+ // a leaf row an <a>; either one nesting a button is invalid HTML. It sits
207
+ // beside the row instead, overlaid like the toc toggle.
208
+ const showTrailing = files && !!renderTrailing && !renaming
209
+
210
+ const rowClassName = cn(
211
+ "group/row relative flex w-full min-w-0 items-center gap-2 rounded-md px-2",
212
+ "text-left text-muted-foreground transition-colors duration-150 outline-none",
213
+ "hover:bg-accent hover:text-accent-foreground",
214
+ "focus-visible:ring-2 focus-visible:ring-ring",
215
+ files ? "h-7 text-[0.8125rem]" : "h-8 text-sm",
216
+ node.disabled && "pointer-events-none opacity-50",
217
+ active && "bg-accent font-medium text-accent-foreground",
218
+ dragging && "opacity-50",
219
+ drop === "inside" && "bg-accent/60 ring-1 ring-primary/50 ring-inset",
220
+ ctx.variant === "sidebar" && collapsed && "justify-center px-0",
221
+ // Room for the overlay expand toggle on toc parent rows.
222
+ ctx.variant === "toc" && kind === "link" && children.length > 0 && "pr-7",
223
+ // …and for the overlay trailing actions on files rows. Sized for one
224
+ // icon button plus its inset; a wider slot clips the truncated label,
225
+ // which is why the prop is documented as row actions, not free content.
226
+ showTrailing && "pr-9"
227
+ )
228
+
229
+ const rowInteractionProps = {
230
+ "data-slot": "tree-row" as const,
231
+ "data-id": node.id,
232
+ "data-branch": canExpand ? "" : undefined,
233
+ "data-expanded": canExpand && open ? "" : undefined,
234
+ "data-active": active ? "" : undefined,
235
+ "data-cuelume-press": ctx.sound ? "" : undefined,
236
+ draggable: dnd.enabled && !renaming ? true : undefined,
237
+ onDragStart: dnd.enabled
238
+ ? (event: React.DragEvent) => dnd.onRowDragStart(event, node.id)
239
+ : undefined,
240
+ onDragOver: dnd.enabled
241
+ ? (event: React.DragEvent) => dnd.onRowDragOver(event, node)
242
+ : undefined,
243
+ onDragLeave: dnd.enabled ? dnd.onRowDragLeave : undefined,
244
+ onDrop: dnd.enabled
245
+ ? (event: React.DragEvent) => dnd.onRowDrop(event, node)
246
+ : undefined,
247
+ onDragEnd: dnd.enabled ? dnd.onRowDragEnd : undefined,
248
+ }
249
+
250
+ const content = (
251
+ <>
252
+ {kind !== "section" && files && chevron}
253
+ {kind !== "section" && node.icon && (
254
+ <span
255
+ className={cn(
256
+ "flex size-4 shrink-0 items-center justify-center [&>svg]:size-4",
257
+ active && "text-accent-foreground"
258
+ )}
259
+ >
260
+ {node.icon}
261
+ </span>
262
+ )}
263
+ {label}
264
+ {node.badge !== undefined &&
265
+ !(ctx.variant === "sidebar" && collapsed) && (
266
+ <span className="ml-auto shrink-0 text-xs text-muted-foreground/80">
267
+ {node.badge}
268
+ </span>
269
+ )}
270
+ {/* nav toggle chevrons trail like the sidebar block; files lead;
271
+ toc link rows get a separate overlay toggle instead. */}
272
+ {kind === "toggle" && !files && (
273
+ <span
274
+ className={cn(
275
+ node.badge === undefined ? "ml-auto" : "ml-1",
276
+ "flex shrink-0"
277
+ )}
278
+ >
279
+ {chevron}
280
+ </span>
281
+ )}
282
+ </>
283
+ )
284
+
285
+ let row: React.ReactElement
286
+ if (kind === "section") {
287
+ row = (
288
+ <button
289
+ {...rowInteractionProps}
290
+ aria-expanded={open}
291
+ className={cn(
292
+ "group/row flex w-full items-center gap-1 rounded-md px-2 pt-1 pb-1",
293
+ "text-xs font-medium tracking-wide text-muted-foreground/80 uppercase",
294
+ "transition-colors duration-150 outline-none hover:text-foreground",
295
+ "focus-visible:ring-2 focus-visible:ring-ring"
296
+ )}
297
+ onClick={handlePress}
298
+ type="button"
299
+ >
300
+ <span className="truncate">{node.label}</span>
301
+ {chevron}
302
+ </button>
303
+ )
304
+ } else if (href !== undefined) {
305
+ row = (
306
+ <a
307
+ {...rowInteractionProps}
308
+ aria-current={active ? "page" : undefined}
309
+ aria-label={
310
+ ctx.variant === "sidebar" && collapsed ? node.label : undefined
311
+ }
312
+ className={rowClassName}
313
+ href={href}
314
+ onClick={handlePress}
315
+ target={node.target}
316
+ >
317
+ {content}
318
+ </a>
319
+ )
320
+ } else {
321
+ row = (
322
+ <button
323
+ {...rowInteractionProps}
324
+ aria-current={active ? "page" : undefined}
325
+ aria-expanded={kind === "toggle" ? open : undefined}
326
+ aria-label={
327
+ ctx.variant === "sidebar" && collapsed ? node.label : undefined
328
+ }
329
+ className={rowClassName}
330
+ disabled={node.disabled}
331
+ onClick={handlePress}
332
+ type="button"
333
+ >
334
+ {content}
335
+ </button>
336
+ )
337
+ }
338
+
339
+ // A collapsed rail row is icon-only — a tooltip names it on hover/focus.
340
+ // Expanded, the visible label already does, so the tooltip is disabled
341
+ // rather than unmounted: swapping the wrapper in and out with `collapsed`
342
+ // would remount the row and cut the label's fold animation short.
343
+ if (ctx.variant === "sidebar") {
344
+ row = (
345
+ <Tooltip disabled={!collapsed || node.disabled}>
346
+ <TooltipTrigger render={row} />
347
+ <TooltipContent side="right">{node.label}</TooltipContent>
348
+ </Tooltip>
349
+ )
350
+ }
351
+
352
+ // toc parents navigate on the row itself, so their chevron lives beside
353
+ // the row as its own toggle.
354
+ const tocToggle =
355
+ ctx.variant === "toc" && kind === "link" && children.length > 0 ? (
356
+ <button
357
+ aria-expanded={open}
358
+ aria-label={`${open ? "Collapse" : "Expand"} ${node.label}`}
359
+ className={cn(
360
+ "absolute top-1/2 right-1 flex size-5 -translate-y-1/2 items-center",
361
+ "justify-center rounded-sm text-muted-foreground/70 outline-none",
362
+ "hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
363
+ )}
364
+ onClick={(event) => {
365
+ event.stopPropagation()
366
+ ctx.toggleExpanded(node.id)
367
+ }}
368
+ type="button"
369
+ >
370
+ {chevron}
371
+ </button>
372
+ ) : null
373
+
374
+ // Revealed by hovering/focusing anywhere in the row wrapper — the row and
375
+ // the actions are siblings now, so the trigger group lives on the wrapper.
376
+ const trailing = showTrailing ? (
377
+ <span
378
+ className={cn(
379
+ "absolute top-1/2 right-1 flex -translate-y-1/2 items-center",
380
+ "opacity-0 transition-opacity duration-150",
381
+ "group-focus-within/row-actions:opacity-100 group-hover/row-actions:opacity-100",
382
+ node.disabled && "pointer-events-none opacity-50"
383
+ )}
384
+ data-slot="tree-trailing"
385
+ >
386
+ {renderTrailing?.(node)}
387
+ </span>
388
+ ) : null
389
+
390
+ const branchClassName =
391
+ kind === "section"
392
+ ? "flex flex-col gap-0.5 overflow-clip"
393
+ : cn(
394
+ "flex flex-col overflow-clip border-l border-border",
395
+ files ? "mt-0.5 ml-3 gap-px pl-1.5" : "mt-1 ml-4 gap-0.5 pl-2.5"
396
+ )
397
+
398
+ return (
399
+ <motion.li
400
+ aria-expanded={canExpand ? open : undefined}
401
+ aria-level={depth + 1}
402
+ aria-selected={active || undefined}
403
+ className="relative flex min-w-0 flex-col"
404
+ data-slot="tree-item"
405
+ role="treeitem"
406
+ variants={reduce ? undefined : TREE_ROW_VARIANTS}
407
+ >
408
+ {drop === "before" && <TreeDropLine side="top" />}
409
+ <div className={cn("relative", showTrailing && "group/row-actions")}>
410
+ {row}
411
+ {tocToggle}
412
+ {trailing}
413
+ </div>
414
+ {canExpand && children.length > 0 && (
415
+ <AnimatePresence initial={false}>
416
+ {open && (
417
+ <motion.ul
418
+ animate={reduce ? { opacity: 1 } : "open"}
419
+ className={branchClassName}
420
+ exit={reduce ? { opacity: 0 } : TREE_BRANCH_EXIT}
421
+ initial={reduce ? { opacity: 0 } : "closed"}
422
+ key="branch"
423
+ role="group"
424
+ transition={reduce ? { duration: 0.12 } : undefined}
425
+ variants={reduce ? undefined : TREE_BRANCH_VARIANTS}
426
+ >
427
+ {children.map((child) => (
428
+ <TreeRow depth={depth + 1} key={child.id} node={child} />
429
+ ))}
430
+ </motion.ul>
431
+ )}
432
+ </AnimatePresence>
433
+ )}
434
+ {drop === "after" && <TreeDropLine side="bottom" />}
435
+ </motion.li>
436
+ )
437
+ }
438
+
439
+ /** Insertion indicator above or below a row while a drag hovers it. */
440
+ function TreeDropLine({ side }: { side: "top" | "bottom" }) {
441
+ return (
442
+ <span
443
+ aria-hidden
444
+ className={cn(
445
+ "pointer-events-none absolute inset-x-1 z-10 h-0.5 rounded-full bg-primary",
446
+ side === "top" ? "top-0 -translate-y-1/2" : "bottom-0 translate-y-1/2"
447
+ )}
448
+ data-slot="tree-drop-indicator"
449
+ />
450
+ )
451
+ }