@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.
Files changed (114) hide show
  1. package/dist-lib/components/blocks/nav/sidebar-block.d.ts +1 -1
  2. package/dist-lib/components/blocks/scaffold/__tests__/scaffold.test.d.ts +1 -0
  3. package/dist-lib/components/blocks/scaffold/constants.d.ts +23 -0
  4. package/dist-lib/components/blocks/scaffold/index.d.ts +27 -0
  5. package/dist-lib/components/blocks/scaffold/panel-close-button.d.ts +4 -0
  6. package/dist-lib/components/blocks/scaffold/panel-menu-button.d.ts +8 -0
  7. package/dist-lib/components/blocks/scaffold/scaffold-actions.d.ts +10 -0
  8. package/dist-lib/components/blocks/scaffold/scaffold-canvas.d.ts +7 -0
  9. package/dist-lib/components/blocks/scaffold/scaffold-context.d.ts +5 -0
  10. package/dist-lib/components/blocks/scaffold/scaffold-inspector.d.ts +9 -0
  11. package/dist-lib/components/blocks/scaffold/scaffold-main.d.ts +8 -0
  12. package/dist-lib/components/blocks/scaffold/scaffold-panel.d.ts +10 -0
  13. package/dist-lib/components/blocks/scaffold/scaffold-root.d.ts +8 -0
  14. package/dist-lib/components/blocks/scaffold/scaffold-sidebar.d.ts +7 -0
  15. package/dist-lib/components/blocks/scaffold/scaffold-sub-panel.d.ts +3 -0
  16. package/dist-lib/components/blocks/scaffold/scaffold-tab.d.ts +9 -0
  17. package/dist-lib/components/blocks/scaffold/type.d.ts +135 -0
  18. package/dist-lib/components/blocks/scaffold/use-scaffold.d.ts +8 -0
  19. package/dist-lib/components/blocks/scaffold/utils.d.ts +21 -0
  20. package/dist-lib/components/blocks/shell/animated-panel-inset.d.ts +2 -0
  21. package/dist-lib/components/blocks/shell/animated-panel-provider.d.ts +2 -0
  22. package/dist-lib/components/blocks/shell/animated-panel-trigger.d.ts +2 -0
  23. package/dist-lib/components/blocks/shell/animated-panel.d.ts +2 -0
  24. package/dist-lib/components/blocks/shell/animated-sidebar-content.d.ts +2 -0
  25. package/dist-lib/components/blocks/shell/animated-sidebar-footer.d.ts +2 -0
  26. package/dist-lib/components/blocks/shell/animated-sidebar-group-content.d.ts +2 -0
  27. package/dist-lib/components/blocks/shell/animated-sidebar-group-label.d.ts +2 -0
  28. package/dist-lib/components/blocks/shell/animated-sidebar-group.d.ts +2 -0
  29. package/dist-lib/components/blocks/shell/animated-sidebar-header.d.ts +2 -0
  30. package/dist-lib/components/blocks/shell/animated-sidebar-menu-button.d.ts +3 -0
  31. package/dist-lib/components/blocks/shell/animated-sidebar-menu-item.d.ts +2 -0
  32. package/dist-lib/components/blocks/shell/animated-sidebar-menu-sub-button.d.ts +3 -0
  33. package/dist-lib/components/blocks/shell/animated-sidebar-menu-sub-item.d.ts +2 -0
  34. package/dist-lib/components/blocks/shell/animated-sidebar-menu-sub.d.ts +2 -0
  35. package/dist-lib/components/blocks/shell/animated-sidebar-menu.d.ts +2 -0
  36. package/dist-lib/components/blocks/shell/index.d.ts +16 -0
  37. package/dist-lib/components/blocks/shell/type.d.ts +90 -4
  38. package/dist-lib/components/blocks/shell/utils.d.ts +37 -0
  39. package/dist-lib/components/motion/index.d.ts +1 -0
  40. package/dist-lib/components/motion/overflow-actions.d.ts +35 -0
  41. package/dist-lib/components/ui/button-group.d.ts +11 -0
  42. package/dist-lib/components/ui/glasscn/glass-button-group.d.ts +5 -0
  43. package/dist-lib/components/ui/glasscn/glass-container.d.ts +7 -0
  44. package/dist-lib/components/ui/glasscn/glass-separator.d.ts +5 -0
  45. package/dist-lib/components/ui/group.d.ts +17 -0
  46. package/dist-lib/components/ui/separator.d.ts +2 -3
  47. package/dist-lib/components/ui/sheet.d.ts +14 -0
  48. package/dist-lib/components/ui/tooltip.d.ts +6 -0
  49. package/dist-lib/index.d.ts +2 -1
  50. package/dist-lib/index.js +1631 -1101
  51. package/dist-lib/index.js.map +1 -1
  52. package/dist-lib/lib/ease.d.ts +3 -0
  53. package/dist-lib/lib/hooks/use-hover-capable.d.ts +6 -0
  54. package/package.json +2 -1
  55. package/src/components/beste/piece/browser-frame.tsx +2 -2
  56. package/src/components/blocks/nav/sidebar-block.tsx +5 -5
  57. package/src/components/blocks/nav/sidebar-nav-row.tsx +4 -5
  58. package/src/components/blocks/scaffold/__tests__/scaffold.test.tsx +286 -0
  59. package/src/components/blocks/scaffold/constants.ts +32 -0
  60. package/src/components/blocks/scaffold/index.ts +41 -0
  61. package/src/components/blocks/scaffold/panel-close-button.tsx +36 -0
  62. package/src/components/blocks/scaffold/panel-menu-button.tsx +218 -0
  63. package/src/components/blocks/scaffold/scaffold-actions.tsx +88 -0
  64. package/src/components/blocks/scaffold/scaffold-canvas.tsx +29 -0
  65. package/src/components/blocks/scaffold/scaffold-context.ts +18 -0
  66. package/src/components/blocks/scaffold/scaffold-inspector.tsx +80 -0
  67. package/src/components/blocks/scaffold/scaffold-main.tsx +28 -0
  68. package/src/components/blocks/scaffold/scaffold-panel.tsx +88 -0
  69. package/src/components/blocks/scaffold/scaffold-root.tsx +68 -0
  70. package/src/components/blocks/scaffold/scaffold-sidebar.tsx +31 -0
  71. package/src/components/blocks/scaffold/scaffold-sub-panel.tsx +51 -0
  72. package/src/components/blocks/scaffold/scaffold-tab.tsx +66 -0
  73. package/src/components/blocks/scaffold/type.ts +172 -0
  74. package/src/components/blocks/scaffold/use-scaffold.ts +48 -0
  75. package/src/components/blocks/scaffold/utils.ts +46 -0
  76. package/src/components/blocks/shell/animated-panel-inset.tsx +23 -0
  77. package/src/components/blocks/shell/animated-panel-provider.tsx +155 -0
  78. package/src/components/blocks/shell/animated-panel-trigger.tsx +42 -0
  79. package/src/components/blocks/shell/animated-panel.tsx +135 -0
  80. package/src/components/blocks/shell/animated-sidebar-content.tsx +19 -0
  81. package/src/components/blocks/shell/animated-sidebar-footer.tsx +19 -0
  82. package/src/components/blocks/shell/animated-sidebar-group-content.tsx +16 -0
  83. package/src/components/blocks/shell/animated-sidebar-group-label.tsx +29 -0
  84. package/src/components/blocks/shell/animated-sidebar-group.tsx +16 -0
  85. package/src/components/blocks/shell/animated-sidebar-header.tsx +16 -0
  86. package/src/components/blocks/shell/animated-sidebar-menu-button.tsx +151 -0
  87. package/src/components/blocks/shell/animated-sidebar-menu-item.tsx +20 -0
  88. package/src/components/blocks/shell/animated-sidebar-menu-sub-button.tsx +85 -0
  89. package/src/components/blocks/shell/animated-sidebar-menu-sub-item.tsx +19 -0
  90. package/src/components/blocks/shell/animated-sidebar-menu-sub.tsx +44 -0
  91. package/src/components/blocks/shell/animated-sidebar-menu.tsx +26 -0
  92. package/src/components/blocks/shell/index.ts +18 -0
  93. package/src/components/blocks/shell/shell-layout.tsx +28 -36
  94. package/src/components/blocks/shell/type.ts +112 -6
  95. package/src/components/blocks/shell/use-shell-panels.ts +1 -1
  96. package/src/components/blocks/shell/utils.ts +142 -1
  97. package/src/components/docs/demo-controls.tsx +1 -1
  98. package/src/components/motion/index.ts +0 -0
  99. package/src/components/motion/overflow-actions.tsx +376 -0
  100. package/src/components/ui/button-group.tsx +88 -0
  101. package/src/components/ui/glasscn/glass-button-group.tsx +48 -0
  102. package/src/components/ui/glasscn/glass-container.tsx +125 -0
  103. package/src/components/ui/glasscn/glass-separator.tsx +39 -0
  104. package/src/components/ui/glasscn/liquid-glass.tsx +250 -210
  105. package/src/components/ui/group.tsx +92 -0
  106. package/src/components/ui/separator.tsx +11 -11
  107. package/src/components/ui/sheet.tsx +135 -0
  108. package/src/components/ui/tooltip.tsx +66 -0
  109. package/src/index.css +1 -1
  110. package/src/index.ts +13 -1
  111. package/src/lib/ease.ts +26 -0
  112. package/src/lib/hooks/use-hover-capable.ts +23 -0
  113. package/dist-lib/components/motion/animated-sidebar.d.ts +0 -106
  114. 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
+ }