@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
@@ -1,1196 +0,0 @@
1
- "use client"
2
- // beui.dev/components/motion/animated-sidebar
3
-
4
- import { ChevronRight } from "lucide-react"
5
- import {
6
- AnimatePresence,
7
- type HTMLMotionProps,
8
- motion,
9
- useReducedMotion,
10
- type Variants,
11
- } from "motion/react"
12
- import {
13
- type ButtonHTMLAttributes,
14
- type CSSProperties,
15
- createContext,
16
- forwardRef,
17
- type HTMLAttributes,
18
- type ReactNode,
19
- useCallback,
20
- useContext,
21
- useEffect,
22
- useId,
23
- useMemo,
24
- useRef,
25
- useState,
26
- useSyncExternalStore,
27
- } from "react"
28
- import { createPortal } from "react-dom"
29
- import { SharedLayoutBg } from "@/components/motion/shared-layout-bg"
30
- import { EASE_DRAWER, EASE_OUT, SPRING_LAYOUT, SPRING_PRESS } from "@/lib/ease"
31
- import { cn } from "@/lib/utils"
32
-
33
- export type SidebarSide = "left" | "right"
34
- /** Open flags keyed by the side. Sides left out stay uncontrolled / at their
35
- * default — there is no per-side prop, the record IS the API. */
36
- export type SidebarOpenState = Partial<Record<SidebarSide, boolean>>
37
- type SidebarVariant = "sidebar" | "floating" | "inset"
38
- type SidebarCollapsible = "offcanvas" | "icon" | "none"
39
-
40
- const MOBILE_QUERY = "(max-width: 767px)"
41
- const SIDEBAR_KEYBOARD_SHORTCUT = "b"
42
-
43
- const PANEL_TRANSITION = {
44
- duration: 0.36,
45
- ease: EASE_DRAWER,
46
- } as const
47
-
48
- // The desktop rail settles at a hard zero-width boundary. Keep the spring
49
- // critically damped so it cannot overshoot, pause against that boundary, and
50
- // then snap back during the final frame.
51
- const SIDEBAR_MORPH_TRANSITION = {
52
- type: "spring",
53
- stiffness: 380,
54
- damping: 35,
55
- mass: 0.75,
56
- } as const
57
-
58
- const LABEL_ENTER_TRANSITION = {
59
- duration: 0.2,
60
- delay: 0.08,
61
- ease: EASE_OUT,
62
- } as const
63
-
64
- const LABEL_EXIT_TRANSITION = {
65
- duration: 0.12,
66
- ease: EASE_OUT,
67
- } as const
68
-
69
- const SUBMENU_TRANSITION = {
70
- duration: 0.18,
71
- ease: EASE_OUT,
72
- } as const
73
-
74
- const SUBMENU_VARIANTS: Variants = {
75
- closed: {
76
- opacity: 0,
77
- clipPath: "inset(0 0 100% 0 round 8px)",
78
- transition: {
79
- duration: 0.14,
80
- ease: EASE_OUT,
81
- staggerChildren: 0.025,
82
- staggerDirection: -1,
83
- },
84
- },
85
- open: {
86
- opacity: 1,
87
- clipPath: "inset(0 0 0% 0 round 8px)",
88
- transition: {
89
- duration: 0.2,
90
- delayChildren: 0.035,
91
- ease: EASE_OUT,
92
- staggerChildren: 0.045,
93
- },
94
- },
95
- }
96
-
97
- const SUBMENU_ITEM_VARIANTS: Variants = {
98
- closed: {
99
- opacity: 0,
100
- y: -6,
101
- filter: "blur(3px)",
102
- },
103
- open: {
104
- opacity: 1,
105
- y: 0,
106
- filter: "blur(0px)",
107
- transition: SUBMENU_TRANSITION,
108
- },
109
- }
110
-
111
- const REDUCED_TRANSITION = {
112
- duration: 0.16,
113
- ease: EASE_OUT,
114
- } as const
115
-
116
- const FOCUSABLE_SELECTOR = [
117
- "a[href]",
118
- "button:not([disabled])",
119
- "input:not([disabled])",
120
- "select:not([disabled])",
121
- "textarea:not([disabled])",
122
- "[tabindex]:not([tabindex='-1'])",
123
- ].join(",")
124
-
125
- function subscribeToMobileQuery(callback: () => void) {
126
- const query = window.matchMedia(MOBILE_QUERY)
127
- query.addEventListener("change", callback)
128
- return () => query.removeEventListener("change", callback)
129
- }
130
-
131
- function getMobileSnapshot() {
132
- return window.matchMedia(MOBILE_QUERY).matches
133
- }
134
-
135
- function getServerMobileSnapshot() {
136
- return false
137
- }
138
-
139
- function useIsMobile() {
140
- return useSyncExternalStore(
141
- subscribeToMobileQuery,
142
- getMobileSnapshot,
143
- getServerMobileSnapshot
144
- )
145
- }
146
-
147
- interface AnimatedSidebarContextValue {
148
- isMobile: boolean
149
- layoutId: string
150
- /** Desktop open state, keyed by side. */
151
- open: Record<SidebarSide, boolean>
152
- /** Mobile overlay open state, keyed by side. */
153
- openMobile: Record<SidebarSide, boolean>
154
- reduce: boolean
155
- setOpen: (open: boolean, side: SidebarSide) => void
156
- setOpenMobile: (open: boolean, side: SidebarSide) => void
157
- /** Mobile-aware: toggles the overlay below md, the docked panel above. */
158
- toggleSidebar: (side: SidebarSide) => void
159
- triggerRefs: Record<SidebarSide, React.RefObject<HTMLButtonElement | null>>
160
- }
161
-
162
- const AnimatedSidebarContext =
163
- createContext<AnimatedSidebarContextValue | null>(null)
164
-
165
- interface AnimatedSidebarPanelContextValue {
166
- collapsed: boolean
167
- collapsible: SidebarCollapsible
168
- side: SidebarSide
169
- }
170
-
171
- const AnimatedSidebarPanelContext =
172
- createContext<AnimatedSidebarPanelContextValue | null>(null)
173
-
174
- export function useAnimatedSidebar() {
175
- const context = useContext(AnimatedSidebarContext)
176
- if (!context) {
177
- throw new Error(
178
- "useAnimatedSidebar must be used inside AnimatedSidebarProvider."
179
- )
180
- }
181
- return context
182
- }
183
-
184
- function useAnimatedSidebarPanel() {
185
- const context = useContext(AnimatedSidebarPanelContext)
186
- if (!context) {
187
- throw new Error(
188
- "Animated Sidebar parts must be used inside AnimatedSidebar."
189
- )
190
- }
191
- return context
192
- }
193
-
194
- type SidebarProviderStyle = CSSProperties & {
195
- "--sidebar-width"?: string
196
- "--sidebar-width-icon"?: string
197
- "--sidebar-width-mobile"?: string
198
- }
199
-
200
- export interface AnimatedSidebarProviderProps extends HTMLAttributes<HTMLDivElement> {
201
- /** Controlled desktop open state, keyed by the side. A side left undefined
202
- * stays uncontrolled. */
203
- open?: SidebarOpenState
204
- /** Initial desktop open state, merged over `{ left: true, right: false }`. */
205
- defaultOpen?: SidebarOpenState
206
- onOpenChange?: (open: boolean, side: SidebarSide) => void
207
- /** Controlled mobile overlay state, keyed by side. */
208
- openMobile?: SidebarOpenState
209
- /** Initial mobile overlay state — every side starts closed. */
210
- defaultOpenMobile?: SidebarOpenState
211
- onOpenMobileChange?: (open: boolean, side: SidebarSide) => void
212
- style?: SidebarProviderStyle
213
- }
214
-
215
- export function AnimatedSidebarProvider({
216
- children,
217
- open,
218
- defaultOpen,
219
- onOpenChange,
220
- openMobile,
221
- defaultOpenMobile,
222
- onOpenMobileChange,
223
- className,
224
- style,
225
- ...props
226
- }: AnimatedSidebarProviderProps) {
227
- const [internalOpen, setInternalOpen] = useState<
228
- Record<SidebarSide, boolean>
229
- >(() => ({
230
- left: defaultOpen?.left ?? true,
231
- right: defaultOpen?.right ?? false,
232
- }))
233
- const [internalOpenMobile, setInternalOpenMobile] = useState<
234
- Record<SidebarSide, boolean>
235
- >(() => ({
236
- left: defaultOpenMobile?.left ?? false,
237
- right: defaultOpenMobile?.right ?? false,
238
- }))
239
- const isMobile = useIsMobile()
240
- const reduce = useReducedMotion() ?? false
241
- const generatedId = useId()
242
- const leftTriggerRef = useRef<HTMLButtonElement>(null)
243
- const rightTriggerRef = useRef<HTMLButtonElement>(null)
244
-
245
- // Per-side controlled/uncontrolled merge — a side is controlled exactly
246
- // when its key is present on the controlled record.
247
- const controlledLeft = open?.left
248
- const controlledRight = open?.right
249
- const controlledMobileLeft = openMobile?.left
250
- const controlledMobileRight = openMobile?.right
251
-
252
- const openState = useMemo<Record<SidebarSide, boolean>>(
253
- () => ({
254
- left: controlledLeft ?? internalOpen.left,
255
- right: controlledRight ?? internalOpen.right,
256
- }),
257
- [controlledLeft, controlledRight, internalOpen]
258
- )
259
- const openMobileState = useMemo<Record<SidebarSide, boolean>>(
260
- () => ({
261
- left: controlledMobileLeft ?? internalOpenMobile.left,
262
- right: controlledMobileRight ?? internalOpenMobile.right,
263
- }),
264
- [controlledMobileLeft, controlledMobileRight, internalOpenMobile]
265
- )
266
-
267
- const setOpen = useCallback(
268
- (nextOpen: boolean, side: SidebarSide) => {
269
- const controlled = side === "left" ? controlledLeft : controlledRight
270
- if (controlled === undefined) {
271
- setInternalOpen((prev) =>
272
- prev[side] === nextOpen ? prev : { ...prev, [side]: nextOpen }
273
- )
274
- }
275
- onOpenChange?.(nextOpen, side)
276
- },
277
- [controlledLeft, controlledRight, onOpenChange]
278
- )
279
-
280
- const setOpenMobile = useCallback(
281
- (nextOpen: boolean, side: SidebarSide) => {
282
- const controlled =
283
- side === "left" ? controlledMobileLeft : controlledMobileRight
284
- if (controlled === undefined) {
285
- setInternalOpenMobile((prev) =>
286
- prev[side] === nextOpen ? prev : { ...prev, [side]: nextOpen }
287
- )
288
- }
289
- onOpenMobileChange?.(nextOpen, side)
290
- },
291
- [controlledMobileLeft, controlledMobileRight, onOpenMobileChange]
292
- )
293
-
294
- const toggleSidebar = useCallback(
295
- (side: SidebarSide) => {
296
- if (isMobile) setOpenMobile(!openMobileState[side], side)
297
- else setOpen(!openState[side], side)
298
- },
299
- [isMobile, openMobileState, openState, setOpen, setOpenMobile]
300
- )
301
-
302
- const triggerRefs = useMemo(
303
- () => ({ left: leftTriggerRef, right: rightTriggerRef }),
304
- []
305
- )
306
-
307
- useEffect(() => {
308
- const handleShortcut = (event: KeyboardEvent) => {
309
- if (
310
- event.key.toLowerCase() === SIDEBAR_KEYBOARD_SHORTCUT &&
311
- (event.metaKey || event.ctrlKey)
312
- ) {
313
- event.preventDefault()
314
- toggleSidebar("left")
315
- }
316
- }
317
-
318
- window.addEventListener("keydown", handleShortcut)
319
- return () => window.removeEventListener("keydown", handleShortcut)
320
- }, [toggleSidebar])
321
-
322
- return (
323
- <AnimatedSidebarContext.Provider
324
- value={{
325
- isMobile,
326
- layoutId: `${generatedId}-active`,
327
- open: openState,
328
- openMobile: openMobileState,
329
- reduce,
330
- setOpen,
331
- setOpenMobile,
332
- toggleSidebar,
333
- triggerRefs,
334
- }}
335
- >
336
- <div
337
- {...props}
338
- data-slot="sidebar-wrapper"
339
- data-state-left={openState.left ? "expanded" : "collapsed"}
340
- data-state-right={openState.right ? "expanded" : "collapsed"}
341
- style={{
342
- "--sidebar-width": "19rem",
343
- "--sidebar-width-icon": "4.25rem",
344
- "--sidebar-width-mobile": "18rem",
345
- ...style,
346
- }}
347
- className={cn(
348
- "group/sidebar-wrapper flex min-h-svh w-full min-w-0",
349
- className
350
- )}
351
- >
352
- {children}
353
- </div>
354
- </AnimatedSidebarContext.Provider>
355
- )
356
- }
357
-
358
- function MobileSidebar({
359
- ariaLabel,
360
- children,
361
- className,
362
- side,
363
- }: {
364
- ariaLabel: string
365
- children: ReactNode
366
- className?: string
367
- side: SidebarSide
368
- }) {
369
- const context = useAnimatedSidebar()
370
- const open = context.openMobile[side]
371
- const setOpen = (nextOpen: boolean) => context.setOpenMobile(nextOpen, side)
372
- const triggerRef = context.triggerRefs[side]
373
- const panelRef = useRef<HTMLDivElement>(null)
374
- const [mounted, setMounted] = useState(false)
375
-
376
- useEffect(() => setMounted(true), [])
377
-
378
- useEffect(() => {
379
- if (!open) return
380
-
381
- const body = document.body
382
- const scrollY = window.scrollY
383
- const trigger = triggerRef.current
384
- const previousBodyStyles = {
385
- left: body.style.left,
386
- overflow: body.style.overflow,
387
- position: body.style.position,
388
- right: body.style.right,
389
- top: body.style.top,
390
- }
391
-
392
- body.style.position = "fixed"
393
- body.style.top = `-${scrollY}px`
394
- body.style.left = "0"
395
- body.style.right = "0"
396
- body.style.overflow = "hidden"
397
-
398
- const focusFrame = requestAnimationFrame(() => {
399
- const firstFocusable =
400
- panelRef.current?.querySelector<HTMLElement>(FOCUSABLE_SELECTOR)
401
- ;(firstFocusable ?? panelRef.current)?.focus({ preventScroll: true })
402
- })
403
-
404
- return () => {
405
- cancelAnimationFrame(focusFrame)
406
- body.style.position = previousBodyStyles.position
407
- body.style.top = previousBodyStyles.top
408
- body.style.left = previousBodyStyles.left
409
- body.style.right = previousBodyStyles.right
410
- body.style.overflow = previousBodyStyles.overflow
411
- window.scrollTo(0, scrollY)
412
- trigger?.focus({ preventScroll: true })
413
- }
414
- }, [open, triggerRef])
415
-
416
- if (!mounted) return null
417
-
418
- return createPortal(
419
- <div
420
- className={cn(
421
- "pointer-events-none fixed inset-0 z-50 md:hidden",
422
- open ? "visible" : "invisible"
423
- )}
424
- >
425
- <motion.button
426
- type="button"
427
- aria-label="Close sidebar"
428
- tabIndex={open ? 0 : -1}
429
- initial={false}
430
- animate={{ opacity: open ? 1 : 0 }}
431
- transition={context.reduce ? REDUCED_TRANSITION : PANEL_TRANSITION}
432
- onClick={() => setOpen(false)}
433
- className={cn(
434
- "absolute inset-0",
435
- open ? "pointer-events-auto" : "pointer-events-none"
436
- )}
437
- />
438
-
439
- <motion.div
440
- ref={panelRef}
441
- role="dialog"
442
- aria-modal="true"
443
- aria-label={ariaLabel}
444
- aria-hidden={!open}
445
- inert={!open}
446
- tabIndex={-1}
447
- data-mobile="true"
448
- data-state={open ? "expanded" : "collapsed"}
449
- data-side={side}
450
- initial={false}
451
- animate={{
452
- opacity: context.reduce ? (open ? 1 : 0) : 1,
453
- x: context.reduce
454
- ? 0
455
- : open
456
- ? "0%"
457
- : side === "left"
458
- ? "-100%"
459
- : "100%",
460
- }}
461
- transition={context.reduce ? REDUCED_TRANSITION : PANEL_TRANSITION}
462
- onKeyDown={(event) => {
463
- if (event.key === "Escape") {
464
- event.preventDefault()
465
- setOpen(false)
466
- return
467
- }
468
-
469
- if (event.key !== "Tab") return
470
- const focusable = panelRef.current
471
- ? Array.from(
472
- panelRef.current.querySelectorAll<HTMLElement>(
473
- FOCUSABLE_SELECTOR
474
- )
475
- )
476
- : []
477
-
478
- if (focusable.length === 0) {
479
- event.preventDefault()
480
- panelRef.current?.focus()
481
- return
482
- }
483
-
484
- const first = focusable[0]
485
- const last = focusable[focusable.length - 1]
486
- if (event.shiftKey && document.activeElement === first) {
487
- event.preventDefault()
488
- last.focus()
489
- } else if (!event.shiftKey && document.activeElement === last) {
490
- event.preventDefault()
491
- first.focus()
492
- }
493
- }}
494
- className={cn(
495
- "pointer-events-auto absolute inset-y-0 flex h-dvh w-(--sidebar-width-mobile) max-w-[88vw] flex-col overflow-hidden",
496
- "will-change-transform",
497
- side === "left" ? "left-0 border-r" : "right-0",
498
- !open && "pointer-events-none",
499
- className
500
- )}
501
- >
502
- <AnimatedSidebarPanelContext.Provider
503
- value={{ collapsed: false, collapsible: "none", side }}
504
- >
505
- {children}
506
- </AnimatedSidebarPanelContext.Provider>
507
- </motion.div>
508
- </div>,
509
- document.body
510
- )
511
- }
512
-
513
- export interface AnimatedSidebarProps extends Omit<
514
- HTMLMotionProps<"aside">,
515
- "children"
516
- > {
517
- children?: ReactNode
518
- side?: SidebarSide
519
- variant?: SidebarVariant
520
- collapsible?: SidebarCollapsible
521
- ariaLabel?: string
522
- panelClassName?: string
523
- }
524
-
525
- export const AnimatedSidebar = forwardRef<HTMLElement, AnimatedSidebarProps>(
526
- function AnimatedSidebar(
527
- {
528
- side = "left",
529
- variant = "sidebar",
530
- collapsible = "icon",
531
- ariaLabel = "Sidebar",
532
- children,
533
- className,
534
- panelClassName,
535
- style,
536
- ...props
537
- },
538
- forwardedRef
539
- ) {
540
- const context = useAnimatedSidebar()
541
- const sideOpen = context.open[side]
542
- const collapsed = collapsible !== "none" && !sideOpen
543
- const offcanvas = collapsed && collapsible === "offcanvas"
544
- const width = offcanvas
545
- ? "0px"
546
- : collapsed
547
- ? "var(--sidebar-width-icon)"
548
- : "var(--sidebar-width)"
549
-
550
- if (context.isMobile) {
551
- return (
552
- <MobileSidebar ariaLabel={ariaLabel} className={className} side={side}>
553
- {children}
554
- </MobileSidebar>
555
- )
556
- }
557
-
558
- return (
559
- <motion.aside
560
- {...props}
561
- ref={forwardedRef}
562
- initial={false}
563
- aria-label={ariaLabel}
564
- data-slot="sidebar"
565
- data-state={collapsed ? "collapsed" : "expanded"}
566
- data-collapsible={collapsible}
567
- data-variant={variant}
568
- data-side={side}
569
- animate={{ width }}
570
- transition={context.reduce ? { duration: 0 } : SIDEBAR_MORPH_TRANSITION}
571
- style={style}
572
- className={cn(
573
- "group/sidebar relative hidden h-auto shrink-0 will-change-[width] md:block",
574
- "peer flex!",
575
- // The off canvas panel keeps its full width and slides out of the
576
- // zero-width rail, so clip it rather than letting it bleed past the
577
- // shell edge. The clip margin lets the panel's own shadow escape
578
- // without letting the slid-out panel escape with it
579
- collapsible === "offcanvas" &&
580
- "overflow-x-visible [overflow-clip-margin:1rem]",
581
- side === "right" && "order-last",
582
- className
583
- )}
584
- >
585
- {side === "right" && !collapsed && (
586
- <motion.div className="h-full p-0.5 flex flex-1 bg-radial/decreasing from-amber-400 to-0% hover:to-50% ease-smooth-out to-amber-500/0 cursor-col-resize transition-colors duration-1000 animate-in" />
587
- )}
588
- <motion.div
589
- initial={false}
590
- animate={{
591
- opacity: offcanvas ? 0 : 1,
592
- x: offcanvas ? (side === "left" ? "-100%" : "100%") : "0%",
593
- }}
594
- transition={context.reduce ? REDUCED_TRANSITION : PANEL_TRANSITION}
595
- className={cn(
596
- "sticky top-0 flex h-svh w-full flex-col overflow-hidden",
597
- collapsible === "offcanvas" && "w-(--sidebar-width)",
598
-
599
- variant === "floating" && "m-2 h-[calc(100svh-1rem)]",
600
- variant === "inset" && "m-2 h-[calc(100svh-1rem)]",
601
- panelClassName
602
- )}
603
- >
604
- <AnimatedSidebarPanelContext.Provider
605
- value={{ collapsed, collapsible, side }}
606
- >
607
- {children}
608
- </AnimatedSidebarPanelContext.Provider>
609
- </motion.div>
610
- </motion.aside>
611
- )
612
- }
613
- )
614
-
615
- export interface AnimatedSidebarTriggerProps extends ButtonHTMLAttributes<HTMLButtonElement> {
616
- side?: SidebarSide
617
- }
618
-
619
- export const AnimatedSidebarTrigger = forwardRef<
620
- HTMLButtonElement,
621
- AnimatedSidebarTriggerProps
622
- >(function AnimatedSidebarTrigger(
623
- { className, onClick, side = "left", type = "button", ...props },
624
- forwardedRef
625
- ) {
626
- const context = useAnimatedSidebar()
627
- const expanded = context.isMobile
628
- ? context.openMobile[side]
629
- : context.open[side]
630
- const triggerRef = context.triggerRefs[side]
631
-
632
- return (
633
- <button
634
- {...props}
635
- ref={(node) => {
636
- triggerRef.current = node
637
- if (typeof forwardedRef === "function") forwardedRef(node)
638
- else if (forwardedRef) forwardedRef.current = node
639
- }}
640
- type={type}
641
- aria-label={props["aria-label"] ?? "Toggle sidebar"}
642
- aria-expanded={expanded}
643
- data-slot="sidebar-trigger"
644
- data-side={side}
645
- data-state={expanded ? "expanded" : "collapsed"}
646
- onClick={(event) => {
647
- onClick?.(event)
648
- if (event.defaultPrevented) return
649
- context.toggleSidebar(side)
650
- }}
651
- className={cn(
652
- "inline-flex size-10 shrink-0 items-center justify-center rounded-xl outline-none",
653
- "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
654
- className
655
- )}
656
- />
657
- )
658
- })
659
-
660
- export interface AnimatedSidebarCloseProps extends ButtonHTMLAttributes<HTMLButtonElement> {
661
- side?: SidebarSide
662
- }
663
-
664
- export const AnimatedSidebarClose = forwardRef<
665
- HTMLButtonElement,
666
- AnimatedSidebarCloseProps
667
- >(function AnimatedSidebarClose(
668
- { className, onClick, side = "left", type = "button", ...props },
669
- forwardedRef
670
- ) {
671
- const context = useAnimatedSidebar()
672
-
673
- return (
674
- <button
675
- {...props}
676
- ref={forwardedRef}
677
- type={type}
678
- data-side={side}
679
- aria-label={props["aria-label"] ?? "Close sidebar"}
680
- onClick={(event) => {
681
- onClick?.(event)
682
- if (event.defaultPrevented) return
683
- if (context.isMobile) context.setOpenMobile(false, side)
684
- else context.setOpen(false, side)
685
- }}
686
- className={cn(
687
- "inline-flex size-10 shrink-0 items-center justify-center rounded-xl outline-none",
688
- "focus-visible:ring-2 focus-visible:ring-ring",
689
- className
690
- )}
691
- />
692
- )
693
- })
694
-
695
- export interface AnimatedSidebarRailProps extends ButtonHTMLAttributes<HTMLButtonElement> {}
696
-
697
- export const AnimatedSidebarRail = forwardRef<
698
- HTMLButtonElement,
699
- AnimatedSidebarRailProps
700
- >(function AnimatedSidebarRail(
701
- { className, onClick, type = "button", ...props },
702
- forwardedRef
703
- ) {
704
- const context = useAnimatedSidebar()
705
- const panel = useAnimatedSidebarPanel()
706
-
707
- return (
708
- <button
709
- {...props}
710
- ref={forwardedRef}
711
- type={type}
712
- data-side={panel.side}
713
- aria-label={props["aria-label"] ?? "Toggle sidebar"}
714
- title="Toggle sidebar"
715
- tabIndex={-1}
716
- onClick={(event) => {
717
- onClick?.(event)
718
- if (event.defaultPrevented) return
719
- context.toggleSidebar(panel.side)
720
- }}
721
- className={cn(
722
- "absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 outline-none md:block",
723
- "after:absolute after:inset-y-0 after:left-1/2 after:w-px after:bg-transparent after:transition-colors hover:after:bg-border",
724
- "data-[side=left]:left-full data-[side=right]:right-0 data-[side=right]:translate-x-1/2",
725
- className
726
- )}
727
- />
728
- )
729
- })
730
-
731
- export interface AnimatedSidebarInsetProps extends HTMLMotionProps<"main"> {}
732
-
733
- export const AnimatedSidebarInset = forwardRef<
734
- HTMLElement,
735
- AnimatedSidebarInsetProps
736
- >(function AnimatedSidebarInset({ className, ...props }, forwardedRef) {
737
- return (
738
- <motion.main
739
- {...props}
740
- ref={forwardedRef}
741
- data-slot="sidebar-inset"
742
- className={cn(
743
- "relative flex min-w-0 flex-1 flex-col bg-background",
744
- "md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:mr-1.5 md:peer-data-[variant=inset]:rounded-md md:peer-data-[variant=inset]:shadow-md",
745
- className
746
- )}
747
- />
748
- )
749
- })
750
-
751
- export const AnimatedSidebarHeader = forwardRef<
752
- HTMLDivElement,
753
- HTMLAttributes<HTMLDivElement>
754
- >(function AnimatedSidebarHeader({ className, ...props }, forwardedRef) {
755
- return (
756
- <div
757
- {...props}
758
- ref={forwardedRef}
759
- data-slot="sidebar-header"
760
- className={cn("flex shrink-0 flex-col gap-2 p-3", className)}
761
- />
762
- )
763
- })
764
-
765
- export const AnimatedSidebarContent = forwardRef<
766
- HTMLDivElement,
767
- HTMLAttributes<HTMLDivElement>
768
- >(function AnimatedSidebarContent({ className, ...props }, forwardedRef) {
769
- return (
770
- <div
771
- {...props}
772
- ref={forwardedRef}
773
- data-slot="sidebar-content"
774
- className={cn(
775
- "flex min-h-0 flex-1 flex-col gap-2 overflow-x-hidden overflow-y-auto overscroll-contain px-2 py-2",
776
- className
777
- )}
778
- />
779
- )
780
- })
781
-
782
- export const AnimatedSidebarFooter = forwardRef<
783
- HTMLDivElement,
784
- HTMLAttributes<HTMLDivElement>
785
- >(function AnimatedSidebarFooter({ className, ...props }, forwardedRef) {
786
- return (
787
- <div
788
- {...props}
789
- ref={forwardedRef}
790
- data-slot="sidebar-footer"
791
- className={cn(
792
- "flex shrink-0 flex-col gap-2 border-t border-border p-3 pb-[max(0.75rem,env(safe-area-inset-bottom))]",
793
- className
794
- )}
795
- />
796
- )
797
- })
798
-
799
- export const AnimatedSidebarGroup = forwardRef<
800
- HTMLDivElement,
801
- HTMLAttributes<HTMLDivElement>
802
- >(function AnimatedSidebarGroup({ className, ...props }, forwardedRef) {
803
- return (
804
- <div
805
- {...props}
806
- ref={forwardedRef}
807
- data-slot="sidebar-group"
808
- className={cn("flex w-full min-w-0 flex-col px-1 py-1.5", className)}
809
- />
810
- )
811
- })
812
-
813
- export const AnimatedSidebarGroupLabel = forwardRef<
814
- HTMLDivElement,
815
- HTMLAttributes<HTMLDivElement>
816
- >(function AnimatedSidebarGroupLabel(
817
- { children, className, ...props },
818
- forwardedRef
819
- ) {
820
- const { collapsed } = useAnimatedSidebarPanel()
821
-
822
- return (
823
- <div
824
- {...props}
825
- ref={forwardedRef}
826
- aria-hidden={collapsed}
827
- data-slot="sidebar-group-label"
828
- className={cn(
829
- "mb-1 h-7 overflow-hidden px-2 text-[10px] font-medium tracking-[0.14em] text-muted-foreground uppercase transition-opacity",
830
- collapsed ? "opacity-0" : "opacity-100",
831
- className
832
- )}
833
- >
834
- {children}
835
- </div>
836
- )
837
- })
838
-
839
- export const AnimatedSidebarGroupContent = forwardRef<
840
- HTMLDivElement,
841
- HTMLAttributes<HTMLDivElement>
842
- >(function AnimatedSidebarGroupContent({ className, ...props }, forwardedRef) {
843
- return (
844
- <div
845
- {...props}
846
- ref={forwardedRef}
847
- data-slot="sidebar-group-content"
848
- className={cn("w-full min-w-0", className)}
849
- />
850
- )
851
- })
852
-
853
- export const AnimatedSidebarMenu = forwardRef<
854
- HTMLUListElement,
855
- HTMLAttributes<HTMLUListElement>
856
- >(function AnimatedSidebarMenu(
857
- { children, className, ...props },
858
- forwardedRef
859
- ) {
860
- return (
861
- <SharedLayoutBg
862
- {...props}
863
- ref={forwardedRef as React.Ref<HTMLElement>}
864
- as="ul"
865
- inset={0}
866
- pillClassName="rounded-xl bg-muted/70"
867
- pillContainerClassName="inset-y-auto top-0 h-9"
868
- data-slot="sidebar-menu"
869
- className={cn(
870
- "flex w-full min-w-0 list-none flex-col gap-0.5",
871
- className
872
- )}
873
- >
874
- {children}
875
- </SharedLayoutBg>
876
- )
877
- })
878
-
879
- export const AnimatedSidebarMenuItem = forwardRef<
880
- HTMLLIElement,
881
- HTMLMotionProps<"li">
882
- >(function AnimatedSidebarMenuItem({ className, ...props }, forwardedRef) {
883
- return (
884
- <motion.li
885
- {...props}
886
- ref={forwardedRef}
887
- layout="position"
888
- transition={SPRING_LAYOUT}
889
- data-slot="sidebar-menu-item"
890
- className={cn("relative", className)}
891
- />
892
- )
893
- })
894
-
895
- export interface AnimatedSidebarMenuSubProps extends Omit<
896
- HTMLMotionProps<"ul">,
897
- "children"
898
- > {
899
- open: boolean
900
- children?: ReactNode
901
- }
902
-
903
- export const AnimatedSidebarMenuSub = forwardRef<
904
- HTMLUListElement,
905
- AnimatedSidebarMenuSubProps
906
- >(function AnimatedSidebarMenuSub(
907
- { open, children, className, ...props },
908
- forwardedRef
909
- ) {
910
- const context = useAnimatedSidebar()
911
- const panel = useAnimatedSidebarPanel()
912
-
913
- return (
914
- <AnimatePresence initial={false} mode="popLayout">
915
- {open && !panel.collapsed ? (
916
- <motion.ul
917
- {...props}
918
- ref={forwardedRef}
919
- key="sidebar-submenu"
920
- variants={context.reduce ? undefined : SUBMENU_VARIANTS}
921
- initial={context.reduce ? false : "closed"}
922
- animate={context.reduce ? { opacity: 1 } : "open"}
923
- exit={context.reduce ? { opacity: 0 } : "closed"}
924
- transition={context.reduce ? { duration: 0.12 } : undefined}
925
- data-slot="sidebar-menu-sub"
926
- className={cn(
927
- "relative mt-1 ml-5 flex min-w-0 flex-col gap-0.5 border-l border-border pl-3",
928
- className
929
- )}
930
- >
931
- {children}
932
- </motion.ul>
933
- ) : null}
934
- </AnimatePresence>
935
- )
936
- })
937
-
938
- export const AnimatedSidebarMenuSubItem = forwardRef<
939
- HTMLLIElement,
940
- HTMLMotionProps<"li">
941
- >(function AnimatedSidebarMenuSubItem({ className, ...props }, forwardedRef) {
942
- return (
943
- <motion.li
944
- {...props}
945
- ref={forwardedRef}
946
- variants={SUBMENU_ITEM_VARIANTS}
947
- data-slot="sidebar-menu-sub-item"
948
- className={cn("relative min-w-0", className)}
949
- />
950
- )
951
- })
952
-
953
- export interface AnimatedSidebarMenuSubButtonProps {
954
- children: ReactNode
955
- icon?: ReactNode
956
- href?: string
957
- isActive?: boolean
958
- disabled?: boolean
959
- closeOnSelect?: boolean
960
- target?: "_blank" | "_self" | "_parent" | "_top"
961
- rel?: string
962
- onSelect?: () => void
963
- className?: string
964
- }
965
-
966
- export function AnimatedSidebarMenuSubButton({
967
- children,
968
- icon,
969
- href,
970
- isActive = false,
971
- disabled = false,
972
- closeOnSelect = true,
973
- target,
974
- rel,
975
- onSelect,
976
- className,
977
- }: AnimatedSidebarMenuSubButtonProps) {
978
- const context = useAnimatedSidebar()
979
- const panel = useAnimatedSidebarPanel()
980
-
981
- const select = (
982
- event: React.MouseEvent<HTMLAnchorElement | HTMLButtonElement>
983
- ) => {
984
- if (disabled) {
985
- event.preventDefault()
986
- return
987
- }
988
- onSelect?.()
989
- if (context.isMobile && closeOnSelect) {
990
- context.setOpenMobile(false, panel.side)
991
- }
992
- }
993
-
994
- const content = (
995
- <>
996
- <span
997
- aria-hidden="true"
998
- className="grid size-4 shrink-0 place-items-center"
999
- >
1000
- {icon ?? <span className="size-1 rounded-full bg-current" />}
1001
- </span>
1002
- <span className="min-w-0 flex-1 truncate">{children}</span>
1003
- </>
1004
- )
1005
-
1006
- const interactiveClassName = cn(
1007
- "flex min-h-8 w-full min-w-0 items-center gap-2 rounded-lg px-2 text-left text-xs outline-none",
1008
- "text-muted-foreground transition-colors hover:bg-muted/60 hover:text-foreground",
1009
- "focus-visible:bg-muted/70 focus-visible:ring-2 focus-visible:ring-ring",
1010
- isActive && "bg-muted/70 text-foreground",
1011
- disabled && "cursor-not-allowed opacity-40",
1012
- className
1013
- )
1014
-
1015
- return href ? (
1016
- <motion.a
1017
- href={href}
1018
- target={target}
1019
- rel={rel ?? (target === "_blank" ? "noreferrer noopener" : undefined)}
1020
- aria-current={isActive ? "page" : undefined}
1021
- aria-disabled={disabled || undefined}
1022
- tabIndex={disabled ? -1 : undefined}
1023
- onClick={select}
1024
- whileTap={context.reduce || disabled ? undefined : { scale: 0.98 }}
1025
- transition={SPRING_PRESS}
1026
- className={interactiveClassName}
1027
- >
1028
- {content}
1029
- </motion.a>
1030
- ) : (
1031
- <motion.button
1032
- type="button"
1033
- disabled={disabled}
1034
- aria-current={isActive ? "page" : undefined}
1035
- onClick={select}
1036
- whileTap={context.reduce || disabled ? undefined : { scale: 0.98 }}
1037
- transition={SPRING_PRESS}
1038
- className={interactiveClassName}
1039
- >
1040
- {content}
1041
- </motion.button>
1042
- )
1043
- }
1044
-
1045
- export interface AnimatedSidebarMenuButtonProps {
1046
- children: ReactNode
1047
- icon?: ReactNode
1048
- badge?: ReactNode
1049
- href?: string
1050
- isActive?: boolean
1051
- ariaExpanded?: boolean
1052
- disabled?: boolean
1053
- closeOnSelect?: boolean
1054
- target?: "_blank" | "_self" | "_parent" | "_top"
1055
- rel?: string
1056
- onSelect?: () => void
1057
- className?: string
1058
- }
1059
-
1060
- export function AnimatedSidebarMenuButton({
1061
- children,
1062
- icon,
1063
- badge,
1064
- href,
1065
- isActive = false,
1066
- ariaExpanded,
1067
- disabled = false,
1068
- closeOnSelect,
1069
- target,
1070
- rel,
1071
- onSelect,
1072
- className,
1073
- }: AnimatedSidebarMenuButtonProps) {
1074
- const context = useAnimatedSidebar()
1075
- const panel = useAnimatedSidebarPanel()
1076
- const textLabel = typeof children === "string" ? children : undefined
1077
-
1078
- const select = (
1079
- event: React.MouseEvent<HTMLAnchorElement | HTMLButtonElement>
1080
- ) => {
1081
- if (disabled) {
1082
- event.preventDefault()
1083
- return
1084
- }
1085
- onSelect?.()
1086
- const shouldCloseOnSelect = closeOnSelect ?? ariaExpanded === undefined
1087
- if (context.isMobile && shouldCloseOnSelect) {
1088
- context.setOpenMobile(false, panel.side)
1089
- }
1090
- }
1091
-
1092
- const content = (
1093
- <>
1094
- {isActive ? (
1095
- <motion.span
1096
- layoutId={context.layoutId}
1097
- transition={context.reduce ? { duration: 0 } : SPRING_LAYOUT}
1098
- className="absolute inset-0 rounded-xl bg-muted"
1099
- />
1100
- ) : null}
1101
- {icon ? (
1102
- <span
1103
- aria-hidden="true"
1104
- className="relative z-10 grid size-5 shrink-0 place-items-center"
1105
- >
1106
- {icon}
1107
- </span>
1108
- ) : null}
1109
- <motion.span
1110
- initial={false}
1111
- animate={{
1112
- opacity: panel.collapsed ? 0 : 1,
1113
- x: panel.collapsed ? -4 : 0,
1114
- }}
1115
- transition={
1116
- context.reduce
1117
- ? REDUCED_TRANSITION
1118
- : panel.collapsed
1119
- ? LABEL_EXIT_TRANSITION
1120
- : LABEL_ENTER_TRANSITION
1121
- }
1122
- aria-hidden={panel.collapsed}
1123
- className={cn(
1124
- "relative z-10 min-w-0 flex-1 truncate",
1125
- panel.collapsed && "pointer-events-none"
1126
- )}
1127
- >
1128
- {children}
1129
- </motion.span>
1130
- {badge && !panel.collapsed ? (
1131
- <span className="relative z-10 shrink-0 text-xs text-muted-foreground">
1132
- {badge}
1133
- </span>
1134
- ) : null}
1135
- {ariaExpanded !== undefined ? (
1136
- <motion.span
1137
- aria-hidden="true"
1138
- initial={false}
1139
- animate={{
1140
- opacity: panel.collapsed ? 0 : 1,
1141
- rotate: ariaExpanded ? 90 : 0,
1142
- x: panel.collapsed ? 4 : 0,
1143
- }}
1144
- transition={context.reduce ? { duration: 0 } : SPRING_LAYOUT}
1145
- className="relative z-10 grid size-4 shrink-0 place-items-center text-muted-foreground"
1146
- >
1147
- <ChevronRight className="size-3.5" />
1148
- </motion.span>
1149
- ) : null}
1150
- </>
1151
- )
1152
-
1153
- const interactiveClassName = cn(
1154
- "relative flex min-h-9 w-full min-w-0 items-center gap-2.5 overflow-hidden rounded-xl px-3 text-left text-sm font-medium outline-none",
1155
- "text-muted-foreground transition-colors hover:text-foreground",
1156
- "focus-visible:bg-muted/70 focus-visible:ring-2 focus-visible:ring-ring",
1157
- isActive && "text-foreground",
1158
- disabled && "cursor-not-allowed opacity-40",
1159
- className
1160
- )
1161
-
1162
- return href ? (
1163
- <motion.a
1164
- href={href}
1165
- target={target}
1166
- rel={rel ?? (target === "_blank" ? "noreferrer noopener" : undefined)}
1167
- aria-current={isActive ? "page" : undefined}
1168
- aria-expanded={ariaExpanded}
1169
- aria-disabled={disabled || undefined}
1170
- aria-label={panel.collapsed ? textLabel : undefined}
1171
- title={panel.collapsed ? textLabel : undefined}
1172
- tabIndex={disabled ? -1 : undefined}
1173
- onClick={select}
1174
- whileTap={context.reduce || disabled ? undefined : { scale: 0.98 }}
1175
- transition={SPRING_PRESS}
1176
- className={interactiveClassName}
1177
- >
1178
- {content}
1179
- </motion.a>
1180
- ) : (
1181
- <motion.button
1182
- type="button"
1183
- disabled={disabled}
1184
- aria-current={isActive ? "page" : undefined}
1185
- aria-expanded={ariaExpanded}
1186
- aria-label={panel.collapsed ? textLabel : undefined}
1187
- title={panel.collapsed ? textLabel : undefined}
1188
- onClick={select}
1189
- whileTap={context.reduce || disabled ? undefined : { scale: 0.98 }}
1190
- transition={SPRING_PRESS}
1191
- className={interactiveClassName}
1192
- >
1193
- {content}
1194
- </motion.button>
1195
- )
1196
- }