@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.
@@ -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
+ }