@exegia/corpora-ui 0.19.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-lib/components/blocks/nav/sidebar/__tests__/use-ai-sidebar.test.d.ts +1 -0
- package/dist-lib/components/blocks/nav/sidebar/ai-sidebar.d.ts +11 -2
- package/dist-lib/components/blocks/nav/sidebar/type.d.ts +83 -1
- package/dist-lib/components/blocks/nav/sidebar/use-ai-sidebar.d.ts +22 -31
- package/dist-lib/components/blocks/nav/sidebar/utils.d.ts +5 -0
- package/dist-lib/components/blocks/scaffold/constants.d.ts +5 -0
- package/dist-lib/components/blocks/scaffold/scaffold-canvas.d.ts +2 -0
- package/dist-lib/components/blocks/scaffold/scaffold-panel.d.ts +1 -1
- package/dist-lib/components/blocks/scaffold/scaffold-tab.d.ts +6 -1
- package/dist-lib/components/blocks/scaffold/type.d.ts +28 -0
- package/dist-lib/components/blocks/scaffold/use-panel-visibility.d.ts +19 -0
- package/dist-lib/components/blocks/scaffold/utils.d.ts +6 -0
- package/dist-lib/components/composed/tree/__tests__/tree.test.d.ts +1 -0
- package/dist-lib/components/composed/tree/__tests__/use-tree.test.d.ts +1 -0
- package/dist-lib/components/composed/tree/constants.d.ts +35 -0
- package/dist-lib/components/composed/tree/index.d.ts +4 -0
- package/dist-lib/components/composed/tree/tree-context.d.ts +4 -0
- package/dist-lib/components/composed/tree/tree-node.d.ts +7 -0
- package/dist-lib/components/composed/tree/tree.d.ts +19 -0
- package/dist-lib/components/composed/tree/type.d.ts +192 -0
- package/dist-lib/components/composed/tree/use-tree-dnd.d.ts +7 -0
- package/dist-lib/components/composed/tree/use-tree.d.ts +20 -0
- package/dist-lib/components/composed/tree/utils.d.ts +28 -0
- package/dist-lib/components/user-avatar/audio-wave.d.ts +2 -0
- package/dist-lib/components/user-avatar/component.d.ts +10 -0
- package/dist-lib/components/user-avatar/fallback.d.ts +5 -0
- package/dist-lib/components/user-avatar/index.d.ts +13 -0
- package/dist-lib/components/user-avatar/type.d.ts +26 -0
- package/dist-lib/components/user-avatar/utils.d.ts +2 -0
- package/dist-lib/index.d.ts +1 -0
- package/dist-lib/index.js +2141 -1139
- package/dist-lib/index.js.map +1 -1
- package/package.json +2 -1
- package/src/components/beste/piece/browser-frame.tsx +9 -6
- package/src/components/blocks/nav/sidebar/__tests__/use-ai-sidebar.test.tsx +310 -0
- package/src/components/blocks/nav/sidebar/ai-sidebar.tsx +90 -60
- package/src/components/blocks/nav/sidebar/type.ts +105 -1
- package/src/components/blocks/nav/sidebar/use-ai-sidebar.ts +293 -116
- package/src/components/blocks/nav/sidebar/utils.ts +28 -0
- package/src/components/blocks/scaffold/__tests__/scaffold.test.tsx +160 -1
- package/src/components/blocks/scaffold/constants.ts +7 -0
- package/src/components/blocks/scaffold/panel-menu-button.tsx +3 -0
- package/src/components/blocks/scaffold/scaffold-canvas.tsx +48 -5
- package/src/components/blocks/scaffold/scaffold-inspector.tsx +3 -2
- package/src/components/blocks/scaffold/scaffold-main.tsx +1 -1
- package/src/components/blocks/scaffold/scaffold-panel.tsx +41 -5
- package/src/components/blocks/scaffold/scaffold-root.tsx +18 -3
- package/src/components/blocks/scaffold/scaffold-sidebar.tsx +1 -1
- package/src/components/blocks/scaffold/scaffold-tab.tsx +45 -7
- package/src/components/blocks/scaffold/type.ts +28 -0
- package/src/components/blocks/scaffold/use-panel-visibility.ts +180 -0
- package/src/components/blocks/scaffold/utils.ts +19 -0
- package/src/components/composed/tree/__tests__/tree.test.tsx +400 -0
- package/src/components/composed/tree/__tests__/use-tree.test.tsx +332 -0
- package/src/components/composed/tree/constants.ts +60 -0
- package/src/components/composed/tree/index.ts +13 -0
- package/src/components/composed/tree/tree-context.ts +13 -0
- package/src/components/composed/tree/tree-node.tsx +451 -0
- package/src/components/composed/tree/tree.tsx +278 -0
- package/src/components/composed/tree/type.ts +212 -0
- package/src/components/composed/tree/use-tree-dnd.ts +148 -0
- package/src/components/composed/tree/use-tree.ts +306 -0
- package/src/components/composed/tree/utils.ts +172 -0
- package/src/components/composed/user-avatar.tsx +4 -1
- package/src/components/docs/component-preview.tsx +11 -11
- package/src/components/user-avatar/audio-wave.tsx +22 -0
- package/src/components/user-avatar/component.tsx +53 -0
- package/src/components/user-avatar/fallback.tsx +26 -0
- package/src/components/user-avatar/index.ts +15 -0
- package/src/components/user-avatar/type.ts +35 -0
- package/src/components/user-avatar/utils.ts +8 -0
- package/src/index.css +211 -153
- package/src/index.ts +1 -0
|
@@ -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
|
+
}
|