@exegia/corpora-ui 0.16.0 → 0.18.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 +20 -0
- package/dist-lib/components/blocks/scaffold/index.d.ts +25 -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 +7 -0
- package/dist-lib/components/blocks/scaffold/scaffold-actions.d.ts +9 -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/type.d.ts +133 -0
- package/dist-lib/components/blocks/scaffold/use-scaffold.d.ts +8 -0
- package/dist-lib/components/blocks/scaffold/utils.d.ts +15 -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/shell-layout.d.ts +1 -1
- package/dist-lib/components/blocks/shell/type.d.ts +91 -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 +1432 -932
- 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 +203 -0
- package/src/components/blocks/scaffold/constants.ts +28 -0
- package/src/components/blocks/scaffold/index.ts +38 -0
- package/src/components/blocks/scaffold/panel-close-button.tsx +36 -0
- package/src/components/blocks/scaffold/panel-menu-button.tsx +240 -0
- package/src/components/blocks/scaffold/scaffold-actions.tsx +97 -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 +57 -0
- package/src/components/blocks/scaffold/scaffold-main.tsx +28 -0
- package/src/components/blocks/scaffold/scaffold-panel.tsx +98 -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/type.ts +171 -0
- package/src/components/blocks/scaffold/use-scaffold.ts +48 -0
- package/src/components/blocks/scaffold/utils.ts +30 -0
- package/src/components/blocks/shell/__tests__/shell-layout.test.tsx +3 -3
- 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 +44 -64
- package/src/components/blocks/shell/type.ts +113 -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/button.tsx +3 -3
- 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 -109
- package/src/components/motion/animated-sidebar.tsx +0 -1196
|
@@ -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,30 @@
|
|
|
1
|
+
import type { ClassNameValue } from "tailwind-merge"
|
|
2
|
+
import type { TSubPanelVariant } from "@/components/blocks/scaffold/type.ts"
|
|
3
|
+
|
|
4
|
+
/** Desktop backdrop the whole scaffold sits on — a soft warm-gray wash. */
|
|
5
|
+
export const scaffoldBackgroundClass: ClassNameValue =
|
|
6
|
+
"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"
|
|
7
|
+
|
|
8
|
+
/** Card surface shared by a panel's primary area and secondary strip. */
|
|
9
|
+
export const panelSurfaceClass: ClassNameValue =
|
|
10
|
+
"rounded-2xl bg-slate-100 inset-ring-1 inset-ring-white/60 dark:bg-neutral-900 dark:inset-ring-white/5"
|
|
11
|
+
|
|
12
|
+
/** Floating icon button hovering over a panel (close, swap). */
|
|
13
|
+
export const floatingButtonClass: ClassNameValue =
|
|
14
|
+
"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"
|
|
15
|
+
|
|
16
|
+
/** Hidden until the pointer rests on the panel (always shown for touch and
|
|
17
|
+
* while focused) — the affordances stay quiet until the panel is engaged. */
|
|
18
|
+
export const revealOnPanelHoverClass: ClassNameValue =
|
|
19
|
+
"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"
|
|
20
|
+
|
|
21
|
+
/** Pill segment inside the actions cluster (Add, browse). */
|
|
22
|
+
export const actionSegmentClass: ClassNameValue =
|
|
23
|
+
"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"
|
|
24
|
+
|
|
25
|
+
/** Map of sub-panel variants to their corresponding side. */
|
|
26
|
+
export const subPanelVariant: Record<TSubPanelVariant, ClassNameValue> = {
|
|
27
|
+
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",
|
|
28
|
+
subtle: "bg-neutral-100 dark:bg-neutral-800",
|
|
29
|
+
inset: "bg-neutral-200 dark:bg-neutral-700",
|
|
30
|
+
} as const
|
|
@@ -92,12 +92,12 @@ describe("ShellLayout", () => {
|
|
|
92
92
|
expect(rightDrawer().getAttribute("data-state")).toBe("expanded")
|
|
93
93
|
})
|
|
94
94
|
|
|
95
|
-
test("a panel's own
|
|
95
|
+
test("a panel's own defaultOpen flag seeds its side's initial state", () => {
|
|
96
96
|
render(
|
|
97
97
|
<ShellLayout
|
|
98
98
|
panels={{
|
|
99
99
|
...PANELS,
|
|
100
|
-
right: { ...PANELS.right!,
|
|
100
|
+
right: { ...PANELS.right!, defaultOpen: true },
|
|
101
101
|
}}
|
|
102
102
|
variant="web"
|
|
103
103
|
/>
|
|
@@ -112,7 +112,7 @@ describe("ShellLayout", () => {
|
|
|
112
112
|
defaultOpen={{ left: false, right: false }}
|
|
113
113
|
panels={{
|
|
114
114
|
...PANELS,
|
|
115
|
-
right: { ...PANELS.right!,
|
|
115
|
+
right: { ...PANELS.right!, defaultOpen: true },
|
|
116
116
|
}}
|
|
117
117
|
variant="web"
|
|
118
118
|
/>
|
|
@@ -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
|
+
})
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
// beui.dev/components/motion/animated-sidebar
|
|
3
|
+
|
|
4
|
+
import { motion } from "motion/react"
|
|
5
|
+
import { EASE_OUT } from "@/lib/ease.ts"
|
|
6
|
+
import { cn } from "@/lib/utils"
|
|
7
|
+
import type { AnimatedSidebarProps } from "./type"
|
|
8
|
+
import {
|
|
9
|
+
AnimatedSidebarPanelContext,
|
|
10
|
+
SIDEBAR_MORPH_TRANSITION,
|
|
11
|
+
PANEL_TRANSITION,
|
|
12
|
+
REDUCED_TRANSITION,
|
|
13
|
+
useAnimatedSidebar
|
|
14
|
+
} from "./utils"
|
|
15
|
+
|
|
16
|
+
export function AnimatedPanel({
|
|
17
|
+
side = "left",
|
|
18
|
+
variant = "sidebar",
|
|
19
|
+
collapsible = "icon",
|
|
20
|
+
ariaLabel = "Sidebar",
|
|
21
|
+
children,
|
|
22
|
+
className,
|
|
23
|
+
panelClassName,
|
|
24
|
+
style,
|
|
25
|
+
ref,
|
|
26
|
+
...props
|
|
27
|
+
}: AnimatedSidebarProps) {
|
|
28
|
+
const context = useAnimatedSidebar()
|
|
29
|
+
const sideOpen = context.open[side]
|
|
30
|
+
const collapsed = collapsible !== "none" && !sideOpen
|
|
31
|
+
const offcanvas = collapsed && collapsible === "offcanvas"
|
|
32
|
+
const width = offcanvas
|
|
33
|
+
? "0px"
|
|
34
|
+
: collapsed
|
|
35
|
+
? "var(--sidebar-width-icon)"
|
|
36
|
+
: "var(--sidebar-width)"
|
|
37
|
+
|
|
38
|
+
if (context.isMobile) return <div>TODO: Create Mobile nav experience</div>
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<motion.aside
|
|
42
|
+
{...props}
|
|
43
|
+
ref={ref}
|
|
44
|
+
initial={false}
|
|
45
|
+
aria-label={ariaLabel}
|
|
46
|
+
data-slot="sidebar"
|
|
47
|
+
data-state={collapsed ? "collapsed" : "expanded"}
|
|
48
|
+
data-collapsible={collapsible}
|
|
49
|
+
data-variant={variant}
|
|
50
|
+
data-side={side}
|
|
51
|
+
animate={{
|
|
52
|
+
width,
|
|
53
|
+
// Swallow the shell's gap-x-2 column gap while off canvas so the
|
|
54
|
+
// shell edge keeps a constant spacing-2 inset whether the panel is
|
|
55
|
+
// open (gap + panel + margin) or collapsed (gap + this).
|
|
56
|
+
...(side === "right"
|
|
57
|
+
? { marginLeft: offcanvas ? "-0.5rem" : "0rem" }
|
|
58
|
+
: { marginRight: offcanvas ? "-0.5rem" : "0rem" }),
|
|
59
|
+
}}
|
|
60
|
+
transition={context.reduce ? { duration: 0 } : SIDEBAR_MORPH_TRANSITION}
|
|
61
|
+
style={style}
|
|
62
|
+
className={cn(
|
|
63
|
+
"group/sidebar relative hidden h-full shrink-0 will-change-[width] md:block",
|
|
64
|
+
"peer flex!",
|
|
65
|
+
side === "right" && "order-last",
|
|
66
|
+
className
|
|
67
|
+
)}
|
|
68
|
+
>
|
|
69
|
+
{side === "right" && !collapsed && (
|
|
70
|
+
// The handle occupies the shell's gap-x-2 column gap. It must stay
|
|
71
|
+
// outside the clipping mask below or the gap would never receive
|
|
72
|
+
// hover — clipping cuts hit-testing, not just painting.
|
|
73
|
+
<motion.div
|
|
74
|
+
className="absolute inset-y-0 -left-2 z-10 w-2 cursor-col-resize"
|
|
75
|
+
initial="idle"
|
|
76
|
+
whileHover="hover"
|
|
77
|
+
>
|
|
78
|
+
<motion.div
|
|
79
|
+
variants={{ idle: { scaleY: 0 }, hover: { scaleY: 1 } }}
|
|
80
|
+
transition={
|
|
81
|
+
context.reduce
|
|
82
|
+
? { duration: 0 }
|
|
83
|
+
: { duration: 0.2, ease: EASE_OUT }
|
|
84
|
+
}
|
|
85
|
+
className="h-full w-full bg-radial/decreasing from-amber-300/70 to-amber-500/0 to-40%"
|
|
86
|
+
/>
|
|
87
|
+
</motion.div>
|
|
88
|
+
)}
|
|
89
|
+
{/* The off canvas panel keeps its full width and slides out of the
|
|
90
|
+
zero-width rail, so mask it to the aside's box here instead of
|
|
91
|
+
clipping the aside itself — the aside must stay unclipped for the
|
|
92
|
+
gap handle above to be hoverable. */}
|
|
93
|
+
<div
|
|
94
|
+
className={
|
|
95
|
+
collapsible === "offcanvas"
|
|
96
|
+
? "absolute inset-0 overflow-hidden rounded-[inherit]"
|
|
97
|
+
: "contents"
|
|
98
|
+
}
|
|
99
|
+
>
|
|
100
|
+
<motion.div
|
|
101
|
+
initial={false}
|
|
102
|
+
animate={{
|
|
103
|
+
opacity: offcanvas ? 0 : 1,
|
|
104
|
+
x: offcanvas ? (side === "left" ? "-100%" : "100%") : "0%",
|
|
105
|
+
}}
|
|
106
|
+
transition={context.reduce ? REDUCED_TRANSITION : PANEL_TRANSITION}
|
|
107
|
+
className={cn(
|
|
108
|
+
"sticky top-0 flex flex-col overflow-hidden",
|
|
109
|
+
// collapsible === "offcanvas" && "w-(--sidebar-width)",
|
|
110
|
+
// Pin the right panel to the aside's trailing edge so the width
|
|
111
|
+
// animation expands the panel out of it instead of revealing a
|
|
112
|
+
// detached strip.
|
|
113
|
+
side === "right" && "ml-auto",
|
|
114
|
+
|
|
115
|
+
// Panels fill the shell's container, not the viewport — the shell
|
|
116
|
+
// owns its own scrolling, so a bounded host (docs demo, split
|
|
117
|
+
// view) must bound the panels too. Full-page apps size the shell
|
|
118
|
+
// itself (e.g., h-svh). The bottom gap comes from the shell
|
|
119
|
+
// container's padding, so panels only carry their top offset.
|
|
120
|
+
variant === "floating" && "mx-2 mt-2 h-[calc(100%-0.5rem)]",
|
|
121
|
+
variant === "inset" && "mt-2 h-[calc(100%-0.5rem)]",
|
|
122
|
+
|
|
123
|
+
panelClassName
|
|
124
|
+
)}
|
|
125
|
+
>
|
|
126
|
+
<AnimatedSidebarPanelContext.Provider
|
|
127
|
+
value={{ collapsed, collapsible, side }}
|
|
128
|
+
>
|
|
129
|
+
{children}
|
|
130
|
+
</AnimatedSidebarPanelContext.Provider>
|
|
131
|
+
</motion.div>
|
|
132
|
+
</div>
|
|
133
|
+
</motion.aside>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { forwardRef, type HTMLAttributes } from "react"
|
|
2
|
+
import { cn } from "@/lib/utils"
|
|
3
|
+
|
|
4
|
+
export const AnimatedSidebarContent = forwardRef<
|
|
5
|
+
HTMLDivElement,
|
|
6
|
+
HTMLAttributes<HTMLDivElement>
|
|
7
|
+
>(function AnimatedSidebarContent({ className, ...props }, forwardedRef) {
|
|
8
|
+
return (
|
|
9
|
+
<div
|
|
10
|
+
{...props}
|
|
11
|
+
ref={forwardedRef}
|
|
12
|
+
data-slot="sidebar-content"
|
|
13
|
+
className={cn(
|
|
14
|
+
"flex min-h-0 flex-1 flex-col gap-2 overflow-x-hidden overflow-y-auto overscroll-contain px-2 py-2",
|
|
15
|
+
className
|
|
16
|
+
)}
|
|
17
|
+
/>
|
|
18
|
+
)
|
|
19
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { forwardRef, type HTMLAttributes } from "react"
|
|
2
|
+
import { cn } from "@/lib/utils"
|
|
3
|
+
|
|
4
|
+
export const AnimatedSidebarFooter = forwardRef<
|
|
5
|
+
HTMLDivElement,
|
|
6
|
+
HTMLAttributes<HTMLDivElement>
|
|
7
|
+
>(function AnimatedSidebarFooter({ className, ...props }, forwardedRef) {
|
|
8
|
+
return (
|
|
9
|
+
<div
|
|
10
|
+
{...props}
|
|
11
|
+
ref={forwardedRef}
|
|
12
|
+
data-slot="sidebar-footer"
|
|
13
|
+
className={cn(
|
|
14
|
+
"flex shrink-0 flex-col gap-2 border-t border-border p-3 pb-[max(0.75rem,env(safe-area-inset-bottom))]",
|
|
15
|
+
className
|
|
16
|
+
)}
|
|
17
|
+
/>
|
|
18
|
+
)
|
|
19
|
+
})
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { forwardRef, type HTMLAttributes } from "react"
|
|
2
|
+
import { cn } from "@/lib/utils"
|
|
3
|
+
|
|
4
|
+
export const AnimatedSidebarGroupContent = forwardRef<
|
|
5
|
+
HTMLDivElement,
|
|
6
|
+
HTMLAttributes<HTMLDivElement>
|
|
7
|
+
>(function AnimatedSidebarGroupContent({ className, ...props }, forwardedRef) {
|
|
8
|
+
return (
|
|
9
|
+
<div
|
|
10
|
+
{...props}
|
|
11
|
+
ref={forwardedRef}
|
|
12
|
+
data-slot="sidebar-group-content"
|
|
13
|
+
className={cn("w-full min-w-0", className)}
|
|
14
|
+
/>
|
|
15
|
+
)
|
|
16
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { forwardRef, type HTMLAttributes } from "react"
|
|
2
|
+
import { cn } from "@/lib/utils"
|
|
3
|
+
import { useAnimatedSidebarPanel } from "./utils"
|
|
4
|
+
|
|
5
|
+
export const AnimatedSidebarGroupLabel = forwardRef<
|
|
6
|
+
HTMLDivElement,
|
|
7
|
+
HTMLAttributes<HTMLDivElement>
|
|
8
|
+
>(function AnimatedSidebarGroupLabel(
|
|
9
|
+
{ children, className, ...props },
|
|
10
|
+
forwardedRef
|
|
11
|
+
) {
|
|
12
|
+
const { collapsed } = useAnimatedSidebarPanel()
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div
|
|
16
|
+
{...props}
|
|
17
|
+
ref={forwardedRef}
|
|
18
|
+
aria-hidden={collapsed}
|
|
19
|
+
data-slot="sidebar-group-label"
|
|
20
|
+
className={cn(
|
|
21
|
+
"mb-1 h-7 overflow-hidden px-2 text-[10px] font-medium tracking-[0.14em] text-muted-foreground uppercase transition-opacity",
|
|
22
|
+
collapsed ? "opacity-0" : "opacity-100",
|
|
23
|
+
className
|
|
24
|
+
)}
|
|
25
|
+
>
|
|
26
|
+
{children}
|
|
27
|
+
</div>
|
|
28
|
+
)
|
|
29
|
+
})
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { forwardRef, type HTMLAttributes } from "react"
|
|
2
|
+
import { cn } from "@/lib/utils"
|
|
3
|
+
|
|
4
|
+
export const AnimatedSidebarGroup = forwardRef<
|
|
5
|
+
HTMLDivElement,
|
|
6
|
+
HTMLAttributes<HTMLDivElement>
|
|
7
|
+
>(function AnimatedSidebarGroup({ className, ...props }, forwardedRef) {
|
|
8
|
+
return (
|
|
9
|
+
<div
|
|
10
|
+
{...props}
|
|
11
|
+
ref={forwardedRef}
|
|
12
|
+
data-slot="sidebar-group"
|
|
13
|
+
className={cn("flex w-full min-w-0 flex-col px-1 py-1.5", className)}
|
|
14
|
+
/>
|
|
15
|
+
)
|
|
16
|
+
})
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { forwardRef, type HTMLAttributes } from "react"
|
|
2
|
+
import { cn } from "@/lib/utils"
|
|
3
|
+
|
|
4
|
+
export const AnimatedSidebarHeader = forwardRef<
|
|
5
|
+
HTMLDivElement,
|
|
6
|
+
HTMLAttributes<HTMLDivElement>
|
|
7
|
+
>(function AnimatedSidebarHeader({ className, ...props }, forwardedRef) {
|
|
8
|
+
return (
|
|
9
|
+
<div
|
|
10
|
+
{...props}
|
|
11
|
+
ref={forwardedRef}
|
|
12
|
+
data-slot="sidebar-header"
|
|
13
|
+
className={cn("flex shrink-0 flex-col gap-2 p-3", className)}
|
|
14
|
+
/>
|
|
15
|
+
)
|
|
16
|
+
})
|