src/components/ui/sidebar.tsx 21.1 K raw
1
import * as React from "react";
2
import { Slot } from "@radix-ui/react-slot";
3
import { cva, type VariantProps } from "class-variance-authority";
4
import { PanelLeftIcon } from "lucide-react";
5
6
import { useIsMobile } from "@/hooks/use-mobile";
7
import { cn } from "@/lib/utils";
8
import { Button } from "@/components/ui/button";
9
import { Input } from "@/components/ui/input";
10
import { Separator } from "@/components/ui/separator";
11
import {
12
	Sheet,
13
	SheetContent,
14
	SheetDescription,
15
	SheetHeader,
16
	SheetTitle,
17
} from "@/components/ui/sheet";
18
import { Skeleton } from "@/components/ui/skeleton";
19
import {
20
	Tooltip,
21
	TooltipContent,
22
	TooltipProvider,
23
	TooltipTrigger,
24
} from "@/components/ui/tooltip";
25
26
const SIDEBAR_COOKIE_NAME = "sidebar_state";
27
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
28
const SIDEBAR_WIDTH = "16rem";
29
const SIDEBAR_WIDTH_MOBILE = "18rem";
30
const SIDEBAR_WIDTH_ICON = "3rem";
31
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
32
33
type SidebarContextProps = {
34
	state: "expanded" | "collapsed";
35
	open: boolean;
36
	setOpen: (open: boolean) => void;
37
	openMobile: boolean;
38
	setOpenMobile: (open: boolean) => void;
39
	isMobile: boolean;
40
	toggleSidebar: () => void;
41
	hidden: boolean;
42
	setHidden: (hidden: boolean) => void;
43
	hideSidebar: () => void;
44
	showSidebar: () => void;
45
};
46
47
const SidebarContext = React.createContext<SidebarContextProps | null>(null);
48
49
function useSidebar() {
50
	const context = React.useContext(SidebarContext);
51
	if (!context) {
52
		throw new Error("useSidebar must be used within a SidebarProvider.");
53
	}
54
55
	return context;
56
}
57
58
function SidebarProvider({
59
	defaultOpen = true,
60
	open: openProp,
61
	onOpenChange: setOpenProp,
62
	className,
63
	style,
64
	children,
65
	...props
66
}: React.ComponentProps<"div"> & {
67
	defaultOpen?: boolean;
68
	open?: boolean;
69
	onOpenChange?: (open: boolean) => void;
70
}) {
71
	const isMobile = useIsMobile();
72
	const [openMobile, setOpenMobile] = React.useState(false);
73
	const [hidden, setHidden] = React.useState(false);
74
75
	// This is the internal state of the sidebar.
76
	// We use openProp and setOpenProp for control from outside the component.
77
	const [_open, _setOpen] = React.useState(defaultOpen);
78
	const open = openProp ?? _open;
79
	const setOpen = React.useCallback(
80
		(value: boolean | ((value: boolean) => boolean)) => {
81
			const openState = typeof value === "function" ? value(open) : value;
82
			if (setOpenProp) {
83
				setOpenProp(openState);
84
			} else {
85
				_setOpen(openState);
86
			}
87
88
			// This sets the cookie to keep the sidebar state.
89
			document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
90
		},
91
		[setOpenProp, open],
92
	);
93
94
	// Helper to toggle the sidebar.
95
	const toggleSidebar = React.useCallback(() => {
96
		return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
97
	}, [isMobile, setOpen, setOpenMobile]);
98
99
	// Helper to completely hide the sidebar
100
	const hideSidebar = React.useCallback(() => {
101
		setHidden(true);
102
		if (!isMobile) {
103
			setOpen(false);
104
		}
105
	}, [isMobile, setOpen]);
106
107
	// Helper to show the sidebar
108
	const showSidebar = React.useCallback(() => {
109
		setHidden(false);
110
		if (!isMobile) {
111
			setOpen(true);
112
		}
113
	}, [isMobile, setOpen]);
114
115
	// Adds a keyboard shortcut to toggle the sidebar.
116
	React.useEffect(() => {
117
		const handleKeyDown = (event: KeyboardEvent) => {
118
			if (
119
				event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
120
				(event.metaKey || event.ctrlKey)
121
			) {
122
				event.preventDefault();
123
				toggleSidebar();
124
			}
125
		};
126
127
		window.addEventListener("keydown", handleKeyDown);
128
		return () => window.removeEventListener("keydown", handleKeyDown);
129
	}, [toggleSidebar]);
130
131
	// We add a state so that we can do data-state="expanded" or "collapsed".
132
	// This makes it easier to style the sidebar with Tailwind classes.
133
	const state = open ? "expanded" : "collapsed";
134
135
	const contextValue = React.useMemo<SidebarContextProps>(
136
		() => ({
137
			state,
138
			open,
139
			setOpen,
140
			isMobile,
141
			openMobile,
142
			setOpenMobile,
143
			toggleSidebar,
144
			hidden,
145
			setHidden,
146
			hideSidebar,
147
			showSidebar,
148
		}),
149
		[
150
			state,
151
			open,
152
			setOpen,
153
			isMobile,
154
			openMobile,
155
			setOpenMobile,
156
			toggleSidebar,
157
			hidden,
158
			hideSidebar,
159
			showSidebar,
160
		],
161
	);
162
163
	return (
164
		<SidebarContext.Provider value={contextValue}>
165
			<TooltipProvider delayDuration={0}>
166
				<div
167
					data-slot="sidebar-wrapper"
168
					style={
169
						{
170
							"--sidebar-width": SIDEBAR_WIDTH,
171
							"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
172
							...style,
173
						} as React.CSSProperties
174
					}
175
					className={cn(
176
						"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
177
						className,
178
					)}
179
					{...props}
180
				>
181
					{children}
182
				</div>
183
			</TooltipProvider>
184
		</SidebarContext.Provider>
185
	);
186
}
187
188
function Sidebar({
189
	side = "left",
190
	variant = "sidebar",
191
	collapsible = "offcanvas",
192
	className,
193
	children,
194
	...props
195
}: React.ComponentProps<"div"> & {
196
	side?: "left" | "right";
197
	variant?: "sidebar" | "floating" | "inset";
198
	collapsible?: "offcanvas" | "icon" | "none";
199
}) {
200
	const { isMobile, state, openMobile, setOpenMobile, hidden } = useSidebar();
201
202
	if (collapsible === "none") {
203
		return (
204
			<div
205
				data-slot="sidebar"
206
				className={cn(
207
					"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
208
					className,
209
				)}
210
				{...props}
211
			>
212
				{children}
213
			</div>
214
		);
215
	}
216
217
	if (isMobile) {
218
		return (
219
			<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
220
				<SheetContent
221
					data-sidebar="sidebar"
222
					data-slot="sidebar"
223
					data-mobile="true"
224
					className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
225
					style={
226
						{
227
							"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
228
						} as React.CSSProperties
229
					}
230
					side={side}
231
				>
232
					<SheetHeader className="sr-only">
233
						<SheetTitle>Sidebar</SheetTitle>
234
						<SheetDescription>Displays the mobile sidebar.</SheetDescription>
235
					</SheetHeader>
236
					<div className="flex h-full w-full flex-col">{children}</div>
237
				</SheetContent>
238
			</Sheet>
239
		);
240
	}
241
242
	return (
243
		<div
244
			className={cn(
245
				"group peer text-sidebar-foreground hidden md:block transition-all duration-300 ease-in-out",
246
				hidden && "!hidden",
247
			)}
248
			data-state={state}
249
			data-collapsible={state === "collapsed" ? collapsible : ""}
250
			data-variant={variant}
251
			data-side={side}
252
			data-slot="sidebar"
253
			data-hidden={hidden}
254
		>
255
			{/* This is what handles the sidebar gap on desktop */}
256
			<div
257
				data-slot="sidebar-gap"
258
				className={cn(
259
					"relative w-(--sidebar-width) bg-transparent transition-[width] duration-300 ease-in-out",
260
					"group-data-[collapsible=offcanvas]:w-0",
261
					"group-data-[side=right]:rotate-180",
262
					variant === "floating" || variant === "inset"
263
						? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
264
						: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)",
265
				)}
266
			/>
267
			<div
268
				data-slot="sidebar-container"
269
				className={cn(
270
					"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width,opacity] duration-300 ease-in-out md:flex",
271
					side === "left"
272
						? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
273
						: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
274
					// Adjust the padding for floating and inset variants.
275
					variant === "floating" || variant === "inset"
276
						? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
277
						: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
278
					className,
279
				)}
280
				{...props}
281
			>
282
				<div
283
					data-sidebar="sidebar"
284
					data-slot="sidebar-inner"
285
					className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
286
				>
287
					{children}
288
				</div>
289
			</div>
290
		</div>
291
	);
