@c-rex/ui 0.1.20 → 0.1.21

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