@exegia/corpora-ui 0.17.0 → 0.19.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-block.d.ts +1 -1
- package/dist-lib/components/blocks/scaffold/__tests__/scaffold.test.d.ts +1 -0
- package/dist-lib/components/blocks/scaffold/constants.d.ts +23 -0
- package/dist-lib/components/blocks/scaffold/index.d.ts +27 -0
- package/dist-lib/components/blocks/scaffold/panel-close-button.d.ts +4 -0
- package/dist-lib/components/blocks/scaffold/panel-menu-button.d.ts +8 -0
- package/dist-lib/components/blocks/scaffold/scaffold-actions.d.ts +10 -0
- package/dist-lib/components/blocks/scaffold/scaffold-canvas.d.ts +7 -0
- package/dist-lib/components/blocks/scaffold/scaffold-context.d.ts +5 -0
- package/dist-lib/components/blocks/scaffold/scaffold-inspector.d.ts +9 -0
- package/dist-lib/components/blocks/scaffold/scaffold-main.d.ts +8 -0
- package/dist-lib/components/blocks/scaffold/scaffold-panel.d.ts +10 -0
- package/dist-lib/components/blocks/scaffold/scaffold-root.d.ts +8 -0
- package/dist-lib/components/blocks/scaffold/scaffold-sidebar.d.ts +7 -0
- package/dist-lib/components/blocks/scaffold/scaffold-sub-panel.d.ts +3 -0
- package/dist-lib/components/blocks/scaffold/scaffold-tab.d.ts +9 -0
- package/dist-lib/components/blocks/scaffold/type.d.ts +135 -0
- package/dist-lib/components/blocks/scaffold/use-scaffold.d.ts +8 -0
- package/dist-lib/components/blocks/scaffold/utils.d.ts +21 -0
- package/dist-lib/components/blocks/shell/animated-panel-inset.d.ts +2 -0
- package/dist-lib/components/blocks/shell/animated-panel-provider.d.ts +2 -0
- package/dist-lib/components/blocks/shell/animated-panel-trigger.d.ts +2 -0
- package/dist-lib/components/blocks/shell/animated-panel.d.ts +2 -0
- package/dist-lib/components/blocks/shell/animated-sidebar-content.d.ts +2 -0
- package/dist-lib/components/blocks/shell/animated-sidebar-footer.d.ts +2 -0
- package/dist-lib/components/blocks/shell/animated-sidebar-group-content.d.ts +2 -0
- package/dist-lib/components/blocks/shell/animated-sidebar-group-label.d.ts +2 -0
- package/dist-lib/components/blocks/shell/animated-sidebar-group.d.ts +2 -0
- package/dist-lib/components/blocks/shell/animated-sidebar-header.d.ts +2 -0
- package/dist-lib/components/blocks/shell/animated-sidebar-menu-button.d.ts +3 -0
- package/dist-lib/components/blocks/shell/animated-sidebar-menu-item.d.ts +2 -0
- package/dist-lib/components/blocks/shell/animated-sidebar-menu-sub-button.d.ts +3 -0
- package/dist-lib/components/blocks/shell/animated-sidebar-menu-sub-item.d.ts +2 -0
- package/dist-lib/components/blocks/shell/animated-sidebar-menu-sub.d.ts +2 -0
- package/dist-lib/components/blocks/shell/animated-sidebar-menu.d.ts +2 -0
- package/dist-lib/components/blocks/shell/index.d.ts +16 -0
- package/dist-lib/components/blocks/shell/type.d.ts +90 -4
- package/dist-lib/components/blocks/shell/utils.d.ts +37 -0
- package/dist-lib/components/motion/index.d.ts +1 -0
- package/dist-lib/components/motion/overflow-actions.d.ts +35 -0
- package/dist-lib/components/ui/button-group.d.ts +11 -0
- package/dist-lib/components/ui/glasscn/glass-button-group.d.ts +5 -0
- package/dist-lib/components/ui/glasscn/glass-container.d.ts +7 -0
- package/dist-lib/components/ui/glasscn/glass-separator.d.ts +5 -0
- package/dist-lib/components/ui/group.d.ts +17 -0
- package/dist-lib/components/ui/separator.d.ts +2 -3
- package/dist-lib/components/ui/sheet.d.ts +14 -0
- package/dist-lib/components/ui/tooltip.d.ts +6 -0
- package/dist-lib/index.d.ts +2 -1
- package/dist-lib/index.js +1631 -1101
- package/dist-lib/index.js.map +1 -1
- package/dist-lib/lib/ease.d.ts +3 -0
- package/dist-lib/lib/hooks/use-hover-capable.d.ts +6 -0
- package/package.json +2 -1
- package/src/components/beste/piece/browser-frame.tsx +2 -2
- package/src/components/blocks/nav/sidebar-block.tsx +5 -5
- package/src/components/blocks/nav/sidebar-nav-row.tsx +4 -5
- package/src/components/blocks/scaffold/__tests__/scaffold.test.tsx +286 -0
- package/src/components/blocks/scaffold/constants.ts +32 -0
- package/src/components/blocks/scaffold/index.ts +41 -0
- package/src/components/blocks/scaffold/panel-close-button.tsx +36 -0
- package/src/components/blocks/scaffold/panel-menu-button.tsx +218 -0
- package/src/components/blocks/scaffold/scaffold-actions.tsx +88 -0
- package/src/components/blocks/scaffold/scaffold-canvas.tsx +29 -0
- package/src/components/blocks/scaffold/scaffold-context.ts +18 -0
- package/src/components/blocks/scaffold/scaffold-inspector.tsx +80 -0
- package/src/components/blocks/scaffold/scaffold-main.tsx +28 -0
- package/src/components/blocks/scaffold/scaffold-panel.tsx +88 -0
- package/src/components/blocks/scaffold/scaffold-root.tsx +68 -0
- package/src/components/blocks/scaffold/scaffold-sidebar.tsx +31 -0
- package/src/components/blocks/scaffold/scaffold-sub-panel.tsx +51 -0
- package/src/components/blocks/scaffold/scaffold-tab.tsx +66 -0
- package/src/components/blocks/scaffold/type.ts +172 -0
- package/src/components/blocks/scaffold/use-scaffold.ts +48 -0
- package/src/components/blocks/scaffold/utils.ts +46 -0
- package/src/components/blocks/shell/animated-panel-inset.tsx +23 -0
- package/src/components/blocks/shell/animated-panel-provider.tsx +155 -0
- package/src/components/blocks/shell/animated-panel-trigger.tsx +42 -0
- package/src/components/blocks/shell/animated-panel.tsx +135 -0
- package/src/components/blocks/shell/animated-sidebar-content.tsx +19 -0
- package/src/components/blocks/shell/animated-sidebar-footer.tsx +19 -0
- package/src/components/blocks/shell/animated-sidebar-group-content.tsx +16 -0
- package/src/components/blocks/shell/animated-sidebar-group-label.tsx +29 -0
- package/src/components/blocks/shell/animated-sidebar-group.tsx +16 -0
- package/src/components/blocks/shell/animated-sidebar-header.tsx +16 -0
- package/src/components/blocks/shell/animated-sidebar-menu-button.tsx +151 -0
- package/src/components/blocks/shell/animated-sidebar-menu-item.tsx +20 -0
- package/src/components/blocks/shell/animated-sidebar-menu-sub-button.tsx +85 -0
- package/src/components/blocks/shell/animated-sidebar-menu-sub-item.tsx +19 -0
- package/src/components/blocks/shell/animated-sidebar-menu-sub.tsx +44 -0
- package/src/components/blocks/shell/animated-sidebar-menu.tsx +26 -0
- package/src/components/blocks/shell/index.ts +18 -0
- package/src/components/blocks/shell/shell-layout.tsx +28 -36
- package/src/components/blocks/shell/type.ts +112 -6
- package/src/components/blocks/shell/use-shell-panels.ts +1 -1
- package/src/components/blocks/shell/utils.ts +142 -1
- package/src/components/docs/demo-controls.tsx +1 -1
- package/src/components/motion/index.ts +0 -0
- package/src/components/motion/overflow-actions.tsx +376 -0
- package/src/components/ui/button-group.tsx +88 -0
- package/src/components/ui/glasscn/glass-button-group.tsx +48 -0
- package/src/components/ui/glasscn/glass-container.tsx +125 -0
- package/src/components/ui/glasscn/glass-separator.tsx +39 -0
- package/src/components/ui/glasscn/liquid-glass.tsx +250 -210
- package/src/components/ui/group.tsx +92 -0
- package/src/components/ui/separator.tsx +11 -11
- package/src/components/ui/sheet.tsx +135 -0
- package/src/components/ui/tooltip.tsx +66 -0
- package/src/index.css +1 -1
- package/src/index.ts +13 -1
- package/src/lib/ease.ts +26 -0
- package/src/lib/hooks/use-hover-capable.ts +23 -0
- package/dist-lib/components/motion/animated-sidebar.d.ts +0 -106
- package/src/components/motion/animated-sidebar.tsx +0 -1171
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
|
|
5
|
+
import { More } from "iconsax-reactjs"
|
|
6
|
+
import { LucideArrowUpDown, LucideChevronDown, LucideX } from "lucide-react"
|
|
7
|
+
|
|
8
|
+
import { cn } from "@/lib/utils"
|
|
9
|
+
import type { PanelMenuButtonProps } from "./type"
|
|
10
|
+
import { floatingButtonClass, revealOnPanelHoverClass } from "./utils"
|
|
11
|
+
import { Button } from "@/components/ui/button"
|
|
12
|
+
import { GlassButtonGroup } from "@/components/ui/glasscn/glass-button-group.tsx"
|
|
13
|
+
import { useHoverCapable } from "@/lib/hooks/use-hover-capable"
|
|
14
|
+
import { SCAFFOLD_MENU_COLLAPSE_DELAY } from "./constants"
|
|
15
|
+
|
|
16
|
+
/** One side of the seam menu. Both sides stay mounted; the pill morphs by
|
|
17
|
+
* collapsing one side's grid column to 0fr while the other grows — pure
|
|
18
|
+
* CSS, driven by `data-expanded` on the group. The hidden side is inert
|
|
19
|
+
* and aria-hidden so it can't be clicked, focused or found by queries. */
|
|
20
|
+
const menuCellClass =
|
|
21
|
+
"grid transition-[grid-template-columns,opacity] duration-300 ease-smooth-out motion-reduce:transition-none"
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Floating menu affordance centered on the seam between a panel's primary
|
|
25
|
+
* surface and its secondary strip: a compact ⋯ toggle that morphs into the
|
|
26
|
+
* Expand | Swap (| Close) actions.
|
|
27
|
+
*/
|
|
28
|
+
export function PanelMenuButton({
|
|
29
|
+
onClick,
|
|
30
|
+
onExpand,
|
|
31
|
+
onCloseSecondary,
|
|
32
|
+
secondaryExpanded = false,
|
|
33
|
+
swapped = false,
|
|
34
|
+
label,
|
|
35
|
+
sound = true,
|
|
36
|
+
className,
|
|
37
|
+
}: PanelMenuButtonProps): React.ReactElement {
|
|
38
|
+
const canHover = useHoverCapable()
|
|
39
|
+
const actionsId = React.useId()
|
|
40
|
+
const groupRef = React.useRef<HTMLDivElement>(null)
|
|
41
|
+
const actionsRef = React.useRef<HTMLDivElement>(null)
|
|
42
|
+
const [expanded, setExpanded] = React.useState(false);
|
|
43
|
+
|
|
44
|
+
const collapseTimerRef = React.useRef<ReturnType<typeof setTimeout>>(undefined)
|
|
45
|
+
const suppressHoverOpenUntilRef = React.useRef(0)
|
|
46
|
+
const focusActionsOnOpenRef = React.useRef(false)
|
|
47
|
+
|
|
48
|
+
React.useEffect(() => () => clearTimeout(collapseTimerRef.current), [])
|
|
49
|
+
|
|
50
|
+
const cancelCollapse = React.useCallback(() => {
|
|
51
|
+
clearTimeout(collapseTimerRef.current)
|
|
52
|
+
}, [])
|
|
53
|
+
|
|
54
|
+
// Arms when the pointer stops hovering the group: the menu lingers for the
|
|
55
|
+
// grace period, then collapses — unless the pointer (or focus) came back.
|
|
56
|
+
const armCollapse = React.useCallback(() => {
|
|
57
|
+
if (!canHover) return
|
|
58
|
+
clearTimeout(collapseTimerRef.current)
|
|
59
|
+
collapseTimerRef.current = setTimeout(() => {
|
|
60
|
+
// Focus keeps the menu open — don't collapse under the keyboard.
|
|
61
|
+
if (groupRef.current?.contains(document.activeElement)) return
|
|
62
|
+
setExpanded(false)
|
|
63
|
+
}, SCAFFOLD_MENU_COLLAPSE_DELAY)
|
|
64
|
+
}, [canHover])
|
|
65
|
+
|
|
66
|
+
// The grace period must keep counting when the pointer leaves the whole
|
|
67
|
+
// app — leaving the window can skip the group's own pointerleave, so arm
|
|
68
|
+
// on viewport exit and window blur as well.
|
|
69
|
+
React.useEffect(() => {
|
|
70
|
+
if (!expanded || !canHover) return
|
|
71
|
+
const root = document.documentElement
|
|
72
|
+
root.addEventListener("pointerleave", armCollapse)
|
|
73
|
+
window.addEventListener("blur", armCollapse)
|
|
74
|
+
return () => {
|
|
75
|
+
root.removeEventListener("pointerleave", armCollapse)
|
|
76
|
+
window.removeEventListener("blur", armCollapse)
|
|
77
|
+
}
|
|
78
|
+
}, [expanded, canHover, armCollapse])
|
|
79
|
+
|
|
80
|
+
// Click-opens replace the toggle with the actions — hand focus over so
|
|
81
|
+
// keyboard users aren't dropped onto an inert control.
|
|
82
|
+
React.useEffect(() => {
|
|
83
|
+
if (!expanded || !focusActionsOnOpenRef.current) return
|
|
84
|
+
focusActionsOnOpenRef.current = false
|
|
85
|
+
actionsRef.current?.querySelector<HTMLButtonElement>("button")?.focus()
|
|
86
|
+
}, [expanded])
|
|
87
|
+
|
|
88
|
+
const collapse = () => {
|
|
89
|
+
// The shrinking toggle can glide under a resting cursor and hover-reopen
|
|
90
|
+
// the menu it just closed — ignore hover-opens until it has settled.
|
|
91
|
+
suppressHoverOpenUntilRef.current = Date.now() + 600
|
|
92
|
+
setExpanded(false)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Acting collapses the menu — the pill returns to the compact toggle.
|
|
96
|
+
const handleAction = (action?: () => void) => {
|
|
97
|
+
action?.()
|
|
98
|
+
collapse()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Click open — the touch and keyboard path.
|
|
102
|
+
const handleOnExpand = () => {
|
|
103
|
+
if (groupRef.current?.contains(document.activeElement)) {
|
|
104
|
+
focusActionsOnOpenRef.current = true
|
|
105
|
+
}
|
|
106
|
+
setExpanded(true)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Hover on the toggle opens the menu (hover-capable devices only — touch
|
|
110
|
+
// and keyboard go through the click toggle).
|
|
111
|
+
const handleToggleHover = () => {
|
|
112
|
+
if (!canHover || Date.now() < suppressHoverOpenUntilRef.current) return
|
|
113
|
+
cancelCollapse()
|
|
114
|
+
setExpanded(true)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<GlassButtonGroup
|
|
119
|
+
ref={groupRef}
|
|
120
|
+
onPointerEnter={cancelCollapse}
|
|
121
|
+
onPointerLeave={armCollapse}
|
|
122
|
+
glassVariant="subtle"
|
|
123
|
+
data-expanded={expanded ? "" : undefined}
|
|
124
|
+
className={cn(
|
|
125
|
+
floatingButtonClass,
|
|
126
|
+
revealOnPanelHoverClass,
|
|
127
|
+
"group/menu absolute top-1/2 z-10 -translate-y-1/2",
|
|
128
|
+
className
|
|
129
|
+
)}
|
|
130
|
+
>
|
|
131
|
+
<div
|
|
132
|
+
aria-hidden={expanded || undefined}
|
|
133
|
+
inert={expanded || undefined}
|
|
134
|
+
className={cn(
|
|
135
|
+
menuCellClass,
|
|
136
|
+
"grid-cols-[1fr] opacity-100 group-data-expanded/menu:grid-cols-[0fr] group-data-expanded/menu:opacity-0"
|
|
137
|
+
)}
|
|
138
|
+
>
|
|
139
|
+
<div className="flex min-w-0 overflow-hidden">
|
|
140
|
+
<Button
|
|
141
|
+
id="scaffold-panel-menu-toggle-button"
|
|
142
|
+
aria-label={label}
|
|
143
|
+
aria-expanded={expanded}
|
|
144
|
+
aria-controls={actionsId}
|
|
145
|
+
size="icon-sm"
|
|
146
|
+
variant="ghost"
|
|
147
|
+
sound={sound}
|
|
148
|
+
data-slot="scaffold-panel-menu-toggle"
|
|
149
|
+
onClick={handleOnExpand}
|
|
150
|
+
onPointerEnter={handleToggleHover}
|
|
151
|
+
>
|
|
152
|
+
<More size={16} />
|
|
153
|
+
</Button>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
<div
|
|
157
|
+
id={actionsId}
|
|
158
|
+
ref={actionsRef}
|
|
159
|
+
aria-hidden={!expanded || undefined}
|
|
160
|
+
inert={!expanded || undefined}
|
|
161
|
+
className={cn(
|
|
162
|
+
menuCellClass,
|
|
163
|
+
"grid-cols-[0fr] opacity-0 group-data-expanded/menu:grid-cols-[1fr] group-data-expanded/menu:opacity-100"
|
|
164
|
+
)}
|
|
165
|
+
>
|
|
166
|
+
<div className="flex min-w-0 overflow-hidden">
|
|
167
|
+
<Button
|
|
168
|
+
id="scaffold-panel-menu-expand"
|
|
169
|
+
size="sm"
|
|
170
|
+
variant="ghost"
|
|
171
|
+
sound={sound}
|
|
172
|
+
data-slot="scaffold-panel-menu-expand"
|
|
173
|
+
className="rounded-r-none"
|
|
174
|
+
onClick={() => handleAction(onExpand)}
|
|
175
|
+
>
|
|
176
|
+
{/* Points at the sub-panel that grows next, in VISUAL space —
|
|
177
|
+
a swapped panel is column-reverse, so the direction mirrors.
|
|
178
|
+
Button's [&_svg]:transition-transform eases the flip. */}
|
|
179
|
+
<LucideChevronDown
|
|
180
|
+
size={16}
|
|
181
|
+
className={cn(secondaryExpanded !== swapped && "rotate-180")}
|
|
182
|
+
/>
|
|
183
|
+
Expand
|
|
184
|
+
</Button>
|
|
185
|
+
<Button
|
|
186
|
+
id="scaffold-panel-menu-swap"
|
|
187
|
+
size="sm"
|
|
188
|
+
variant="ghost"
|
|
189
|
+
sound={sound}
|
|
190
|
+
data-slot="scaffold-panel-menu-swap"
|
|
191
|
+
className={cn(
|
|
192
|
+
"border-l-0",
|
|
193
|
+
onCloseSecondary ? "rounded-none" : "rounded-l-none"
|
|
194
|
+
)}
|
|
195
|
+
onClick={() => handleAction(onClick)}
|
|
196
|
+
>
|
|
197
|
+
<LucideArrowUpDown size={16} />
|
|
198
|
+
Swap
|
|
199
|
+
</Button>
|
|
200
|
+
{onCloseSecondary && (
|
|
201
|
+
<Button
|
|
202
|
+
id="scaffold-panel-menu-close"
|
|
203
|
+
size="sm"
|
|
204
|
+
variant="ghost"
|
|
205
|
+
sound={sound}
|
|
206
|
+
data-slot="scaffold-panel-menu-close"
|
|
207
|
+
className="rounded-l-none border-l-0"
|
|
208
|
+
onClick={() => handleAction(onCloseSecondary)}
|
|
209
|
+
>
|
|
210
|
+
<LucideX size={16} />
|
|
211
|
+
Close
|
|
212
|
+
</Button>
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
</GlassButtonGroup>
|
|
217
|
+
)
|
|
218
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { AnimatePresence, motion, useReducedMotion } from "motion/react"
|
|
4
|
+
import * as React from "react"
|
|
5
|
+
|
|
6
|
+
import { cn } from "@/lib/utils"
|
|
7
|
+
import { SCAFFOLD_EASE, SCAFFOLD_EDGE_GUTTER, SCAFFOLD_MORPH_DURATION } from "./constants"
|
|
8
|
+
import { useScaffoldContext } from "./scaffold-context"
|
|
9
|
+
import type { ScaffoldActionsProps } from "./type"
|
|
10
|
+
import { segmentVariants } from "./utils"
|
|
11
|
+
import { Button } from "@/components/ui/button"
|
|
12
|
+
import { SPRING_LAYOUT } from "@/lib/ease.ts"
|
|
13
|
+
import { LucidePlus } from "lucide-react"
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The floating pill cluster in the main region's top-right corner — the
|
|
17
|
+
* design's "Panel Context Menu": one tab per open panel (`Scaffold.Tab`
|
|
18
|
+
* children) plus an Add segment. Add renders only while `onAdd` is
|
|
19
|
+
* present — omit it once the canvas holds `SCAFFOLD_PANEL_CAPACITY`
|
|
20
|
+
* panels. Slides left in step with the inspector opening.
|
|
21
|
+
*/
|
|
22
|
+
export function ScaffoldActions({
|
|
23
|
+
onAdd,
|
|
24
|
+
addLabel = "Panel",
|
|
25
|
+
addIcon,
|
|
26
|
+
sound = true,
|
|
27
|
+
className,
|
|
28
|
+
children,
|
|
29
|
+
...rest
|
|
30
|
+
}: ScaffoldActionsProps): React.ReactElement {
|
|
31
|
+
const { inspectorOpen, inspectorWidth } = useScaffoldContext()
|
|
32
|
+
const reducedMotion = useReducedMotion()
|
|
33
|
+
const transition = reducedMotion ? { duration: 0 } : SPRING_LAYOUT
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<motion.div
|
|
37
|
+
id="scaffold-actions"
|
|
38
|
+
animate={{
|
|
39
|
+
x: inspectorOpen ? -(inspectorWidth + SCAFFOLD_EDGE_GUTTER) : 0,
|
|
40
|
+
}}
|
|
41
|
+
className={cn(
|
|
42
|
+
"sticky inline-flex h-12 items-center gap-0.5",
|
|
43
|
+
"justify-end",
|
|
44
|
+
className
|
|
45
|
+
)}
|
|
46
|
+
data-slot="scaffold-actions"
|
|
47
|
+
initial={false}
|
|
48
|
+
transition={{
|
|
49
|
+
duration: reducedMotion ? 0 : SCAFFOLD_MORPH_DURATION,
|
|
50
|
+
ease: SCAFFOLD_EASE,
|
|
51
|
+
}}
|
|
52
|
+
{...rest}
|
|
53
|
+
>
|
|
54
|
+
<motion.div
|
|
55
|
+
layout
|
|
56
|
+
transition={transition}
|
|
57
|
+
className="flex items-center gap-1.5"
|
|
58
|
+
>
|
|
59
|
+
<AnimatePresence mode="popLayout" initial={false}>
|
|
60
|
+
{onAdd && (
|
|
61
|
+
<motion.div
|
|
62
|
+
key="scaffold-add"
|
|
63
|
+
layout
|
|
64
|
+
animate={reducedMotion ? { opacity: 1 } : "visible"}
|
|
65
|
+
className="flex shrink-0"
|
|
66
|
+
exit={reducedMotion ? { opacity: 0 } : "exit"}
|
|
67
|
+
initial={reducedMotion ? { opacity: 0 } : "hidden"}
|
|
68
|
+
transition={transition}
|
|
69
|
+
variants={segmentVariants}
|
|
70
|
+
>
|
|
71
|
+
<Button
|
|
72
|
+
onClick={onAdd}
|
|
73
|
+
size="sm"
|
|
74
|
+
sound={sound}
|
|
75
|
+
variant="ghost"
|
|
76
|
+
className="h-full gap-2 rounded-sm py-1.5 pr-3 pl-2"
|
|
77
|
+
>
|
|
78
|
+
{addIcon ?? <LucidePlus className="stroke-2" />}
|
|
79
|
+
{addLabel}
|
|
80
|
+
</Button>
|
|
81
|
+
</motion.div>
|
|
82
|
+
)}
|
|
83
|
+
{children}
|
|
84
|
+
</AnimatePresence>
|
|
85
|
+
</motion.div>
|
|
86
|
+
</motion.div>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { AnimatePresence } from "motion/react"
|
|
4
|
+
import * as React from "react"
|
|
5
|
+
|
|
6
|
+
import { cn } from "@/lib/utils"
|
|
7
|
+
import type { ScaffoldCanvasProps } from "./type"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The panel row below the action cluster: 8px gutters, 8px gaps, panels
|
|
11
|
+
* side by side. Key each `Scaffold.Panel` child so closes animate out.
|
|
12
|
+
*/
|
|
13
|
+
export function ScaffoldCanvas({
|
|
14
|
+
className,
|
|
15
|
+
children,
|
|
16
|
+
...rest
|
|
17
|
+
}: ScaffoldCanvasProps): React.ReactElement {
|
|
18
|
+
return (
|
|
19
|
+
<div
|
|
20
|
+
className={cn("relative flex w-full flex-1 gap-2", className)}
|
|
21
|
+
data-slot="scaffold-canvas"
|
|
22
|
+
{...rest}
|
|
23
|
+
>
|
|
24
|
+
<AnimatePresence initial={false} mode="popLayout">
|
|
25
|
+
{children}
|
|
26
|
+
</AnimatePresence>
|
|
27
|
+
</div>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
|
|
5
|
+
import type { ScaffoldContextValue } from "./type"
|
|
6
|
+
|
|
7
|
+
export const ScaffoldContext = React.createContext<ScaffoldContextValue | null>(
|
|
8
|
+
null
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
/** Read the scaffold's shared state; must run under `Scaffold.Root`. */
|
|
12
|
+
export function useScaffoldContext(): ScaffoldContextValue {
|
|
13
|
+
const context = React.useContext(ScaffoldContext)
|
|
14
|
+
if (!context) {
|
|
15
|
+
throw new Error("useScaffoldContext must be used within <Scaffold.Root>")
|
|
16
|
+
}
|
|
17
|
+
return context
|
|
18
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { motion, useReducedMotion } from "motion/react"
|
|
4
|
+
import * as React from "react"
|
|
5
|
+
|
|
6
|
+
import { cn } from "@/lib/utils"
|
|
7
|
+
import { SCAFFOLD_EASE, SCAFFOLD_EDGE_GUTTER, SCAFFOLD_MORPH_DURATION } from "./constants"
|
|
8
|
+
import { useScaffoldContext } from "./scaffold-context"
|
|
9
|
+
import type { ScaffoldInspectorProps } from "./type"
|
|
10
|
+
import { GlassContainer } from "@/components/ui/glasscn/glass-container.tsx"
|
|
11
|
+
import { Button } from "@/components/ui/button"
|
|
12
|
+
import { LucideX } from "lucide-react"
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The right-hand drawer: a brighter card that slides in over the canvas,
|
|
16
|
+
* inset from the viewport edges. Open state lives on `Scaffold.Root` (or
|
|
17
|
+
* `useScaffold().providerProps`); the drawer stays mounted so it can slide
|
|
18
|
+
* back out.
|
|
19
|
+
*/
|
|
20
|
+
export function ScaffoldInspector({
|
|
21
|
+
children,
|
|
22
|
+
name = "Inspector",
|
|
23
|
+
className,
|
|
24
|
+
...rest
|
|
25
|
+
}: ScaffoldInspectorProps): React.ReactElement {
|
|
26
|
+
const { inspectorOpen, inspectorWidth, toggleInspector } =
|
|
27
|
+
useScaffoldContext()
|
|
28
|
+
const reducedMotion = useReducedMotion()
|
|
29
|
+
|
|
30
|
+
// Slide fully past the edge gutter and the drop shadow's blur radius.
|
|
31
|
+
const offcanvasX = inspectorWidth + SCAFFOLD_EDGE_GUTTER + 12
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<motion.aside
|
|
35
|
+
id="scaffold-inspector"
|
|
36
|
+
animate={{ x: inspectorOpen ? 0 : offcanvasX }}
|
|
37
|
+
aria-hidden={inspectorOpen ? undefined : true}
|
|
38
|
+
aria-label={name}
|
|
39
|
+
className={cn(
|
|
40
|
+
"absolute right-0 z-20 my-2 flex h-full max-h-[calc(100%-0.5rem)] w-96 flex-col",
|
|
41
|
+
!inspectorOpen && "pointer-events-none",
|
|
42
|
+
className
|
|
43
|
+
)}
|
|
44
|
+
data-slot="scaffold-inspector"
|
|
45
|
+
inert={inspectorOpen ? undefined : true}
|
|
46
|
+
initial={false}
|
|
47
|
+
role="complementary"
|
|
48
|
+
style={{ width: inspectorWidth }}
|
|
49
|
+
transition={{
|
|
50
|
+
duration: reducedMotion ? 0 : SCAFFOLD_MORPH_DURATION,
|
|
51
|
+
ease: SCAFFOLD_EASE,
|
|
52
|
+
}}
|
|
53
|
+
{...rest}
|
|
54
|
+
>
|
|
55
|
+
<GlassContainer
|
|
56
|
+
className="flex flex-1 flex-col rounded-md border-t border-white bg-neutral-50/10 shadow-inner shadow-md dark:border-neutral-800 dark:bg-black/5"
|
|
57
|
+
glassVariant="subtle"
|
|
58
|
+
>
|
|
59
|
+
<div
|
|
60
|
+
id="scaffold-inspector-header"
|
|
61
|
+
className="flex h-12 items-center justify-between gap-2 border-b pr-3 pl-4"
|
|
62
|
+
>
|
|
63
|
+
<span className="text-base font-normal text-card-foreground/70">
|
|
64
|
+
{name}
|
|
65
|
+
</span>
|
|
66
|
+
<Button
|
|
67
|
+
size="icon-xs"
|
|
68
|
+
variant="secondary"
|
|
69
|
+
className="scale-85"
|
|
70
|
+
aria-label="Close inspector"
|
|
71
|
+
onClick={toggleInspector}
|
|
72
|
+
>
|
|
73
|
+
<LucideX className="stroke-3" />
|
|
74
|
+
</Button>
|
|
75
|
+
</div>
|
|
76
|
+
{children}
|
|
77
|
+
</GlassContainer>
|
|
78
|
+
</motion.aside>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
import type { ScaffoldMainProps } from "./type"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The region right of the rail. Hosts `Scaffold.Actions` (top-right),
|
|
10
|
+
* `Scaffold.Canvas` and the `Scaffold.Inspector` overlay — all positioned
|
|
11
|
+
* against this box.
|
|
12
|
+
*/
|
|
13
|
+
export function ScaffoldMain({
|
|
14
|
+
className,
|
|
15
|
+
children,
|
|
16
|
+
...rest
|
|
17
|
+
}: ScaffoldMainProps): React.ReactElement {
|
|
18
|
+
return (
|
|
19
|
+
<div
|
|
20
|
+
id="scaffold-main"
|
|
21
|
+
className={cn("relative flex h-full min-w-0 flex-1 flex-col", className)}
|
|
22
|
+
data-slot="scaffold-main"
|
|
23
|
+
{...rest}
|
|
24
|
+
>
|
|
25
|
+
{children}
|
|
26
|
+
</div>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { motion, useReducedMotion } from "motion/react"
|
|
4
|
+
import * as React from "react"
|
|
5
|
+
import { useState } from "react"
|
|
6
|
+
|
|
7
|
+
import { cn } from "@/lib/utils"
|
|
8
|
+
import { SCAFFOLD_EASE, SCAFFOLD_MORPH_DURATION } from "./constants"
|
|
9
|
+
import { PanelMenuButton } from "./panel-menu-button.tsx"
|
|
10
|
+
import type { ScaffoldPanelProps, TSubPanelPosition } from "./type"
|
|
11
|
+
import { ScaffoldSubPanel } from "@/components/blocks/scaffold/scaffold-sub-panel.tsx"
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A canvas panel: a primary card plus an optional secondary strip below it,
|
|
15
|
+
* separated by a slice of the desktop backdrop. Floating close and swap
|
|
16
|
+
* buttons reveal on hover when their callbacks are provided. Swapping trades
|
|
17
|
+
* the two cards with a layout morph — the strip glides up and grows into the
|
|
18
|
+
* primary slot while the primary card shrinks down into the strip.
|
|
19
|
+
*/
|
|
20
|
+
export function ScaffoldPanel({
|
|
21
|
+
children,
|
|
22
|
+
SecondaryPanel,
|
|
23
|
+
onSwap,
|
|
24
|
+
onCloseSecondary,
|
|
25
|
+
name,
|
|
26
|
+
swapLabel = "Swap panel content",
|
|
27
|
+
sound = true,
|
|
28
|
+
className,
|
|
29
|
+
}: ScaffoldPanelProps): React.ReactElement {
|
|
30
|
+
const reducedMotion = useReducedMotion()
|
|
31
|
+
|
|
32
|
+
const transition = {
|
|
33
|
+
duration: reducedMotion ? 0 : SCAFFOLD_MORPH_DURATION,
|
|
34
|
+
ease: SCAFFOLD_EASE,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const [subPanelPosition, setSubPanelPosition] =
|
|
38
|
+
useState<TSubPanelPosition>("top")
|
|
39
|
+
const [isSwapped, setIsSwapped] = useState<boolean>(false)
|
|
40
|
+
const handleSwap = () => {
|
|
41
|
+
setIsSwapped((prev) => !prev)
|
|
42
|
+
onSwap?.()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Trades which sub-panel holds the flexible slot — the collapsed card grows
|
|
46
|
+
// into `flex-1` while the expanded one shrinks to the `min-h-14` strip.
|
|
47
|
+
const handleExpand = () => {
|
|
48
|
+
setSubPanelPosition((prev) => (prev === "top" ? "bottom" : "top"))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<motion.section
|
|
53
|
+
id="scaffold-panel"
|
|
54
|
+
animate={{ flexDirection: isSwapped ? "column-reverse" : "column" }}
|
|
55
|
+
aria-label={name}
|
|
56
|
+
className={cn(
|
|
57
|
+
"group/panel relative flex min-w-80 flex-1 flex-col",
|
|
58
|
+
className
|
|
59
|
+
)}
|
|
60
|
+
layout
|
|
61
|
+
data-slot="scaffold-panel"
|
|
62
|
+
data-swapped={isSwapped ? "" : undefined}
|
|
63
|
+
transition={transition}
|
|
64
|
+
>
|
|
65
|
+
<ScaffoldSubPanel id="top" expanded={subPanelPosition === "top"} primary>
|
|
66
|
+
{children}
|
|
67
|
+
</ScaffoldSubPanel>
|
|
68
|
+
{(onSwap || onCloseSecondary) && SecondaryPanel && (
|
|
69
|
+
<div className="relative flex h-2 items-center justify-center">
|
|
70
|
+
<PanelMenuButton
|
|
71
|
+
label={swapLabel}
|
|
72
|
+
onClick={handleSwap}
|
|
73
|
+
onExpand={handleExpand}
|
|
74
|
+
onCloseSecondary={onCloseSecondary}
|
|
75
|
+
secondaryExpanded={subPanelPosition === "bottom"}
|
|
76
|
+
swapped={isSwapped}
|
|
77
|
+
sound={sound}
|
|
78
|
+
/>
|
|
79
|
+
</div>
|
|
80
|
+
)}
|
|
81
|
+
{SecondaryPanel && (
|
|
82
|
+
<ScaffoldSubPanel id="bottom" expanded={subPanelPosition === "bottom"}>
|
|
83
|
+
{SecondaryPanel}
|
|
84
|
+
</ScaffoldSubPanel>
|
|
85
|
+
)}
|
|
86
|
+
</motion.section>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
import { SCAFFOLD_INSPECTOR_WIDTH } from "./constants"
|
|
7
|
+
import { ScaffoldContext } from "./scaffold-context"
|
|
8
|
+
import type { ScaffoldContextValue, ScaffoldRootProps } from "./type"
|
|
9
|
+
import type { ClassNameValue } from "tailwind-merge"
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The scaffold's viewport: desktop backdrop + horizontal row of rail and
|
|
13
|
+
* main region, and the provider for the shared inspector state. Fills its
|
|
14
|
+
* container — size it from the outside (e.g. `h-svh` for a full page).
|
|
15
|
+
*/
|
|
16
|
+
export function ScaffoldRoot({
|
|
17
|
+
inspectorOpen: inspectorOpenProp,
|
|
18
|
+
defaultInspectorOpen,
|
|
19
|
+
onInspectorOpenChange,
|
|
20
|
+
inspectorWidth = SCAFFOLD_INSPECTOR_WIDTH,
|
|
21
|
+
className,
|
|
22
|
+
children,
|
|
23
|
+
...rest
|
|
24
|
+
}: ScaffoldRootProps): React.ReactElement {
|
|
25
|
+
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(
|
|
26
|
+
defaultInspectorOpen ?? false
|
|
27
|
+
)
|
|
28
|
+
const inspectorOpen = inspectorOpenProp ?? uncontrolledOpen
|
|
29
|
+
const background: ClassNameValue = `bg-linear-to-tr/increasing from-neutral-200 via-neutral-100 to-stone-200 dark:from-neutral-900 dark:via-neutral-950 dark:to-stone-950`
|
|
30
|
+
|
|
31
|
+
const setInspectorOpen = React.useCallback(
|
|
32
|
+
(open: boolean) => {
|
|
33
|
+
if (inspectorOpenProp === undefined) setUncontrolledOpen(open)
|
|
34
|
+
onInspectorOpenChange?.(open)
|
|
35
|
+
},
|
|
36
|
+
[inspectorOpenProp, onInspectorOpenChange]
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
const toggleInspector = React.useCallback(
|
|
40
|
+
() => setInspectorOpen(!inspectorOpen),
|
|
41
|
+
[inspectorOpen, setInspectorOpen]
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
const value = React.useMemo<ScaffoldContextValue>(
|
|
45
|
+
() => ({ inspectorOpen, inspectorWidth, setInspectorOpen, toggleInspector }),
|
|
46
|
+
[inspectorOpen, inspectorWidth, setInspectorOpen, toggleInspector]
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<ScaffoldContext.Provider value={value}>
|
|
51
|
+
<div
|
|
52
|
+
id={"scaffold-root"}
|
|
53
|
+
className={cn(
|
|
54
|
+
// `clip`, not `hidden`: the off canvas inspector extends the
|
|
55
|
+
// scrollable overflow, and focus/scrollIntoView would scroll a
|
|
56
|
+
// hidden-overflow root sideways to reveal it.
|
|
57
|
+
"relative isolate flex h-full w-full flex-1 overflow-clip pr-2 pb-2",
|
|
58
|
+
background,
|
|
59
|
+
className
|
|
60
|
+
)}
|
|
61
|
+
data-slot="scaffold"
|
|
62
|
+
{...rest}
|
|
63
|
+
>
|
|
64
|
+
{children}
|
|
65
|
+
</div>
|
|
66
|
+
</ScaffoldContext.Provider>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
import type { ScaffoldSidebarProps } from "./type"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The 64px icon rail on the left edge. Transparent — it sits directly on
|
|
10
|
+
* the desktop backdrop; fill it with icon buttons.
|
|
11
|
+
*/
|
|
12
|
+
export function ScaffoldSidebar({
|
|
13
|
+
className,
|
|
14
|
+
children,
|
|
15
|
+
...rest
|
|
16
|
+
}: ScaffoldSidebarProps): React.ReactElement {
|
|
17
|
+
return (
|
|
18
|
+
<nav
|
|
19
|
+
id="scaffold-sidebar"
|
|
20
|
+
aria-label="Primary"
|
|
21
|
+
className={cn(
|
|
22
|
+
"flex min-w-12 shrink-0 flex-col items-center gap-1 pt-10 pb-2",
|
|
23
|
+
className
|
|
24
|
+
)}
|
|
25
|
+
data-slot="scaffold-sidebar"
|
|
26
|
+
{...rest}
|
|
27
|
+
>
|
|
28
|
+
{children}
|
|
29
|
+
</nav>
|
|
30
|
+
)
|
|
31
|
+
}
|