@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.
- package/dist-lib/components/blocks/auth/__tests__/auth-state-atom.test.d.ts +1 -0
- package/dist-lib/components/blocks/auth/auth-flow-atom.d.ts +77 -0
- package/dist-lib/components/blocks/auth/auth-session-atom.d.ts +28 -0
- package/dist-lib/components/blocks/auth/auth-state-type.d.ts +71 -0
- package/dist-lib/components/blocks/auth/auth-state.d.ts +9 -0
- package/dist-lib/components/blocks/auth/use-auth-state.d.ts +52 -0
- package/dist-lib/components/blocks/nav/sidebar/__tests__/ai-sidebar-atom.test.d.ts +1 -0
- 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-atom.d.ts +217 -0
- package/dist-lib/components/blocks/nav/sidebar/ai-sidebar.d.ts +11 -2
- package/dist-lib/components/blocks/nav/sidebar/index.d.ts +3 -1
- package/dist-lib/components/blocks/nav/sidebar/sidebar-context.d.ts +3 -0
- package/dist-lib/components/blocks/nav/sidebar/sidebar-row.d.ts +3 -1
- package/dist-lib/components/blocks/nav/sidebar/type.d.ts +191 -29
- package/dist-lib/components/blocks/nav/sidebar/use-ai-sidebar-state.d.ts +29 -0
- package/dist-lib/components/blocks/nav/sidebar/use-ai-sidebar.d.ts +27 -31
- package/dist-lib/components/blocks/nav/sidebar/utils.d.ts +6 -1
- package/dist-lib/components/blocks/profile/index.d.ts +7 -0
- package/dist-lib/components/blocks/profile/profile-card-atom.d.ts +68 -0
- package/dist-lib/components/blocks/profile/profile-card-block.d.ts +28 -39
- package/dist-lib/components/blocks/profile/type.d.ts +80 -0
- package/dist-lib/components/blocks/profile/use-profile-card-state.d.ts +24 -0
- package/dist-lib/components/blocks/profile/use-profile-card.d.ts +34 -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/blocks/shell/__tests__/shell-fit-atom.test.d.ts +1 -0
- package/dist-lib/components/blocks/shell/__tests__/shell-metrics.test.d.ts +1 -0
- package/dist-lib/components/blocks/shell/animated-panel-provider.d.ts +1 -1
- package/dist-lib/components/blocks/shell/animated-panel.d.ts +1 -1
- package/dist-lib/components/blocks/shell/index.d.ts +4 -0
- package/dist-lib/components/blocks/shell/shell-fit-atom.d.ts +68 -0
- package/dist-lib/components/blocks/shell/shell-metrics.d.ts +54 -0
- package/dist-lib/components/blocks/shell/type.d.ts +115 -3
- package/dist-lib/components/blocks/shell/use-shell-fit-state.d.ts +25 -0
- package/dist-lib/components/blocks/shell/use-shell-fit.d.ts +14 -0
- package/dist-lib/components/blocks/shell/use-shell-panels.d.ts +12 -1
- package/dist-lib/components/blocks/shell/utils.d.ts +32 -0
- package/dist-lib/components/composed/tree/__tests__/tree-atom.test.d.ts +1 -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 +6 -0
- package/dist-lib/components/composed/tree/tree-atom.d.ts +160 -0
- package/dist-lib/components/composed/tree/tree-context.d.ts +4 -0
- package/dist-lib/components/composed/tree/tree-node.d.ts +12 -0
- package/dist-lib/components/composed/tree/tree.d.ts +19 -0
- package/dist-lib/components/composed/tree/type.d.ts +290 -0
- package/dist-lib/components/composed/tree/use-tree-dnd.d.ts +18 -0
- package/dist-lib/components/composed/tree/use-tree-state.d.ts +25 -0
- package/dist-lib/components/composed/tree/use-tree.d.ts +24 -0
- package/dist-lib/components/composed/tree/utils.d.ts +28 -0
- package/dist-lib/components/composed/user-avatar.d.ts +5 -28
- package/dist-lib/components/user-avatar/__tests__/user-avatar.test.d.ts +1 -0
- package/dist-lib/components/user-avatar/audio-wave.d.ts +2 -0
- package/dist-lib/components/user-avatar/component.d.ts +3 -0
- package/dist-lib/components/user-avatar/fallback.d.ts +5 -0
- package/dist-lib/components/user-avatar/index.d.ts +18 -0
- package/dist-lib/components/user-avatar/presence-badge.d.ts +16 -0
- package/dist-lib/components/user-avatar/type.d.ts +71 -0
- package/dist-lib/components/user-avatar/use-user-avatar-state.d.ts +26 -0
- package/dist-lib/components/user-avatar/use-user-avatar.d.ts +32 -0
- package/dist-lib/components/user-avatar/user-avatar-atom.d.ts +55 -0
- package/dist-lib/components/user-avatar/utils.d.ts +2 -0
- package/dist-lib/index.d.ts +6 -2
- package/dist-lib/index.js +3576 -1366
- package/dist-lib/index.js.map +1 -1
- package/dist-lib/state/exegia-provider.d.ts +51 -0
- package/dist-lib/state/index.d.ts +4 -0
- package/dist-lib/state/store.d.ts +17 -0
- package/package.json +16 -12
- package/src/components/beste/piece/browser-frame.tsx +9 -6
- package/src/components/blocks/auth/__tests__/auth-state-atom.test.tsx +247 -0
- package/src/components/blocks/auth/auth-flow-atom.ts +238 -0
- package/src/components/blocks/auth/auth-session-atom.ts +82 -0
- package/src/components/blocks/auth/auth-state-type.ts +97 -0
- package/src/components/blocks/auth/auth-state.ts +52 -0
- package/src/components/blocks/auth/use-auth-state.ts +127 -0
- package/src/components/blocks/nav/sidebar/__tests__/ai-sidebar-atom.test.tsx +325 -0
- package/src/components/blocks/nav/sidebar/__tests__/use-ai-sidebar.test.tsx +310 -0
- package/src/components/blocks/nav/sidebar/ai-sidebar-atom.ts +698 -0
- package/src/components/blocks/nav/sidebar/ai-sidebar.tsx +112 -76
- package/src/components/blocks/nav/sidebar/index.ts +53 -1
- package/src/components/blocks/nav/sidebar/sidebar-context.ts +15 -0
- package/src/components/blocks/nav/sidebar/sidebar-row.tsx +119 -50
- package/src/components/blocks/nav/sidebar/type.ts +228 -29
- package/src/components/blocks/nav/sidebar/use-ai-sidebar-state.ts +123 -0
- package/src/components/blocks/nav/sidebar/use-ai-sidebar.ts +455 -225
- package/src/components/blocks/nav/sidebar/utils.ts +29 -1
- package/src/components/blocks/profile/__tests__/profile-card-block.test.tsx +194 -0
- package/src/components/blocks/profile/index.ts +28 -0
- package/src/components/blocks/profile/profile-card-atom.ts +247 -0
- package/src/components/blocks/profile/profile-card-block.tsx +153 -74
- package/src/components/blocks/profile/type.ts +95 -0
- package/src/components/blocks/profile/use-profile-card-state.ts +67 -0
- package/src/components/blocks/profile/use-profile-card.ts +126 -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/blocks/shell/__tests__/shell-fit-atom.test.tsx +360 -0
- package/src/components/blocks/shell/__tests__/shell-layout.test.tsx +192 -3
- package/src/components/blocks/shell/__tests__/shell-metrics.test.ts +108 -0
- package/src/components/blocks/shell/animated-panel-inset.tsx +5 -1
- package/src/components/blocks/shell/animated-panel-provider.tsx +77 -11
- package/src/components/blocks/shell/animated-panel-trigger.tsx +4 -0
- package/src/components/blocks/shell/animated-panel.tsx +126 -29
- package/src/components/blocks/shell/index.ts +18 -0
- package/src/components/blocks/shell/shell-fit-atom.ts +243 -0
- package/src/components/blocks/shell/shell-layout.tsx +55 -53
- package/src/components/blocks/shell/shell-metrics.ts +79 -0
- package/src/components/blocks/shell/type.ts +130 -3
- package/src/components/blocks/shell/use-shell-fit-state.ts +49 -0
- package/src/components/blocks/shell/use-shell-fit.ts +135 -0
- package/src/components/blocks/shell/use-shell-panels.ts +57 -4
- package/src/components/blocks/shell/utils.ts +44 -4
- package/src/components/composed/tree/CLAUDE.md +132 -0
- package/src/components/composed/tree/__tests__/tree-atom.test.tsx +217 -0
- package/src/components/composed/tree/__tests__/tree.test.tsx +525 -0
- package/src/components/composed/tree/__tests__/use-tree.test.tsx +333 -0
- package/src/components/composed/tree/constants.ts +60 -0
- package/src/components/composed/tree/index.ts +51 -0
- package/src/components/composed/tree/tree-atom.ts +590 -0
- package/src/components/composed/tree/tree-context.ts +13 -0
- package/src/components/composed/tree/tree-node.tsx +490 -0
- package/src/components/composed/tree/tree.tsx +286 -0
- package/src/components/composed/tree/type.ts +322 -0
- package/src/components/composed/tree/use-tree-dnd.ts +179 -0
- package/src/components/composed/tree/use-tree-state.ts +105 -0
- package/src/components/composed/tree/use-tree.ts +307 -0
- package/src/components/composed/tree/utils.ts +172 -0
- package/src/components/composed/user-avatar.tsx +11 -96
- package/src/components/docs/component-preview.tsx +11 -11
- package/src/components/user-avatar/__tests__/user-avatar.test.tsx +245 -0
- package/src/components/user-avatar/audio-wave.tsx +22 -0
- package/src/components/user-avatar/component.tsx +152 -0
- package/src/components/user-avatar/fallback.tsx +26 -0
- package/src/components/user-avatar/index.ts +34 -0
- package/src/components/user-avatar/presence-badge.tsx +56 -0
- package/src/components/user-avatar/type.ts +85 -0
- package/src/components/user-avatar/use-user-avatar-state.ts +60 -0
- package/src/components/user-avatar/use-user-avatar.ts +144 -0
- package/src/components/user-avatar/user-avatar-atom.ts +198 -0
- package/src/components/user-avatar/utils.ts +8 -0
- package/src/index.css +211 -153
- package/src/index.ts +35 -2
- package/src/state/exegia-provider.tsx +79 -0
- package/src/state/index.ts +4 -0
- 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
|
+
}
|