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