sidebar.tsx 23 KB

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