@c-rex/ui 0.1.6 → 0.1.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c-rex/ui",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src"
@@ -10,6 +10,10 @@
10
10
  "access": "public"
11
11
  },
12
12
  "exports": {
13
+ "./hooks": {
14
+ "types": "./src/hooks/index.tsx",
15
+ "import": "./src/hooks/index.tsx"
16
+ },
13
17
  "./collapsible": {
14
18
  "types": "./src/collapsible.tsx",
15
19
  "import": "./src/collapsible.tsx"
@@ -18,6 +22,10 @@
18
22
  "types": "./src/alert.tsx",
19
23
  "import": "./src/alert.tsx"
20
24
  },
25
+ "./drawer": {
26
+ "types": "./src/drawer.tsx",
27
+ "import": "./src/drawer.tsx"
28
+ },
21
29
  "./breadcrumb": {
22
30
  "types": "./src/breadcrumb.tsx",
23
31
  "import": "./src/breadcrumb.tsx"
@@ -152,7 +160,7 @@
152
160
  "@radix-ui/react-checkbox": "^1.3.2",
153
161
  "@radix-ui/react-collapsible": "^1.1.11",
154
162
  "@radix-ui/react-context-menu": "^2.2.15",
155
- "@radix-ui/react-dialog": "^1.1.6",
163
+ "@radix-ui/react-dialog": "^1.1.15",
156
164
  "@radix-ui/react-dropdown-menu": "^2.1.15",
157
165
  "@radix-ui/react-label": "^2.1.7",
158
166
  "@radix-ui/react-popover": "^1.1.6",
@@ -173,6 +181,11 @@
173
181
  "react-dom": "^18.3.1",
174
182
  "sonner": "^2.0.5",
175
183
  "tailwind-merge": "^3.3.0",
176
- "tw-animate-css": "^1.2.9"
184
+ "tw-animate-css": "^1.2.9",
185
+ "vaul": "^1.1.2"
186
+ },
187
+ "peerDependencies": {
188
+ "react": "^18.3.1",
189
+ "react-dom": "^18.3.1"
177
190
  }
178
191
  }
package/src/badge.tsx CHANGED
@@ -5,42 +5,42 @@ import { cva, type VariantProps } from "class-variance-authority"
5
5
  import { cn } from "@c-rex/utils"
6
6
 
7
7
  const badgeVariants = cva(
8
- "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
9
- {
10
- variants: {
11
- variant: {
12
- default:
13
- "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
14
- secondary:
15
- "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
16
- destructive:
17
- "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
18
- outline:
19
- "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
20
- },
21
- },
22
- defaultVariants: {
23
- variant: "default",
24
- },
25
- }
8
+ "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default:
13
+ "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
14
+ secondary:
15
+ "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
16
+ destructive:
17
+ "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
18
+ outline:
19
+ "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
20
+ },
21
+ },
22
+ defaultVariants: {
23
+ variant: "default",
24
+ },
25
+ }
26
26
  )
27
27
 
28
28
  function Badge({
29
- className,
30
- variant,
31
- asChild = false,
32
- ...props
29
+ className,
30
+ variant,
31
+ asChild = false,
32
+ ...props
33
33
  }: React.ComponentProps<"span"> &
34
- VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
35
- const Comp = asChild ? Slot : "span"
34
+ VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
35
+ const Comp = asChild ? Slot : "span"
36
36
 
37
- return (
38
- <Comp
39
- data-slot="badge"
40
- className={cn(badgeVariants({ variant }), className)}
41
- {...props}
42
- />
43
- )
37
+ return (
38
+ <Comp
39
+ data-slot="badge"
40
+ className={cn(badgeVariants({ variant }), className)}
41
+ {...props}
42
+ />
43
+ )
44
44
  }
45
45
 
46
46
  export { Badge, badgeVariants }
