sidebar.tsx 21 KB

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