← full-stack-fastapi-template  /  frontend/src/components/ui/sidebar.tsx

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