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