@djangocfg/ui-core 2.1.320 → 2.1.322

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,895 @@
1
+ "use client"
2
+
3
+ import { cva, VariantProps } from 'class-variance-authority';
4
+ import { Menu, PanelLeft } from 'lucide-react';
5
+ import * as React from 'react';
6
+
7
+ import { cn } from '../../../lib';
8
+ import { useIsMobile } from '../../../hooks/media';
9
+ import { useShortcutModLabel } from '../../../hooks/device';
10
+ import { Slot } from '@radix-ui/react-slot';
11
+
12
+ import { Button } from '../../forms/button';
13
+ import { Input } from '../../forms/input';
14
+ import { Drawer, DrawerContent, DrawerDescription, DrawerHeader, DrawerTitle } from '../../overlay/drawer';
15
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../overlay/tooltip';
16
+ import { Separator } from '../../layout/separator';
17
+ import { Skeleton } from '../../layout/skeleton';
18
+ import { Link } from '../link';
19
+
20
+ const SIDEBAR_COOKIE_NAME = "sidebar_state"
21
+ const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
22
+ const SIDEBAR_WIDTH = "16rem"
23
+ const SIDEBAR_WIDTH_ICON = "3rem"
24
+ const SIDEBAR_KEYBOARD_SHORTCUT = "b"
25
+
26
+ type SidebarContextProps = {
27
+ state: "expanded" | "collapsed"
28
+ open: boolean
29
+ setOpen: (open: boolean) => void
30
+ openMobile: boolean
31
+ setOpenMobile: (open: boolean) => void
32
+ isMobile: boolean
33
+ toggleSidebar: () => void
34
+ }
35
+
36
+ const SidebarContext = React.createContext<SidebarContextProps | null>(null)
37
+
38
+ function useSidebar() {
39
+ const context = React.useContext(SidebarContext)
40
+ if (!context) {
41
+ throw new Error("useSidebar must be used within a SidebarProvider.")
42
+ }
43
+
44
+ return context
45
+ }
46
+
47
+ const SidebarProvider = React.forwardRef<
48
+ HTMLDivElement,
49
+ React.ComponentProps<"div"> & {
50
+ defaultOpen?: boolean
51
+ open?: boolean
52
+ onOpenChange?: (open: boolean) => void
53
+ }
54
+ >(
55
+ (
56
+ {
57
+ defaultOpen = true,
58
+ open: openProp,
59
+ onOpenChange: setOpenProp,
60
+ className,
61
+ style,
62
+ children,
63
+ ...props
64
+ },
65
+ ref
66
+ ) => {
67
+ const isMobile = useIsMobile()
68
+ const [openMobile, setOpenMobile] = React.useState(false)
69
+
70
+ const [_open, _setOpen] = React.useState(defaultOpen)
71
+ const open = openProp ?? _open
72
+ const setOpen = React.useCallback(
73
+ (value: boolean | ((value: boolean) => boolean)) => {
74
+ const openState = typeof value === "function" ? value(open) : value
75
+ if (setOpenProp) {
76
+ setOpenProp(openState)
77
+ } else {
78
+ _setOpen(openState)
79
+ }
80
+ document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
81
+ },
82
+ [setOpenProp, open]
83
+ )
84
+
85
+ const toggleSidebar = React.useCallback(() => {
86
+ return isMobile
87
+ ? setOpenMobile((open) => !open)
88
+ : setOpen((open) => !open)
89
+ }, [isMobile, setOpen, setOpenMobile])
90
+
91
+ React.useEffect(() => {
92
+ const handleKeyDown = (event: KeyboardEvent) => {
93
+ if (
94
+ event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
95
+ (event.metaKey || event.ctrlKey)
96
+ ) {
97
+ event.preventDefault()
98
+ toggleSidebar()
99
+ }
100
+ }
101
+
102
+ window.addEventListener("keydown", handleKeyDown)
103
+ return () => window.removeEventListener("keydown", handleKeyDown)
104
+ }, [toggleSidebar])
105
+
106
+ const state = open ? "expanded" : "collapsed"
107
+
108
+ const contextValue = React.useMemo<SidebarContextProps>(
109
+ () => ({
110
+ state,
111
+ open,
112
+ setOpen,
113
+ isMobile,
114
+ openMobile,
115
+ setOpenMobile,
116
+ toggleSidebar,
117
+ }),
118
+ [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
119
+ )
120
+
121
+ return (
122
+ <SidebarContext.Provider value={contextValue}>
123
+ <TooltipProvider delayDuration={0}>
124
+ <div
125
+ style={
126
+ {
127
+ "--sidebar-width": SIDEBAR_WIDTH,
128
+ "--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
129
+ ...style,
130
+ } as React.CSSProperties
131
+ }
132
+ className={cn(
133
+ "group/sidebar-wrapper flex min-h-svh w-full has-[&_[data-variant=inset]]:bg-sidebar",
134
+ className
135
+ )}
136
+ ref={ref}
137
+ {...props}
138
+ >
139
+ {children}
140
+ </div>
141
+ </TooltipProvider>
142
+ </SidebarContext.Provider>
143
+ )
144
+ }
145
+ )
146
+ SidebarProvider.displayName = "SidebarProvider"
147
+
148
+ const Sidebar = React.forwardRef<
149
+ HTMLDivElement,
150
+ React.ComponentProps<"div"> & {
151
+ side?: "left" | "right"
152
+ variant?: "sidebar" | "floating" | "inset"
153
+ collapsible?: "offcanvas" | "icon" | "none"
154
+ }
155
+ >(
156
+ (
157
+ {
158
+ side = "left",
159
+ variant = "sidebar",
160
+ collapsible = "offcanvas",
161
+ className,
162
+ children,
163
+ ...props
164
+ },
165
+ ref
166
+ ) => {
167
+ const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
168
+
169
+ if (collapsible === "none") {
170
+ return (
171
+ <div
172
+ className={cn(
173
+ "flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground",
174
+ className
175
+ )}
176
+ ref={ref}
177
+ {...props}
178
+ >
179
+ {children}
180
+ </div>
181
+ )
182
+ }
183
+
184
+ if (isMobile) {
185
+ const drawerSide = side === "right" ? "right" : "left"
186
+ return (
187
+ <div ref={ref} className={cn("contents", className)} {...props}>
188
+ <Drawer
189
+ open={openMobile}
190
+ onOpenChange={setOpenMobile}
191
+ direction={drawerSide}
192
+ shouldScaleBackground={false}
193
+ >
194
+ <DrawerContent
195
+ direction={drawerSide}
196
+ data-sidebar="sidebar"
197
+ data-mobile="true"
198
+ className={cn(
199
+ "flex h-[100dvh] max-h-[100dvh] min-h-0 !w-[min(80vw,320px)] max-w-[min(80vw,320px)] flex-col gap-0 border-sidebar-border/60 p-0 text-sidebar-foreground shadow-xl",
200
+ drawerSide === "left" ? "border-r" : "border-l",
201
+ )}
202
+ style={
203
+ {
204
+ backgroundColor: "hsl(var(--sidebar-background))",
205
+ } as React.CSSProperties
206
+ }
207
+ >
208
+ <DrawerHeader className="sr-only">
209
+ <DrawerTitle>Sidebar</DrawerTitle>
210
+ <DrawerDescription>Mobile navigation sidebar.</DrawerDescription>
211
+ </DrawerHeader>
212
+ <div className="flex min-h-0 flex-1 flex-col overflow-hidden">{children}</div>
213
+ </DrawerContent>
214
+ </Drawer>
215
+ </div>
216
+ )
217
+ }
218
+
219
+ const getGapWidth = () => {
220
+ if (state === "collapsed") {
221
+ if (collapsible === "offcanvas") return "0"
222
+ if (collapsible === "icon") {
223
+ return variant === "floating" || variant === "inset"
224
+ ? "calc(var(--sidebar-width-icon) + 1rem)"
225
+ : "var(--sidebar-width-icon)"
226
+ }
227
+ }
228
+ return "var(--sidebar-width)"
229
+ }
230
+
231
+ const getFixedWidth = () => {
232
+ if (state === "collapsed" && collapsible === "icon") {
233
+ return variant === "floating" || variant === "inset"
234
+ ? "calc(var(--sidebar-width-icon) + 1rem + 2px)"
235
+ : "var(--sidebar-width-icon)"
236
+ }
237
+ return "var(--sidebar-width)"
238
+ }
239
+
240
+ return (
241
+ <div
242
+ ref={ref}
243
+ className="group peer hidden text-sidebar-foreground md:block"
244
+ data-state={state}
245
+ data-collapsible={collapsible}
246
+ data-variant={variant}
247
+ data-side={side}
248
+ >
249
+ <div
250
+ className={cn(
251
+ "relative h-full bg-transparent transition-[width] duration-200 ease-linear",
252
+ side === "right" && "rotate-180"
253
+ )}
254
+ style={{
255
+ width: getGapWidth()
256
+ }}
257
+ />
258
+ <div
259
+ className={cn(
260
+ "fixed inset-y-0 z-10 hidden h-svh transition-[left,right,width] duration-200 ease-linear md:flex",
261
+ side === "left"
262
+ ? state === "collapsed" && collapsible === "offcanvas"
263
+ ? "left-[calc(var(--sidebar-width)*-1)]"
264
+ : "left-0"
265
+ : state === "collapsed" && collapsible === "offcanvas"
266
+ ? "right-[calc(var(--sidebar-width)*-1)]"
267
+ : "right-0",
268
+ variant === "floating" || variant === "inset"
269
+ ? "p-2"
270
+ : undefined,
271
+ className
272
+ )}
273
+ style={{
274
+ width: getFixedWidth()
275
+ }}
276
+ {...props}
277
+ >
278
+ <div
279
+ data-sidebar="sidebar"
280
+ className={cn(
281
+ "relative flex h-full w-full flex-col overflow-hidden text-sidebar-foreground",
282
+ variant === "floating"
283
+ ? "rounded-lg border border-sidebar-border bg-sidebar shadow"
284
+ : "bg-sidebar",
285
+ )}
286
+ >
287
+ {variant === "sidebar" && (
288
+ <div
289
+ aria-hidden
290
+ className={cn(
291
+ "pointer-events-none absolute inset-y-0 right-0 z-[1] w-px",
292
+ "bg-[linear-gradient(180deg,hsl(var(--sidebar-border)_/_0.02)_0%,hsl(var(--sidebar-border)_/_0.22)_18%,hsl(var(--sidebar-border)_/_0.4)_50%,hsl(var(--sidebar-border)_/_0.2)_82%,hsl(var(--sidebar-border)_/_0.03)_100%)]",
293
+ "dark:bg-[linear-gradient(180deg,hsl(var(--sidebar-border)_/_0.08)_0%,hsl(var(--sidebar-border)_/_0.34)_22%,hsl(var(--sidebar-border)_/_0.55)_50%,hsl(var(--sidebar-border)_/_0.28)_78%,hsl(var(--sidebar-border)_/_0.06)_100%)]"
294
+ )}
295
+ />
296
+ )}
297
+ <div className="relative z-0 flex min-h-0 flex-1 flex-col">{children}</div>
298
+ </div>
299
+ </div>
300
+ </div>
301
+ )
302
+ }
303
+ )
304
+ Sidebar.displayName = "Sidebar"
305
+
306
+ const SidebarTrigger = React.forwardRef<
307
+ React.ElementRef<typeof Button>,
308
+ React.ComponentProps<typeof Button>
309
+ >(({ className, onClick, ...props }, ref) => {
310
+ const { toggleSidebar, openMobile } = useSidebar()
311
+ const mod = useShortcutModLabel()
312
+ const isMobile = useIsMobile()
313
+ const desktopHint = `Toggle sidebar (${mod}+B)`
314
+
315
+ const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
316
+ onClick?.(event)
317
+ toggleSidebar()
318
+ }
319
+
320
+ const buttonClass = cn("h-7 w-7", className)
321
+
322
+ if (isMobile) {
323
+ return (
324
+ <Button
325
+ ref={ref}
326
+ data-sidebar="trigger"
327
+ variant="ghost"
328
+ size="icon"
329
+ className={buttonClass}
330
+ onClick={handleClick}
331
+ {...props}
332
+ >
333
+ <Menu className="h-4 w-4" />
334
+ <span className="sr-only">{openMobile ? "Close menu" : "Open menu"}</span>
335
+ </Button>
336
+ )
337
+ }
338
+
339
+ return (
340
+ <Tooltip delayDuration={400}>
341
+ <TooltipTrigger asChild>
342
+ <Button
343
+ ref={ref}
344
+ data-sidebar="trigger"
345
+ variant="ghost"
346
+ size="icon"
347
+ className={buttonClass}
348
+ onClick={handleClick}
349
+ {...props}
350
+ >
351
+ <PanelLeft className="h-4 w-4" />
352
+ <span className="sr-only">{desktopHint}</span>
353
+ </Button>
354
+ </TooltipTrigger>
355
+ <TooltipContent side="right" align="center" sideOffset={8} className="max-w-[16rem]">
356
+ {desktopHint}
357
+ </TooltipContent>
358
+ </Tooltip>
359
+ )
360
+ })
361
+ SidebarTrigger.displayName = "SidebarTrigger"
362
+
363
+ const SidebarRail = React.forwardRef<
364
+ HTMLButtonElement,
365
+ React.ComponentProps<"button">
366
+ >(({ className, ...props }, ref) => {
367
+ const { toggleSidebar } = useSidebar()
368
+
369
+ return (
370
+ <button
371
+ ref={ref}
372
+ data-sidebar="rail"
373
+ aria-label="Toggle Sidebar"
374
+ tabIndex={-1}
375
+ onClick={toggleSidebar}
376
+ className={cn(
377
+ "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",
378
+ "group-data-[side=left]:cursor-w-resize group-data-[side=right]:cursor-e-resize",
379
+ "group-data-[side=left]:group-data-[state=collapsed]:cursor-e-resize group-data-[side=right]:group-data-[state=collapsed]:cursor-w-resize",
380
+ "group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar",
381
+ "group-data-[side=left]:group-data-[collapsible=offcanvas]:-right-2",
382
+ "group-data-[side=right]:group-data-[collapsible=offcanvas]:-left-2",
383
+ className
384
+ )}
385
+ {...props}
386
+ />
387
+ )
388
+ })
389
+ SidebarRail.displayName = "SidebarRail"
390
+
391
+ const SidebarInset = React.forwardRef<
392
+ HTMLDivElement,
393
+ React.ComponentProps<"main">
394
+ >(({ className, ...props }, ref) => {
395
+ return (
396
+ <main
397
+ ref={ref}
398
+ className={cn(
399
+ "relative flex w-full flex-1 flex-col bg-background",
400
+ "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",
401
+ className
402
+ )}
403
+ {...props}
404
+ />
405
+ )
406
+ })
407
+ SidebarInset.displayName = "SidebarInset"
408
+
409
+ const SidebarInput: React.ForwardRefExoticComponent<
410
+ React.ComponentProps<typeof Input> & React.RefAttributes<HTMLInputElement>
411
+ > = React.forwardRef<
412
+ React.ElementRef<typeof Input>,
413
+ React.ComponentProps<typeof Input>
414
+ >(({ className, ...props }, ref) => {
415
+ return (
416
+ <Input
417
+ ref={ref}
418
+ data-sidebar="input"
419
+ className={cn(
420
+ "h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring",
421
+ className
422
+ )}
423
+ {...props}
424
+ />
425
+ )
426
+ })
427
+ SidebarInput.displayName = "SidebarInput"
428
+
429
+ const SidebarHeader = React.forwardRef<
430
+ HTMLDivElement,
431
+ React.ComponentProps<"div">
432
+ >(({ className, style, ...props }, ref) => {
433
+ const { state, isMobile } = useSidebar()
434
+
435
+ const headerStyle = state === "collapsed" && !isMobile ? {
436
+ paddingLeft: '0',
437
+ paddingRight: '0',
438
+ paddingTop: '0.5rem',
439
+ paddingBottom: '0.5rem',
440
+ transition: 'padding 200ms ease-in-out',
441
+ ...style
442
+ } : {
443
+ transition: 'padding 200ms ease-in-out',
444
+ ...style
445
+ }
446
+
447
+ return (
448
+ <div
449
+ ref={ref}
450
+ data-sidebar="header"
451
+ className={cn("flex flex-col gap-2 p-2", className)}
452
+ style={headerStyle}
453
+ {...props}
454
+ />
455
+ )
456
+ })
457
+ SidebarHeader.displayName = "SidebarHeader"
458
+
459
+ const SidebarFooter = React.forwardRef<
460
+ HTMLDivElement,
461
+ React.ComponentProps<"div">
462
+ >(({ className, ...props }, ref) => {
463
+ return (
464
+ <div
465
+ ref={ref}
466
+ data-sidebar="footer"
467
+ className={cn("flex flex-col gap-2 p-2", className)}
468
+ {...props}
469
+ />
470
+ )
471
+ })
472
+ SidebarFooter.displayName = "SidebarFooter"
473
+
474
+ const SidebarSeparator: React.ForwardRefExoticComponent<
475
+ React.ComponentProps<typeof Separator> & React.RefAttributes<React.ElementRef<typeof Separator>>
476
+ > = React.forwardRef<
477
+ React.ElementRef<typeof Separator>,
478
+ React.ComponentProps<typeof Separator>
479
+ >(({ className, ...props }, ref) => {
480
+ return (
481
+ <Separator
482
+ ref={ref}
483
+ data-sidebar="separator"
484
+ className={cn("mx-2 w-auto bg-sidebar-border", className)}
485
+ {...props}
486
+ />
487
+ )
488
+ })
489
+ SidebarSeparator.displayName = "SidebarSeparator"
490
+
491
+ const SidebarContent = React.forwardRef<
492
+ HTMLDivElement,
493
+ React.ComponentProps<"div">
494
+ >(({ className, ...props }, ref) => {
495
+ return (
496
+ <div
497
+ ref={ref}
498
+ data-sidebar="content"
499
+ className={cn(
500
+ "flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[state=collapsed]:group-data-[collapsible=icon]:overflow-hidden",
501
+ className
502
+ )}
503
+ {...props}
504
+ />
505
+ )
506
+ })
507
+ SidebarContent.displayName = "SidebarContent"
508
+
509
+ const SidebarGroup = React.forwardRef<
510
+ HTMLDivElement,
511
+ React.ComponentProps<"div">
512
+ >(({ className, ...props }, ref) => {
513
+ return (
514
+ <div
515
+ ref={ref}
516
+ data-sidebar="group"
517
+ className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
518
+ {...props}
519
+ />
520
+ )
521
+ })
522
+ SidebarGroup.displayName = "SidebarGroup"
523
+
524
+ const SidebarGroupLabel = React.forwardRef<
525
+ HTMLDivElement,
526
+ React.ComponentProps<"div"> & { asChild?: boolean }
527
+ >(({ className, asChild = false, ...props }, ref) => {
528
+ const Comp = asChild ? Slot : "div"
529
+
530
+ return (
531
+ <Comp
532
+ ref={ref}
533
+ data-sidebar="group-label"
534
+ className={cn(
535
+ "flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-semibold text-muted-foreground uppercase tracking-wider outline-none ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
536
+ "group-data-[state=collapsed]:group-data-[collapsible=icon]:pointer-events-none group-data-[state=collapsed]:group-data-[collapsible=icon]:-mt-8 group-data-[state=collapsed]:group-data-[collapsible=icon]:opacity-0",
537
+ className
538
+ )}
539
+ {...props}
540
+ />
541
+ )
542
+ })
543
+ SidebarGroupLabel.displayName = "SidebarGroupLabel"
544
+
545
+ const SidebarGroupAction = React.forwardRef<
546
+ HTMLButtonElement,
547
+ React.ComponentProps<"button"> & { asChild?: boolean }
548
+ >(({ className, asChild = false, ...props }, ref) => {
549
+ const Comp = asChild ? Slot : "button"
550
+
551
+ return (
552
+ <Comp
553
+ ref={ref}
554
+ data-sidebar="group-action"
555
+ className={cn(
556
+ "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",
557
+ "after:absolute after:-inset-2 after:md:hidden",
558
+ "group-data-[state=collapsed]:group-data-[collapsible=icon]:hidden",
559
+ className
560
+ )}
561
+ {...props}
562
+ />
563
+ )
564
+ })
565
+ SidebarGroupAction.displayName = "SidebarGroupAction"
566
+
567
+ const SidebarGroupContent = React.forwardRef<
568
+ HTMLDivElement,
569
+ React.ComponentProps<"div">
570
+ >(({ className, ...props }, ref) => (
571
+ <div
572
+ ref={ref}
573
+ data-sidebar="group-content"
574
+ className={cn("w-full text-sm", className)}
575
+ {...props}
576
+ />
577
+ ))
578
+ SidebarGroupContent.displayName = "SidebarGroupContent"
579
+
580
+ const SidebarMenu = React.forwardRef<
581
+ HTMLUListElement,
582
+ React.ComponentProps<"ul">
583
+ >(({ className, ...props }, ref) => (
584
+ <ul
585
+ ref={ref}
586
+ data-sidebar="menu"
587
+ className={cn("flex w-full min-w-0 flex-col gap-1", className)}
588
+ {...props}
589
+ />
590
+ ))
591
+ SidebarMenu.displayName = "SidebarMenu"
592
+
593
+ const SidebarMenuItem = React.forwardRef<
594
+ HTMLLIElement,
595
+ React.ComponentProps<"li">
596
+ >(({ className, ...props }, ref) => (
597
+ <li
598
+ ref={ref}
599
+ data-sidebar="menu-item"
600
+ className={cn("group/menu-item relative", className)}
601
+ {...props}
602
+ />
603
+ ))
604
+ SidebarMenuItem.displayName = "SidebarMenuItem"
605
+
606
+ const sidebarMenuButtonVariants = cva(
607
+ "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm font-medium 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-semibold data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-muted-foreground [&>svg]:opacity-70 data-[active=true]:[&>svg]:text-sidebar-accent-foreground data-[active=true]:[&>svg]:opacity-100 hover:[&>svg]:text-sidebar-accent-foreground hover:[&>svg]:opacity-100 group-data-[state=collapsed]:group-data-[collapsible=icon]:justify-center group-data-[state=collapsed]:group-data-[collapsible=icon]:[&>span]:hidden",
608
+ {
609
+ variants: {
610
+ variant: {
611
+ default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
612
+ outline:
613
+ "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))]",
614
+ },
615
+ size: {
616
+ default: "h-8 text-sm",
617
+ sm: "h-7 text-xs",
618
+ lg: "h-12 text-sm group-data-[state=collapsed]:group-data-[collapsible=icon]:!p-0",
619
+ },
620
+ },
621
+ defaultVariants: {
622
+ variant: "default",
623
+ size: "default",
624
+ },
625
+ }
626
+ )
627
+
628
+ const SidebarMenuButton = React.forwardRef<
629
+ HTMLButtonElement,
630
+ React.ComponentProps<"button"> & {
631
+ asChild?: boolean
632
+ isActive?: boolean
633
+ tooltip?: string | React.ComponentProps<typeof TooltipContent>
634
+ href?: string
635
+ } & VariantProps<typeof sidebarMenuButtonVariants>
636
+ >(
637
+ (
638
+ {
639
+ asChild = false,
640
+ isActive = false,
641
+ variant = "default",
642
+ size = "default",
643
+ tooltip,
644
+ className,
645
+ style,
646
+ href,
647
+ children,
648
+ ...props
649
+ },
650
+ ref
651
+ ) => {
652
+ const { isMobile, state } = useSidebar()
653
+
654
+ const buttonContent = React.useMemo(() => {
655
+ if (href) {
656
+ return (
657
+ <Link
658
+ href={href}
659
+ className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
660
+ style={style}
661
+ data-sidebar="menu-button"
662
+ data-size={size}
663
+ data-active={isActive}
664
+ >
665
+ {children}
666
+ </Link>
667
+ )
668
+ }
669
+
670
+ const Comp = asChild ? Slot : "button"
671
+ return (
672
+ <Comp
673
+ ref={ref}
674
+ data-sidebar="menu-button"
675
+ data-size={size}
676
+ data-active={isActive}
677
+ className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
678
+ style={style}
679
+ {...props}
680
+ >
681
+ {children}
682
+ </Comp>
683
+ )
684
+ }, [href, asChild, ref, variant, size, className, style, isActive, children, props])
685
+
686
+ if (!tooltip) {
687
+ return buttonContent
688
+ }
689
+
690
+ const tooltipContentProps: React.ComponentProps<typeof TooltipContent> =
691
+ typeof tooltip === "string" ? { children: tooltip } : { ...tooltip }
692
+
693
+ return (
694
+ <Tooltip>
695
+ <TooltipTrigger asChild>{buttonContent}</TooltipTrigger>
696
+ <TooltipContent
697
+ side="right"
698
+ align="center"
699
+ sideOffset={8}
700
+ avoidCollisions={false}
701
+ hidden={state !== "collapsed" || isMobile}
702
+ {...tooltipContentProps}
703
+ />
704
+ </Tooltip>
705
+ )
706
+ }
707
+ )
708
+ SidebarMenuButton.displayName = "SidebarMenuButton"
709
+
710
+ const SidebarMenuAction = React.forwardRef<
711
+ HTMLButtonElement,
712
+ React.ComponentProps<"button"> & {
713
+ asChild?: boolean
714
+ showOnHover?: boolean
715
+ }
716
+ >(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
717
+ const Comp = asChild ? Slot : "button"
718
+
719
+ return (
720
+ <Comp
721
+ ref={ref}
722
+ data-sidebar="menu-action"
723
+ className={cn(
724
+ "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",
725
+ "after:absolute after:-inset-2 after:md:hidden",
726
+ "peer-data-[size=sm]/menu-button:top-1",
727
+ "peer-data-[size=default]/menu-button:top-1.5",
728
+ "peer-data-[size=lg]/menu-button:top-2.5",
729
+ "group-data-[state=collapsed]:group-data-[collapsible=icon]:hidden",
730
+ showOnHover &&
731
+ "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",
732
+ className
733
+ )}
734
+ {...props}
735
+ />
736
+ )
737
+ })
738
+ SidebarMenuAction.displayName = "SidebarMenuAction"
739
+
740
+ const SidebarMenuBadge = React.forwardRef<
741
+ HTMLDivElement,
742
+ React.ComponentProps<"div">
743
+ >(({ className, ...props }, ref) => (
744
+ <div
745
+ ref={ref}
746
+ data-sidebar="menu-badge"
747
+ className={cn(
748
+ "pointer-events-none absolute right-1 flex h-4 min-w-4 select-none items-center justify-center rounded-md px-1 text-[10px] font-medium tabular-nums leading-none text-muted-foreground/75",
749
+ "peer-hover/menu-button:text-muted-foreground peer-data-[active=true]/menu-button:text-foreground/65",
750
+ "peer-data-[size=sm]/menu-button:top-1",
751
+ "peer-data-[size=default]/menu-button:top-1.5",
752
+ "peer-data-[size=lg]/menu-button:top-2",
753
+ "group-data-[state=collapsed]:group-data-[collapsible=icon]:hidden",
754
+ className
755
+ )}
756
+ {...props}
757
+ />
758
+ ))
759
+ SidebarMenuBadge.displayName = "SidebarMenuBadge"
760
+
761
+ const SidebarMenuSkeleton = React.forwardRef<
762
+ HTMLDivElement,
763
+ React.ComponentProps<"div"> & {
764
+ showIcon?: boolean
765
+ }
766
+ >(({ className, showIcon = false, ...props }, ref) => {
767
+ const width = React.useMemo(() => {
768
+ return `${Math.floor(Math.random() * 40) + 50}%`
769
+ }, [])
770
+
771
+ return (
772
+ <div
773
+ ref={ref}
774
+ data-sidebar="menu-skeleton"
775
+ className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
776
+ {...props}
777
+ >
778
+ {showIcon && (
779
+ <Skeleton
780
+ className="size-4 rounded-md"
781
+ data-sidebar="menu-skeleton-icon"
782
+ />
783
+ )}
784
+ <Skeleton
785
+ className="h-4 max-w-[--skeleton-width] flex-1"
786
+ data-sidebar="menu-skeleton-text"
787
+ style={
788
+ {
789
+ "--skeleton-width": width,
790
+ } as React.CSSProperties
791
+ }
792
+ />
793
+ </div>
794
+ )
795
+ })
796
+ SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton"
797
+
798
+ const SidebarMenuSub = React.forwardRef<
799
+ HTMLUListElement,
800
+ React.ComponentProps<"ul">
801
+ >(({ className, ...props }, ref) => (
802
+ <ul
803
+ ref={ref}
804
+ data-sidebar="menu-sub"
805
+ className={cn(
806
+ "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",
807
+ "group-data-[state=collapsed]:group-data-[collapsible=icon]:hidden",
808
+ className
809
+ )}
810
+ {...props}
811
+ />
812
+ ))
813
+ SidebarMenuSub.displayName = "SidebarMenuSub"
814
+
815
+ const SidebarMenuSubItem = React.forwardRef<
816
+ HTMLLIElement,
817
+ React.ComponentProps<"li">
818
+ >(({ ...props }, ref) => <li ref={ref} {...props} />)
819
+ SidebarMenuSubItem.displayName = "SidebarMenuSubItem"
820
+
821
+ const SidebarMenuSubButton = React.forwardRef<
822
+ HTMLAnchorElement,
823
+ React.ComponentProps<"a"> & {
824
+ asChild?: boolean
825
+ size?: "sm" | "md"
826
+ isActive?: boolean
827
+ href?: string
828
+ }
829
+ >(({ asChild = false, size = "md", isActive, className, href, children, ...props }, ref) => {
830
+ const Comp = asChild ? Slot : "a"
831
+
832
+ const buttonClasses = cn(
833
+ "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-muted-foreground [&>svg]:opacity-70 data-[active=true]:[&>svg]:text-sidebar-accent-foreground data-[active=true]:[&>svg]:opacity-100 hover:[&>svg]:text-sidebar-accent-foreground hover:[&>svg]:opacity-100",
834
+ "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
835
+ size === "sm" && "text-xs",
836
+ size === "md" && "text-sm",
837
+ "group-data-[state=collapsed]:group-data-[collapsible=icon]:hidden",
838
+ className
839
+ )
840
+
841
+ if (href) {
842
+ return (
843
+ <Link
844
+ href={href}
845
+ className={buttonClasses}
846
+ data-sidebar="menu-sub-button"
847
+ data-size={size}
848
+ data-active={isActive}
849
+ >
850
+ {children}
851
+ </Link>
852
+ )
853
+ }
854
+
855
+ return (
856
+ <Comp
857
+ ref={ref}
858
+ data-sidebar="menu-sub-button"
859
+ data-size={size}
860
+ data-active={isActive}
861
+ className={buttonClasses}
862
+ {...props}
863
+ >
864
+ {children}
865
+ </Comp>
866
+ )
867
+ })
868
+ SidebarMenuSubButton.displayName = "SidebarMenuSubButton"
869
+
870
+ export {
871
+ Sidebar,
872
+ SidebarContent,
873
+ SidebarFooter,
874
+ SidebarGroup,
875
+ SidebarGroupAction,
876
+ SidebarGroupContent,
877
+ SidebarGroupLabel,
878
+ SidebarHeader,
879
+ SidebarInput,
880
+ SidebarInset,
881
+ SidebarMenu,
882
+ SidebarMenuAction,
883
+ SidebarMenuBadge,
884
+ SidebarMenuButton,
885
+ SidebarMenuItem,
886
+ SidebarMenuSkeleton,
887
+ SidebarMenuSub,
888
+ SidebarMenuSubButton,
889
+ SidebarMenuSubItem,
890
+ SidebarProvider,
891
+ SidebarRail,
892
+ SidebarSeparator,
893
+ SidebarTrigger,
894
+ useSidebar,
895
+ }