sidebar.tsx 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598
  1. import { Slot } from '@radix-ui/react-slot';
  2. import { VariantProps, cva } from 'class-variance-authority';
  3. import { PanelLeftIcon } from 'lucide-react';
  4. import * as React from 'react';
  5. import { cn } from '@/common/helpers/cn';
  6. import { useIsMobile } from '@/common/hooks/useMobile';
  7. import { Button } from '@/shadcn/button';
  8. import { Input } from '@/shadcn/input';
  9. import { Separator } from '@/shadcn/separator';
  10. import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/shadcn/sheet';
  11. import { Skeleton } from '@/shadcn/skeleton';
  12. import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/shadcn/tooltip';
  13. const SIDEBAR_COOKIE_NAME = 'sidebar_state';
  14. const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
  15. const SIDEBAR_WIDTH = '16rem';
  16. const SIDEBAR_WIDTH_MOBILE = '18rem';
  17. const SIDEBAR_WIDTH_ICON = '3rem';
  18. const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
  19. type SidebarContext = {
  20. state: 'expanded' | 'collapsed';
  21. open: boolean;
  22. setOpen: (open: boolean) => void;
  23. openMobile: boolean;
  24. setOpenMobile: (open: boolean) => void;
  25. isMobile: boolean;
  26. toggleSidebar: () => void;
  27. };
  28. const SidebarContext = React.createContext<SidebarContext | null>(null);
  29. function useSidebar() {
  30. const context = React.useContext(SidebarContext);
  31. if (!context) {
  32. throw new Error('useSidebar must be used within a SidebarProvider.');
  33. }
  34. return context;
  35. }
  36. function SidebarProvider({
  37. defaultOpen = true,
  38. open: openProp,
  39. onOpenChange: setOpenProp,
  40. className,
  41. style,
  42. children,
  43. ...props
  44. }: React.ComponentProps<'div'> & {
  45. defaultOpen?: boolean;
  46. open?: boolean;
  47. onOpenChange?: (open: boolean) => void;
  48. }) {
  49. const isMobile = useIsMobile();
  50. const [openMobile, setOpenMobile] = React.useState(false);
  51. // This is the internal state of the sidebar.
  52. // We use openProp and setOpenProp for control from outside the component.
  53. const [_open, _setOpen] = React.useState(defaultOpen);
  54. const open = openProp ?? _open;
  55. const setOpen = React.useCallback(
  56. (value: boolean | ((value: boolean) => boolean)) => {
  57. const openState = typeof value === 'function' ? value(open) : value;
  58. if (setOpenProp) {
  59. setOpenProp(openState);
  60. } else {
  61. _setOpen(openState);
  62. }
  63. // This sets the cookie to keep the sidebar state.
  64. document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
  65. },
  66. [setOpenProp, open],
  67. );
  68. // Helper to toggle the sidebar.
  69. const toggleSidebar = React.useCallback(() => {
  70. return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
  71. }, [isMobile, setOpen, setOpenMobile]);
  72. // Adds a keyboard shortcut to toggle the sidebar.
  73. React.useEffect(() => {
  74. const handleKeyDown = (event: KeyboardEvent) => {
  75. if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {
  76. event.preventDefault();
  77. toggleSidebar();
  78. }
  79. };
  80. window.addEventListener('keydown', handleKeyDown);
  81. return () => window.removeEventListener('keydown', handleKeyDown);
  82. }, [toggleSidebar]);
  83. // We add a state so that we can do data-state="expanded" or "collapsed".
  84. // This makes it easier to style the sidebar with Tailwind classes.
  85. const state = open ? 'expanded' : 'collapsed';
  86. const contextValue = React.useMemo<SidebarContext>(
  87. () => ({
  88. state,
  89. open,
  90. setOpen,
  91. isMobile,
  92. openMobile,
  93. setOpenMobile,
  94. toggleSidebar,
  95. }),
  96. [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
  97. );
  98. return (
  99. <SidebarContext.Provider value={contextValue}>
  100. <TooltipProvider delayDuration={0}>
  101. <div
  102. data-slot="sidebar-wrapper"
  103. style={
  104. {
  105. '--sidebar-width': SIDEBAR_WIDTH,
  106. '--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
  107. ...style,
  108. } as React.CSSProperties
  109. }
  110. className={cn('group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-sidebar', className)}
  111. {...props}
  112. >
  113. {children}
  114. </div>
  115. </TooltipProvider>
  116. </SidebarContext.Provider>
  117. );
  118. }
  119. function Sidebar({
  120. side = 'left',
  121. variant = 'sidebar',
  122. collapsible = 'offcanvas',
  123. className,
  124. children,
  125. ...props
  126. }: React.ComponentProps<'div'> & {
  127. side?: 'left' | 'right';
  128. variant?: 'sidebar' | 'floating' | 'inset';
  129. collapsible?: 'offcanvas' | 'icon' | 'none';
  130. }) {
  131. const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
  132. if (collapsible === 'none') {
  133. return (
  134. <div
  135. data-slot="sidebar"
  136. className={cn('flex h-full w-(--sidebar-width) flex-col bg-sidebar text-sidebar-foreground', className)}
  137. {...props}
  138. >
  139. {children}
  140. </div>
  141. );
  142. }
  143. if (isMobile) {
  144. return (
  145. <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
  146. <SheetHeader className="sr-only">
  147. <SheetTitle>Sidebar</SheetTitle>
  148. <SheetDescription>Displays the mobile sidebar.</SheetDescription>
  149. </SheetHeader>
  150. <SheetContent
  151. data-sidebar="sidebar"
  152. data-slot="sidebar"
  153. data-mobile="true"
  154. className="w-(--sidebar-width) bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
  155. style={
  156. {
  157. '--sidebar-width': SIDEBAR_WIDTH_MOBILE,
  158. } as React.CSSProperties
  159. }
  160. side={side}
  161. >
  162. <div className="flex h-full w-full flex-col">{children}</div>
  163. </SheetContent>
  164. </Sheet>
  165. );
  166. }
  167. return (
  168. <div
  169. className="group peer hidden text-sidebar-foreground md:block"
  170. data-state={state}
  171. data-collapsible={state === 'collapsed' ? collapsible : ''}
  172. data-variant={variant}
  173. data-side={side}
  174. data-slot="sidebar"
  175. >
  176. {/* This is what handles the sidebar gap on desktop */}
  177. <div
  178. className={cn(
  179. 'relative h-svh w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear',
  180. 'group-data-[collapsible=offcanvas]:w-0',
  181. 'group-data-[side=right]:rotate-180',
  182. variant === 'floating' || variant === 'inset'
  183. ? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]'
  184. : 'group-data-[collapsible=icon]:w-(--sidebar-width-icon)',
  185. )}
  186. />
  187. <div
  188. className={cn(
  189. 'fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex',
  190. side === 'left'
  191. ? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]'
  192. : 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
  193. // Adjust the padding for floating and inset variants.
  194. variant === 'floating' || variant === 'inset'
  195. ? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]'
  196. : 'group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l',
  197. className,
  198. )}
  199. {...props}
  200. >
  201. <div
  202. data-sidebar="sidebar"
  203. 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"
  204. >
  205. {children}
  206. </div>
  207. </div>
  208. </div>
  209. );
  210. }
  211. function SidebarTrigger({ className, onClick, ...props }: React.ComponentProps<typeof Button>) {
  212. const { toggleSidebar } = useSidebar();
  213. return (
  214. <Button
  215. data-sidebar="trigger"
  216. data-slot="sidebar-trigger"
  217. variant="ghost"
  218. size="icon"
  219. className={cn('h-7 w-7', className)}
  220. onClick={(event) => {
  221. onClick?.(event);
  222. toggleSidebar();
  223. }}
  224. {...props}
  225. >
  226. <PanelLeftIcon />
  227. <span className="sr-only">Toggle Sidebar</span>
  228. </Button>
  229. );
  230. }
  231. function SidebarRail({ className, ...props }: React.ComponentProps<'button'>) {
  232. const { toggleSidebar } = useSidebar();
  233. return (
  234. <button
  235. data-sidebar="rail"
  236. data-slot="sidebar-rail"
  237. aria-label="Toggle Sidebar"
  238. tabIndex={-1}
  239. onClick={toggleSidebar}
  240. title="Toggle Sidebar"
  241. className={cn(
  242. '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',
  243. 'in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize',
  244. '[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
  245. 'group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full hover:group-data-[collapsible=offcanvas]:bg-sidebar',
  246. '[[data-side=left][data-collapsible=offcanvas]_&]:-right-2',
  247. '[[data-side=right][data-collapsible=offcanvas]_&]:-left-2',
  248. className,
  249. )}
  250. {...props}
  251. />
  252. );
  253. }
  254. function SidebarInset({ className, ...props }: React.ComponentProps<'main'>) {
  255. return (
  256. <main
  257. data-slot="sidebar-inset"
  258. className={cn(
  259. 'relative flex min-h-svh max-w-full flex-1 flex-col bg-background',
  260. 'peer-data-[variant=inset]:min-h-[calc(100svh-(--spacing(4)))] 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-0',
  261. className,
  262. )}
  263. {...props}
  264. />
  265. );
  266. }
  267. function SidebarInput({ className, ...props }: React.ComponentProps<typeof Input>) {
  268. return <Input data-slot="sidebar-input" data-sidebar="input" className={cn('h-8 w-full bg-background shadow-none', className)} {...props} />;
  269. }
  270. function SidebarHeader({ className, ...props }: React.ComponentProps<'div'>) {
  271. return <div data-slot="sidebar-header" data-sidebar="header" className={cn('flex flex-col gap-2 p-2', className)} {...props} />;
  272. }
  273. function SidebarFooter({ className, ...props }: React.ComponentProps<'div'>) {
  274. return <div data-slot="sidebar-footer" data-sidebar="footer" className={cn('flex flex-col gap-2 p-2', className)} {...props} />;
  275. }
  276. function SidebarSeparator({ className, ...props }: React.ComponentProps<typeof Separator>) {
  277. return <Separator data-slot="sidebar-separator" data-sidebar="separator" className={cn('mx-2 w-auto bg-sidebar-border', className)} {...props} />;
  278. }
  279. function SidebarContent({ className, ...props }: React.ComponentProps<'div'>) {
  280. return (
  281. <div
  282. data-slot="sidebar-content"
  283. data-sidebar="content"
  284. className={cn('flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden', className)}
  285. {...props}
  286. />
  287. );
  288. }
  289. function SidebarGroup({ className, ...props }: React.ComponentProps<'div'>) {
  290. return <div data-slot="sidebar-group" data-sidebar="group" className={cn('relative flex w-full min-w-0 flex-col p-2', className)} {...props} />;
  291. }
  292. function SidebarGroupLabel({ className, asChild = false, ...props }: React.ComponentProps<'div'> & { asChild?: boolean }) {
  293. const Comp = asChild ? Slot : 'div';
  294. return (
  295. <Comp
  296. data-slot="sidebar-group-label"
  297. data-sidebar="group-label"
  298. className={cn(
  299. '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',
  300. 'group-data-[collapsible=icon]:pointer-events-none group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 group-data-[collapsible=icon]:select-none',
  301. className,
  302. )}
  303. {...props}
  304. />
  305. );
  306. }
  307. function SidebarGroupAction({ className, asChild = false, ...props }: React.ComponentProps<'button'> & { asChild?: boolean }) {
  308. const Comp = asChild ? Slot : 'button';
  309. return (
  310. <Comp
  311. data-slot="sidebar-group-action"
  312. data-sidebar="group-action"
  313. className={cn(
  314. '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',
  315. // Increases the hit area of the button on mobile.
  316. 'after:absolute after:-inset-2 md:after:hidden',
  317. 'group-data-[collapsible=icon]:hidden',
  318. className,
  319. )}
  320. {...props}
  321. />
  322. );
  323. }
  324. function SidebarGroupContent({ className, ...props }: React.ComponentProps<'div'>) {
  325. return <div data-slot="sidebar-group-content" data-sidebar="group-content" className={cn('w-full text-sm', className)} {...props} />;
  326. }
  327. function SidebarMenu({ className, ...props }: React.ComponentProps<'ul'>) {
  328. return <ul data-slot="sidebar-menu" data-sidebar="menu" className={cn('flex w-full min-w-0 flex-col gap-1', className)} {...props} />;
  329. }
  330. function SidebarMenuItem({ className, ...props }: React.ComponentProps<'li'>) {
  331. return <li data-slot="sidebar-menu-item" data-sidebar="menu-item" className={cn('group/menu-item relative', className)} {...props} />;
  332. }
  333. const sidebarMenuButtonVariants = cva(
  334. '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',
  335. {
  336. variants: {
  337. variant: {
  338. default: 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
  339. outline:
  340. '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))]',
  341. },
  342. size: {
  343. default: 'h-8 text-sm',
  344. sm: 'h-7 text-xs',
  345. lg: 'h-12 text-sm group-data-[collapsible=icon]:p-0!',
  346. },
  347. },
  348. defaultVariants: {
  349. variant: 'default',
  350. size: 'default',
  351. },
  352. },
  353. );
  354. function SidebarMenuButton({
  355. asChild = false,
  356. isActive = false,
  357. variant = 'default',
  358. size = 'default',
  359. tooltip,
  360. className,
  361. ...props
  362. }: React.ComponentProps<'button'> & {
  363. asChild?: boolean;
  364. isActive?: boolean;
  365. tooltip?: string | React.ComponentProps<typeof TooltipContent>;
  366. } & VariantProps<typeof sidebarMenuButtonVariants>) {
  367. const Comp = asChild ? Slot : 'button';
  368. const { isMobile, state } = useSidebar();
  369. const button = (
  370. <Comp
  371. data-slot="sidebar-menu-button"
  372. data-sidebar="menu-button"
  373. data-size={size}
  374. data-active={isActive}
  375. className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
  376. {...props}
  377. />
  378. );
  379. if (!tooltip) {
  380. return button;
  381. }
  382. if (typeof tooltip === 'string') {
  383. tooltip = {
  384. children: tooltip,
  385. };
  386. }
  387. return (
  388. <Tooltip>
  389. <TooltipTrigger asChild>{button}</TooltipTrigger>
  390. <TooltipContent side="right" align="center" hidden={state !== 'collapsed' || isMobile} {...tooltip} />
  391. </Tooltip>
  392. );
  393. }
  394. function SidebarMenuAction({
  395. className,
  396. asChild = false,
  397. showOnHover = false,
  398. ...props
  399. }: React.ComponentProps<'button'> & {
  400. asChild?: boolean;
  401. showOnHover?: boolean;
  402. }) {
  403. const Comp = asChild ? Slot : 'button';
  404. return (
  405. <Comp
  406. data-slot="sidebar-menu-action"
  407. data-sidebar="menu-action"
  408. className={cn(
  409. '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',
  410. // Increases the hit area of the button on mobile.
  411. 'after:absolute after:-inset-2 md:after:hidden',
  412. 'peer-data-[size=sm]/menu-button:top-1',
  413. 'peer-data-[size=default]/menu-button:top-1.5',
  414. 'peer-data-[size=lg]/menu-button:top-2.5',
  415. 'group-data-[collapsible=icon]:hidden',
  416. showOnHover &&
  417. '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',
  418. className,
  419. )}
  420. {...props}
  421. />
  422. );
  423. }
  424. function SidebarMenuBadge({ className, ...props }: React.ComponentProps<'div'>) {
  425. return (
  426. <div
  427. data-slot="sidebar-menu-badge"
  428. data-sidebar="menu-badge"
  429. className={cn(
  430. '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',
  431. 'peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground',
  432. 'peer-data-[size=sm]/menu-button:top-1',
  433. 'peer-data-[size=default]/menu-button:top-1.5',
  434. 'peer-data-[size=lg]/menu-button:top-2.5',
  435. 'group-data-[collapsible=icon]:hidden',
  436. className,
  437. )}
  438. {...props}
  439. />
  440. );
  441. }
  442. function SidebarMenuSkeleton({
  443. className,
  444. showIcon = false,
  445. ...props
  446. }: React.ComponentProps<'div'> & {
  447. showIcon?: boolean;
  448. }) {
  449. // Random width between 50 to 90%.
  450. const width = React.useMemo(() => {
  451. return `${Math.floor(Math.random() * 40) + 50}%`;
  452. }, []);
  453. return (
  454. <div
  455. data-slot="sidebar-menu-skeleton"
  456. data-sidebar="menu-skeleton"
  457. className={cn('flex h-8 items-center gap-2 rounded-md px-2', className)}
  458. {...props}
  459. >
  460. {showIcon && <Skeleton className="size-4 rounded-md" data-sidebar="menu-skeleton-icon" />}
  461. <Skeleton
  462. className="h-4 max-w-(--skeleton-width) flex-1"
  463. data-sidebar="menu-skeleton-text"
  464. style={
  465. {
  466. '--skeleton-width': width,
  467. } as React.CSSProperties
  468. }
  469. />
  470. </div>
  471. );
  472. }
  473. function SidebarMenuSub({ className, ...props }: React.ComponentProps<'ul'>) {
  474. return (
  475. <ul
  476. data-slot="sidebar-menu-sub"
  477. data-sidebar="menu-sub"
  478. className={cn(
  479. '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',
  480. 'group-data-[collapsible=icon]:hidden',
  481. className,
  482. )}
  483. {...props}
  484. />
  485. );
  486. }
  487. function SidebarMenuSubItem({ className, ...props }: React.ComponentProps<'li'>) {
  488. return <li data-slot="sidebar-menu-sub-item" data-sidebar="menu-sub-item" className={cn('group/menu-sub-item relative', className)} {...props} />;
  489. }
  490. function SidebarMenuSubButton({
  491. asChild = false,
  492. size = 'md',
  493. isActive = false,
  494. className,
  495. ...props
  496. }: React.ComponentProps<'a'> & {
  497. asChild?: boolean;
  498. size?: 'sm' | 'md';
  499. isActive?: boolean;
  500. }) {
  501. const Comp = asChild ? Slot : 'a';
  502. return (
  503. <Comp
  504. data-slot="sidebar-menu-sub-button"
  505. data-sidebar="menu-sub-button"
  506. data-size={size}
  507. data-active={isActive}
  508. className={cn(
  509. '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',
  510. 'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground',
  511. size === 'sm' && 'text-xs',
  512. size === 'md' && 'text-sm',
  513. 'group-data-[collapsible=icon]:hidden',
  514. className,
  515. )}
  516. {...props}
  517. />
  518. );
  519. }
  520. export {
  521. Sidebar,
  522. SidebarContent,
  523. SidebarFooter,
  524. SidebarGroup,
  525. SidebarGroupAction,
  526. SidebarGroupContent,
  527. SidebarGroupLabel,
  528. SidebarHeader,
  529. SidebarInput,
  530. SidebarInset,
  531. SidebarMenu,
  532. SidebarMenuAction,
  533. SidebarMenuBadge,
  534. SidebarMenuButton,
  535. SidebarMenuItem,
  536. SidebarMenuSkeleton,
  537. SidebarMenuSub,
  538. SidebarMenuSubButton,
  539. SidebarMenuSubItem,
  540. SidebarProvider,
  541. SidebarRail,
  542. SidebarSeparator,
  543. SidebarTrigger,
  544. useSidebar,
  545. };