sidebar.tsx 21 KB

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