@octanejs/shadcn 0.0.1

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