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