package/src/drawer.tsx ADDED
@@ -0,0 +1,133 @@
1
+ import * as React from "react"
2
+ import { Drawer as DrawerPrimitive } from "vaul"
3
+
4
+ import { cn } from "@c-rex/utils"
5
+
6
+ function Drawer({
7
+ ...props
8
+ }: React.ComponentProps<typeof DrawerPrimitive.Root>) {
9
+ return <DrawerPrimitive.Root data-slot="drawer" {...props} />
10
+ }
11
+
12
+ function DrawerTrigger({
13
+ ...props
14
+ }: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
15
+ return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
16
+ }
17
+
18
+ function DrawerPortal({
19
+ ...props
20
+ }: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
21
+ return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
22
+ }
23
+
24
+ function DrawerClose({
25
+ ...props
26
+ }: React.ComponentProps<typeof DrawerPrimitive.Close>) {
27
+ return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
28
+ }
29
+
30
+ function DrawerOverlay({
31
+ className,
32
+ ...props
33
+ }: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
34
+ return (
35
+ <DrawerPrimitive.Overlay
36
+ data-slot="drawer-overlay"
37
+ className={cn(
38
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
39
+ className
40
+ )}
41
+ {...props}
42
+ />
43
+ )
44
+ }
45
+
46
+ function DrawerContent({
47
+ className,
48
+ children,
49
+ ...props
50
+ }: React.ComponentProps<typeof DrawerPrimitive.Content>) {
51
+ return (
52
+ <DrawerPortal data-slot="drawer-portal">
53
+ <DrawerOverlay />
54
+ <DrawerPrimitive.Content
55
+ data-slot="drawer-content"
56
+ className={cn(
57
+ "group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
58
+ "data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
59
+ "data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
60
+ "data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
61
+ "data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
62
+ className
63
+ )}
64
+ {...props}
65
+ >
66
+ <div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
67
+ {children}
68
+ </DrawerPrimitive.Content>
69
+ </DrawerPortal>
70
+ )
71
+ }
72
+
73
+ function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
74
+ return (
75
+ <div
76
+ data-slot="drawer-header"
77
+ className={cn(
78
+ "flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left",
79
+ className
80
+ )}
81
+ {...props}
82
+ />
83
+ )
84
+ }
85
+
86
+ function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
87
+ return (
88
+ <div
89
+ data-slot="drawer-footer"
90
+ className={cn("mt-auto flex flex-col gap-2 p-4", className)}
91
+ {...props}
92
+ />
93
+ )
94
+ }
95
+
96
+ function DrawerTitle({
97
+ className,
98
+ ...props
99
+ }: React.ComponentProps<typeof DrawerPrimitive.Title>) {
100
+ return (
101
+ <DrawerPrimitive.Title
102
+ data-slot="drawer-title"
103
+ className={cn("text-foreground font-semibold", className)}
104
+ {...props}
105
+ />
106
+ )
107
+ }
108
+
109
+ function DrawerDescription({
110
+ className,
111
+ ...props
112
+ }: React.ComponentProps<typeof DrawerPrimitive.Description>) {
113
+ return (
114
+ <DrawerPrimitive.Description
115
+ data-slot="drawer-description"
116
+ className={cn("text-muted-foreground text-sm", className)}
117
+ {...props}
118
+ />
119
+ )
120
+ }
121
+
122
+ export {
123
+ Drawer,
124
+ DrawerPortal,
125
+ DrawerOverlay,
126
+ DrawerTrigger,
127
+ DrawerClose,
128
+ DrawerContent,
129
+ DrawerHeader,
130
+ DrawerFooter,
131
+ DrawerTitle,
132
+ DrawerDescription,
133
+ }
@@ -0,0 +1,29 @@
1
+ import { useEffect, useState } from "react";
2
+ import { BREAKPOINTS, DEVICE_OPTIONS } from "@c-rex/constants";
3
+ import { DeviceType } from "@c-rex/types";
4
+
5
+
6
+ export const useBreakpoint = (): DeviceType => {
7
+ const [device, setDevice] = useState<DeviceType>(DEVICE_OPTIONS.MOBILE);
8
+
9
+ useEffect(() => {
10
+ function handleResize() {
11
+ const width = window.innerWidth;
12
+
13
+ if (width < BREAKPOINTS.md) {
14
+ setDevice(DEVICE_OPTIONS.MOBILE);
15
+ } else if (width < BREAKPOINTS.lg) {
16
+ setDevice(DEVICE_OPTIONS.TABLET);
17
+ } else {
18
+ setDevice(DEVICE_OPTIONS.DESKTOP);
19
+ }
20
+ }
21
+
22
+ handleResize();
23
+ window.addEventListener("resize", handleResize);
24
+
25
+ return () => window.removeEventListener("resize", handleResize);
26
+ }, []);
27
+
28
+ return device;
29
+ };
package/src/sheet.tsx CHANGED
@@ -80,7 +80,7 @@ const SheetHeader = ({
80
80
  }: React.HTMLAttributes<HTMLDivElement>) => (
81
81
  <div
82
82
  className={cn(
83
- "flex flex-col space-y-2 text-center sm:text-left",
83
+ "flex flex-row gap-2 text-center",
84
84
  className,
85
85
  )}
86
86
  {...props}
package/src/sidebar.tsx CHANGED
@@ -3,19 +3,13 @@
3
3
  import * as React from "react";
4
4
  import { Slot } from "@radix-ui/react-slot";
5
5
  import { VariantProps, cva } from "class-variance-authority";
6
- import { PanelLeft } from "lucide-react";
6
+ import { Info, PanelLeft } from "lucide-react";
7
7
  import { cn } from "@c-rex/utils";
8
- import { useIsMobile } from "./hooks/use-mobile";
8
+ import { useBreakpoint } from "./hooks";
9
9
  import { Button } from "./button";
10
10
  import { Input } from "./input";
11
11
  import { Separator } from "./separator";
12
- import {
13
- Sheet,
14
- SheetContent,
15
- SheetDescription,
16
- SheetHeader,
17
- SheetTitle,
18
- } from "./sheet";
12
+ import { Sheet, SheetContent } from "./sheet";
19
13
  import { Skeleton } from "./skeleton";
20
14
  import {
21
15
  Tooltip,
@@ -23,15 +17,16 @@ import {
23
17
  TooltipProvider,
24
18
  TooltipTrigger,
25
19
  } from "./tooltip";
20
+ import { DEVICE_OPTIONS } from "@c-rex/constants";
26
21
 
27
22
  const SIDEBAR_COOKIE_NAME = "sidebar_state";
28
23
  const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
29
24
  const SIDEBAR_WIDTH = "16rem";
30
25
  const SIDEBAR_WIDTH_MOBILE = "18rem";
26
+ const RIGHT_SIDEBAR_WIDTH = "19rem";
31
27
  const SIDEBAR_WIDTH_ICON = "3rem";
32
- const SIDEBAR_KEYBOARD_SHORTCUT = "b";
33
28
 
34
- type SidebarContext = {
29
+ type SidebarContextType = {
35
30
  state: "expanded" | "collapsed";
36
31
  open: boolean;
37
32
  setOpen: (open: boolean) => void;
@@ -39,122 +34,153 @@ type SidebarContext = {
39
34
  setOpenMobile: (open: boolean) => void;
40
35
  isMobile: boolean;
41
36
  toggleSidebar: () => void;
37
+ side: "left" | "right";
38
+ };
39
+
40
+ type MultiSidebarContextType = {
41
+ leftSidebar: SidebarContextType;
42
+ rightSidebar: SidebarContextType;
42
43
  };
43
44
 
44
- const SidebarContext = React.createContext<SidebarContext | null>(null);
45
+ const MultiSidebarContext = React.createContext<MultiSidebarContextType | null>(null);
45
46
 
46
- function useSidebar() {
47
- const context = React.useContext(SidebarContext);
47
+ function useMultiSidebar() {
48
+ const context = React.useContext(MultiSidebarContext);
48
49
  if (!context) {
49
- throw new Error("useSidebar must be used within a SidebarProvider.");
50
+ throw new Error(
51
+ "useMultiSidebar must be used within a MultiSidebarProvider."
52
+ );
50
53
  }
51
-
52
54
  return context;
53
55
  }
54
56
 
55
- const SidebarProvider = React.forwardRef<
57
+ const MultiSidebarProvider = React.forwardRef<
56
58
  HTMLDivElement,
57
59
  React.ComponentProps<"div"> & {
58
- defaultOpen?: boolean;
59
- open?: boolean;
60
- onOpenChange?: (open: boolean) => void;
60
+ defaultLeftOpen?: boolean;
61
+ defaultRightOpen?: boolean;
62
+ leftOpen?: boolean;
63
+ rightOpen?: boolean;
64
+ onLeftOpenChange?: (open: boolean) => void;
65
+ onRightOpenChange?: (open: boolean) => void;
61
66
  }
62
67
  >(
63
68
  (
64
69
  {
65
- defaultOpen = true,
66
- open: openProp,
67
- onOpenChange: setOpenProp,
70
+ defaultLeftOpen = true,
71
+ defaultRightOpen = true,
72
+ leftOpen: leftOpenProp,
73
+ rightOpen: rightOpenProp,
74
+ onLeftOpenChange: setLeftOpenProp,
75
+ onRightOpenChange: setRightOpenProp,
68
76
  className,
69
77
  style,
70
78
  children,
71
79
  ...props
72
80
  },
73
- ref,
81
+ ref
74
82
  ) => {
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
+ const device = useBreakpoint();
84
+ const isMobile = (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(
83
90
  (value: boolean | ((value: boolean) => boolean)) => {
84
- const openState = typeof value === "function" ? value(open) : value;
85
- if (setOpenProp) {
86
- setOpenProp(openState);
91
+ const openState = typeof value === "function" ? value(leftOpen) : value;
92
+ if (setLeftOpenProp) {
93
+ setLeftOpenProp(openState);
87
94
  } else {
88
- _setOpen(openState);
95
+ _setLeftOpen(openState);
89
96
  }
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}`;
97
+ document.cookie = `${SIDEBAR_COOKIE_NAME}:left=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
93
98
  },
94
- [setOpenProp, open],
99
+ [setLeftOpenProp, leftOpen]
95
100
  );
96
101
 
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();
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
113
  }
114
- };
115
-
116
- window.addEventListener("keydown", handleKeyDown);
117
- return () => window.removeEventListener("keydown", handleKeyDown);
118
- }, [toggleSidebar]);
114
+ document.cookie = `${SIDEBAR_COOKIE_NAME}:right=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
115
+ },
116
+ [setRightOpenProp, rightOpen]
117
+ );
119
118
 
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";
119
+ // Mobile state for each sidebar
120
+ const [leftOpenMobile, setLeftOpenMobile] = React.useState(false);
121
+ const [rightOpenMobile, setRightOpenMobile] = React.useState(false);
123
122
 
124
- const contextValue = React.useMemo<SidebarContext>(
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(
125
135
  () => ({
126
- state,
127
- open,
128
- setOpen,
136
+ state: leftOpen ? "expanded" : "collapsed",
137
+ open: leftOpen,
138
+ setOpen: setLeftOpen,
139
+ openMobile: leftOpenMobile,
140
+ setOpenMobile: setLeftOpenMobile,
129
141
  isMobile,
130
- openMobile,
131
- setOpenMobile,
132
- toggleSidebar,
142
+ toggleSidebar: toggleLeftSidebar,
143
+ side: "left",
133
144
  }),
134
- [
135
- state,
136
- open,
137
- setOpen,
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,
138
155
  isMobile,
139
- openMobile,
140
- setOpenMobile,
141
- toggleSidebar,
142
- ],
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]
143
168
  );
144
169
 
145
170
  return (
146
- <SidebarContext.Provider value={contextValue}>
171
+ <MultiSidebarContext.Provider value={contextValue}>
147
172
  <TooltipProvider delayDuration={0}>
148
173
  <div
149
174
  style={
150
175
  {
151
176
  "--sidebar-width": SIDEBAR_WIDTH,
152
177
  "--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
178
+ "--right-sidebar-width": RIGHT_SIDEBAR_WIDTH,
153
179
  ...style,
154
180
  } as React.CSSProperties
155
181
  }
156
182
  className={cn(
157
- "group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar",
183
+ "items-center flex flex-col",
158
184
  className,
159
185
  )}
160
186
  ref={ref}
@@ -163,16 +189,16 @@ const SidebarProvider = React.forwardRef<
163
189
  {children}
164
190
  </div>
165
191
  </TooltipProvider>
166
- </SidebarContext.Provider>
192
+ </MultiSidebarContext.Provider>
167
193
  );
168
194
  },
169
195
  );
170
- SidebarProvider.displayName = "SidebarProvider";
196
+ MultiSidebarProvider.displayName = "MultiSidebarProvider";
171
197
 
172
198
  const Sidebar = React.forwardRef<
173
199
  HTMLDivElement,
174
200
  React.ComponentProps<"div"> & {
175
- side?: "left" | "right";
201
+ side: "left" | "right";
176
202
  variant?: "sidebar" | "floating" | "inset";
177
203
  collapsible?: "offcanvas" | "icon" | "none";
178
204
  }
@@ -186,15 +212,21 @@ const Sidebar = React.forwardRef<
186
212
  children,
187
213
  ...props
188
214
  },
189
- ref,
215
+ ref
190
216
  ) => {
191
- const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
217
+ const {
218
+ [side === "left" ? "leftSidebar" : "rightSidebar"]: sidebarContext,
219
+ } = useMultiSidebar();
220
+ const { isMobile, state, openMobile, setOpenMobile } = sidebarContext;
192
221
 
193
222
  if (collapsible === "none") {
194
223
  return (
195
224
  <aside
196
225
  className={cn(
197
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]",
198
230
  className,
199
231
  )}
200
232
  ref={ref}
@@ -219,10 +251,6 @@ const Sidebar = React.forwardRef<
219
251
  }
220
252
  side={side}
221
253
  >
222
- <SheetHeader className="sr-only">
223
- <SheetTitle>Sidebar</SheetTitle>
224
- <SheetDescription>Displays the mobile sidebar.</SheetDescription>
225
- </SheetHeader>
226
254
  <div className="flex h-full w-full flex-col">{children}</div>
227
255
  </SheetContent>
228
256
  </Sheet>
@@ -241,62 +269,71 @@ const Sidebar = React.forwardRef<
241
269
  {/* This is what handles the sidebar gap on desktop */}
242
270
  <div
243
271
  className={cn(
244
- "relative w-[--sidebar-width] bg-transparent transition-[width] duration-200 ease-linear",
272
+ "duration-200 relative bg-transparent transition-[width] ease-linear",
245
273
  "group-data-[collapsible=offcanvas]:w-0",
246
274
  "group-data-[side=right]:rotate-180",
247
275
  variant === "floating" || variant === "inset"
248
276
  ? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]"
249
277
  : "group-data-[collapsible=icon]:w-[--sidebar-width-icon]",
278
+ side === "left"
279
+ ? "w-[--sidebar-width]"
280
+ : "w-[--right-sidebar-width]"
250
281
  )}
251
282
  />
252
283
  <div
253
284
  className={cn(
254
- "fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] duration-200 ease-linear md:flex",
285
+ "fixed h-full max-h-[calc(100%-81px)] z-10 hidden w-[--sidebar-width] transition-[left,right,width] duration-200 ease-linear md:flex",
255
286
  side === "left"
256
- ? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
257
- : "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
287
+ ? "left-0 w-[--sidebar-width] group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
288
+ : "right-0 w-[--right-sidebar-width] group-data-[collapsible=offcanvas]:right-[calc(var(--right-sidebar-width)*-1)]",
258
289
  // Adjust the padding for floating and inset variants.
259
290
  variant === "floating" || variant === "inset"
260
291
  ? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]"
261
- : "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l",
262
- className,
292
+ : "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r",
293
+ className
263
294
  )}
264
295
  {...props}
265
296
  >
266
297
  <div
267
298
  data-sidebar="sidebar"
268
- className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow"
269
- >
299
+ className={cn(
300
+ "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",
301
+ side === "left" && "bg-sidebar"
302
+ )}>
270
303
  {children}
271
304
  </div>
272
305
  </div>
273
306
  </div>
274
307
  );
275
- },
308
+ }
276
309
  );
277
310
  Sidebar.displayName = "Sidebar";
278
311
 
279
312
  const SidebarTrigger = React.forwardRef<
280
313
  React.ElementRef<typeof Button>,
281
- React.ComponentProps<typeof Button>
282
- >(({ className, onClick, ...props }, ref) => {
283
- const { toggleSidebar } = useSidebar();
314
+ React.ComponentProps<typeof Button> & { side?: "left" | "right" }
315
+ >(({ className, onClick, side = "left", variant = "ghost", size = "icon", ...props }, ref) => {
316
+ const { [side === "left" ? "leftSidebar" : "rightSidebar"]: sidebarContext } =
317
+ useMultiSidebar();
318
+ const { toggleSidebar } = sidebarContext;
284
319
 
285
320
  return (
286
321
  <Button
287
322
  ref={ref}
288
323
  data-sidebar="trigger"
289
- variant="ghost"
290
- size="icon"
291
- className={cn("h-7 w-7", className)}
324
+ variant={variant}
325
+ size={size}
326
+ className={cn("h-8 w-8", className)}
292
327
  onClick={(event) => {
293
328
  onClick?.(event);
294
329
  toggleSidebar();
295
330
  }}
296
331
  {...props}
297
332
  >
298
- <PanelLeft />
299
- <span className="sr-only">Toggle Sidebar</span>
333
+ {side === "left" ? <PanelLeft className="!h-5 !w-5" /> : <Info className="!h-5 !w-5" />}
334
+ <span className="sr-only">
335
+ Toggle {side === "left" ? "Left" : "Right"} Sidebar
336
+ </span>
300
337
  </Button>
301
338
  );
302
339
  });
@@ -304,9 +341,11 @@ SidebarTrigger.displayName = "SidebarTrigger";
304
341
 
305
342
  const SidebarRail = React.forwardRef<
306
343
  HTMLButtonElement,
307
- React.ComponentProps<"button">
308
- >(({ className, ...props }, ref) => {
309
- const { toggleSidebar } = useSidebar();
344
+ React.ComponentProps<"button"> & { side?: "left" | "right" }
345
+ >(({ className, side = "left", ...props }, ref) => {
346
+ const { [side === "left" ? "leftSidebar" : "rightSidebar"]: sidebarContext } =
347
+ useMultiSidebar();
348
+ const { toggleSidebar } = sidebarContext;
310
349
 
311
350
  return (
312
351
  <button
@@ -339,9 +378,9 @@ const SidebarInset = React.forwardRef<
339
378
  <main
340
379
  ref={ref}
341
380
  className={cn(
342
- "relative flex w-full flex-1 flex-col bg-background",
343
- "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",
344
- className,
381
+ "relative flex min-h-svh flex-1 flex-col bg-background",
382
+ "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",
383
+ className
345
384
  )}
346
385
  {...props}
347
386
  />
@@ -375,7 +414,7 @@ const SidebarHeader = React.forwardRef<
375
414
  <div
376
415
  ref={ref}
377
416
  data-sidebar="header"
378
- className={cn("flex flex-col gap-2 p-2", className)}
417
+ className={cn("flex flex-row gap-2 p-2", className)}
379
418
  {...props}
380
419
  />
381
420
  );
@@ -558,10 +597,12 @@ const SidebarMenuButton = React.forwardRef<
558
597
  asChild?: boolean;
559
598
  isActive?: boolean;
560
599
  tooltip?: string | React.ComponentProps<typeof TooltipContent>;
600
+ side?: "left" | "right";
561
601
  } & VariantProps<typeof sidebarMenuButtonVariants>
562
602
  >(
563
603
  (
564
604
  {
605
+ side = "left",
565
606
  asChild = false,
566
607
  isActive = false,
567
608
  variant = "default",
@@ -570,10 +611,13 @@ const SidebarMenuButton = React.forwardRef<
570
611
  className,
571
612
  ...props
572
613
  },
573
- ref,
614
+ ref
574
615
  ) => {
575
616
  const Comp = asChild ? Slot : "button";
576
- const { isMobile, state } = useSidebar();
617
+ const {
618
+ [side === "left" ? "leftSidebar" : "rightSidebar"]: sidebarContext,
619
+ } = useMultiSidebar();
620
+ const { isMobile, state } = sidebarContext;
577
621
 
578
622
  const button = (
579
623
  <Comp
@@ -779,9 +823,9 @@ export {
779
823
  SidebarMenuSub,
780
824
  SidebarMenuSubButton,
781
825
  SidebarMenuSubItem,
782
- SidebarProvider,
826
+ MultiSidebarProvider,
783
827
  SidebarRail,
784
828
  SidebarSeparator,
785
829
  SidebarTrigger,
786
- useSidebar,
787
- };
830
+ useMultiSidebar,
831
+ };
@@ -1,19 +0,0 @@
1
- import { useEffect, useState } from "react";
2
-
3
- const MOBILE_BREAKPOINT = 768;
4
-
5
- export function useIsMobile() {
6
- const [isMobile, setIsMobile] = useState<boolean | undefined>(undefined);
7
-
8
- useEffect(() => {
9
- const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
10
- const onChange = () => {
11
- setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
12
- };
13
- mql.addEventListener("change", onChange);
14
- setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
15
- return () => mql.removeEventListener("change", onChange);
16
- }, []);
17
-
18
- return !!isMobile;
19
- }