@exegia/corpora-ui 0.17.0 → 0.19.0

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