sidebar.tsx 23 KB

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