@octanejs/shadcn 0.0.9 → 0.0.11
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/README.md +6 -7
- package/package.json +14 -9
- package/src/bases/base-ui/ui/context-menu.tsrx +277 -0
- package/src/bases/base-ui/ui/hover-card.tsrx +100 -0
- package/src/bases/base-ui/ui/menubar.tsrx +288 -0
- package/src/bases/base-ui/ui/sidebar.tsrx +628 -0
- package/src/bases/base-ui/ui/tabs.tsrx +90 -0
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
// Base UI base sidebar — class strings from the maintainer-supplied radix source, verbatim. Composes
|
|
2
|
+
// this base's own button, input, separator, sheet, skeleton and tooltip, so it inherits their
|
|
3
|
+
// dialects (the sheet's transition motion, the separator's aria-orientation) for free.
|
|
4
|
+
//
|
|
5
|
+
// THE TOOLTIP TRIGGER USES `render`, WHICH IS THE WHOLE REASON THIS FAMILY IS PORTABLE. Radix wraps
|
|
6
|
+
// the menu button with `<TooltipTrigger asChild>{button}</TooltipTrigger>`; Base UI has no Slot, but
|
|
7
|
+
// `Tooltip.Trigger` accepts a `render` prop taking an element, which is the same composition —
|
|
8
|
+
// `render={button}`. That is a real primitive with a documented prop, not a guess.
|
|
9
|
+
//
|
|
10
|
+
// `asChild` IS ABSENT FROM THIS BASE'S OWN PARTS, matching badge, breadcrumb and item. Five of them
|
|
11
|
+
// take it in the radix source — GroupLabel, GroupAction, MenuButton, MenuAction, MenuSubButton — and
|
|
12
|
+
// each renders a plain host element, so there is no primitive here whose `render` prop to borrow and
|
|
13
|
+
// nothing settles which spelling upstream's Base UI sidebar uses. It is typed `never` so markup
|
|
14
|
+
// carried over from the radix base fails to compile rather than silently rendering the wrapper AND
|
|
15
|
+
// its child.
|
|
16
|
+
//
|
|
17
|
+
// THAT GAP IS LOAD-BEARING HERE, more than it was for badge: `<SidebarMenuButton asChild>` wrapping
|
|
18
|
+
// a router link is the ordinary way to build a nav. Until the upstream source settles the spelling,
|
|
19
|
+
// build the link inside the button, or apply the exported `sidebarMenuButtonVariants({ variant, size })`
|
|
20
|
+
// to your own element.
|
|
21
|
+
//
|
|
22
|
+
// Octane adaptations: "use client" dropped, lucide-react -> @octanejs/lucide; the provider/Sheet
|
|
23
|
+
// trees compose with createElement where a child must be a real element descriptor, and CSS custom
|
|
24
|
+
// properties in `style` are cast (they sit outside CSSProperties).
|
|
25
|
+
import {
|
|
26
|
+
createContext,
|
|
27
|
+
createElement,
|
|
28
|
+
useCallback,
|
|
29
|
+
useContext,
|
|
30
|
+
useEffect,
|
|
31
|
+
useMemo,
|
|
32
|
+
useState,
|
|
33
|
+
type OctaneNode,
|
|
34
|
+
} from 'octane';
|
|
35
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
36
|
+
import { PanelLeftIcon } from '@octanejs/lucide';
|
|
37
|
+
|
|
38
|
+
import { cn } from '../../../lib/utils';
|
|
39
|
+
import { useIsMobile } from '../../../hooks/use-mobile';
|
|
40
|
+
import { Button } from './button.tsrx';
|
|
41
|
+
import { Input } from './input.tsrx';
|
|
42
|
+
import { Separator } from './separator.tsrx';
|
|
43
|
+
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from './sheet.tsrx';
|
|
44
|
+
import { Skeleton } from './skeleton.tsrx';
|
|
45
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from './tooltip.tsrx';
|
|
46
|
+
|
|
47
|
+
const SIDEBAR_COOKIE_NAME = 'sidebar_state';
|
|
48
|
+
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
|
49
|
+
const SIDEBAR_WIDTH = '16rem';
|
|
50
|
+
const SIDEBAR_WIDTH_MOBILE = '18rem';
|
|
51
|
+
const SIDEBAR_WIDTH_ICON = '3rem';
|
|
52
|
+
const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
|
|
53
|
+
|
|
54
|
+
type Props = { className?: string; children?: OctaneNode } & Record<string, unknown>;
|
|
55
|
+
|
|
56
|
+
export interface SidebarContextProps {
|
|
57
|
+
state: 'expanded' | 'collapsed';
|
|
58
|
+
open: boolean;
|
|
59
|
+
setOpen: (open: boolean) => void;
|
|
60
|
+
openMobile: boolean;
|
|
61
|
+
setOpenMobile: (open: boolean) => void;
|
|
62
|
+
isMobile: boolean;
|
|
63
|
+
toggleSidebar: () => void;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const SidebarContext = createContext<SidebarContextProps | null>(null);
|
|
67
|
+
|
|
68
|
+
export function useSidebar(): SidebarContextProps {
|
|
69
|
+
const context = useContext(SidebarContext);
|
|
70
|
+
if (!context) {
|
|
71
|
+
throw new Error('useSidebar must be used within a SidebarProvider.');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return context;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function SidebarProvider(props: Props & {
|
|
78
|
+
defaultOpen?: boolean;
|
|
79
|
+
open?: boolean;
|
|
80
|
+
onOpenChange?: (open: boolean) => void;
|
|
81
|
+
style?: Record<string, string>;
|
|
82
|
+
}) {
|
|
83
|
+
const {
|
|
84
|
+
defaultOpen = true,
|
|
85
|
+
open: openProp,
|
|
86
|
+
onOpenChange: setOpenProp,
|
|
87
|
+
className,
|
|
88
|
+
style,
|
|
89
|
+
children: _children,
|
|
90
|
+
...rest
|
|
91
|
+
} = props;
|
|
92
|
+
const isMobile = useIsMobile();
|
|
93
|
+
const [openMobile, setOpenMobile] = useState(false);
|
|
94
|
+
|
|
95
|
+
// This is the internal state of the sidebar.
|
|
96
|
+
// We use openProp and setOpenProp for control from outside the component.
|
|
97
|
+
const [_open, _setOpen] = useState(defaultOpen);
|
|
98
|
+
const open = openProp ?? _open;
|
|
99
|
+
const setOpen = useCallback((value: boolean | ((value: boolean) => boolean)) => {
|
|
100
|
+
const openState =
|
|
101
|
+
typeof value === 'function' ? value(open) : value;
|
|
102
|
+
if (setOpenProp) {
|
|
103
|
+
setOpenProp(openState);
|
|
104
|
+
} else {
|
|
105
|
+
_setOpen(openState);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// This sets the cookie to keep the sidebar state.
|
|
109
|
+
document.cookie =
|
|
110
|
+
`${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Helper to toggle the sidebar.
|
|
114
|
+
const toggleSidebar = useCallback(() => {
|
|
115
|
+
return isMobile ? setOpenMobile((value) => !value) : setOpen((value) => !value);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Adds a keyboard shortcut to toggle the sidebar.
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
121
|
+
if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {
|
|
122
|
+
event.preventDefault();
|
|
123
|
+
toggleSidebar();
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
128
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// We add a state so that we can do data-state="expanded" or "collapsed".
|
|
132
|
+
// This makes it easier to style the sidebar with Tailwind classes.
|
|
133
|
+
const state = open ? 'expanded' : 'collapsed';
|
|
134
|
+
|
|
135
|
+
const contextValue = useMemo<SidebarContextProps>(
|
|
136
|
+
() => ({
|
|
137
|
+
state,
|
|
138
|
+
open,
|
|
139
|
+
setOpen,
|
|
140
|
+
isMobile,
|
|
141
|
+
openMobile,
|
|
142
|
+
setOpenMobile,
|
|
143
|
+
toggleSidebar,
|
|
144
|
+
}),
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
return createElement(
|
|
148
|
+
SidebarContext.Provider,
|
|
149
|
+
{ value: contextValue },
|
|
150
|
+
createElement(
|
|
151
|
+
'div',
|
|
152
|
+
{
|
|
153
|
+
'data-slot': 'sidebar-wrapper',
|
|
154
|
+
style: {
|
|
155
|
+
'--sidebar-width': SIDEBAR_WIDTH,
|
|
156
|
+
'--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
|
|
157
|
+
...style,
|
|
158
|
+
} as Record<string, string>,
|
|
159
|
+
className: cn(
|
|
160
|
+
'group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-sidebar',
|
|
161
|
+
className,
|
|
162
|
+
),
|
|
163
|
+
...rest,
|
|
164
|
+
},
|
|
165
|
+
props.children,
|
|
166
|
+
),
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function Sidebar(props: Props & {
|
|
171
|
+
side?: 'left' | 'right';
|
|
172
|
+
variant?: 'sidebar' | 'floating' | 'inset';
|
|
173
|
+
collapsible?: 'offcanvas' | 'icon' | 'none';
|
|
174
|
+
dir?: string;
|
|
175
|
+
}) {
|
|
176
|
+
const {
|
|
177
|
+
side = 'left',
|
|
178
|
+
variant = 'sidebar',
|
|
179
|
+
collapsible = 'offcanvas',
|
|
180
|
+
className,
|
|
181
|
+
children: _children,
|
|
182
|
+
dir,
|
|
183
|
+
...rest
|
|
184
|
+
} = props;
|
|
185
|
+
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
|
|
186
|
+
|
|
187
|
+
if (collapsible === 'none') {
|
|
188
|
+
return createElement(
|
|
189
|
+
'div',
|
|
190
|
+
{
|
|
191
|
+
'data-slot': 'sidebar',
|
|
192
|
+
className: cn(
|
|
193
|
+
'flex h-full w-(--sidebar-width) flex-col bg-sidebar text-sidebar-foreground',
|
|
194
|
+
className,
|
|
195
|
+
),
|
|
196
|
+
...rest,
|
|
197
|
+
},
|
|
198
|
+
props.children,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (isMobile) {
|
|
203
|
+
return createElement(
|
|
204
|
+
Sheet,
|
|
205
|
+
{ open: openMobile, onOpenChange: setOpenMobile, ...rest },
|
|
206
|
+
createElement(
|
|
207
|
+
SheetContent,
|
|
208
|
+
{
|
|
209
|
+
dir,
|
|
210
|
+
'data-sidebar': 'sidebar',
|
|
211
|
+
'data-slot': 'sidebar',
|
|
212
|
+
'data-mobile': 'true',
|
|
213
|
+
className: 'w-(--sidebar-width) bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden',
|
|
214
|
+
style: { '--sidebar-width': SIDEBAR_WIDTH_MOBILE } as Record<string, string>,
|
|
215
|
+
side,
|
|
216
|
+
},
|
|
217
|
+
createElement(
|
|
218
|
+
SheetHeader,
|
|
219
|
+
{ key: 'header', className: 'sr-only' },
|
|
220
|
+
createElement(SheetTitle, { key: 'title' }, 'Sidebar'),
|
|
221
|
+
createElement(SheetDescription, { key: 'desc' }, 'Displays the mobile sidebar.'),
|
|
222
|
+
),
|
|
223
|
+
createElement(
|
|
224
|
+
'div',
|
|
225
|
+
{ key: 'body', className: 'flex h-full w-full flex-col' },
|
|
226
|
+
props.children,
|
|
227
|
+
),
|
|
228
|
+
),
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return createElement(
|
|
233
|
+
'div',
|
|
234
|
+
{
|
|
235
|
+
className: 'group peer hidden text-sidebar-foreground md:block',
|
|
236
|
+
'data-state': state,
|
|
237
|
+
'data-collapsible': state === 'collapsed' ? collapsible : '',
|
|
238
|
+
'data-variant': variant,
|
|
239
|
+
'data-side': side,
|
|
240
|
+
'data-slot': 'sidebar',
|
|
241
|
+
},
|
|
242
|
+
// This is what handles the sidebar gap on desktop
|
|
243
|
+
createElement('div', {
|
|
244
|
+
key: 'gap',
|
|
245
|
+
'data-slot': 'sidebar-gap',
|
|
246
|
+
className: cn(
|
|
247
|
+
'relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear',
|
|
248
|
+
'group-data-[collapsible=offcanvas]:w-0',
|
|
249
|
+
'group-data-[side=right]:rotate-180',
|
|
250
|
+
variant === 'floating' || variant === 'inset'
|
|
251
|
+
? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]'
|
|
252
|
+
: 'group-data-[collapsible=icon]:w-(--sidebar-width-icon)',
|
|
253
|
+
),
|
|
254
|
+
}),
|
|
255
|
+
createElement(
|
|
256
|
+
'div',
|
|
257
|
+
{
|
|
258
|
+
key: 'container',
|
|
259
|
+
'data-slot': 'sidebar-container',
|
|
260
|
+
'data-side': side,
|
|
261
|
+
className: cn(
|
|
262
|
+
'fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear 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',
|
|
263
|
+
// Adjust the padding for floating and inset variants.
|
|
264
|
+
variant === 'floating' || variant === 'inset'
|
|
265
|
+
? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]'
|
|
266
|
+
: 'group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l',
|
|
267
|
+
className,
|
|
268
|
+
),
|
|
269
|
+
...rest,
|
|
270
|
+
},
|
|
271
|
+
createElement(
|
|
272
|
+
'div',
|
|
273
|
+
{
|
|
274
|
+
'data-sidebar': 'sidebar',
|
|
275
|
+
'data-slot': 'sidebar-inner',
|
|
276
|
+
className: 'flex size-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:shadow-sm group-data-[variant=floating]:ring-1 group-data-[variant=floating]:ring-sidebar-border',
|
|
277
|
+
},
|
|
278
|
+
props.children,
|
|
279
|
+
),
|
|
280
|
+
),
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function SidebarTrigger({ className, onClick, ...props }: Props & {
|
|
285
|
+
onClick?: (event: MouseEvent) => void;
|
|
286
|
+
}) @{
|
|
287
|
+
const { toggleSidebar } = useSidebar();
|
|
288
|
+
|
|
289
|
+
<Button
|
|
290
|
+
data-sidebar="trigger"
|
|
291
|
+
data-slot="sidebar-trigger"
|
|
292
|
+
variant="ghost"
|
|
293
|
+
size="icon-sm"
|
|
294
|
+
className={cn(className)}
|
|
295
|
+
onClick={(event: MouseEvent) => {
|
|
296
|
+
onClick?.(event);
|
|
297
|
+
toggleSidebar();
|
|
298
|
+
}}
|
|
299
|
+
{...props}
|
|
300
|
+
>
|
|
301
|
+
<PanelLeftIcon className="cn-rtl-flip" />
|
|
302
|
+
<span className="sr-only">Toggle Sidebar</span>
|
|
303
|
+
</Button>
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function SidebarRail({ className, ...props }: Props) @{
|
|
307
|
+
const { toggleSidebar } = useSidebar();
|
|
308
|
+
|
|
309
|
+
<button
|
|
310
|
+
data-sidebar="rail"
|
|
311
|
+
data-slot="sidebar-rail"
|
|
312
|
+
aria-label="Toggle Sidebar"
|
|
313
|
+
tabIndex={-1}
|
|
314
|
+
onClick={toggleSidebar}
|
|
315
|
+
title="Toggle Sidebar"
|
|
316
|
+
className={cn(
|
|
317
|
+
'absolute inset-y-0 z-20 hidden w-4 transition-all ease-linear 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] hover:after:bg-sidebar-border sm:flex ltr:-translate-x-1/2 rtl:-translate-x-1/2',
|
|
318
|
+
'in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize',
|
|
319
|
+
'[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
|
|
320
|
+
'group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full hover:group-data-[collapsible=offcanvas]:bg-sidebar',
|
|
321
|
+
'[[data-side=left][data-collapsible=offcanvas]_&]:-right-2',
|
|
322
|
+
'[[data-side=right][data-collapsible=offcanvas]_&]:-left-2',
|
|
323
|
+
className,
|
|
324
|
+
)}
|
|
325
|
+
{...props}
|
|
326
|
+
/>
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function SidebarInset({ className, ...props }: Props) @{
|
|
330
|
+
<main
|
|
331
|
+
data-slot="sidebar-inset"
|
|
332
|
+
className={cn(
|
|
333
|
+
'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-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2',
|
|
334
|
+
className,
|
|
335
|
+
)}
|
|
336
|
+
{...props}
|
|
337
|
+
/>
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export function SidebarInput({ className, ...props }: Props) @{
|
|
341
|
+
<Input
|
|
342
|
+
data-slot="sidebar-input"
|
|
343
|
+
data-sidebar="input"
|
|
344
|
+
className={cn('h-8 w-full bg-background shadow-none', className)}
|
|
345
|
+
{...props}
|
|
346
|
+
/>
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export function SidebarHeader({ className, ...props }: Props) @{
|
|
350
|
+
<div
|
|
351
|
+
data-slot="sidebar-header"
|
|
352
|
+
data-sidebar="header"
|
|
353
|
+
className={cn('flex flex-col gap-2 p-2', className)}
|
|
354
|
+
{...props}
|
|
355
|
+
/>
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export function SidebarFooter({ className, ...props }: Props) @{
|
|
359
|
+
<div
|
|
360
|
+
data-slot="sidebar-footer"
|
|
361
|
+
data-sidebar="footer"
|
|
362
|
+
className={cn('flex flex-col gap-2 p-2', className)}
|
|
363
|
+
{...props}
|
|
364
|
+
/>
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export function SidebarSeparator({ className, ...props }: Props) @{
|
|
368
|
+
<Separator
|
|
369
|
+
data-slot="sidebar-separator"
|
|
370
|
+
data-sidebar="separator"
|
|
371
|
+
className={cn('mx-2 w-auto bg-sidebar-border', className)}
|
|
372
|
+
{...props}
|
|
373
|
+
/>
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export function SidebarContent({ className, ...props }: Props) @{
|
|
377
|
+
<div
|
|
378
|
+
data-slot="sidebar-content"
|
|
379
|
+
data-sidebar="content"
|
|
380
|
+
className={cn(
|
|
381
|
+
'no-scrollbar flex min-h-0 flex-1 flex-col gap-0 overflow-auto group-data-[collapsible=icon]:overflow-hidden',
|
|
382
|
+
className,
|
|
383
|
+
)}
|
|
384
|
+
{...props}
|
|
385
|
+
/>
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export function SidebarGroup({ className, ...props }: Props) @{
|
|
389
|
+
<div
|
|
390
|
+
data-slot="sidebar-group"
|
|
391
|
+
data-sidebar="group"
|
|
392
|
+
className={cn('relative flex w-full min-w-0 flex-col p-2', className)}
|
|
393
|
+
{...props}
|
|
394
|
+
/>
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export function SidebarGroupLabel({ className, ...props }: Props & {
|
|
398
|
+
/** Not supported in this base — see the header. Declared so it fails to compile, not silently. */
|
|
399
|
+
asChild?: never;
|
|
400
|
+
}) @{
|
|
401
|
+
<div
|
|
402
|
+
data-slot="sidebar-group-label"
|
|
403
|
+
data-sidebar="group-label"
|
|
404
|
+
className={cn(
|
|
405
|
+
'flex h-8 shrink-0 items-center rounded-md px-2 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',
|
|
406
|
+
className,
|
|
407
|
+
)}
|
|
408
|
+
{...props}
|
|
409
|
+
/>
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export function SidebarGroupAction({ className, ...props }: Props & {
|
|
413
|
+
/** Not supported in this base — see the header. Declared so it fails to compile, not silently. */
|
|
414
|
+
asChild?: never;
|
|
415
|
+
}) @{
|
|
416
|
+
<button
|
|
417
|
+
data-slot="sidebar-group-action"
|
|
418
|
+
data-sidebar="group-action"
|
|
419
|
+
className={cn(
|
|
420
|
+
'absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md 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',
|
|
421
|
+
className,
|
|
422
|
+
)}
|
|
423
|
+
{...props}
|
|
424
|
+
/>
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export function SidebarGroupContent({ className, ...props }: Props) @{
|
|
428
|
+
<div
|
|
429
|
+
data-slot="sidebar-group-content"
|
|
430
|
+
data-sidebar="group-content"
|
|
431
|
+
className={cn('w-full text-sm', className)}
|
|
432
|
+
{...props}
|
|
433
|
+
/>
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export function SidebarMenu({ className, ...props }: Props) @{
|
|
437
|
+
<ul
|
|
438
|
+
data-slot="sidebar-menu"
|
|
439
|
+
data-sidebar="menu"
|
|
440
|
+
className={cn('flex w-full min-w-0 flex-col gap-0', className)}
|
|
441
|
+
{...props}
|
|
442
|
+
/>
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export function SidebarMenuItem({ className, ...props }: Props) @{
|
|
446
|
+
<li
|
|
447
|
+
data-slot="sidebar-menu-item"
|
|
448
|
+
data-sidebar="menu-item"
|
|
449
|
+
className={cn('group/menu-item relative', className)}
|
|
450
|
+
{...props}
|
|
451
|
+
/>
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// EXPORTED, unlike the radix source it was ported from. This base drops `asChild`, and the header
|
|
455
|
+
// above sends consumers here as the substitute for it — a helper they cannot import is not a
|
|
456
|
+
// workaround. Every sibling family in this base already exports its cva map (badge, item, toggle),
|
|
457
|
+
// so this also brings sidebar in line with them.
|
|
458
|
+
export const sidebarMenuButtonVariants = cva(
|
|
459
|
+
'peer/menu-button group/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-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',
|
|
460
|
+
{
|
|
461
|
+
variants: {
|
|
462
|
+
variant: {
|
|
463
|
+
default: 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
|
|
464
|
+
outline: '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)]',
|
|
465
|
+
},
|
|
466
|
+
size: {
|
|
467
|
+
default: 'h-8 text-sm',
|
|
468
|
+
sm: 'h-7 text-xs',
|
|
469
|
+
lg: 'h-12 text-sm group-data-[collapsible=icon]:p-0!',
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
defaultVariants: {
|
|
473
|
+
variant: 'default',
|
|
474
|
+
size: 'default',
|
|
475
|
+
},
|
|
476
|
+
},
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
export function SidebarMenuButton(props: Props & {
|
|
480
|
+
/** Not supported in this base — see the header. Declared so it fails to compile, not silently. */
|
|
481
|
+
asChild?: never;
|
|
482
|
+
isActive?: boolean;
|
|
483
|
+
tooltip?: string | Record<string, unknown>;
|
|
484
|
+
variant?: VariantProps<typeof sidebarMenuButtonVariants>['variant'];
|
|
485
|
+
size?: VariantProps<typeof sidebarMenuButtonVariants>['size'];
|
|
486
|
+
}) {
|
|
487
|
+
const {
|
|
488
|
+
isActive = false,
|
|
489
|
+
variant = 'default',
|
|
490
|
+
size = 'default',
|
|
491
|
+
tooltip,
|
|
492
|
+
className,
|
|
493
|
+
children: _children,
|
|
494
|
+
...rest
|
|
495
|
+
} = props;
|
|
496
|
+
const { isMobile, state } = useSidebar();
|
|
497
|
+
|
|
498
|
+
const button = createElement(
|
|
499
|
+
'button',
|
|
500
|
+
{
|
|
501
|
+
'data-slot': 'sidebar-menu-button',
|
|
502
|
+
'data-sidebar': 'menu-button',
|
|
503
|
+
'data-size': size,
|
|
504
|
+
'data-active': isActive,
|
|
505
|
+
className: cn(sidebarMenuButtonVariants({ variant, size }), className),
|
|
506
|
+
...rest,
|
|
507
|
+
},
|
|
508
|
+
props.children,
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
if (!tooltip) {
|
|
512
|
+
return button;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const tooltipProps =
|
|
516
|
+
typeof tooltip === 'string' ? { children: tooltip } : tooltip;
|
|
517
|
+
|
|
518
|
+
return createElement(
|
|
519
|
+
Tooltip,
|
|
520
|
+
{},
|
|
521
|
+
createElement(TooltipTrigger, { key: 'trigger', render: button }),
|
|
522
|
+
createElement(TooltipContent, {
|
|
523
|
+
key: 'content',
|
|
524
|
+
side: 'right',
|
|
525
|
+
align: 'center',
|
|
526
|
+
hidden: state !== 'collapsed' || isMobile,
|
|
527
|
+
...tooltipProps,
|
|
528
|
+
}),
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
export function SidebarMenuAction({ className, showOnHover = false, ...props }: Props & {
|
|
533
|
+
/** Not supported in this base — see the header. Declared so it fails to compile, not silently. */
|
|
534
|
+
asChild?: never;
|
|
535
|
+
showOnHover?: boolean;
|
|
536
|
+
}) @{
|
|
537
|
+
<button
|
|
538
|
+
data-slot="sidebar-menu-action"
|
|
539
|
+
data-sidebar="menu-action"
|
|
540
|
+
className={cn(
|
|
541
|
+
'absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md 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-1.5 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',
|
|
542
|
+
showOnHover &&
|
|
543
|
+
'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',
|
|
544
|
+
className,
|
|
545
|
+
)}
|
|
546
|
+
{...props}
|
|
547
|
+
/>
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
export function SidebarMenuBadge({ className, ...props }: Props) @{
|
|
551
|
+
<div
|
|
552
|
+
data-slot="sidebar-menu-badge"
|
|
553
|
+
data-sidebar="menu-badge"
|
|
554
|
+
className={cn(
|
|
555
|
+
'pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium text-sidebar-foreground tabular-nums select-none group-data-[collapsible=icon]:hidden peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 peer-data-active/menu-button:text-sidebar-accent-foreground',
|
|
556
|
+
className,
|
|
557
|
+
)}
|
|
558
|
+
{...props}
|
|
559
|
+
/>
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
export function SidebarMenuSkeleton({ className, showIcon = false, ...props }: Props & {
|
|
563
|
+
showIcon?: boolean;
|
|
564
|
+
}) @{
|
|
565
|
+
// Random width between 50 to 90%.
|
|
566
|
+
const [width] = useState(() => `${Math.floor(Math.random() * 40) + 50}%`);
|
|
567
|
+
|
|
568
|
+
<div
|
|
569
|
+
data-slot="sidebar-menu-skeleton"
|
|
570
|
+
data-sidebar="menu-skeleton"
|
|
571
|
+
className={cn('flex h-8 items-center gap-2 rounded-md px-2', className)}
|
|
572
|
+
{...props}
|
|
573
|
+
>
|
|
574
|
+
@if (showIcon) {
|
|
575
|
+
<Skeleton className="size-4 rounded-md" data-sidebar="menu-skeleton-icon" />
|
|
576
|
+
}
|
|
577
|
+
<Skeleton
|
|
578
|
+
className="h-4 max-w-(--skeleton-width) flex-1"
|
|
579
|
+
data-sidebar="menu-skeleton-text"
|
|
580
|
+
style={{ '--skeleton-width': width } as Record<string, string>}
|
|
581
|
+
/>
|
|
582
|
+
</div>
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
export function SidebarMenuSub({ className, ...props }: Props) @{
|
|
586
|
+
<ul
|
|
587
|
+
data-slot="sidebar-menu-sub"
|
|
588
|
+
data-sidebar="menu-sub"
|
|
589
|
+
className={cn(
|
|
590
|
+
'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',
|
|
591
|
+
className,
|
|
592
|
+
)}
|
|
593
|
+
{...props}
|
|
594
|
+
/>
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
export function SidebarMenuSubItem({ className, ...props }: Props) @{
|
|
598
|
+
<li
|
|
599
|
+
data-slot="sidebar-menu-sub-item"
|
|
600
|
+
data-sidebar="menu-sub-item"
|
|
601
|
+
className={cn('group/menu-sub-item relative', className)}
|
|
602
|
+
{...props}
|
|
603
|
+
/>
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
export function SidebarMenuSubButton({
|
|
607
|
+
size = 'md',
|
|
608
|
+
isActive = false,
|
|
609
|
+
className,
|
|
610
|
+
...props
|
|
611
|
+
}: Props & {
|
|
612
|
+
/** Not supported in this base — see the header. Declared so it fails to compile, not silently. */
|
|
613
|
+
asChild?: never;
|
|
614
|
+
size?: 'sm' | 'md';
|
|
615
|
+
isActive?: boolean;
|
|
616
|
+
}) @{
|
|
617
|
+
<a
|
|
618
|
+
data-slot="sidebar-menu-sub-button"
|
|
619
|
+
data-sidebar="menu-sub-button"
|
|
620
|
+
data-size={size}
|
|
621
|
+
data-active={isActive}
|
|
622
|
+
className={cn(
|
|
623
|
+
'flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 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',
|
|
624
|
+
className,
|
|
625
|
+
)}
|
|
626
|
+
{...props}
|
|
627
|
+
/>
|
|
628
|
+
}
|