@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.
Files changed (115) 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 +20 -0
  4. package/dist-lib/components/blocks/scaffold/index.d.ts +25 -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 +7 -0
  7. package/dist-lib/components/blocks/scaffold/scaffold-actions.d.ts +9 -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/type.d.ts +133 -0
  17. package/dist-lib/components/blocks/scaffold/use-scaffold.d.ts +8 -0
  18. package/dist-lib/components/blocks/scaffold/utils.d.ts +15 -0
  19. package/dist-lib/components/blocks/shell/animated-panel-inset.d.ts +2 -0
  20. package/dist-lib/components/blocks/shell/animated-panel-provider.d.ts +2 -0
  21. package/dist-lib/components/blocks/shell/animated-panel-trigger.d.ts +2 -0
  22. package/dist-lib/components/blocks/shell/animated-panel.d.ts +2 -0
  23. package/dist-lib/components/blocks/shell/animated-sidebar-content.d.ts +2 -0
  24. package/dist-lib/components/blocks/shell/animated-sidebar-footer.d.ts +2 -0
  25. package/dist-lib/components/blocks/shell/animated-sidebar-group-content.d.ts +2 -0
  26. package/dist-lib/components/blocks/shell/animated-sidebar-group-label.d.ts +2 -0
  27. package/dist-lib/components/blocks/shell/animated-sidebar-group.d.ts +2 -0
  28. package/dist-lib/components/blocks/shell/animated-sidebar-header.d.ts +2 -0
  29. package/dist-lib/components/blocks/shell/animated-sidebar-menu-button.d.ts +3 -0
  30. package/dist-lib/components/blocks/shell/animated-sidebar-menu-item.d.ts +2 -0
  31. package/dist-lib/components/blocks/shell/animated-sidebar-menu-sub-button.d.ts +3 -0
  32. package/dist-lib/components/blocks/shell/animated-sidebar-menu-sub-item.d.ts +2 -0
  33. package/dist-lib/components/blocks/shell/animated-sidebar-menu-sub.d.ts +2 -0
  34. package/dist-lib/components/blocks/shell/animated-sidebar-menu.d.ts +2 -0
  35. package/dist-lib/components/blocks/shell/index.d.ts +16 -0
  36. package/dist-lib/components/blocks/shell/shell-layout.d.ts +1 -1
  37. package/dist-lib/components/blocks/shell/type.d.ts +91 -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 +1432 -932
  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 +203 -0
  59. package/src/components/blocks/scaffold/constants.ts +28 -0
  60. package/src/components/blocks/scaffold/index.ts +38 -0
  61. package/src/components/blocks/scaffold/panel-close-button.tsx +36 -0
  62. package/src/components/blocks/scaffold/panel-menu-button.tsx +240 -0
  63. package/src/components/blocks/scaffold/scaffold-actions.tsx +97 -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 +57 -0
  67. package/src/components/blocks/scaffold/scaffold-main.tsx +28 -0
  68. package/src/components/blocks/scaffold/scaffold-panel.tsx +98 -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/type.ts +171 -0
  73. package/src/components/blocks/scaffold/use-scaffold.ts +48 -0
  74. package/src/components/blocks/scaffold/utils.ts +30 -0
  75. package/src/components/blocks/shell/__tests__/shell-layout.test.tsx +3 -3
  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 +44 -64
  94. package/src/components/blocks/shell/type.ts +113 -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/button.tsx +3 -3
  102. package/src/components/ui/glasscn/glass-button-group.tsx +48 -0
  103. package/src/components/ui/glasscn/glass-container.tsx +125 -0
  104. package/src/components/ui/glasscn/glass-separator.tsx +39 -0
  105. package/src/components/ui/glasscn/liquid-glass.tsx +250 -210
  106. package/src/components/ui/group.tsx +92 -0
  107. package/src/components/ui/separator.tsx +11 -11
  108. package/src/components/ui/sheet.tsx +135 -0
  109. package/src/components/ui/tooltip.tsx +66 -0
  110. package/src/index.css +1 -1
  111. package/src/index.ts +13 -1
  112. package/src/lib/ease.ts +26 -0
  113. package/src/lib/hooks/use-hover-capable.ts +23 -0
  114. package/dist-lib/components/motion/animated-sidebar.d.ts +0 -109
  115. 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 open flag seeds its side's initial state", () => {
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!, open: true },
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!, open: true },
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
+ })