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