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