@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,51 @@
|
|
|
1
|
+
|
|
2
|
+
import * as React from "react"
|
|
3
|
+
import { motion, useReducedMotion } from "framer-motion"
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
import type { ScaffoldSubPanelProps } from "./type"
|
|
6
|
+
import { subPanelVariant } from "@/components/blocks/scaffold/utils.ts"
|
|
7
|
+
import { SCAFFOLD_EASE, SCAFFOLD_MORPH_DURATION } from "./constants"
|
|
8
|
+
|
|
9
|
+
export function ScaffoldSubPanel({
|
|
10
|
+
className,
|
|
11
|
+
children,
|
|
12
|
+
variant = 'card',
|
|
13
|
+
primary = false,
|
|
14
|
+
expanded,
|
|
15
|
+
...rest
|
|
16
|
+
}: ScaffoldSubPanelProps): React.ReactElement {
|
|
17
|
+
const reduce = useReducedMotion()
|
|
18
|
+
// The primary card holds the flexible slot unless the panel says otherwise.
|
|
19
|
+
const isExpanded = expanded ?? primary
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<motion.div
|
|
23
|
+
layout={"size"}
|
|
24
|
+
id={`scaffold-sub-panel-${variant}`}
|
|
25
|
+
// Grow is animated as a style, not a class flip — a class flip rides the
|
|
26
|
+
// layout FLIP, which scales both cards into a swap-like morph. Animating
|
|
27
|
+
// flex-grow slides only the shared boundary: the collapsed card grows up
|
|
28
|
+
// (or down) while the other is squeezed and cropped in place.
|
|
29
|
+
initial={false}
|
|
30
|
+
animate={{ flexGrow: isExpanded ? 1 : 0 }}
|
|
31
|
+
transition={
|
|
32
|
+
reduce
|
|
33
|
+
? { duration: 0 }
|
|
34
|
+
: { duration: SCAFFOLD_MORPH_DURATION, ease: SCAFFOLD_EASE }
|
|
35
|
+
}
|
|
36
|
+
className={cn(
|
|
37
|
+
"flex w-full flex-col",
|
|
38
|
+
subPanelVariant[variant],
|
|
39
|
+
// basis-14 + clip let the primary squeeze down to the strip height;
|
|
40
|
+
// the strip keeps its natural (basis-auto) height when collapsed.
|
|
41
|
+
primary ? "basis-14 min-h-14 overflow-clip" : "basis-auto min-h-14",
|
|
42
|
+
className
|
|
43
|
+
)}
|
|
44
|
+
data-slot={`scaffold-sub-panel-${variant}`}
|
|
45
|
+
data-expanded={isExpanded ? "" : undefined}
|
|
46
|
+
{...rest}
|
|
47
|
+
>
|
|
48
|
+
{children}
|
|
49
|
+
</motion.div>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useReducedMotion } from "motion/react"
|
|
4
|
+
import * as React from "react"
|
|
5
|
+
|
|
6
|
+
import { cn } from "@/lib/utils"
|
|
7
|
+
import { SPRING_LAYOUT } from "@/lib/ease.ts"
|
|
8
|
+
import type { ScaffoldTabProps } from "./type"
|
|
9
|
+
import { segmentVariants } from "./utils"
|
|
10
|
+
import { Button } from "@/components/ui/button"
|
|
11
|
+
import { LucideAppWindow, LucideX } from "lucide-react"
|
|
12
|
+
import { GlassButtonGroup } from "@/components/ui/glasscn/glass-button-group.tsx"
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A panel's tab inside the action cluster: its label plus a close
|
|
16
|
+
* affordance. Render one per open panel as Actions children, keyed —
|
|
17
|
+
* closes animate out of the pill. Omit `onClose` on the last remaining
|
|
18
|
+
* panel's tab; the canvas keeps at least one panel open.
|
|
19
|
+
*/
|
|
20
|
+
export function ScaffoldTab({
|
|
21
|
+
children,
|
|
22
|
+
onClose,
|
|
23
|
+
closeLabel = "Close panel",
|
|
24
|
+
sound = true,
|
|
25
|
+
className,
|
|
26
|
+
...rest
|
|
27
|
+
}: ScaffoldTabProps): React.ReactElement {
|
|
28
|
+
const reduce = useReducedMotion()
|
|
29
|
+
const transition = reduce ? { duration: 0 } : SPRING_LAYOUT
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<GlassButtonGroup
|
|
33
|
+
layout
|
|
34
|
+
animate={reduce ? { opacity: 1 } : "visible"}
|
|
35
|
+
data-slot="scaffold-tab"
|
|
36
|
+
exit={reduce ? { opacity: 0 } : "exit"}
|
|
37
|
+
initial={reduce ? { opacity: 0 } : "hidden"}
|
|
38
|
+
transition={transition}
|
|
39
|
+
variants={segmentVariants}
|
|
40
|
+
glassVariant="subtle"
|
|
41
|
+
className={cn("h-full rounded-sm", className)}
|
|
42
|
+
{...rest}
|
|
43
|
+
>
|
|
44
|
+
<Button
|
|
45
|
+
size="sm"
|
|
46
|
+
variant="ghost"
|
|
47
|
+
className={cn("gap-2.5 rounded-sm pl-2", onClose && "pr-2")}
|
|
48
|
+
>
|
|
49
|
+
<LucideAppWindow size={16} />
|
|
50
|
+
{children}
|
|
51
|
+
</Button>
|
|
52
|
+
{onClose && (
|
|
53
|
+
<Button
|
|
54
|
+
aria-label={closeLabel}
|
|
55
|
+
onClick={onClose}
|
|
56
|
+
size="icon-sm"
|
|
57
|
+
sound={sound}
|
|
58
|
+
variant="ghost"
|
|
59
|
+
className="rounded-none!"
|
|
60
|
+
>
|
|
61
|
+
<LucideX size={16} />
|
|
62
|
+
</Button>
|
|
63
|
+
)}
|
|
64
|
+
</GlassButtonGroup>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import React, { type ComponentProps, type ReactElement, type ReactNode, } from "react"
|
|
2
|
+
|
|
3
|
+
/** Inspector state shared by every scaffold part through ScaffoldContext. */
|
|
4
|
+
export interface ScaffoldContextValue {
|
|
5
|
+
/** Whether the inspector drawer is currently shown. */
|
|
6
|
+
inspectorOpen: boolean
|
|
7
|
+
/** Drawer width in px — Actions reads it to slide out of the drawer's way. */
|
|
8
|
+
inspectorWidth: number
|
|
9
|
+
setInspectorOpen: (open: boolean) => void
|
|
10
|
+
toggleInspector: () => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ScaffoldRootProps extends ComponentProps<"div"> {
|
|
14
|
+
/** Controlled inspector state — pair with `onInspectorOpenChange`, or
|
|
15
|
+
* spread `useScaffold().providerProps` instead of wiring by hand. */
|
|
16
|
+
inspectorOpen?: boolean
|
|
17
|
+
/** Initial inspector state when uncontrolled. Closed by default. */
|
|
18
|
+
defaultInspectorOpen?: boolean
|
|
19
|
+
onInspectorOpenChange?: (open: boolean) => void
|
|
20
|
+
/** Drawer width in px. Also drives how far Actions slides aside. */
|
|
21
|
+
inspectorWidth?: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type ScaffoldSidebarProps = ComponentProps<"nav">
|
|
25
|
+
|
|
26
|
+
export type ScaffoldMainProps = ComponentProps<"div">
|
|
27
|
+
|
|
28
|
+
export type ScaffoldCanvasProps = ComponentProps<"div">
|
|
29
|
+
|
|
30
|
+
/** React's drag/animation DOM handlers collide with motion's gesture props
|
|
31
|
+
* on `motion.*` elements — strip them from pass-through HTML props. */
|
|
32
|
+
type MotionSafe<T> = Omit<
|
|
33
|
+
T,
|
|
34
|
+
| "onDrag"
|
|
35
|
+
| "onDragStart"
|
|
36
|
+
| "onDragEnd"
|
|
37
|
+
| "onAnimationStart"
|
|
38
|
+
| "onAnimationEnd"
|
|
39
|
+
| "onAnimationIteration"
|
|
40
|
+
>
|
|
41
|
+
|
|
42
|
+
export interface ScaffoldActionsProps extends MotionSafe<
|
|
43
|
+
Omit<ComponentProps<"div">, "children">
|
|
44
|
+
> {
|
|
45
|
+
/** Called when the Add segment is pressed. The segment renders only
|
|
46
|
+
* while this is present — omit it to hide Add, e.g. once the canvas
|
|
47
|
+
* holds `SCAFFOLD_PANEL_CAPACITY` panels. */
|
|
48
|
+
onAdd?: () => void
|
|
49
|
+
/** Label of the Add segment. */
|
|
50
|
+
addLabel?: ReactNode
|
|
51
|
+
/** Replaces the default add icon. */
|
|
52
|
+
addIcon?: ReactNode
|
|
53
|
+
/** The panel tabs (`Scaffold.Tab`, one per open panel) and any extra
|
|
54
|
+
* segments, rendered before Add. Key each tab — closes animate out. */
|
|
55
|
+
children?: ReactNode
|
|
56
|
+
/** Emit interaction-sound attributes (inert until an app binds cuelume). */
|
|
57
|
+
sound?: boolean
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type TScaffoldPanelChild<
|
|
61
|
+
T extends ScaffoldSubPanelProps = ScaffoldSubPanelProps,
|
|
62
|
+
> = ReactElement<T, React.JSXElementConstructor<T>>
|
|
63
|
+
|
|
64
|
+
export interface ScaffoldPanelProps {
|
|
65
|
+
/** The panel's content, rendered in a card with a drop shadow. */
|
|
66
|
+
children: ReactNode
|
|
67
|
+
/** The panel's content, rendered in a card with a drop shadow. */
|
|
68
|
+
SecondaryPanel?: TScaffoldPanelChild
|
|
69
|
+
/** Reveals the Swap action in the seam menu (needs `SecondaryPanel`).
|
|
70
|
+
* Swapping trades the two cards with a layout morph — flip the slot
|
|
71
|
+
* content here so each card carries its subject to its new home. */
|
|
72
|
+
onSwap?: () => void
|
|
73
|
+
/** Reveals the Close action in the seam menu — remove `SecondaryPanel`
|
|
74
|
+
* here; the strip and the menu leave together. */
|
|
75
|
+
onCloseSecondary?: () => void
|
|
76
|
+
/** Accessible name of the panel region. */
|
|
77
|
+
name?: string
|
|
78
|
+
/** Emit interaction-sound attributes on the floating buttons. */
|
|
79
|
+
sound?: boolean
|
|
80
|
+
/** Accessible name of the seam-menu toggle. */
|
|
81
|
+
swapLabel?: string
|
|
82
|
+
/** Extra class names for the panel's root element. */
|
|
83
|
+
className?: string
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface ScaffoldTabProps extends MotionSafe<
|
|
87
|
+
Omit<ComponentProps<"div">, "children">
|
|
88
|
+
> {
|
|
89
|
+
/** The tab's label. */
|
|
90
|
+
children?: ReactNode
|
|
91
|
+
/** Renders the tab's close button. The button renders only while this
|
|
92
|
+
* is present — omit it to pin the tab, e.g. on the last remaining
|
|
93
|
+
* panel (the canvas keeps at least one open). */
|
|
94
|
+
onClose?: () => void
|
|
95
|
+
/** Accessible name of the close button. */
|
|
96
|
+
closeLabel?: string
|
|
97
|
+
/** Emit interaction-sound attributes on the close button. */
|
|
98
|
+
sound?: boolean
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export type TSubPanelPosition = "bottom" | "top"
|
|
102
|
+
export type TSubPanelVariant = "card" | "subtle" | "inset"
|
|
103
|
+
|
|
104
|
+
export interface ScaffoldSubPanelProps extends MotionSafe<
|
|
105
|
+
Omit<ComponentProps<"div">, "children">
|
|
106
|
+
> {
|
|
107
|
+
children?: ReactNode
|
|
108
|
+
/** Accessible name of the sub-panel region. */
|
|
109
|
+
name?: string
|
|
110
|
+
/** Whether this is the primary or secondary sub-panel. */
|
|
111
|
+
primary?: boolean
|
|
112
|
+
/** Whether this sub-panel holds the flexible slot. Defaults to `primary` —
|
|
113
|
+
* the panel flips it when the Expand action trades the slots. */
|
|
114
|
+
expanded?: boolean
|
|
115
|
+
/** Visual style of the sub-panel. */
|
|
116
|
+
variant?: TSubPanelVariant
|
|
117
|
+
/** Fixed width in px; omitted, the sub-panel flexes to share the canvas. */
|
|
118
|
+
width?: number
|
|
119
|
+
/** Emit interaction-sound attributes on the floating buttons. */
|
|
120
|
+
sound?: boolean
|
|
121
|
+
/** Extra class names for the sub-panel's root element. */
|
|
122
|
+
className?: string
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface ScaffoldInspectorProps extends MotionSafe<
|
|
126
|
+
Omit<ComponentProps<"aside">, "children">
|
|
127
|
+
> {
|
|
128
|
+
children?: ReactNode
|
|
129
|
+
/** Accessible name of the drawer region. */
|
|
130
|
+
name?: string
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface PanelFloatingButtonProps {
|
|
134
|
+
onClick?: () => void
|
|
135
|
+
/** Accessible name — the button is icon-only. */
|
|
136
|
+
label: string
|
|
137
|
+
sound?: boolean
|
|
138
|
+
className?: string
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface PanelMenuButtonProps extends PanelFloatingButtonProps {
|
|
142
|
+
/** Called when the Expand action is pressed — the panel trades which
|
|
143
|
+
* sub-panel holds the flexible slot. */
|
|
144
|
+
onExpand?: () => void
|
|
145
|
+
/** Renders the Close action; called when it is pressed — the consumer
|
|
146
|
+
* removes the secondary strip. */
|
|
147
|
+
onCloseSecondary?: () => void
|
|
148
|
+
/** Whether the secondary strip currently holds the flexible slot —
|
|
149
|
+
* flips the Expand chevron between down and up. */
|
|
150
|
+
secondaryExpanded?: boolean
|
|
151
|
+
/** Whether the panel is swapped (column-reverse). Mirrors the Expand
|
|
152
|
+
* chevron so it keeps pointing at the sub-panel that grows next. */
|
|
153
|
+
swapped?: boolean
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface UseScaffoldOptions {
|
|
157
|
+
/** Initial inspector state. Closed by default. */
|
|
158
|
+
defaultInspectorOpen?: boolean
|
|
159
|
+
/** Fires on every inspector open/close. */
|
|
160
|
+
onInspectorChange?: (open: boolean) => void
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface ScaffoldControls {
|
|
164
|
+
inspectorOpen: boolean
|
|
165
|
+
setInspectorOpen: (open: boolean) => void
|
|
166
|
+
toggleInspector: () => void
|
|
167
|
+
/** Spread onto Scaffold.Root. */
|
|
168
|
+
providerProps: Pick<
|
|
169
|
+
ScaffoldRootProps,
|
|
170
|
+
"inspectorOpen" | "onInspectorOpenChange"
|
|
171
|
+
>
|
|
172
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useCallback, useMemo, useState } from "react"
|
|
4
|
+
|
|
5
|
+
import type { ScaffoldControls, UseScaffoldOptions } from "./type"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Owns the scaffold's inspector state for UI living outside the scaffold
|
|
9
|
+
* (title-bar buttons, command palette, shortcuts). Spread the returned
|
|
10
|
+
* `providerProps` onto `Scaffold.Root`; without this hook the root manages
|
|
11
|
+
* the same state internally via `defaultInspectorOpen`.
|
|
12
|
+
*/
|
|
13
|
+
export function useScaffold({
|
|
14
|
+
defaultInspectorOpen,
|
|
15
|
+
onInspectorChange,
|
|
16
|
+
}: UseScaffoldOptions = {}): ScaffoldControls {
|
|
17
|
+
const [inspectorOpen, setOpenState] = useState(defaultInspectorOpen ?? false)
|
|
18
|
+
|
|
19
|
+
const setInspectorOpen = useCallback(
|
|
20
|
+
(open: boolean) => {
|
|
21
|
+
setOpenState((prev) => {
|
|
22
|
+
if (prev !== open) onInspectorChange?.(open)
|
|
23
|
+
return open
|
|
24
|
+
})
|
|
25
|
+
},
|
|
26
|
+
[onInspectorChange]
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
const toggleInspector = useCallback(
|
|
30
|
+
() => setInspectorOpen(!inspectorOpen),
|
|
31
|
+
[inspectorOpen, setInspectorOpen]
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
const providerProps = useMemo(
|
|
35
|
+
() => ({
|
|
36
|
+
inspectorOpen,
|
|
37
|
+
onInspectorOpenChange: setInspectorOpen,
|
|
38
|
+
}),
|
|
39
|
+
[inspectorOpen, setInspectorOpen]
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
inspectorOpen,
|
|
44
|
+
setInspectorOpen,
|
|
45
|
+
toggleInspector,
|
|
46
|
+
providerProps,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Variants } from "motion/react"
|
|
2
|
+
import type { ClassNameValue } from "tailwind-merge"
|
|
3
|
+
import type { TSubPanelVariant } from "@/components/blocks/scaffold/type.ts"
|
|
4
|
+
import { EASE_OUT } from "@/lib/ease.ts"
|
|
5
|
+
|
|
6
|
+
/** Desktop backdrop the whole scaffold sits on — a soft warm-gray wash. */
|
|
7
|
+
export const scaffoldBackgroundClass: ClassNameValue =
|
|
8
|
+
"bg-linear-to-tr/increasing from-neutral-400 via-stone-200 to-neutral-300 dark:from-neutral-900 dark:via-neutral-950 dark:to-stone-950"
|
|
9
|
+
|
|
10
|
+
/** Card surface shared by a panel's primary area and secondary strip. */
|
|
11
|
+
export const panelSurfaceClass: ClassNameValue =
|
|
12
|
+
"rounded-2xl bg-slate-100 inset-ring-1 inset-ring-white/60 dark:bg-neutral-900 dark:inset-ring-white/5"
|
|
13
|
+
|
|
14
|
+
/** Floating icon button hovering over a panel (close, swap). */
|
|
15
|
+
export const floatingButtonClass: ClassNameValue =
|
|
16
|
+
"transition-[opacity,scale,box-shadow] duration-200 ease-smooth-out hover:text-neutral-900 hover:shadow-md active:scale-97 dark:bg-neutral-800/95 dark:text-neutral-300 dark:inset-ring-white/10 dark:hover:text-white motion-reduce:transition-none motion-reduce:active:scale-100 z-20"
|
|
17
|
+
|
|
18
|
+
/** Hidden until the pointer rests on the panel (always shown for touch and
|
|
19
|
+
* while focused) — the affordances stay quiet until the panel is engaged. */
|
|
20
|
+
export const revealOnPanelHoverClass: ClassNameValue =
|
|
21
|
+
"opacity-0 scale-95 group-hover/panel:opacity-100 group-hover/panel:scale-100 focus-visible:opacity-100 focus-visible:scale-100 pointer-coarse:opacity-100 pointer-coarse:scale-100"
|
|
22
|
+
|
|
23
|
+
/** Pill segment inside the actions cluster (Add, browse). */
|
|
24
|
+
export const actionSegmentClass: ClassNameValue =
|
|
25
|
+
"flex h-[30px] items-center rounded-full text-neutral-700 transition-[background-color,color,scale,box-shadow] duration-150 ease-smooth-out hover:bg-white hover:text-neutral-900 hover:shadow-xs active:scale-97 dark:text-neutral-300 dark:hover:bg-neutral-700 dark:hover:text-white motion-reduce:transition-none motion-reduce:active:scale-100"
|
|
26
|
+
|
|
27
|
+
/** Pill segments (seam-menu actions, tabs, Add) blur/fade in place while
|
|
28
|
+
* the track morphs around them. The exit is a fast tween, not the layout
|
|
29
|
+
* spring — the track stays overflow-visible, so popped-out segments must be
|
|
30
|
+
* gone before the shrinking pill leaves them behind as a ghost. */
|
|
31
|
+
export const segmentVariants: Variants = {
|
|
32
|
+
hidden: { opacity: 0, filter: "blur(4px)" },
|
|
33
|
+
visible: { opacity: 1, filter: "blur(0px)" },
|
|
34
|
+
exit: {
|
|
35
|
+
opacity: 0,
|
|
36
|
+
filter: "blur(4px)",
|
|
37
|
+
transition: { duration: 0.18, ease: EASE_OUT },
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Map of sub-panel variants to their corresponding side. */
|
|
42
|
+
export const subPanelVariant: Record<TSubPanelVariant, ClassNameValue> = {
|
|
43
|
+
card: "bg-neutral-50 dark:bg-neutral-900 rounded-md border-t border-b border-t-white dark:border-t-neutral-700/50 border-b-neutral-900/10 dark:border-b-black/70 shadow-sm shadow-neutral-900/10 dark:shadow-black/50 inset-shadow-sm inset-shadow-white dark:inset-shadow-neutral-800",
|
|
44
|
+
subtle: "bg-neutral-100 dark:bg-neutral-800",
|
|
45
|
+
inset: "bg-neutral-200 dark:bg-neutral-700",
|
|
46
|
+
} as const
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { forwardRef } from "react"
|
|
2
|
+
import { motion } from "motion/react"
|
|
3
|
+
import { cn } from "@/lib/utils.ts"
|
|
4
|
+
import type { AnimatedSidebarInsetProps } from "./type"
|
|
5
|
+
|
|
6
|
+
export const AnimatedPanelInset = forwardRef<
|
|
7
|
+
HTMLElement,
|
|
8
|
+
AnimatedSidebarInsetProps
|
|
9
|
+
>(function AnimatedSidebarInset({ className, ...props }, forwardedRef) {
|
|
10
|
+
return (
|
|
11
|
+
<motion.main
|
|
12
|
+
{...props}
|
|
13
|
+
ref={forwardedRef}
|
|
14
|
+
data-slot="sidebar-inset"
|
|
15
|
+
className={cn(
|
|
16
|
+
"relative flex min-w-0 flex-1 flex-col bg-neutral-50 dark:bg-neutral-900",
|
|
17
|
+
"outline-offset-0.5 overflow-hidden border-t-3 border-white outline-neutral-100 dark:border-neutral-800 dark:inset-ring-black",
|
|
18
|
+
"rounded-lg shadow-md shadow-neutral-200 dark:shadow-neutral-950",
|
|
19
|
+
className
|
|
20
|
+
)}
|
|
21
|
+
/>
|
|
22
|
+
)
|
|
23
|
+
})
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react"
|
|
2
|
+
import { useReducedMotion } from "motion/react"
|
|
3
|
+
import { cn } from "@/lib/utils.ts"
|
|
4
|
+
import type {
|
|
5
|
+
AnimatedSidebarProviderProps,
|
|
6
|
+
SidebarSide,
|
|
7
|
+
} from "./type"
|
|
8
|
+
import {
|
|
9
|
+
AnimatedSidebarContext,
|
|
10
|
+
SIDEBAR_KEYBOARD_SHORTCUT,
|
|
11
|
+
useIsMobile,
|
|
12
|
+
} from "./utils"
|
|
13
|
+
|
|
14
|
+
export function AnimatedPanelProvider({
|
|
15
|
+
children,
|
|
16
|
+
open,
|
|
17
|
+
defaultOpen,
|
|
18
|
+
onOpenChange,
|
|
19
|
+
openMobile,
|
|
20
|
+
defaultOpenMobile,
|
|
21
|
+
onOpenMobileChange,
|
|
22
|
+
className,
|
|
23
|
+
style,
|
|
24
|
+
...props
|
|
25
|
+
}: AnimatedSidebarProviderProps) {
|
|
26
|
+
const [internalOpen, setInternalOpen] = useState<
|
|
27
|
+
Record<SidebarSide, boolean>
|
|
28
|
+
>(() => ({
|
|
29
|
+
left: defaultOpen?.left ?? true,
|
|
30
|
+
right: defaultOpen?.right ?? false,
|
|
31
|
+
}))
|
|
32
|
+
const [internalOpenMobile, setInternalOpenMobile] = useState<
|
|
33
|
+
Record<SidebarSide, boolean>
|
|
34
|
+
>(() => ({
|
|
35
|
+
left: defaultOpenMobile?.left ?? false,
|
|
36
|
+
right: defaultOpenMobile?.right ?? false,
|
|
37
|
+
}))
|
|
38
|
+
const isMobile = useIsMobile()
|
|
39
|
+
const reduce = useReducedMotion() ?? false
|
|
40
|
+
const generatedId = useId()
|
|
41
|
+
const leftTriggerRef = useRef<HTMLButtonElement>(null)
|
|
42
|
+
const rightTriggerRef = useRef<HTMLButtonElement>(null)
|
|
43
|
+
|
|
44
|
+
// Per-side controlled/uncontrolled merge — a side is controlled exactly
|
|
45
|
+
// when its key is present on the controlled record.
|
|
46
|
+
const controlledLeft = open?.left
|
|
47
|
+
const controlledRight = open?.right
|
|
48
|
+
const controlledMobileLeft = openMobile?.left
|
|
49
|
+
const controlledMobileRight = openMobile?.right
|
|
50
|
+
|
|
51
|
+
const openState = useMemo<Record<SidebarSide, boolean>>(
|
|
52
|
+
() => ({
|
|
53
|
+
left: controlledLeft ?? internalOpen.left,
|
|
54
|
+
right: controlledRight ?? internalOpen.right,
|
|
55
|
+
}),
|
|
56
|
+
[controlledLeft, controlledRight, internalOpen]
|
|
57
|
+
)
|
|
58
|
+
const openMobileState = useMemo<Record<SidebarSide, boolean>>(
|
|
59
|
+
() => ({
|
|
60
|
+
left: controlledMobileLeft ?? internalOpenMobile.left,
|
|
61
|
+
right: controlledMobileRight ?? internalOpenMobile.right,
|
|
62
|
+
}),
|
|
63
|
+
[controlledMobileLeft, controlledMobileRight, internalOpenMobile]
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
const setOpen = useCallback(
|
|
67
|
+
(nextOpen: boolean, side: SidebarSide) => {
|
|
68
|
+
const controlled = side === "left" ? controlledLeft : controlledRight
|
|
69
|
+
if (controlled === undefined) {
|
|
70
|
+
setInternalOpen((prev) =>
|
|
71
|
+
prev[side] === nextOpen ? prev : { ...prev, [side]: nextOpen }
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
onOpenChange?.(nextOpen, side)
|
|
75
|
+
},
|
|
76
|
+
[controlledLeft, controlledRight, onOpenChange]
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
const setOpenMobile = useCallback(
|
|
80
|
+
(nextOpen: boolean, side: SidebarSide) => {
|
|
81
|
+
const controlled =
|
|
82
|
+
side === "left" ? controlledMobileLeft : controlledMobileRight
|
|
83
|
+
if (controlled === undefined) {
|
|
84
|
+
setInternalOpenMobile((prev) =>
|
|
85
|
+
prev[side] === nextOpen ? prev : { ...prev, [side]: nextOpen }
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
onOpenMobileChange?.(nextOpen, side)
|
|
89
|
+
},
|
|
90
|
+
[controlledMobileLeft, controlledMobileRight, onOpenMobileChange]
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
const toggleSidebar = useCallback(
|
|
94
|
+
(side: SidebarSide) => {
|
|
95
|
+
if (isMobile) setOpenMobile(!openMobileState[side], side)
|
|
96
|
+
else setOpen(!openState[side], side)
|
|
97
|
+
},
|
|
98
|
+
[isMobile, openMobileState, openState, setOpen, setOpenMobile]
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
const triggerRefs = useMemo(
|
|
102
|
+
() => ({ left: leftTriggerRef, right: rightTriggerRef }),
|
|
103
|
+
[]
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
const handleShortcut = (event: KeyboardEvent) => {
|
|
108
|
+
if (
|
|
109
|
+
event.key.toLowerCase() === SIDEBAR_KEYBOARD_SHORTCUT &&
|
|
110
|
+
(event.metaKey || event.ctrlKey)
|
|
111
|
+
) {
|
|
112
|
+
event.preventDefault()
|
|
113
|
+
toggleSidebar("left")
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
window.addEventListener("keydown", handleShortcut)
|
|
118
|
+
return () => window.removeEventListener("keydown", handleShortcut)
|
|
119
|
+
}, [toggleSidebar])
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<AnimatedSidebarContext.Provider
|
|
123
|
+
value={{
|
|
124
|
+
isMobile,
|
|
125
|
+
layoutId: `${generatedId}-active`,
|
|
126
|
+
open: openState,
|
|
127
|
+
openMobile: openMobileState,
|
|
128
|
+
reduce,
|
|
129
|
+
setOpen,
|
|
130
|
+
setOpenMobile,
|
|
131
|
+
toggleSidebar,
|
|
132
|
+
triggerRefs,
|
|
133
|
+
}}
|
|
134
|
+
>
|
|
135
|
+
<div
|
|
136
|
+
{...props}
|
|
137
|
+
data-slot="sidebar-wrapper"
|
|
138
|
+
data-state-left={openState.left ? "expanded" : "collapsed"}
|
|
139
|
+
data-state-right={openState.right ? "expanded" : "collapsed"}
|
|
140
|
+
style={{
|
|
141
|
+
"--sidebar-width": "19rem",
|
|
142
|
+
"--sidebar-width-icon": "4.25rem",
|
|
143
|
+
"--sidebar-width-mobile": "18rem",
|
|
144
|
+
...style,
|
|
145
|
+
}}
|
|
146
|
+
className={cn(
|
|
147
|
+
"group/sidebar-wrapper flex w-full min-w-0 gap-x-2",
|
|
148
|
+
className
|
|
149
|
+
)}
|
|
150
|
+
>
|
|
151
|
+
{children}
|
|
152
|
+
</div>
|
|
153
|
+
</AnimatedSidebarContext.Provider>
|
|
154
|
+
)
|
|
155
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { forwardRef } from "react"
|
|
2
|
+
import { Button } from "@/components/ui/button"
|
|
3
|
+
import { useAnimatedSidebar } from "./utils"
|
|
4
|
+
import type { AnimatedSidebarTriggerProps } from "./type"
|
|
5
|
+
|
|
6
|
+
export const AnimatedPanelTrigger = forwardRef<
|
|
7
|
+
HTMLButtonElement,
|
|
8
|
+
AnimatedSidebarTriggerProps
|
|
9
|
+
>(function AnimatedSidebarTrigger(
|
|
10
|
+
{ onClick, side = "left", type = "button", ...props },
|
|
11
|
+
forwardedRef
|
|
12
|
+
) {
|
|
13
|
+
const context = useAnimatedSidebar()
|
|
14
|
+
const expanded = context.isMobile
|
|
15
|
+
? context.openMobile[side]
|
|
16
|
+
: context.open[side]
|
|
17
|
+
const triggerRef = context.triggerRefs[side]
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Button
|
|
21
|
+
{...props}
|
|
22
|
+
ref={(node) => {
|
|
23
|
+
triggerRef.current = node
|
|
24
|
+
if (typeof forwardedRef === "function") forwardedRef(node)
|
|
25
|
+
else if (forwardedRef) forwardedRef.current = node
|
|
26
|
+
}}
|
|
27
|
+
type={type}
|
|
28
|
+
variant="ghost"
|
|
29
|
+
size="icon-xl"
|
|
30
|
+
aria-label={props["aria-label"] ?? "Toggle sidebar"}
|
|
31
|
+
aria-expanded={expanded}
|
|
32
|
+
data-slot="sidebar-trigger"
|
|
33
|
+
data-side={side}
|
|
34
|
+
data-state={expanded ? "expanded" : "collapsed"}
|
|
35
|
+
onClick={(event) => {
|
|
36
|
+
onClick?.(event)
|
|
37
|
+
if (event.defaultPrevented) return
|
|
38
|
+
context.toggleSidebar(side)
|
|
39
|
+
}}
|
|
40
|
+
/>
|
|
41
|
+
)
|
|
42
|
+
})
|