sidebar.tsx 21 KB

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