292
}
293
294
function SidebarTrigger({
295
	className,
296
	onClick,
297
	...props
298
}: React.ComponentProps<typeof Button>) {
299
	const { toggleSidebar } = useSidebar();
300
301
	return (
302
		<Button
303
			data-sidebar="trigger"
304
			data-slot="sidebar-trigger"
305
			variant="ghost"
306
			size="icon"
307
			className={cn("size-7", className)}
308
			onClick={(event) => {
309
				onClick?.(event);
310
				toggleSidebar();
311
			}}
312
			{...props}
313
		>
314
			<PanelLeftIcon />
315
			<span className="sr-only">Toggle Sidebar</span>
316
		</Button>
317
	);
318
}
319
320
function SidebarShowButton({
321
	className,
322
	onClick,
323
	...props
324
}: React.ComponentProps<typeof Button>) {
325
	const { showSidebar, hidden } = useSidebar();
326
327
	if (!hidden) {
328
		return null;
329
	}
330
331
	return (
332
		<Button
333
			data-sidebar="show-button"
334
			data-slot="sidebar-show-button"
335
			variant="outline"
336
			size="icon"
337
			className={cn("fixed left-4 top-4 z-50 size-9 shadow-lg", className)}
338
			onClick={(event) => {
339
				onClick?.(event);
340
				showSidebar();
341
			}}
342
			{...props}
343
		>
344
			<PanelLeftIcon />
345
			<span className="sr-only">Show Sidebar</span>
346
		</Button>
347
	);
348
}
349
350
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
351
	const { toggleSidebar } = useSidebar();
