sidebar.tsx 23 KB

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