@majapisoftwares/ui 6.0.0 → 6.1.2
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/AGENTS.md +27 -27
- package/README.md +79 -79
- package/THIRD_PARTY_NOTICES.md +39 -39
- package/components.json +25 -25
- package/docs/MIGRATION-0.2.md +20 -20
- package/docs/MIGRATION-6.0.md +33 -33
- package/docs/NEXT-STARTER-VALIDATION.md +14 -14
- package/docs/STANDARDS.md +41 -41
- package/docs/VALIDATION.md +11 -11
- package/docs/VERSIONING.md +28 -28
- package/docs/validation/preference-titles.md +22 -0
- package/package.json +88 -84
- package/scripts/check-standards.mjs +41 -41
- package/src/components/ui/accordion.tsx +82 -82
- package/src/components/ui/alert-dialog.tsx +187 -187
- package/src/components/ui/alert.tsx +77 -77
- package/src/components/ui/avatar.tsx +108 -108
- package/src/components/ui/badge.tsx +55 -55
- package/src/components/ui/breadcrumb.tsx +125 -125
- package/src/components/ui/button-group.tsx +89 -89
- package/src/components/ui/button.tsx +59 -59
- package/src/components/ui/card.tsx +101 -101
- package/src/components/ui/checkbox.tsx +27 -27
- package/src/components/ui/collapsible.tsx +21 -21
- package/src/components/ui/context-menu.tsx +271 -271
- package/src/components/ui/dialog.tsx +161 -161
- package/src/components/ui/dropdown-menu.tsx +271 -271
- package/src/components/ui/empty.tsx +105 -105
- package/src/components/ui/field.tsx +238 -238
- package/src/components/ui/hover-card.tsx +50 -50
- package/src/components/ui/input-group.tsx +155 -155
- package/src/components/ui/input.tsx +21 -21
- package/src/components/ui/kbd.tsx +28 -28
- package/src/components/ui/label.tsx +19 -19
- package/src/components/ui/orb.css +1002 -1002
- package/src/components/ui/orb.tsx +752 -752
- package/src/components/ui/popover.tsx +89 -89
- package/src/components/ui/progress.tsx +82 -82
- package/src/components/ui/radio-group.tsx +37 -37
- package/src/components/ui/scroll-area.tsx +54 -54
- package/src/components/ui/select.tsx +204 -204
- package/src/components/ui/separator.tsx +24 -24
- package/src/components/ui/sheet.tsx +139 -139
- package/src/components/ui/sidebar.tsx +1039 -1035
- package/src/components/ui/skeleton.tsx +15 -15
- package/src/components/ui/slider.tsx +53 -53
- package/src/components/ui/sonner.tsx +44 -44
- package/src/components/ui/spinner.tsx +32 -32
- package/src/components/ui/switch.tsx +31 -31
- package/src/components/ui/table.tsx +121 -121
- package/src/components/ui/tabs.tsx +81 -81
- package/src/components/ui/textarea.tsx +19 -19
- package/src/components/ui/toggle-group.tsx +89 -89
- package/src/components/ui/toggle.tsx +44 -44
- package/src/components/ui/tooltip.tsx +65 -65
- package/src/hooks/use-mobile.ts +20 -20
- package/src/lib/i18n.tsx +109 -109
- package/src/lib/utils.ts +1 -1
- package/src/patterns/app-shell.tsx +177 -175
- package/src/patterns/data-table.tsx +2486 -2486
- package/src/patterns/details-sheet.tsx +54 -53
- package/src/patterns/filter-tabs.tsx +53 -53
- package/src/patterns/majapi-logo.tsx +32 -32
- package/src/patterns/page-header.tsx +62 -62
- package/src/patterns/preferences.tsx +15 -7
- package/src/patterns/status-badge.tsx +24 -24
- package/src/patterns/user-menu.tsx +218 -0
- package/src/patterns/workspace-switcher.tsx +176 -0
- package/src/styles.css +165 -165
- package/src/templates/admin-dashboard.tsx +44 -44
- package/src/templates/auth-layout.tsx +30 -30
- package/src/templates/login-form.tsx +129 -129
- package/src/templates/signup-form.tsx +177 -177
|
@@ -1,1035 +1,1039 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import * as React from "react";
|
|
4
|
-
import { mergeProps } from "@base-ui/react/merge-props";
|
|
5
|
-
import { useRender } from "@base-ui/react/use-render";
|
|
6
|
-
import { cva, type VariantProps } from "class-variance-authority";
|
|
7
|
-
import { cn } from "cn";
|
|
8
|
-
|
|
9
|
-
import { useIsMobile } from "../../hooks/use-mobile";
|
|
10
|
-
import { Button } from "./button";
|
|
11
|
-
import { Input } from "./input";
|
|
12
|
-
import { Separator } from "./separator";
|
|
13
|
-
import {
|
|
14
|
-
Sheet,
|
|
15
|
-
SheetContent,
|
|
16
|
-
SheetDescription,
|
|
17
|
-
SheetHeader,
|
|
18
|
-
SheetTitle,
|
|
19
|
-
} from "./sheet";
|
|
20
|
-
import { Skeleton } from "./skeleton";
|
|
21
|
-
import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip";
|
|
22
|
-
import {
|
|
23
|
-
PanelLeftCloseIcon,
|
|
24
|
-
PanelLeftIcon,
|
|
25
|
-
PanelLeftOpenIcon,
|
|
26
|
-
} from "lucide-react";
|
|
27
|
-
import { useUiMessages } from "../../lib/i18n";
|
|
28
|
-
import {
|
|
29
|
-
ContextMenu,
|
|
30
|
-
ContextMenuContent,
|
|
31
|
-
ContextMenuRadioGroup,
|
|
32
|
-
ContextMenuRadioItem,
|
|
33
|
-
ContextMenuShortcut,
|
|
34
|
-
ContextMenuTrigger,
|
|
35
|
-
} from "./context-menu";
|
|
36
|
-
|
|
37
|
-
const SIDEBAR_COOKIE_NAME = "sidebar_state";
|
|
38
|
-
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 365;
|
|
39
|
-
const SIDEBAR_WIDTH = 256;
|
|
40
|
-
const SIDEBAR_WIDTH_MIN = 208;
|
|
41
|
-
const SIDEBAR_WIDTH_MAX = 448;
|
|
42
|
-
const SIDEBAR_WIDTH_MOBILE = "18rem";
|
|
43
|
-
const SIDEBAR_WIDTH_ICON = "3rem";
|
|
44
|
-
// Drag thresholds (px from the sidebar's edge) that switch modes while resizing.
|
|
45
|
-
const SIDEBAR_CLOSE_BELOW = 24;
|
|
46
|
-
const SIDEBAR_COMPACT_BELOW = 152;
|
|
47
|
-
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
|
|
48
|
-
|
|
49
|
-
export type SidebarMode = "open" | "compact" | "closed";
|
|
50
|
-
|
|
51
|
-
type SidebarContextProps = {
|
|
52
|
-
state: "expanded" | "collapsed";
|
|
53
|
-
mode: SidebarMode;
|
|
54
|
-
setMode: (mode: SidebarMode) => void;
|
|
55
|
-
width: number;
|
|
56
|
-
setWidth: (width: number) => void;
|
|
57
|
-
resizing: boolean;
|
|
58
|
-
setResizing: (resizing: boolean) => void;
|
|
59
|
-
resizeTo: (offset: number) => void;
|
|
60
|
-
open: boolean;
|
|
61
|
-
setOpen: (open: boolean) => void;
|
|
62
|
-
openMobile: boolean;
|
|
63
|
-
setOpenMobile: (open: boolean) => void;
|
|
64
|
-
isMobile: boolean;
|
|
65
|
-
toggleSidebar: () => void;
|
|
66
|
-
toggleCompact: () => void;
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
const SidebarContext = React.createContext<SidebarContextProps | null>(null);
|
|
70
|
-
|
|
71
|
-
function useSidebar() {
|
|
72
|
-
const context = React.useContext(SidebarContext);
|
|
73
|
-
if (!context) {
|
|
74
|
-
throw new Error("useSidebar must be used within a SidebarProvider.");
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return context;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const clampWidth = (width: number) =>
|
|
81
|
-
Math.round(Math.min(SIDEBAR_WIDTH_MAX, Math.max(SIDEBAR_WIDTH_MIN, width)));
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Applies the saved sidebar state to the server-rendered markup before the
|
|
85
|
-
* browser paints (static pages can't read the cookie on the server).
|
|
86
|
-
*/
|
|
87
|
-
function SidebarRestoreScript() {
|
|
88
|
-
const script = `(function(){try{var m=document.cookie.match(/(?:^|; )${SIDEBAR_COOKIE_NAME}=([^;]*)/);if(!m)return;var p=m[1].split(":"),mode=p[0],w=Number(p[1]),wrap=document.currentScript.parentElement;if(w)wrap.setAttribute("style",(wrap.getAttribute("style")||"").replace(/--sidebar-width:[^;]*/,"--sidebar-width:"+Math.round(Math.min(${SIDEBAR_WIDTH_MAX},Math.max(${SIDEBAR_WIDTH_MIN},w)))+"px"));var sb=wrap.querySelector('[data-slot="sidebar"]');var r=wrap.querySelector('[data-slot="sidebar-rail"]');if(r)r.setAttribute("aria-valuenow",String(mode==="compact"?48:mode==="closed"?0:Math.round(Math.min(${SIDEBAR_WIDTH_MAX},Math.max(${SIDEBAR_WIDTH_MIN},w||${SIDEBAR_WIDTH})))));if(sb&&(mode==="compact"||mode==="closed")){sb.setAttribute("data-state","collapsed");sb.setAttribute("data-collapsible",mode==="closed"?"offcanvas":"icon")}}catch(e){}})()`;
|
|
89
|
-
return (
|
|
90
|
-
<script
|
|
91
|
-
suppressHydrationWarning
|
|
92
|
-
dangerouslySetInnerHTML={{ __html: script }}
|
|
93
|
-
/>
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** Reads the persisted `mode:width` cookie, e.g. on the server via `cookies()`. */
|
|
98
|
-
function parseSidebarCookie(value: string | undefined) {
|
|
99
|
-
const [mode, width] = (value ?? "").split(":");
|
|
100
|
-
return {
|
|
101
|
-
mode: (["open", "compact", "closed"].includes(mode) ? mode : undefined) as
|
|
102
|
-
SidebarMode | undefined,
|
|
103
|
-
width: Number(width) ? clampWidth(Number(width)) : undefined,
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function readSidebarCookie() {
|
|
108
|
-
if (typeof document === "undefined") return parseSidebarCookie(undefined);
|
|
109
|
-
const match = document.cookie.match(
|
|
110
|
-
new RegExp(`(?:^|; )${SIDEBAR_COOKIE_NAME}=([^;]*)`),
|
|
111
|
-
);
|
|
112
|
-
return parseSidebarCookie(match?.[1]);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function SidebarProvider({
|
|
116
|
-
defaultMode,
|
|
117
|
-
defaultWidth,
|
|
118
|
-
mode: modeProp,
|
|
119
|
-
onModeChange,
|
|
120
|
-
className,
|
|
121
|
-
style,
|
|
122
|
-
children,
|
|
123
|
-
...props
|
|
124
|
-
}: React.ComponentProps<"div"> & {
|
|
125
|
-
defaultMode?: SidebarMode;
|
|
126
|
-
defaultWidth?: number;
|
|
127
|
-
mode?: SidebarMode;
|
|
128
|
-
onModeChange?: (mode: SidebarMode) => void;
|
|
129
|
-
}) {
|
|
130
|
-
const isMobile = useIsMobile();
|
|
131
|
-
const [openMobile, setOpenMobile] = React.useState(false);
|
|
132
|
-
// Server/static HTML renders the defaults; SidebarRestoreScript patches that
|
|
133
|
-
// markup to the saved state before paint, and the client starts from the
|
|
134
|
-
// same saved state so hydration matches and nothing jumps.
|
|
135
|
-
const [initial] = React.useState(() =>
|
|
136
|
-
defaultMode !== undefined || defaultWidth !== undefined
|
|
137
|
-
? parseSidebarCookie(undefined)
|
|
138
|
-
: readSidebarCookie(),
|
|
139
|
-
);
|
|
140
|
-
const [_mode, _setMode] = React.useState<SidebarMode>(
|
|
141
|
-
defaultMode ?? initial.mode ?? "open",
|
|
142
|
-
);
|
|
143
|
-
const [width, _setWidth] = React.useState(
|
|
144
|
-
clampWidth(defaultWidth ?? initial.width ?? SIDEBAR_WIDTH),
|
|
145
|
-
);
|
|
146
|
-
const [resizing, setResizing] = React.useState(false);
|
|
147
|
-
const mode = modeProp ?? _mode;
|
|
148
|
-
// Mode to restore when a closed sidebar is shown again.
|
|
149
|
-
const lastVisible = React.useRef<SidebarMode>(
|
|
150
|
-
mode === "closed" ? "open" : mode,
|
|
151
|
-
);
|
|
152
|
-
|
|
153
|
-
React.useEffect(() => {
|
|
154
|
-
document.cookie = `${SIDEBAR_COOKIE_NAME}=${mode}:${width}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
|
|
155
|
-
}, [mode, width]);
|
|
156
|
-
|
|
157
|
-
const setMode = React.useCallback(
|
|
158
|
-
(next: SidebarMode) => {
|
|
159
|
-
if (next !== "closed") lastVisible.current = next;
|
|
160
|
-
if (onModeChange) onModeChange(next);
|
|
161
|
-
else _setMode(next);
|
|
162
|
-
},
|
|
163
|
-
[onModeChange],
|
|
164
|
-
);
|
|
165
|
-
const setWidth = React.useCallback(
|
|
166
|
-
(next: number) => _setWidth(clampWidth(next)),
|
|
167
|
-
[],
|
|
168
|
-
);
|
|
169
|
-
|
|
170
|
-
// Maps a dragged width to a mode: tiny closes, narrow shows icons, wide opens.
|
|
171
|
-
const resizeTo = React.useCallback(
|
|
172
|
-
(offset: number) => {
|
|
173
|
-
if (offset < SIDEBAR_CLOSE_BELOW) setMode("closed");
|
|
174
|
-
else if (offset < SIDEBAR_COMPACT_BELOW) setMode("compact");
|
|
175
|
-
else {
|
|
176
|
-
setMode("open");
|
|
177
|
-
setWidth(offset);
|
|
178
|
-
}
|
|
179
|
-
},
|
|
180
|
-
[setMode, setWidth],
|
|
181
|
-
);
|
|
182
|
-
|
|
183
|
-
const open = mode === "open";
|
|
184
|
-
const setOpen = React.useCallback(
|
|
185
|
-
(value: boolean) => setMode(value ? "open" : "compact"),
|
|
186
|
-
[setMode],
|
|
187
|
-
);
|
|
188
|
-
|
|
189
|
-
const toggleSidebar = React.useCallback(() => {
|
|
190
|
-
if (isMobile) return setOpenMobile((value) => !value);
|
|
191
|
-
setMode(mode === "closed" ? lastVisible.current : "closed");
|
|
192
|
-
}, [isMobile, mode, setMode]);
|
|
193
|
-
|
|
194
|
-
const toggleCompact = React.useCallback(() => {
|
|
195
|
-
if (isMobile) return setOpenMobile(false);
|
|
196
|
-
setMode(mode === "open" ? "compact" : "open");
|
|
197
|
-
}, [isMobile, mode, setMode]);
|
|
198
|
-
|
|
199
|
-
React.useEffect(() => {
|
|
200
|
-
const handleKeyDown = (event: KeyboardEvent) => {
|
|
201
|
-
if (
|
|
202
|
-
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
|
|
203
|
-
(event.metaKey || event.ctrlKey)
|
|
204
|
-
) {
|
|
205
|
-
event.preventDefault();
|
|
206
|
-
toggleSidebar();
|
|
207
|
-
}
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
window.addEventListener("keydown", handleKeyDown);
|
|
211
|
-
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
212
|
-
}, [toggleSidebar]);
|
|
213
|
-
|
|
214
|
-
const state = open ? "expanded" : "collapsed";
|
|
215
|
-
|
|
216
|
-
const contextValue = React.useMemo<SidebarContextProps>(
|
|
217
|
-
() => ({
|
|
218
|
-
state,
|
|
219
|
-
mode,
|
|
220
|
-
setMode,
|
|
221
|
-
width,
|
|
222
|
-
setWidth,
|
|
223
|
-
resizing,
|
|
224
|
-
setResizing,
|
|
225
|
-
resizeTo,
|
|
226
|
-
open,
|
|
227
|
-
setOpen,
|
|
228
|
-
isMobile,
|
|
229
|
-
openMobile,
|
|
230
|
-
setOpenMobile,
|
|
231
|
-
toggleSidebar,
|
|
232
|
-
toggleCompact,
|
|
233
|
-
}),
|
|
234
|
-
[
|
|
235
|
-
state,
|
|
236
|
-
mode,
|
|
237
|
-
setMode,
|
|
238
|
-
width,
|
|
239
|
-
setWidth,
|
|
240
|
-
resizing,
|
|
241
|
-
resizeTo,
|
|
242
|
-
open,
|
|
243
|
-
setOpen,
|
|
244
|
-
isMobile,
|
|
245
|
-
openMobile,
|
|
246
|
-
toggleSidebar,
|
|
247
|
-
toggleCompact,
|
|
248
|
-
],
|
|
249
|
-
);
|
|
250
|
-
|
|
251
|
-
return (
|
|
252
|
-
<SidebarContext.Provider value={contextValue}>
|
|
253
|
-
<div
|
|
254
|
-
data-slot="sidebar-wrapper"
|
|
255
|
-
data-resizing={resizing || undefined}
|
|
256
|
-
style={
|
|
257
|
-
{
|
|
258
|
-
"--sidebar-width": `${width}px`,
|
|
259
|
-
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
|
|
260
|
-
...style,
|
|
261
|
-
} as React.CSSProperties
|
|
262
|
-
}
|
|
263
|
-
className={cn(
|
|
264
|
-
"group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-sidebar data-resizing:cursor-col-resize data-resizing:select-none",
|
|
265
|
-
className,
|
|
266
|
-
)}
|
|
267
|
-
{...props}
|
|
268
|
-
>
|
|
269
|
-
{children}
|
|
270
|
-
{defaultMode === undefined && defaultWidth === undefined ? (
|
|
271
|
-
<SidebarRestoreScript />
|
|
272
|
-
) : null}
|
|
273
|
-
</div>
|
|
274
|
-
</SidebarContext.Provider>
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function Sidebar({
|
|
279
|
-
side = "left",
|
|
280
|
-
variant = "sidebar",
|
|
281
|
-
collapsible = "auto",
|
|
282
|
-
className,
|
|
283
|
-
children,
|
|
284
|
-
dir,
|
|
285
|
-
...props
|
|
286
|
-
}: React.ComponentProps<"div"> & {
|
|
287
|
-
side?: "left" | "right";
|
|
288
|
-
variant?: "sidebar" | "floating" | "inset";
|
|
289
|
-
/** `auto` follows the provider mode: compact shows icons, closed hides. */
|
|
290
|
-
collapsible?: "auto" | "offcanvas" | "icon" | "none";
|
|
291
|
-
}) {
|
|
292
|
-
const { isMobile, state, mode, openMobile, setOpenMobile } = useSidebar();
|
|
293
|
-
const messages = useUiMessages();
|
|
294
|
-
const collapsedAs =
|
|
295
|
-
collapsible === "auto"
|
|
296
|
-
? mode === "closed"
|
|
297
|
-
? "offcanvas"
|
|
298
|
-
: "icon"
|
|
299
|
-
: collapsible;
|
|
300
|
-
|
|
301
|
-
if (collapsible === "none") {
|
|
302
|
-
return (
|
|
303
|
-
<div
|
|
304
|
-
data-slot="sidebar"
|
|
305
|
-
className={cn(
|
|
306
|
-
"flex h-full w-(--sidebar-width) flex-col bg-sidebar text-sidebar-foreground",
|
|
307
|
-
className,
|
|
308
|
-
)}
|
|
309
|
-
{...props}
|
|
310
|
-
>
|
|
311
|
-
{children}
|
|
312
|
-
</div>
|
|
313
|
-
);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
if (isMobile) {
|
|
317
|
-
return (
|
|
318
|
-
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
|
|
319
|
-
<SheetContent
|
|
320
|
-
dir={dir}
|
|
321
|
-
data-sidebar="sidebar"
|
|
322
|
-
data-slot="sidebar"
|
|
323
|
-
data-mobile="true"
|
|
324
|
-
className="w-(--sidebar-width) bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
|
|
325
|
-
style={
|
|
326
|
-
{
|
|
327
|
-
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
|
|
328
|
-
} as React.CSSProperties
|
|
329
|
-
}
|
|
330
|
-
side={side}
|
|
331
|
-
>
|
|
332
|
-
<SheetHeader className="sr-only">
|
|
333
|
-
<SheetTitle>{messages.sidebar}</SheetTitle>
|
|
334
|
-
<SheetDescription>{messages.sidebar}</SheetDescription>
|
|
335
|
-
</SheetHeader>
|
|
336
|
-
<div className="flex h-full w-full flex-col">{children}</div>
|
|
337
|
-
</SheetContent>
|
|
338
|
-
</Sheet>
|
|
339
|
-
);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
return (
|
|
343
|
-
<div
|
|
344
|
-
className="group peer hidden text-sidebar-foreground md:block"
|
|
345
|
-
data-state={state}
|
|
346
|
-
data-collapsible={state === "collapsed" ? collapsedAs : ""}
|
|
347
|
-
data-variant={variant}
|
|
348
|
-
data-side={side}
|
|
349
|
-
data-slot="sidebar"
|
|
350
|
-
>
|
|
351
|
-
{/* This is what handles the sidebar gap on desktop */}
|
|
352
|
-
<div
|
|
353
|
-
data-slot="sidebar-gap"
|
|
354
|
-
className={cn(
|
|
355
|
-
"relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear group-data-resizing/sidebar-wrapper:transition-none",
|
|
356
|
-
"group-data-[collapsible=offcanvas]:w-0",
|
|
357
|
-
"group-data-[side=right]:rotate-180",
|
|
358
|
-
variant === "floating" || variant === "inset"
|
|
359
|
-
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
|
|
360
|
-
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)",
|
|
361
|
-
)}
|
|
362
|
-
/>
|
|
363
|
-
<div
|
|
364
|
-
data-slot="sidebar-container"
|
|
365
|
-
data-side={side}
|
|
366
|
-
className={cn(
|
|
367
|
-
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear group-data-resizing/sidebar-wrapper:transition-none data-[side=left]:left-0 data-[side=left]:group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)] data-[side=right]:right-0 data-[side=right]:group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)] md:flex",
|
|
368
|
-
// Adjust the padding for floating and inset variants.
|
|
369
|
-
variant === "floating" || variant === "inset"
|
|
370
|
-
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
|
|
371
|
-
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
|
|
372
|
-
className,
|
|
373
|
-
)}
|
|
374
|
-
{...props}
|
|
375
|
-
>
|
|
376
|
-
<div
|
|
377
|
-
data-sidebar="sidebar"
|
|
378
|
-
data-slot="sidebar-inner"
|
|
379
|
-
className="flex size-full flex-col bg-sidebar group-data-[variant=floating]:rounded-2xl group-data-[variant=floating]:shadow-sm group-data-[variant=floating]:ring-1 group-data-[variant=floating]:ring-sidebar-border"
|
|
380
|
-
>
|
|
381
|
-
{children}
|
|
382
|
-
</div>
|
|
383
|
-
</div>
|
|
384
|
-
</div>
|
|
385
|
-
);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
const isMac = () =>
|
|
389
|
-
typeof navigator !== "undefined" &&
|
|
390
|
-
/Mac|iP(hone|ad)/.test(navigator.platform);
|
|
391
|
-
|
|
392
|
-
/**
|
|
393
|
-
* Wraps a sidebar control with a tooltip (label + shortcut hint) and a
|
|
394
|
-
* right-click menu to pick the mode directly: expanded, compact or hidden.
|
|
395
|
-
*/
|
|
396
|
-
function SidebarModeControl({
|
|
397
|
-
label,
|
|
398
|
-
children,
|
|
399
|
-
}: {
|
|
400
|
-
label: string;
|
|
401
|
-
children: React.ReactElement;
|
|
402
|
-
}) {
|
|
403
|
-
const { mode, setMode, isMobile } = useSidebar();
|
|
404
|
-
const messages = useUiMessages();
|
|
405
|
-
const shortcut = `${isMac() ? "⌘" : "Ctrl+"}${SIDEBAR_KEYBOARD_SHORTCUT.toUpperCase()}`;
|
|
406
|
-
// Clicking changes the sidebar size and moves this control, so the tooltip
|
|
407
|
-
// closes on press and stays closed until the pointer leaves.
|
|
408
|
-
const [tooltipOpen, setTooltipOpen] = React.useState(false);
|
|
409
|
-
const suppressed = React.useRef(false);
|
|
410
|
-
|
|
411
|
-
if (isMobile) return children;
|
|
412
|
-
|
|
413
|
-
return (
|
|
414
|
-
<ContextMenu>
|
|
415
|
-
<Tooltip
|
|
416
|
-
open={tooltipOpen}
|
|
417
|
-
onOpenChange={(open) => setTooltipOpen(open && !suppressed.current)}
|
|
418
|
-
>
|
|
419
|
-
<ContextMenuTrigger
|
|
420
|
-
render={
|
|
421
|
-
<TooltipTrigger
|
|
422
|
-
render={children}
|
|
423
|
-
delay={600}
|
|
424
|
-
onPointerDown={() => {
|
|
425
|
-
suppressed.current = true;
|
|
426
|
-
setTooltipOpen(false);
|
|
427
|
-
}}
|
|
428
|
-
onPointerLeave={() => {
|
|
429
|
-
suppressed.current = false;
|
|
430
|
-
}}
|
|
431
|
-
/>
|
|
432
|
-
}
|
|
433
|
-
/>
|
|
434
|
-
<TooltipContent side="right" className="flex flex-col gap-0.5">
|
|
435
|
-
<span>{label}</span>
|
|
436
|
-
<span className="opacity-70">{messages.sidebarHint(shortcut)}</span>
|
|
437
|
-
</TooltipContent>
|
|
438
|
-
</Tooltip>
|
|
439
|
-
<ContextMenuContent className="min-w-44">
|
|
440
|
-
<ContextMenuRadioGroup
|
|
441
|
-
value={mode}
|
|
442
|
-
onValueChange={(value) => setMode(value as SidebarMode)}
|
|
443
|
-
>
|
|
444
|
-
<ContextMenuRadioItem value="open">
|
|
445
|
-
{messages.modeOpen}
|
|
446
|
-
</ContextMenuRadioItem>
|
|
447
|
-
<ContextMenuRadioItem value="compact">
|
|
448
|
-
{messages.modeCompact}
|
|
449
|
-
</ContextMenuRadioItem>
|
|
450
|
-
<ContextMenuRadioItem value="closed">
|
|
451
|
-
{messages.modeClosed}
|
|
452
|
-
<ContextMenuShortcut>{shortcut}</ContextMenuShortcut>
|
|
453
|
-
</ContextMenuRadioItem>
|
|
454
|
-
</ContextMenuRadioGroup>
|
|
455
|
-
</ContextMenuContent>
|
|
456
|
-
</ContextMenu>
|
|
457
|
-
);
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
function SidebarTrigger({
|
|
461
|
-
className,
|
|
462
|
-
onClick,
|
|
463
|
-
...props
|
|
464
|
-
}: React.ComponentProps<typeof Button>) {
|
|
465
|
-
const { toggleSidebar } = useSidebar();
|
|
466
|
-
const messages = useUiMessages();
|
|
467
|
-
|
|
468
|
-
return (
|
|
469
|
-
<SidebarModeControl label={messages.toggleSidebar}>
|
|
470
|
-
<Button
|
|
471
|
-
data-sidebar="trigger"
|
|
472
|
-
data-slot="sidebar-trigger"
|
|
473
|
-
variant="ghost"
|
|
474
|
-
size="icon-sm"
|
|
475
|
-
className={cn(className)}
|
|
476
|
-
aria-label={messages.toggleSidebar}
|
|
477
|
-
onClick={(event) => {
|
|
478
|
-
onClick?.(event);
|
|
479
|
-
toggleSidebar();
|
|
480
|
-
}}
|
|
481
|
-
{...props}
|
|
482
|
-
>
|
|
483
|
-
<PanelLeftIcon />
|
|
484
|
-
</Button>
|
|
485
|
-
</SidebarModeControl>
|
|
486
|
-
);
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
/** Collapse control meant to live inside the sidebar: toggles open ↔ compact. */
|
|
490
|
-
function SidebarCollapse({
|
|
491
|
-
className,
|
|
492
|
-
onClick,
|
|
493
|
-
...props
|
|
494
|
-
}: React.ComponentProps<typeof Button>) {
|
|
495
|
-
const { mode, toggleCompact, isMobile } = useSidebar();
|
|
496
|
-
const messages = useUiMessages();
|
|
497
|
-
const label =
|
|
498
|
-
mode === "open" || isMobile
|
|
499
|
-
? messages.collapseSidebar
|
|
500
|
-
: messages.expandSidebar;
|
|
501
|
-
|
|
502
|
-
return (
|
|
503
|
-
<SidebarModeControl label={label}>
|
|
504
|
-
<Button
|
|
505
|
-
data-sidebar="collapse"
|
|
506
|
-
data-slot="sidebar-collapse"
|
|
507
|
-
variant="ghost"
|
|
508
|
-
size="icon-sm"
|
|
509
|
-
className={cn(className)}
|
|
510
|
-
// Stable label/icons (CSS-switched) so the pre-paint restored state
|
|
511
|
-
// hydrates without mismatches.
|
|
512
|
-
aria-label={messages.toggleSidebar}
|
|
513
|
-
onClick={(event) => {
|
|
514
|
-
onClick?.(event);
|
|
515
|
-
toggleCompact();
|
|
516
|
-
}}
|
|
517
|
-
{...props}
|
|
518
|
-
>
|
|
519
|
-
<PanelLeftCloseIcon className="group-data-[collapsible=icon]:hidden" />
|
|
520
|
-
<PanelLeftOpenIcon className="hidden group-data-[collapsible=icon]:block" />
|
|
521
|
-
</Button>
|
|
522
|
-
</SidebarModeControl>
|
|
523
|
-
);
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
/**
|
|
527
|
-
* Drag handle on the sidebar edge. Dragging resizes the open sidebar and
|
|
528
|
-
* switches modes by width (closed → compact → open); a click toggles
|
|
529
|
-
* open ↔ compact; arrow keys resize from the keyboard.
|
|
530
|
-
*/
|
|
531
|
-
function SidebarRail({ className, ...props }: React.ComponentProps<"div">) {
|
|
532
|
-
const { mode, width, resizeTo, setResizing, toggleCompact } = useSidebar();
|
|
533
|
-
const messages = useUiMessages();
|
|
534
|
-
const drag = React.useRef<{
|
|
535
|
-
startX: number;
|
|
536
|
-
startOffset: number;
|
|
537
|
-
side: "left" | "right";
|
|
538
|
-
moved: boolean;
|
|
539
|
-
} | null>(null);
|
|
540
|
-
const offset = mode === "open" ? width : mode === "compact" ? 48 : 0;
|
|
541
|
-
|
|
542
|
-
return (
|
|
543
|
-
<div
|
|
544
|
-
role="separator"
|
|
545
|
-
aria-orientation="vertical"
|
|
546
|
-
aria-label={messages.resizeSidebar}
|
|
547
|
-
aria-valuenow={offset}
|
|
548
|
-
aria-valuemin={0}
|
|
549
|
-
aria-valuemax={SIDEBAR_WIDTH_MAX}
|
|
550
|
-
tabIndex={0}
|
|
551
|
-
data-sidebar="rail"
|
|
552
|
-
data-slot="sidebar-rail"
|
|
553
|
-
onPointerDown={(event) => {
|
|
554
|
-
if (event.button !== 0) return;
|
|
555
|
-
event.currentTarget.setPointerCapture(event.pointerId);
|
|
556
|
-
const side =
|
|
557
|
-
event.currentTarget.closest<HTMLElement>("[data-side]")?.dataset
|
|
558
|
-
.side === "right"
|
|
559
|
-
? "right"
|
|
560
|
-
: "left";
|
|
561
|
-
drag.current = {
|
|
562
|
-
startX: event.clientX,
|
|
563
|
-
startOffset: offset,
|
|
564
|
-
side,
|
|
565
|
-
moved: false,
|
|
566
|
-
};
|
|
567
|
-
}}
|
|
568
|
-
onPointerMove={(event) => {
|
|
569
|
-
const current = drag.current;
|
|
570
|
-
if (!current) return;
|
|
571
|
-
const delta =
|
|
572
|
-
(event.clientX - current.startX) * (current.side === "left" ? 1 : -1);
|
|
573
|
-
if (!current.moved && Math.abs(delta) < 4) return;
|
|
574
|
-
if (!current.moved) setResizing(true);
|
|
575
|
-
current.moved = true;
|
|
576
|
-
resizeTo(current.startOffset + delta);
|
|
577
|
-
}}
|
|
578
|
-
onPointerUp={(event) => {
|
|
579
|
-
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
580
|
-
if (drag.current && !drag.current.moved) toggleCompact();
|
|
581
|
-
drag.current = null;
|
|
582
|
-
setResizing(false);
|
|
583
|
-
}}
|
|
584
|
-
onPointerCancel={() => {
|
|
585
|
-
drag.current = null;
|
|
586
|
-
setResizing(false);
|
|
587
|
-
}}
|
|
588
|
-
onKeyDown={(event) => {
|
|
589
|
-
const step = event.shiftKey ? 64 : 16;
|
|
590
|
-
if (event.key === "ArrowRight") resizeTo(offset + step);
|
|
591
|
-
else if (event.key === "ArrowLeft") resizeTo(offset - step);
|
|
592
|
-
else if (event.key === "Enter" || event.key === " ") toggleCompact();
|
|
593
|
-
else return;
|
|
594
|
-
event.preventDefault();
|
|
595
|
-
}}
|
|
596
|
-
className={cn(
|
|
597
|
-
"absolute inset-y-0 z-20 hidden w-4 touch-none cursor-col-resize outline-none group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:start-1/2 after:w-[2px] after:transition-colors hover:after:bg-sidebar-border focus-visible:after:bg-sidebar-ring sm:flex ltr:-translate-x-1/2 rtl:-translate-x-1/2",
|
|
598
|
-
"group-data-resizing/sidebar-wrapper:after:bg-sidebar-ring",
|
|
599
|
-
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
|
|
600
|
-
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
|
|
601
|
-
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
|
|
602
|
-
className,
|
|
603
|
-
)}
|
|
604
|
-
{...props}
|
|
605
|
-
/>
|
|
606
|
-
);
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
|
|
610
|
-
return (
|
|
611
|
-
<main
|
|
612
|
-
data-slot="sidebar-inset"
|
|
613
|
-
className={cn(
|
|
614
|
-
"relative flex w-full flex-1 flex-col bg-background md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-2xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
|
|
615
|
-
className,
|
|
616
|
-
)}
|
|
617
|
-
{...props}
|
|
618
|
-
/>
|
|
619
|
-
);
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
function SidebarInput({
|
|
623
|
-
className,
|
|
624
|
-
...props
|
|
625
|
-
}: React.ComponentProps<typeof Input>) {
|
|
626
|
-
return (
|
|
627
|
-
<Input
|
|
628
|
-
data-slot="sidebar-input"
|
|
629
|
-
data-sidebar="input"
|
|
630
|
-
className={cn("h-8 w-full bg-input/50 shadow-none", className)}
|
|
631
|
-
{...props}
|
|
632
|
-
/>
|
|
633
|
-
);
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
637
|
-
return (
|
|
638
|
-
<div
|
|
639
|
-
data-slot="sidebar-header"
|
|
640
|
-
data-sidebar="header"
|
|
641
|
-
className={cn(
|
|
642
|
-
"flex flex-col gap-2 p-2 [--radius:var(--radius-xl)]",
|
|
643
|
-
className,
|
|
644
|
-
)}
|
|
645
|
-
{...props}
|
|
646
|
-
/>
|
|
647
|
-
);
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
651
|
-
return (
|
|
652
|
-
<div
|
|
653
|
-
data-slot="sidebar-footer"
|
|
654
|
-
data-sidebar="footer"
|
|
655
|
-
className={cn(
|
|
656
|
-
"flex flex-col gap-2 p-2 [--radius:var(--radius-xl)]",
|
|
657
|
-
className,
|
|
658
|
-
)}
|
|
659
|
-
{...props}
|
|
660
|
-
/>
|
|
661
|
-
);
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
function SidebarSeparator({
|
|
665
|
-
className,
|
|
666
|
-
...props
|
|
667
|
-
}: React.ComponentProps<typeof Separator>) {
|
|
668
|
-
return (
|
|
669
|
-
<Separator
|
|
670
|
-
data-slot="sidebar-separator"
|
|
671
|
-
data-sidebar="separator"
|
|
672
|
-
className={cn("mx-2 w-auto bg-sidebar-border", className)}
|
|
673
|
-
{...props}
|
|
674
|
-
/>
|
|
675
|
-
);
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
679
|
-
return (
|
|
680
|
-
<div
|
|
681
|
-
data-slot="sidebar-content"
|
|
682
|
-
data-sidebar="content"
|
|
683
|
-
className={cn(
|
|
684
|
-
"no-scrollbar flex min-h-0 flex-1 flex-col gap-2 overflow-auto scroll-fade [--radius:var(--radius-xl)] group-data-[collapsible=icon]:overflow-hidden",
|
|
685
|
-
className,
|
|
686
|
-
)}
|
|
687
|
-
{...props}
|
|
688
|
-
/>
|
|
689
|
-
);
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
693
|
-
return (
|
|
694
|
-
<div
|
|
695
|
-
data-slot="sidebar-group"
|
|
696
|
-
data-sidebar="group"
|
|
697
|
-
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
|
|
698
|
-
{...props}
|
|
699
|
-
/>
|
|
700
|
-
);
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
function SidebarGroupLabel({
|
|
704
|
-
className,
|
|
705
|
-
render,
|
|
706
|
-
...props
|
|
707
|
-
}: useRender.ComponentProps<"div"> & React.ComponentProps<"div">) {
|
|
708
|
-
return useRender({
|
|
709
|
-
defaultTagName: "div",
|
|
710
|
-
props: mergeProps<"div">(
|
|
711
|
-
{
|
|
712
|
-
className: cn(
|
|
713
|
-
"flex h-8 shrink-0 items-center rounded-xl px-3 text-xs font-medium text-sidebar-foreground/70 ring-sidebar-ring outline-hidden transition-[margin,opacity] duration-200 ease-linear group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
|
714
|
-
className,
|
|
715
|
-
),
|
|
716
|
-
},
|
|
717
|
-
props,
|
|
718
|
-
),
|
|
719
|
-
render,
|
|
720
|
-
state: {
|
|
721
|
-
slot: "sidebar-group-label",
|
|
722
|
-
sidebar: "group-label",
|
|
723
|
-
},
|
|
724
|
-
});
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
function SidebarGroupAction({
|
|
728
|
-
className,
|
|
729
|
-
render,
|
|
730
|
-
...props
|
|
731
|
-
}: useRender.ComponentProps<"button"> & React.ComponentProps<"button">) {
|
|
732
|
-
return useRender({
|
|
733
|
-
defaultTagName: "button",
|
|
734
|
-
props: mergeProps<"button">(
|
|
735
|
-
{
|
|
736
|
-
className: cn(
|
|
737
|
-
"absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-xl p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0",
|
|
738
|
-
className,
|
|
739
|
-
),
|
|
740
|
-
},
|
|
741
|
-
props,
|
|
742
|
-
),
|
|
743
|
-
render,
|
|
744
|
-
state: {
|
|
745
|
-
slot: "sidebar-group-action",
|
|
746
|
-
sidebar: "group-action",
|
|
747
|
-
},
|
|
748
|
-
});
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
function SidebarGroupContent({
|
|
752
|
-
className,
|
|
753
|
-
...props
|
|
754
|
-
}: React.ComponentProps<"div">) {
|
|
755
|
-
return (
|
|
756
|
-
<div
|
|
757
|
-
data-slot="sidebar-group-content"
|
|
758
|
-
data-sidebar="group-content"
|
|
759
|
-
className={cn("w-full text-sm", className)}
|
|
760
|
-
{...props}
|
|
761
|
-
/>
|
|
762
|
-
);
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
|
|
766
|
-
return (
|
|
767
|
-
<ul
|
|
768
|
-
data-slot="sidebar-menu"
|
|
769
|
-
data-sidebar="menu"
|
|
770
|
-
className={cn("flex w-full min-w-0 flex-col gap-0.5", className)}
|
|
771
|
-
{...props}
|
|
772
|
-
/>
|
|
773
|
-
);
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
|
|
777
|
-
return (
|
|
778
|
-
<li
|
|
779
|
-
data-slot="sidebar-menu-item"
|
|
780
|
-
data-sidebar="menu-item"
|
|
781
|
-
className={cn("group/menu-item relative", className)}
|
|
782
|
-
{...props}
|
|
783
|
-
/>
|
|
784
|
-
);
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
const sidebarMenuButtonVariants = cva(
|
|
788
|
-
"peer/menu-button group/menu-button flex w-full items-center gap-2 overflow-hidden rounded-xl px-3 py-2 text-left text-sm ring-sidebar-ring outline-hidden transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! 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 data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:font-medium data-active:text-sidebar-accent-foreground [&_svg]:size-4 [&_svg]:shrink-0 [&>span:last-child]:truncate",
|
|
789
|
-
{
|
|
790
|
-
variants: {
|
|
791
|
-
variant: {
|
|
792
|
-
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
|
793
|
-
outline:
|
|
794
|
-
"bg-background shadow-[0_0_0_1px_var(--sidebar-border)] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_var(--sidebar-accent)]",
|
|
795
|
-
},
|
|
796
|
-
size: {
|
|
797
|
-
default: "h-9 text-sm",
|
|
798
|
-
sm: "h-8 text-xs",
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
{
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { mergeProps } from "@base-ui/react/merge-props";
|
|
5
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
6
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
7
|
+
import { cn } from "cn";
|
|
8
|
+
|
|
9
|
+
import { useIsMobile } from "../../hooks/use-mobile";
|
|
10
|
+
import { Button } from "./button";
|
|
11
|
+
import { Input } from "./input";
|
|
12
|
+
import { Separator } from "./separator";
|
|
13
|
+
import {
|
|
14
|
+
Sheet,
|
|
15
|
+
SheetContent,
|
|
16
|
+
SheetDescription,
|
|
17
|
+
SheetHeader,
|
|
18
|
+
SheetTitle,
|
|
19
|
+
} from "./sheet";
|
|
20
|
+
import { Skeleton } from "./skeleton";
|
|
21
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip";
|
|
22
|
+
import {
|
|
23
|
+
PanelLeftCloseIcon,
|
|
24
|
+
PanelLeftIcon,
|
|
25
|
+
PanelLeftOpenIcon,
|
|
26
|
+
} from "lucide-react";
|
|
27
|
+
import { useUiMessages } from "../../lib/i18n";
|
|
28
|
+
import {
|
|
29
|
+
ContextMenu,
|
|
30
|
+
ContextMenuContent,
|
|
31
|
+
ContextMenuRadioGroup,
|
|
32
|
+
ContextMenuRadioItem,
|
|
33
|
+
ContextMenuShortcut,
|
|
34
|
+
ContextMenuTrigger,
|
|
35
|
+
} from "./context-menu";
|
|
36
|
+
|
|
37
|
+
const SIDEBAR_COOKIE_NAME = "sidebar_state";
|
|
38
|
+
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 365;
|
|
39
|
+
const SIDEBAR_WIDTH = 256;
|
|
40
|
+
const SIDEBAR_WIDTH_MIN = 208;
|
|
41
|
+
const SIDEBAR_WIDTH_MAX = 448;
|
|
42
|
+
const SIDEBAR_WIDTH_MOBILE = "18rem";
|
|
43
|
+
const SIDEBAR_WIDTH_ICON = "3rem";
|
|
44
|
+
// Drag thresholds (px from the sidebar's edge) that switch modes while resizing.
|
|
45
|
+
const SIDEBAR_CLOSE_BELOW = 24;
|
|
46
|
+
const SIDEBAR_COMPACT_BELOW = 152;
|
|
47
|
+
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
|
|
48
|
+
|
|
49
|
+
export type SidebarMode = "open" | "compact" | "closed";
|
|
50
|
+
|
|
51
|
+
type SidebarContextProps = {
|
|
52
|
+
state: "expanded" | "collapsed";
|
|
53
|
+
mode: SidebarMode;
|
|
54
|
+
setMode: (mode: SidebarMode) => void;
|
|
55
|
+
width: number;
|
|
56
|
+
setWidth: (width: number) => void;
|
|
57
|
+
resizing: boolean;
|
|
58
|
+
setResizing: (resizing: boolean) => void;
|
|
59
|
+
resizeTo: (offset: number) => void;
|
|
60
|
+
open: boolean;
|
|
61
|
+
setOpen: (open: boolean) => void;
|
|
62
|
+
openMobile: boolean;
|
|
63
|
+
setOpenMobile: (open: boolean) => void;
|
|
64
|
+
isMobile: boolean;
|
|
65
|
+
toggleSidebar: () => void;
|
|
66
|
+
toggleCompact: () => void;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const SidebarContext = React.createContext<SidebarContextProps | null>(null);
|
|
70
|
+
|
|
71
|
+
function useSidebar() {
|
|
72
|
+
const context = React.useContext(SidebarContext);
|
|
73
|
+
if (!context) {
|
|
74
|
+
throw new Error("useSidebar must be used within a SidebarProvider.");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return context;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const clampWidth = (width: number) =>
|
|
81
|
+
Math.round(Math.min(SIDEBAR_WIDTH_MAX, Math.max(SIDEBAR_WIDTH_MIN, width)));
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Applies the saved sidebar state to the server-rendered markup before the
|
|
85
|
+
* browser paints (static pages can't read the cookie on the server).
|
|
86
|
+
*/
|
|
87
|
+
function SidebarRestoreScript() {
|
|
88
|
+
const script = `(function(){try{var m=document.cookie.match(/(?:^|; )${SIDEBAR_COOKIE_NAME}=([^;]*)/);if(!m)return;var p=m[1].split(":"),mode=p[0],w=Number(p[1]),wrap=document.currentScript.parentElement;if(w)wrap.setAttribute("style",(wrap.getAttribute("style")||"").replace(/--sidebar-width:[^;]*/,"--sidebar-width:"+Math.round(Math.min(${SIDEBAR_WIDTH_MAX},Math.max(${SIDEBAR_WIDTH_MIN},w)))+"px"));var sb=wrap.querySelector('[data-slot="sidebar"]');var r=wrap.querySelector('[data-slot="sidebar-rail"]');if(r)r.setAttribute("aria-valuenow",String(mode==="compact"?48:mode==="closed"?0:Math.round(Math.min(${SIDEBAR_WIDTH_MAX},Math.max(${SIDEBAR_WIDTH_MIN},w||${SIDEBAR_WIDTH})))));if(sb&&(mode==="compact"||mode==="closed")){sb.setAttribute("data-state","collapsed");sb.setAttribute("data-collapsible",mode==="closed"?"offcanvas":"icon")}}catch(e){}})()`;
|
|
89
|
+
return (
|
|
90
|
+
<script
|
|
91
|
+
suppressHydrationWarning
|
|
92
|
+
dangerouslySetInnerHTML={{ __html: script }}
|
|
93
|
+
/>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Reads the persisted `mode:width` cookie, e.g. on the server via `cookies()`. */
|
|
98
|
+
function parseSidebarCookie(value: string | undefined) {
|
|
99
|
+
const [mode, width] = (value ?? "").split(":");
|
|
100
|
+
return {
|
|
101
|
+
mode: (["open", "compact", "closed"].includes(mode) ? mode : undefined) as
|
|
102
|
+
SidebarMode | undefined,
|
|
103
|
+
width: Number(width) ? clampWidth(Number(width)) : undefined,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function readSidebarCookie() {
|
|
108
|
+
if (typeof document === "undefined") return parseSidebarCookie(undefined);
|
|
109
|
+
const match = document.cookie.match(
|
|
110
|
+
new RegExp(`(?:^|; )${SIDEBAR_COOKIE_NAME}=([^;]*)`),
|
|
111
|
+
);
|
|
112
|
+
return parseSidebarCookie(match?.[1]);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function SidebarProvider({
|
|
116
|
+
defaultMode,
|
|
117
|
+
defaultWidth,
|
|
118
|
+
mode: modeProp,
|
|
119
|
+
onModeChange,
|
|
120
|
+
className,
|
|
121
|
+
style,
|
|
122
|
+
children,
|
|
123
|
+
...props
|
|
124
|
+
}: React.ComponentProps<"div"> & {
|
|
125
|
+
defaultMode?: SidebarMode;
|
|
126
|
+
defaultWidth?: number;
|
|
127
|
+
mode?: SidebarMode;
|
|
128
|
+
onModeChange?: (mode: SidebarMode) => void;
|
|
129
|
+
}) {
|
|
130
|
+
const isMobile = useIsMobile();
|
|
131
|
+
const [openMobile, setOpenMobile] = React.useState(false);
|
|
132
|
+
// Server/static HTML renders the defaults; SidebarRestoreScript patches that
|
|
133
|
+
// markup to the saved state before paint, and the client starts from the
|
|
134
|
+
// same saved state so hydration matches and nothing jumps.
|
|
135
|
+
const [initial] = React.useState(() =>
|
|
136
|
+
defaultMode !== undefined || defaultWidth !== undefined
|
|
137
|
+
? parseSidebarCookie(undefined)
|
|
138
|
+
: readSidebarCookie(),
|
|
139
|
+
);
|
|
140
|
+
const [_mode, _setMode] = React.useState<SidebarMode>(
|
|
141
|
+
defaultMode ?? initial.mode ?? "open",
|
|
142
|
+
);
|
|
143
|
+
const [width, _setWidth] = React.useState(
|
|
144
|
+
clampWidth(defaultWidth ?? initial.width ?? SIDEBAR_WIDTH),
|
|
145
|
+
);
|
|
146
|
+
const [resizing, setResizing] = React.useState(false);
|
|
147
|
+
const mode = modeProp ?? _mode;
|
|
148
|
+
// Mode to restore when a closed sidebar is shown again.
|
|
149
|
+
const lastVisible = React.useRef<SidebarMode>(
|
|
150
|
+
mode === "closed" ? "open" : mode,
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
React.useEffect(() => {
|
|
154
|
+
document.cookie = `${SIDEBAR_COOKIE_NAME}=${mode}:${width}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
|
|
155
|
+
}, [mode, width]);
|
|
156
|
+
|
|
157
|
+
const setMode = React.useCallback(
|
|
158
|
+
(next: SidebarMode) => {
|
|
159
|
+
if (next !== "closed") lastVisible.current = next;
|
|
160
|
+
if (onModeChange) onModeChange(next);
|
|
161
|
+
else _setMode(next);
|
|
162
|
+
},
|
|
163
|
+
[onModeChange],
|
|
164
|
+
);
|
|
165
|
+
const setWidth = React.useCallback(
|
|
166
|
+
(next: number) => _setWidth(clampWidth(next)),
|
|
167
|
+
[],
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
// Maps a dragged width to a mode: tiny closes, narrow shows icons, wide opens.
|
|
171
|
+
const resizeTo = React.useCallback(
|
|
172
|
+
(offset: number) => {
|
|
173
|
+
if (offset < SIDEBAR_CLOSE_BELOW) setMode("closed");
|
|
174
|
+
else if (offset < SIDEBAR_COMPACT_BELOW) setMode("compact");
|
|
175
|
+
else {
|
|
176
|
+
setMode("open");
|
|
177
|
+
setWidth(offset);
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
[setMode, setWidth],
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const open = mode === "open";
|
|
184
|
+
const setOpen = React.useCallback(
|
|
185
|
+
(value: boolean) => setMode(value ? "open" : "compact"),
|
|
186
|
+
[setMode],
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const toggleSidebar = React.useCallback(() => {
|
|
190
|
+
if (isMobile) return setOpenMobile((value) => !value);
|
|
191
|
+
setMode(mode === "closed" ? lastVisible.current : "closed");
|
|
192
|
+
}, [isMobile, mode, setMode]);
|
|
193
|
+
|
|
194
|
+
const toggleCompact = React.useCallback(() => {
|
|
195
|
+
if (isMobile) return setOpenMobile(false);
|
|
196
|
+
setMode(mode === "open" ? "compact" : "open");
|
|
197
|
+
}, [isMobile, mode, setMode]);
|
|
198
|
+
|
|
199
|
+
React.useEffect(() => {
|
|
200
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
201
|
+
if (
|
|
202
|
+
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
|
|
203
|
+
(event.metaKey || event.ctrlKey)
|
|
204
|
+
) {
|
|
205
|
+
event.preventDefault();
|
|
206
|
+
toggleSidebar();
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
211
|
+
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
212
|
+
}, [toggleSidebar]);
|
|
213
|
+
|
|
214
|
+
const state = open ? "expanded" : "collapsed";
|
|
215
|
+
|
|
216
|
+
const contextValue = React.useMemo<SidebarContextProps>(
|
|
217
|
+
() => ({
|
|
218
|
+
state,
|
|
219
|
+
mode,
|
|
220
|
+
setMode,
|
|
221
|
+
width,
|
|
222
|
+
setWidth,
|
|
223
|
+
resizing,
|
|
224
|
+
setResizing,
|
|
225
|
+
resizeTo,
|
|
226
|
+
open,
|
|
227
|
+
setOpen,
|
|
228
|
+
isMobile,
|
|
229
|
+
openMobile,
|
|
230
|
+
setOpenMobile,
|
|
231
|
+
toggleSidebar,
|
|
232
|
+
toggleCompact,
|
|
233
|
+
}),
|
|
234
|
+
[
|
|
235
|
+
state,
|
|
236
|
+
mode,
|
|
237
|
+
setMode,
|
|
238
|
+
width,
|
|
239
|
+
setWidth,
|
|
240
|
+
resizing,
|
|
241
|
+
resizeTo,
|
|
242
|
+
open,
|
|
243
|
+
setOpen,
|
|
244
|
+
isMobile,
|
|
245
|
+
openMobile,
|
|
246
|
+
toggleSidebar,
|
|
247
|
+
toggleCompact,
|
|
248
|
+
],
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
return (
|
|
252
|
+
<SidebarContext.Provider value={contextValue}>
|
|
253
|
+
<div
|
|
254
|
+
data-slot="sidebar-wrapper"
|
|
255
|
+
data-resizing={resizing || undefined}
|
|
256
|
+
style={
|
|
257
|
+
{
|
|
258
|
+
"--sidebar-width": `${width}px`,
|
|
259
|
+
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
|
|
260
|
+
...style,
|
|
261
|
+
} as React.CSSProperties
|
|
262
|
+
}
|
|
263
|
+
className={cn(
|
|
264
|
+
"group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-sidebar data-resizing:cursor-col-resize data-resizing:select-none",
|
|
265
|
+
className,
|
|
266
|
+
)}
|
|
267
|
+
{...props}
|
|
268
|
+
>
|
|
269
|
+
{children}
|
|
270
|
+
{defaultMode === undefined && defaultWidth === undefined ? (
|
|
271
|
+
<SidebarRestoreScript />
|
|
272
|
+
) : null}
|
|
273
|
+
</div>
|
|
274
|
+
</SidebarContext.Provider>
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function Sidebar({
|
|
279
|
+
side = "left",
|
|
280
|
+
variant = "sidebar",
|
|
281
|
+
collapsible = "auto",
|
|
282
|
+
className,
|
|
283
|
+
children,
|
|
284
|
+
dir,
|
|
285
|
+
...props
|
|
286
|
+
}: React.ComponentProps<"div"> & {
|
|
287
|
+
side?: "left" | "right";
|
|
288
|
+
variant?: "sidebar" | "floating" | "inset";
|
|
289
|
+
/** `auto` follows the provider mode: compact shows icons, closed hides. */
|
|
290
|
+
collapsible?: "auto" | "offcanvas" | "icon" | "none";
|
|
291
|
+
}) {
|
|
292
|
+
const { isMobile, state, mode, openMobile, setOpenMobile } = useSidebar();
|
|
293
|
+
const messages = useUiMessages();
|
|
294
|
+
const collapsedAs =
|
|
295
|
+
collapsible === "auto"
|
|
296
|
+
? mode === "closed"
|
|
297
|
+
? "offcanvas"
|
|
298
|
+
: "icon"
|
|
299
|
+
: collapsible;
|
|
300
|
+
|
|
301
|
+
if (collapsible === "none") {
|
|
302
|
+
return (
|
|
303
|
+
<div
|
|
304
|
+
data-slot="sidebar"
|
|
305
|
+
className={cn(
|
|
306
|
+
"flex h-full w-(--sidebar-width) flex-col bg-sidebar text-sidebar-foreground",
|
|
307
|
+
className,
|
|
308
|
+
)}
|
|
309
|
+
{...props}
|
|
310
|
+
>
|
|
311
|
+
{children}
|
|
312
|
+
</div>
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (isMobile) {
|
|
317
|
+
return (
|
|
318
|
+
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
|
|
319
|
+
<SheetContent
|
|
320
|
+
dir={dir}
|
|
321
|
+
data-sidebar="sidebar"
|
|
322
|
+
data-slot="sidebar"
|
|
323
|
+
data-mobile="true"
|
|
324
|
+
className="w-(--sidebar-width) bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
|
|
325
|
+
style={
|
|
326
|
+
{
|
|
327
|
+
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
|
|
328
|
+
} as React.CSSProperties
|
|
329
|
+
}
|
|
330
|
+
side={side}
|
|
331
|
+
>
|
|
332
|
+
<SheetHeader className="sr-only">
|
|
333
|
+
<SheetTitle>{messages.sidebar}</SheetTitle>
|
|
334
|
+
<SheetDescription>{messages.sidebar}</SheetDescription>
|
|
335
|
+
</SheetHeader>
|
|
336
|
+
<div className="flex h-full w-full flex-col">{children}</div>
|
|
337
|
+
</SheetContent>
|
|
338
|
+
</Sheet>
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return (
|
|
343
|
+
<div
|
|
344
|
+
className="group peer hidden text-sidebar-foreground md:block"
|
|
345
|
+
data-state={state}
|
|
346
|
+
data-collapsible={state === "collapsed" ? collapsedAs : ""}
|
|
347
|
+
data-variant={variant}
|
|
348
|
+
data-side={side}
|
|
349
|
+
data-slot="sidebar"
|
|
350
|
+
>
|
|
351
|
+
{/* This is what handles the sidebar gap on desktop */}
|
|
352
|
+
<div
|
|
353
|
+
data-slot="sidebar-gap"
|
|
354
|
+
className={cn(
|
|
355
|
+
"relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear group-data-resizing/sidebar-wrapper:transition-none",
|
|
356
|
+
"group-data-[collapsible=offcanvas]:w-0",
|
|
357
|
+
"group-data-[side=right]:rotate-180",
|
|
358
|
+
variant === "floating" || variant === "inset"
|
|
359
|
+
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
|
|
360
|
+
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)",
|
|
361
|
+
)}
|
|
362
|
+
/>
|
|
363
|
+
<div
|
|
364
|
+
data-slot="sidebar-container"
|
|
365
|
+
data-side={side}
|
|
366
|
+
className={cn(
|
|
367
|
+
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear group-data-resizing/sidebar-wrapper:transition-none data-[side=left]:left-0 data-[side=left]:group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)] data-[side=right]:right-0 data-[side=right]:group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)] md:flex",
|
|
368
|
+
// Adjust the padding for floating and inset variants.
|
|
369
|
+
variant === "floating" || variant === "inset"
|
|
370
|
+
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
|
|
371
|
+
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
|
|
372
|
+
className,
|
|
373
|
+
)}
|
|
374
|
+
{...props}
|
|
375
|
+
>
|
|
376
|
+
<div
|
|
377
|
+
data-sidebar="sidebar"
|
|
378
|
+
data-slot="sidebar-inner"
|
|
379
|
+
className="flex size-full flex-col bg-sidebar group-data-[variant=floating]:rounded-2xl group-data-[variant=floating]:shadow-sm group-data-[variant=floating]:ring-1 group-data-[variant=floating]:ring-sidebar-border"
|
|
380
|
+
>
|
|
381
|
+
{children}
|
|
382
|
+
</div>
|
|
383
|
+
</div>
|
|
384
|
+
</div>
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const isMac = () =>
|
|
389
|
+
typeof navigator !== "undefined" &&
|
|
390
|
+
/Mac|iP(hone|ad)/.test(navigator.platform);
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Wraps a sidebar control with a tooltip (label + shortcut hint) and a
|
|
394
|
+
* right-click menu to pick the mode directly: expanded, compact or hidden.
|
|
395
|
+
*/
|
|
396
|
+
function SidebarModeControl({
|
|
397
|
+
label,
|
|
398
|
+
children,
|
|
399
|
+
}: {
|
|
400
|
+
label: string;
|
|
401
|
+
children: React.ReactElement;
|
|
402
|
+
}) {
|
|
403
|
+
const { mode, setMode, isMobile } = useSidebar();
|
|
404
|
+
const messages = useUiMessages();
|
|
405
|
+
const shortcut = `${isMac() ? "⌘" : "Ctrl+"}${SIDEBAR_KEYBOARD_SHORTCUT.toUpperCase()}`;
|
|
406
|
+
// Clicking changes the sidebar size and moves this control, so the tooltip
|
|
407
|
+
// closes on press and stays closed until the pointer leaves.
|
|
408
|
+
const [tooltipOpen, setTooltipOpen] = React.useState(false);
|
|
409
|
+
const suppressed = React.useRef(false);
|
|
410
|
+
|
|
411
|
+
if (isMobile) return children;
|
|
412
|
+
|
|
413
|
+
return (
|
|
414
|
+
<ContextMenu>
|
|
415
|
+
<Tooltip
|
|
416
|
+
open={tooltipOpen}
|
|
417
|
+
onOpenChange={(open) => setTooltipOpen(open && !suppressed.current)}
|
|
418
|
+
>
|
|
419
|
+
<ContextMenuTrigger
|
|
420
|
+
render={
|
|
421
|
+
<TooltipTrigger
|
|
422
|
+
render={children}
|
|
423
|
+
delay={600}
|
|
424
|
+
onPointerDown={() => {
|
|
425
|
+
suppressed.current = true;
|
|
426
|
+
setTooltipOpen(false);
|
|
427
|
+
}}
|
|
428
|
+
onPointerLeave={() => {
|
|
429
|
+
suppressed.current = false;
|
|
430
|
+
}}
|
|
431
|
+
/>
|
|
432
|
+
}
|
|
433
|
+
/>
|
|
434
|
+
<TooltipContent side="right" className="flex flex-col gap-0.5">
|
|
435
|
+
<span>{label}</span>
|
|
436
|
+
<span className="opacity-70">{messages.sidebarHint(shortcut)}</span>
|
|
437
|
+
</TooltipContent>
|
|
438
|
+
</Tooltip>
|
|
439
|
+
<ContextMenuContent className="min-w-44">
|
|
440
|
+
<ContextMenuRadioGroup
|
|
441
|
+
value={mode}
|
|
442
|
+
onValueChange={(value) => setMode(value as SidebarMode)}
|
|
443
|
+
>
|
|
444
|
+
<ContextMenuRadioItem value="open">
|
|
445
|
+
{messages.modeOpen}
|
|
446
|
+
</ContextMenuRadioItem>
|
|
447
|
+
<ContextMenuRadioItem value="compact">
|
|
448
|
+
{messages.modeCompact}
|
|
449
|
+
</ContextMenuRadioItem>
|
|
450
|
+
<ContextMenuRadioItem value="closed">
|
|
451
|
+
{messages.modeClosed}
|
|
452
|
+
<ContextMenuShortcut>{shortcut}</ContextMenuShortcut>
|
|
453
|
+
</ContextMenuRadioItem>
|
|
454
|
+
</ContextMenuRadioGroup>
|
|
455
|
+
</ContextMenuContent>
|
|
456
|
+
</ContextMenu>
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function SidebarTrigger({
|
|
461
|
+
className,
|
|
462
|
+
onClick,
|
|
463
|
+
...props
|
|
464
|
+
}: React.ComponentProps<typeof Button>) {
|
|
465
|
+
const { toggleSidebar } = useSidebar();
|
|
466
|
+
const messages = useUiMessages();
|
|
467
|
+
|
|
468
|
+
return (
|
|
469
|
+
<SidebarModeControl label={messages.toggleSidebar}>
|
|
470
|
+
<Button
|
|
471
|
+
data-sidebar="trigger"
|
|
472
|
+
data-slot="sidebar-trigger"
|
|
473
|
+
variant="ghost"
|
|
474
|
+
size="icon-sm"
|
|
475
|
+
className={cn(className)}
|
|
476
|
+
aria-label={messages.toggleSidebar}
|
|
477
|
+
onClick={(event) => {
|
|
478
|
+
onClick?.(event);
|
|
479
|
+
toggleSidebar();
|
|
480
|
+
}}
|
|
481
|
+
{...props}
|
|
482
|
+
>
|
|
483
|
+
<PanelLeftIcon />
|
|
484
|
+
</Button>
|
|
485
|
+
</SidebarModeControl>
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/** Collapse control meant to live inside the sidebar: toggles open ↔ compact. */
|
|
490
|
+
function SidebarCollapse({
|
|
491
|
+
className,
|
|
492
|
+
onClick,
|
|
493
|
+
...props
|
|
494
|
+
}: React.ComponentProps<typeof Button>) {
|
|
495
|
+
const { mode, toggleCompact, isMobile } = useSidebar();
|
|
496
|
+
const messages = useUiMessages();
|
|
497
|
+
const label =
|
|
498
|
+
mode === "open" || isMobile
|
|
499
|
+
? messages.collapseSidebar
|
|
500
|
+
: messages.expandSidebar;
|
|
501
|
+
|
|
502
|
+
return (
|
|
503
|
+
<SidebarModeControl label={label}>
|
|
504
|
+
<Button
|
|
505
|
+
data-sidebar="collapse"
|
|
506
|
+
data-slot="sidebar-collapse"
|
|
507
|
+
variant="ghost"
|
|
508
|
+
size="icon-sm"
|
|
509
|
+
className={cn(className)}
|
|
510
|
+
// Stable label/icons (CSS-switched) so the pre-paint restored state
|
|
511
|
+
// hydrates without mismatches.
|
|
512
|
+
aria-label={messages.toggleSidebar}
|
|
513
|
+
onClick={(event) => {
|
|
514
|
+
onClick?.(event);
|
|
515
|
+
toggleCompact();
|
|
516
|
+
}}
|
|
517
|
+
{...props}
|
|
518
|
+
>
|
|
519
|
+
<PanelLeftCloseIcon className="group-data-[collapsible=icon]:hidden" />
|
|
520
|
+
<PanelLeftOpenIcon className="hidden group-data-[collapsible=icon]:block" />
|
|
521
|
+
</Button>
|
|
522
|
+
</SidebarModeControl>
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Drag handle on the sidebar edge. Dragging resizes the open sidebar and
|
|
528
|
+
* switches modes by width (closed → compact → open); a click toggles
|
|
529
|
+
* open ↔ compact; arrow keys resize from the keyboard.
|
|
530
|
+
*/
|
|
531
|
+
function SidebarRail({ className, ...props }: React.ComponentProps<"div">) {
|
|
532
|
+
const { mode, width, resizeTo, setResizing, toggleCompact } = useSidebar();
|
|
533
|
+
const messages = useUiMessages();
|
|
534
|
+
const drag = React.useRef<{
|
|
535
|
+
startX: number;
|
|
536
|
+
startOffset: number;
|
|
537
|
+
side: "left" | "right";
|
|
538
|
+
moved: boolean;
|
|
539
|
+
} | null>(null);
|
|
540
|
+
const offset = mode === "open" ? width : mode === "compact" ? 48 : 0;
|
|
541
|
+
|
|
542
|
+
return (
|
|
543
|
+
<div
|
|
544
|
+
role="separator"
|
|
545
|
+
aria-orientation="vertical"
|
|
546
|
+
aria-label={messages.resizeSidebar}
|
|
547
|
+
aria-valuenow={offset}
|
|
548
|
+
aria-valuemin={0}
|
|
549
|
+
aria-valuemax={SIDEBAR_WIDTH_MAX}
|
|
550
|
+
tabIndex={0}
|
|
551
|
+
data-sidebar="rail"
|
|
552
|
+
data-slot="sidebar-rail"
|
|
553
|
+
onPointerDown={(event) => {
|
|
554
|
+
if (event.button !== 0) return;
|
|
555
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
556
|
+
const side =
|
|
557
|
+
event.currentTarget.closest<HTMLElement>("[data-side]")?.dataset
|
|
558
|
+
.side === "right"
|
|
559
|
+
? "right"
|
|
560
|
+
: "left";
|
|
561
|
+
drag.current = {
|
|
562
|
+
startX: event.clientX,
|
|
563
|
+
startOffset: offset,
|
|
564
|
+
side,
|
|
565
|
+
moved: false,
|
|
566
|
+
};
|
|
567
|
+
}}
|
|
568
|
+
onPointerMove={(event) => {
|
|
569
|
+
const current = drag.current;
|
|
570
|
+
if (!current) return;
|
|
571
|
+
const delta =
|
|
572
|
+
(event.clientX - current.startX) * (current.side === "left" ? 1 : -1);
|
|
573
|
+
if (!current.moved && Math.abs(delta) < 4) return;
|
|
574
|
+
if (!current.moved) setResizing(true);
|
|
575
|
+
current.moved = true;
|
|
576
|
+
resizeTo(current.startOffset + delta);
|
|
577
|
+
}}
|
|
578
|
+
onPointerUp={(event) => {
|
|
579
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
580
|
+
if (drag.current && !drag.current.moved) toggleCompact();
|
|
581
|
+
drag.current = null;
|
|
582
|
+
setResizing(false);
|
|
583
|
+
}}
|
|
584
|
+
onPointerCancel={() => {
|
|
585
|
+
drag.current = null;
|
|
586
|
+
setResizing(false);
|
|
587
|
+
}}
|
|
588
|
+
onKeyDown={(event) => {
|
|
589
|
+
const step = event.shiftKey ? 64 : 16;
|
|
590
|
+
if (event.key === "ArrowRight") resizeTo(offset + step);
|
|
591
|
+
else if (event.key === "ArrowLeft") resizeTo(offset - step);
|
|
592
|
+
else if (event.key === "Enter" || event.key === " ") toggleCompact();
|
|
593
|
+
else return;
|
|
594
|
+
event.preventDefault();
|
|
595
|
+
}}
|
|
596
|
+
className={cn(
|
|
597
|
+
"absolute inset-y-0 z-20 hidden w-4 touch-none cursor-col-resize outline-none group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:start-1/2 after:w-[2px] after:transition-colors hover:after:bg-sidebar-border focus-visible:after:bg-sidebar-ring sm:flex ltr:-translate-x-1/2 rtl:-translate-x-1/2",
|
|
598
|
+
"group-data-resizing/sidebar-wrapper:after:bg-sidebar-ring",
|
|
599
|
+
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
|
|
600
|
+
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
|
|
601
|
+
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
|
|
602
|
+
className,
|
|
603
|
+
)}
|
|
604
|
+
{...props}
|
|
605
|
+
/>
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
|
|
610
|
+
return (
|
|
611
|
+
<main
|
|
612
|
+
data-slot="sidebar-inset"
|
|
613
|
+
className={cn(
|
|
614
|
+
"relative flex w-full flex-1 flex-col bg-background md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-2xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
|
|
615
|
+
className,
|
|
616
|
+
)}
|
|
617
|
+
{...props}
|
|
618
|
+
/>
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function SidebarInput({
|
|
623
|
+
className,
|
|
624
|
+
...props
|
|
625
|
+
}: React.ComponentProps<typeof Input>) {
|
|
626
|
+
return (
|
|
627
|
+
<Input
|
|
628
|
+
data-slot="sidebar-input"
|
|
629
|
+
data-sidebar="input"
|
|
630
|
+
className={cn("h-8 w-full bg-input/50 shadow-none", className)}
|
|
631
|
+
{...props}
|
|
632
|
+
/>
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
637
|
+
return (
|
|
638
|
+
<div
|
|
639
|
+
data-slot="sidebar-header"
|
|
640
|
+
data-sidebar="header"
|
|
641
|
+
className={cn(
|
|
642
|
+
"flex flex-col gap-2 p-2 [--radius:var(--radius-xl)]",
|
|
643
|
+
className,
|
|
644
|
+
)}
|
|
645
|
+
{...props}
|
|
646
|
+
/>
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
651
|
+
return (
|
|
652
|
+
<div
|
|
653
|
+
data-slot="sidebar-footer"
|
|
654
|
+
data-sidebar="footer"
|
|
655
|
+
className={cn(
|
|
656
|
+
"flex flex-col gap-2 p-2 [--radius:var(--radius-xl)]",
|
|
657
|
+
className,
|
|
658
|
+
)}
|
|
659
|
+
{...props}
|
|
660
|
+
/>
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function SidebarSeparator({
|
|
665
|
+
className,
|
|
666
|
+
...props
|
|
667
|
+
}: React.ComponentProps<typeof Separator>) {
|
|
668
|
+
return (
|
|
669
|
+
<Separator
|
|
670
|
+
data-slot="sidebar-separator"
|
|
671
|
+
data-sidebar="separator"
|
|
672
|
+
className={cn("mx-2 w-auto bg-sidebar-border", className)}
|
|
673
|
+
{...props}
|
|
674
|
+
/>
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
679
|
+
return (
|
|
680
|
+
<div
|
|
681
|
+
data-slot="sidebar-content"
|
|
682
|
+
data-sidebar="content"
|
|
683
|
+
className={cn(
|
|
684
|
+
"no-scrollbar flex min-h-0 flex-1 flex-col gap-2 overflow-auto scroll-fade [--radius:var(--radius-xl)] group-data-[collapsible=icon]:overflow-hidden",
|
|
685
|
+
className,
|
|
686
|
+
)}
|
|
687
|
+
{...props}
|
|
688
|
+
/>
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
693
|
+
return (
|
|
694
|
+
<div
|
|
695
|
+
data-slot="sidebar-group"
|
|
696
|
+
data-sidebar="group"
|
|
697
|
+
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
|
|
698
|
+
{...props}
|
|
699
|
+
/>
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function SidebarGroupLabel({
|
|
704
|
+
className,
|
|
705
|
+
render,
|
|
706
|
+
...props
|
|
707
|
+
}: useRender.ComponentProps<"div"> & React.ComponentProps<"div">) {
|
|
708
|
+
return useRender({
|
|
709
|
+
defaultTagName: "div",
|
|
710
|
+
props: mergeProps<"div">(
|
|
711
|
+
{
|
|
712
|
+
className: cn(
|
|
713
|
+
"flex h-8 shrink-0 items-center rounded-xl px-3 text-xs font-medium text-sidebar-foreground/70 ring-sidebar-ring outline-hidden transition-[margin,opacity] duration-200 ease-linear group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
|
714
|
+
className,
|
|
715
|
+
),
|
|
716
|
+
},
|
|
717
|
+
props,
|
|
718
|
+
),
|
|
719
|
+
render,
|
|
720
|
+
state: {
|
|
721
|
+
slot: "sidebar-group-label",
|
|
722
|
+
sidebar: "group-label",
|
|
723
|
+
},
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function SidebarGroupAction({
|
|
728
|
+
className,
|
|
729
|
+
render,
|
|
730
|
+
...props
|
|
731
|
+
}: useRender.ComponentProps<"button"> & React.ComponentProps<"button">) {
|
|
732
|
+
return useRender({
|
|
733
|
+
defaultTagName: "button",
|
|
734
|
+
props: mergeProps<"button">(
|
|
735
|
+
{
|
|
736
|
+
className: cn(
|
|
737
|
+
"absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-xl p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0",
|
|
738
|
+
className,
|
|
739
|
+
),
|
|
740
|
+
},
|
|
741
|
+
props,
|
|
742
|
+
),
|
|
743
|
+
render,
|
|
744
|
+
state: {
|
|
745
|
+
slot: "sidebar-group-action",
|
|
746
|
+
sidebar: "group-action",
|
|
747
|
+
},
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function SidebarGroupContent({
|
|
752
|
+
className,
|
|
753
|
+
...props
|
|
754
|
+
}: React.ComponentProps<"div">) {
|
|
755
|
+
return (
|
|
756
|
+
<div
|
|
757
|
+
data-slot="sidebar-group-content"
|
|
758
|
+
data-sidebar="group-content"
|
|
759
|
+
className={cn("w-full text-sm", className)}
|
|
760
|
+
{...props}
|
|
761
|
+
/>
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
|
|
766
|
+
return (
|
|
767
|
+
<ul
|
|
768
|
+
data-slot="sidebar-menu"
|
|
769
|
+
data-sidebar="menu"
|
|
770
|
+
className={cn("flex w-full min-w-0 flex-col gap-0.5", className)}
|
|
771
|
+
{...props}
|
|
772
|
+
/>
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
|
|
777
|
+
return (
|
|
778
|
+
<li
|
|
779
|
+
data-slot="sidebar-menu-item"
|
|
780
|
+
data-sidebar="menu-item"
|
|
781
|
+
className={cn("group/menu-item relative", className)}
|
|
782
|
+
{...props}
|
|
783
|
+
/>
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const sidebarMenuButtonVariants = cva(
|
|
788
|
+
"peer/menu-button group/menu-button flex w-full items-center gap-2 overflow-hidden rounded-xl px-3 py-2 text-left text-sm ring-sidebar-ring outline-hidden transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! 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 data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:font-medium data-active:text-sidebar-accent-foreground [&_svg]:size-4 [&_svg]:shrink-0 [&>span:last-child]:truncate",
|
|
789
|
+
{
|
|
790
|
+
variants: {
|
|
791
|
+
variant: {
|
|
792
|
+
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
|
793
|
+
outline:
|
|
794
|
+
"bg-background shadow-[0_0_0_1px_var(--sidebar-border)] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_var(--sidebar-accent)]",
|
|
795
|
+
},
|
|
796
|
+
size: {
|
|
797
|
+
default: "h-9 text-sm",
|
|
798
|
+
sm: "h-8 text-xs",
|
|
799
|
+
// Compact identity marks fill the button and cover its hover background.
|
|
800
|
+
// Paint feedback above the mark so images and opaque avatars respond too.
|
|
801
|
+
lg: "h-14 px-3 text-sm group-data-[collapsible=icon]:p-0! group-data-[collapsible=icon]:relative group-data-[collapsible=icon]:after:pointer-events-none group-data-[collapsible=icon]:after:absolute group-data-[collapsible=icon]:after:inset-0 group-data-[collapsible=icon]:after:rounded-[inherit] group-data-[collapsible=icon]:after:bg-sidebar-foreground/10 group-data-[collapsible=icon]:after:opacity-0 group-data-[collapsible=icon]:after:transition-opacity group-data-[collapsible=icon]:hover:after:opacity-100 group-data-[collapsible=icon]:data-popup-open:after:opacity-100",
|
|
802
|
+
},
|
|
803
|
+
},
|
|
804
|
+
defaultVariants: {
|
|
805
|
+
variant: "default",
|
|
806
|
+
size: "default",
|
|
807
|
+
},
|
|
808
|
+
},
|
|
809
|
+
);
|
|
810
|
+
|
|
811
|
+
function SidebarMenuButton({
|
|
812
|
+
render,
|
|
813
|
+
isActive = false,
|
|
814
|
+
variant = "default",
|
|
815
|
+
size = "default",
|
|
816
|
+
tooltip,
|
|
817
|
+
className,
|
|
818
|
+
...props
|
|
819
|
+
}: useRender.ComponentProps<"button"> &
|
|
820
|
+
React.ComponentProps<"button"> & {
|
|
821
|
+
isActive?: boolean;
|
|
822
|
+
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
|
|
823
|
+
} & VariantProps<typeof sidebarMenuButtonVariants>) {
|
|
824
|
+
const { isMobile, state } = useSidebar();
|
|
825
|
+
const comp = useRender({
|
|
826
|
+
defaultTagName: "button",
|
|
827
|
+
props: mergeProps<"button">(
|
|
828
|
+
{
|
|
829
|
+
className: cn(sidebarMenuButtonVariants({ variant, size }), className),
|
|
830
|
+
},
|
|
831
|
+
props,
|
|
832
|
+
),
|
|
833
|
+
render: !tooltip ? render : <TooltipTrigger render={render} />,
|
|
834
|
+
state: {
|
|
835
|
+
slot: "sidebar-menu-button",
|
|
836
|
+
sidebar: "menu-button",
|
|
837
|
+
size,
|
|
838
|
+
active: isActive,
|
|
839
|
+
},
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
if (!tooltip) {
|
|
843
|
+
return comp;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
if (typeof tooltip === "string") {
|
|
847
|
+
tooltip = {
|
|
848
|
+
children: tooltip,
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
return (
|
|
853
|
+
<Tooltip>
|
|
854
|
+
{comp}
|
|
855
|
+
<TooltipContent
|
|
856
|
+
side="right"
|
|
857
|
+
align="center"
|
|
858
|
+
hidden={state !== "collapsed" || isMobile}
|
|
859
|
+
{...tooltip}
|
|
860
|
+
/>
|
|
861
|
+
</Tooltip>
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function SidebarMenuAction({
|
|
866
|
+
className,
|
|
867
|
+
render,
|
|
868
|
+
showOnHover = false,
|
|
869
|
+
...props
|
|
870
|
+
}: useRender.ComponentProps<"button"> &
|
|
871
|
+
React.ComponentProps<"button"> & {
|
|
872
|
+
showOnHover?: boolean;
|
|
873
|
+
}) {
|
|
874
|
+
return useRender({
|
|
875
|
+
defaultTagName: "button",
|
|
876
|
+
props: mergeProps<"button">(
|
|
877
|
+
{
|
|
878
|
+
className: cn(
|
|
879
|
+
"absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-xl p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform group-data-[collapsible=icon]:hidden peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[size=default]/menu-button:top-2 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0",
|
|
880
|
+
showOnHover &&
|
|
881
|
+
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 peer-data-active/menu-button:text-sidebar-accent-foreground aria-expanded:opacity-100 md:opacity-0",
|
|
882
|
+
className,
|
|
883
|
+
),
|
|
884
|
+
},
|
|
885
|
+
props,
|
|
886
|
+
),
|
|
887
|
+
render,
|
|
888
|
+
state: {
|
|
889
|
+
slot: "sidebar-menu-action",
|
|
890
|
+
sidebar: "menu-action",
|
|
891
|
+
},
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function SidebarMenuBadge({
|
|
896
|
+
className,
|
|
897
|
+
...props
|
|
898
|
+
}: React.ComponentProps<"div">) {
|
|
899
|
+
return (
|
|
900
|
+
<div
|
|
901
|
+
data-slot="sidebar-menu-badge"
|
|
902
|
+
data-sidebar="menu-badge"
|
|
903
|
+
className={cn(
|
|
904
|
+
// Majapi UI: vertically centered for every button size (upstream offsets
|
|
905
|
+
// assume 32px buttons) and aligned with the button's inner padding.
|
|
906
|
+
"pointer-events-none absolute top-1/2 right-2 flex h-5 min-w-5 -translate-y-1/2 items-center justify-center rounded-xl px-1 text-xs font-medium text-muted-foreground tabular-nums select-none group-data-[collapsible=icon]:hidden peer-hover/menu-button:text-sidebar-accent-foreground peer-data-active/menu-button:text-sidebar-accent-foreground",
|
|
907
|
+
className,
|
|
908
|
+
)}
|
|
909
|
+
{...props}
|
|
910
|
+
/>
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function SidebarMenuSkeleton({
|
|
915
|
+
className,
|
|
916
|
+
showIcon = false,
|
|
917
|
+
...props
|
|
918
|
+
}: React.ComponentProps<"div"> & {
|
|
919
|
+
showIcon?: boolean;
|
|
920
|
+
}) {
|
|
921
|
+
// Random width between 50 to 90%.
|
|
922
|
+
const [width] = React.useState(() => {
|
|
923
|
+
return `${Math.floor(Math.random() * 40) + 50}%`;
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
return (
|
|
927
|
+
<div
|
|
928
|
+
data-slot="sidebar-menu-skeleton"
|
|
929
|
+
data-sidebar="menu-skeleton"
|
|
930
|
+
className={cn("flex h-8 items-center gap-2 rounded-xl px-2", className)}
|
|
931
|
+
{...props}
|
|
932
|
+
>
|
|
933
|
+
{showIcon && (
|
|
934
|
+
<Skeleton
|
|
935
|
+
className="size-4 rounded-xl"
|
|
936
|
+
data-sidebar="menu-skeleton-icon"
|
|
937
|
+
/>
|
|
938
|
+
)}
|
|
939
|
+
<Skeleton
|
|
940
|
+
className="h-4 max-w-(--skeleton-width) flex-1"
|
|
941
|
+
data-sidebar="menu-skeleton-text"
|
|
942
|
+
style={
|
|
943
|
+
{
|
|
944
|
+
"--skeleton-width": width,
|
|
945
|
+
} as React.CSSProperties
|
|
946
|
+
}
|
|
947
|
+
/>
|
|
948
|
+
</div>
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
|
|
953
|
+
return (
|
|
954
|
+
<ul
|
|
955
|
+
data-slot="sidebar-menu-sub"
|
|
956
|
+
data-sidebar="menu-sub"
|
|
957
|
+
className={cn(
|
|
958
|
+
"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 group-data-[collapsible=icon]:hidden",
|
|
959
|
+
className,
|
|
960
|
+
)}
|
|
961
|
+
{...props}
|
|
962
|
+
/>
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
function SidebarMenuSubItem({
|
|
967
|
+
className,
|
|
968
|
+
...props
|
|
969
|
+
}: React.ComponentProps<"li">) {
|
|
970
|
+
return (
|
|
971
|
+
<li
|
|
972
|
+
data-slot="sidebar-menu-sub-item"
|
|
973
|
+
data-sidebar="menu-sub-item"
|
|
974
|
+
className={cn("group/menu-sub-item relative", className)}
|
|
975
|
+
{...props}
|
|
976
|
+
/>
|
|
977
|
+
);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
function SidebarMenuSubButton({
|
|
981
|
+
render,
|
|
982
|
+
size = "md",
|
|
983
|
+
isActive = false,
|
|
984
|
+
className,
|
|
985
|
+
...props
|
|
986
|
+
}: useRender.ComponentProps<"a"> &
|
|
987
|
+
React.ComponentProps<"a"> & {
|
|
988
|
+
size?: "sm" | "md";
|
|
989
|
+
isActive?: boolean;
|
|
990
|
+
}) {
|
|
991
|
+
return useRender({
|
|
992
|
+
defaultTagName: "a",
|
|
993
|
+
props: mergeProps<"a">(
|
|
994
|
+
{
|
|
995
|
+
className: cn(
|
|
996
|
+
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-xl px-3 text-sidebar-foreground ring-sidebar-ring outline-hidden group-data-[collapsible=icon]:hidden 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 data-[size=md]:text-sm data-[size=sm]:text-xs data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
|
|
997
|
+
className,
|
|
998
|
+
),
|
|
999
|
+
},
|
|
1000
|
+
props,
|
|
1001
|
+
),
|
|
1002
|
+
render,
|
|
1003
|
+
state: {
|
|
1004
|
+
slot: "sidebar-menu-sub-button",
|
|
1005
|
+
sidebar: "menu-sub-button",
|
|
1006
|
+
size,
|
|
1007
|
+
active: isActive,
|
|
1008
|
+
},
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
export {
|
|
1013
|
+
Sidebar,
|
|
1014
|
+
SidebarContent,
|
|
1015
|
+
SidebarFooter,
|
|
1016
|
+
SidebarGroup,
|
|
1017
|
+
SidebarGroupAction,
|
|
1018
|
+
SidebarGroupContent,
|
|
1019
|
+
SidebarGroupLabel,
|
|
1020
|
+
SidebarHeader,
|
|
1021
|
+
SidebarInput,
|
|
1022
|
+
SidebarInset,
|
|
1023
|
+
SidebarMenu,
|
|
1024
|
+
SidebarMenuAction,
|
|
1025
|
+
SidebarMenuBadge,
|
|
1026
|
+
SidebarMenuButton,
|
|
1027
|
+
SidebarMenuItem,
|
|
1028
|
+
SidebarMenuSkeleton,
|
|
1029
|
+
SidebarMenuSub,
|
|
1030
|
+
SidebarMenuSubButton,
|
|
1031
|
+
SidebarMenuSubItem,
|
|
1032
|
+
SidebarCollapse,
|
|
1033
|
+
SidebarProvider,
|
|
1034
|
+
SidebarRail,
|
|
1035
|
+
SidebarSeparator,
|
|
1036
|
+
SidebarTrigger,
|
|
1037
|
+
parseSidebarCookie,
|
|
1038
|
+
useSidebar,
|
|
1039
|
+
};
|