352
353
	return (
354
		<button
355
			data-sidebar="rail"
356
			data-slot="sidebar-rail"
357
			aria-label="Toggle Sidebar"
358
			tabIndex={-1}
359
			onClick={toggleSidebar}
360
			title="Toggle Sidebar"
361
			className={cn(
362
				"hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex",
363
				"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
364
				"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
365
				"hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
366
				"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
367
				"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
368
				className,
369
			)}
370
			{...props}
371
		/>
372
	);
373
}
374
375
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
376
	return (
377
		<main
378
			data-slot="sidebar-inset"
379
			className={cn(
380
				"bg-background relative flex w-full flex-1 flex-col",
381
				"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
382
				"peer-data-[hidden=true]:ml-0",
383
				className,
384
			)}
385
			{...props}
386
		/>
387
	);
388
}
389
390
function SidebarInput({
391
	className,
392
	...props
393
}: React.ComponentProps<typeof Input>) {
394
	return (
395
		<Input
396
			data-slot="sidebar-input"
397
			data-sidebar="input"
398
			className={cn("bg-background h-8 w-full shadow-none", className)}
399
			{...props}
400
		/>
401
	);
402
}
403
404
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
405
	return (
406
		<div
407
			data-slot="sidebar-header"
408
			data-sidebar="header"
409
			className={cn("flex flex-col gap-2 p-2", className)}
410
			{...props}
411
		/>
412
	);
413
}
414
415
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
416
	return (
417
		<div
418
			data-slot="sidebar-footer"
419
			data-sidebar="footer"
420
			className={cn("flex flex-col gap-2 p-2", className)}
421
			{...props}
422
		/>
423
	);
424
}
425
426
function SidebarSeparator({
427
	className,
428
	...props
429
}: React.ComponentProps<typeof Separator>) {
430
	return (
431
		<Separator
432
			data-slot="sidebar-separator"
433
			data-sidebar="separator"
434
			className={cn("bg-sidebar-border mx-2 w-auto", className)}
435
			{...props}
436
		/>
437
	);
438
}
439
440
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
441
	return (
442
		<div
443
			data-slot="sidebar-content"
444
			data-sidebar="content"
445
			className={cn(
446
				"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
447
				className,
448
			)}
449
			{...props}
450
		/>
451
	);
452
}
453
454
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
455
	return (
456
		<div
457
			data-slot="sidebar-group"
458
			data-sidebar="group"
459
			className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
460
			{...props}
461
		/>
462
	);
