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