463
}
464
465
function SidebarGroupLabel({
466
	className,
467
	asChild = false,
468
	...props
469
}: React.ComponentProps<"div"> & { asChild?: boolean }) {
470
	const Comp = asChild ? Slot : "div";
471
472
	return (
473
		<Comp
474
			data-slot="sidebar-group-label"
475
			data-sidebar="group-label"
476
			className={cn(
477
				"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
478
				"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
479
				className,
480
			)}
481
			{...props}
482
		/>
483
	);
484
}
485
486
function SidebarGroupAction({
487
	className,
488
	asChild = false,
489
	...props
490
}: React.ComponentProps<"button"> & { asChild?: boolean }) {
491
	const Comp = asChild ? Slot : "button";
492
493
	return (
494
		<Comp
495
			data-slot="sidebar-group-action"
496
			data-sidebar="group-action"
497
			className={cn(
498
				"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
499
				// Increases the hit area of the button on mobile.
500
				"after:absolute after:-inset-2 md:after:hidden",
501
				"group-data-[collapsible=icon]:hidden",
502
				className,
503
			)}
504
			{...props}
505
		/>
506
	);
507
}
508
509
function SidebarGroupContent({
510
	className,
511
	...props
512
}: React.ComponentProps<"div">) {
513
	return (
514
		<div
515
			data-slot="sidebar-group-content"
516
			data-sidebar="group-content"
517
			className={cn("w-full text-sm", className)}
518
			{...props}
519
		/>
520
	);
521
}
522
523
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
524
	return (
525
		<ul
526
			data-slot="sidebar-menu"
527
			data-sidebar="menu"
528
			className={cn("flex w-full min-w-0 flex-col gap-1", className)}
529
			{...props}
530
		/>
531
	);
532
}
533
534
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
535
	return (
536
		<li
537
			data-slot="sidebar-menu-item"
538
			data-sidebar="menu-item"
539
			className={cn("group/menu-item relative", className)}
540
			{...props}
541
		/>
542
	);
543
}
544
545
const sidebarMenuButtonVariants = cva(
546
	"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
547
	{
548
		variants: {
549
			variant: {
550
				default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
551
				outline:
552
					"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
553
			},
554
			size: {
555
				default: "h-8 text-sm",
556
				sm: "h-7 text-xs",
557
				lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
558
			},
559
		},
560
		defaultVariants: {
561
			variant: "default",
562
			size: "default",
563
		},
564
	},
565
);
566
567
function SidebarMenuButton({
568
	asChild = false,
569
	isActive = false,
570
	variant = "default",
571
	size = "default",
572
	tooltip,
573
	className,
574
	...props
575
}: React.ComponentProps<"button"> & {
576
	asChild?: boolean;
577
	isActive?: boolean;
578
	tooltip?: string | React.ComponentProps<typeof TooltipContent>;
579
} & VariantProps<typeof sidebarMenuButtonVariants>) {
580
	const Comp = asChild ? Slot : "button";
581
	const { isMobile, state } = useSidebar();
582
583
	const button = (
584
		<Comp
585
			data-slot="sidebar-menu-button"
586
			data-sidebar="menu-button"
587
			data-size={size}
588
			data-active={isActive}
589
			className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
590
			{...props}
591
		/>
592
	);
593
594
	if (!tooltip) {
595
		return button;
596
	}
597
598
	if (typeof tooltip === "string") {
599
		tooltip = {
600
			children: tooltip,
601
		};
602
	}
603
604
	return (
605
		<Tooltip>
606
			<TooltipTrigger asChild>{button}</TooltipTrigger>
607
			<TooltipContent
608
				side="right"
609
				align="center"
610
				hidden={state !== "collapsed" || isMobile}
611
				{...tooltip}
612
			/>
613
		</Tooltip>
614
	);
615
}
616
617
function SidebarMenuAction({
618
	className,
619
	asChild = false,
620
	showOnHover = false,
621
	...props
622
}: React.ComponentProps<"button"> & {
623
	asChild?: boolean;
624
	showOnHover?: boolean;
625
}) {
626
	const Comp = asChild ? Slot : "button";
627
628
	return (
629
		<Comp
630
			data-slot="sidebar-menu-action"
631
			data-sidebar="menu-action"
632
			className={cn(
633
				"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
634
				// Increases the hit area of the button on mobile.
635
				"after:absolute after:-inset-2 md:after:hidden",
636
				"peer-data-[size=sm]/menu-button:top-1",
637
				"peer-data-[size=default]/menu-button:top-1.5",
638
				"peer-data-[size=lg]/menu-button:top-2.5",
639
				"group-data-[collapsible=icon]:hidden",
640
				showOnHover &&
641
					"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
642
				className,
643
			)}
644
			{...props}
645
		/>
646
	);
647
}
648
649
function SidebarMenuBadge({
650
	className,
651
	...props
652
}: React.ComponentProps<"div">) {
653
	return (
654
		<div
655
			data-slot="sidebar-menu-badge"
656
			data-sidebar="menu-badge"
657
			className={cn(
658
				"text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none",
659
				"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
660
				"peer-data-[size=sm]/menu-button:top-1",
661
				"peer-data-[size=default]/menu-button:top-1.5",
662
				"peer-data-[size=lg]/menu-button:top-2.5",
663
				"group-data-[collapsible=icon]:hidden",
664
				className,
665
			)}
666
			{...props}
667
		/>
668
	);
669
}
670
671
function SidebarMenuSkeleton({
672
	className,
673
	showIcon = false,
674
	...props
675
}: React.ComponentProps<"div"> & {
676
	showIcon?: boolean;
677
}) {
678
	// Random width between 50 to 90%.
679
	const width = React.useMemo(() => {
680
		return `${Math.floor(Math.random() * 40) + 50}%`;
681
	}, []);
682
683
	return (
684
		<div
685
			data-slot="sidebar-menu-skeleton"
686
			data-sidebar="menu-skeleton"
687
			className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
688
			{...props}
689
		>
690
			{showIcon && (
691
				<Skeleton
692
					className="size-4 rounded-md"
693
					data-sidebar="menu-skeleton-icon"
694
				/>
695
			)}
696
			<Skeleton
697
				className="h-4 max-w-(--skeleton-width) flex-1"
698
				data-sidebar="menu-skeleton-text"
699
				style={
700
					{
701
						"--skeleton-width": width,
702
					} as React.CSSProperties
703
				}
704
			/>
705
		</div>
706
	);
707
}
708
709
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
710
	return (
711
		<ul
712
			data-slot="sidebar-menu-sub"
713
			data-sidebar="menu-sub"
714
			className={cn(
715
				"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
716
				"group-data-[collapsible=icon]:hidden",
717
				className,
718
			)}
719
			{...props}
720
		/>
721
	);
722
}
723
724
function SidebarMenuSubItem({
725
	className,
726
	...props
727
}: React.ComponentProps<"li">) {
728
	return (
729
		<li
730
			data-slot="sidebar-menu-sub-item"
731
			data-sidebar="menu-sub-item"
732
			className={cn("group/menu-sub-item relative", className)}
733
			{...props}
734
		/>
735
	);
736
}
737
738
function SidebarMenuSubButton({
739
	asChild = false,
740
	size = "md",
741
	isActive = false,
742
	className,
743
	...props
744
}: React.ComponentProps<"a"> & {
745
	asChild?: boolean;
746
	size?: "sm" | "md";
747
	isActive?: boolean;
748
}) {
749
	const Comp = asChild ? Slot : "a";
750
751
	return (
752
		<Comp
753
			data-slot="sidebar-menu-sub-button"
754
			data-sidebar="menu-sub-button"
755
			data-size={size}
756
			data-active={isActive}
757
			className={cn(
758
				"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
759
				"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
760
				size === "sm" && "text-xs",
761
				size === "md" && "text-sm",
762
				"group-data-[collapsible=icon]:hidden",
763
				className,
764
			)}
765
			{...props}
766
		/>
767
	);
768
}
769
770
export {
771
	Sidebar,
772
	SidebarContent,
773
	SidebarFooter,
774
	SidebarGroup,
775
	SidebarGroupAction,
776
	SidebarGroupContent,
777
	SidebarGroupLabel,
778
	SidebarHeader,
779
	SidebarInput,
780
	SidebarInset,
781
	SidebarMenu,
782
	SidebarMenuAction,
783
	SidebarMenuBadge,
784
	SidebarMenuButton,
785
	SidebarMenuItem,
786
	SidebarMenuSkeleton,
787
	SidebarMenuSub,
788
	SidebarMenuSubButton,
789
	SidebarMenuSubItem,
790
	SidebarProvider,
791
	SidebarRail,
792
	SidebarSeparator,
793
	SidebarTrigger,
794
	SidebarShowButton,
795
	useSidebar,
796
};