vinta-schedule-design-system 0.1.0

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 (107) hide show
  1. package/components.json +21 -0
  2. package/dist/styles.css +3452 -0
  3. package/package.json +103 -0
  4. package/src/layout/app-shell.tsx +115 -0
  5. package/src/layout/app-topbar.stories.tsx +74 -0
  6. package/src/layout/app-topbar.tsx +145 -0
  7. package/src/layout/auth-layout.tsx +45 -0
  8. package/src/layout/box.stories.tsx +86 -0
  9. package/src/layout/box.tsx +34 -0
  10. package/src/layout/center.tsx +24 -0
  11. package/src/layout/container.stories.tsx +44 -0
  12. package/src/layout/container.tsx +39 -0
  13. package/src/layout/divider.tsx +64 -0
  14. package/src/layout/flex.stories.tsx +109 -0
  15. package/src/layout/flex.test.tsx +71 -0
  16. package/src/layout/flex.tsx +112 -0
  17. package/src/layout/grid.stories.tsx +91 -0
  18. package/src/layout/grid.tsx +132 -0
  19. package/src/layout/heading.tsx +53 -0
  20. package/src/layout/index.ts +47 -0
  21. package/src/layout/layout-style.ts +291 -0
  22. package/src/layout/link.stories.tsx +54 -0
  23. package/src/layout/link.test.tsx +75 -0
  24. package/src/layout/link.tsx +100 -0
  25. package/src/layout/navbar.stories.tsx +70 -0
  26. package/src/layout/navbar.tsx +128 -0
  27. package/src/layout/page-header.stories.tsx +80 -0
  28. package/src/layout/page-header.tsx +59 -0
  29. package/src/layout/section.tsx +22 -0
  30. package/src/layout/spacer.tsx +21 -0
  31. package/src/layout/stack.stories.tsx +58 -0
  32. package/src/layout/stack.tsx +19 -0
  33. package/src/layout/text.tsx +139 -0
  34. package/src/layout/typography.stories.tsx +112 -0
  35. package/src/lib/utils.ts +6 -0
  36. package/src/story-types.ts +80 -0
  37. package/src/styles/build.css +33 -0
  38. package/src/styles/globals.css +9 -0
  39. package/src/styles/tokens.css +281 -0
  40. package/src/ui/accordion.stories.tsx +84 -0
  41. package/src/ui/accordion.tsx +57 -0
  42. package/src/ui/alert-dialog.stories.tsx +57 -0
  43. package/src/ui/alert-dialog.tsx +141 -0
  44. package/src/ui/alert.stories.tsx +46 -0
  45. package/src/ui/alert.tsx +59 -0
  46. package/src/ui/avatar.stories.tsx +56 -0
  47. package/src/ui/avatar.tsx +50 -0
  48. package/src/ui/badge.stories.tsx +105 -0
  49. package/src/ui/badge.tsx +57 -0
  50. package/src/ui/breadcrumb.stories.tsx +69 -0
  51. package/src/ui/breadcrumb.tsx +140 -0
  52. package/src/ui/button.stories.tsx +85 -0
  53. package/src/ui/button.tsx +60 -0
  54. package/src/ui/card.stories.tsx +62 -0
  55. package/src/ui/card.tsx +96 -0
  56. package/src/ui/carousel.stories.tsx +55 -0
  57. package/src/ui/carousel.tsx +262 -0
  58. package/src/ui/checkbox.stories.tsx +52 -0
  59. package/src/ui/checkbox.tsx +30 -0
  60. package/src/ui/combobox.stories.tsx +138 -0
  61. package/src/ui/combobox.tsx +217 -0
  62. package/src/ui/command.tsx +184 -0
  63. package/src/ui/dialog.stories.tsx +66 -0
  64. package/src/ui/dialog.tsx +163 -0
  65. package/src/ui/drawer.stories.tsx +80 -0
  66. package/src/ui/drawer.tsx +118 -0
  67. package/src/ui/dropdown-menu.stories.tsx +57 -0
  68. package/src/ui/dropdown-menu.tsx +201 -0
  69. package/src/ui/form.stories.tsx +70 -0
  70. package/src/ui/form.tsx +181 -0
  71. package/src/ui/input-otp.stories.tsx +52 -0
  72. package/src/ui/input-otp.tsx +71 -0
  73. package/src/ui/input.stories.tsx +68 -0
  74. package/src/ui/input.tsx +22 -0
  75. package/src/ui/label.stories.tsx +35 -0
  76. package/src/ui/label.tsx +26 -0
  77. package/src/ui/navigation-menu.stories.tsx +79 -0
  78. package/src/ui/navigation-menu.tsx +128 -0
  79. package/src/ui/pagination.stories.tsx +62 -0
  80. package/src/ui/pagination.tsx +137 -0
  81. package/src/ui/popover.tsx +89 -0
  82. package/src/ui/progress.stories.tsx +33 -0
  83. package/src/ui/progress.tsx +28 -0
  84. package/src/ui/radio-group.stories.tsx +52 -0
  85. package/src/ui/radio-group.tsx +44 -0
  86. package/src/ui/scheduling-chip.stories.tsx +117 -0
  87. package/src/ui/scheduling-chip.tsx +62 -0
  88. package/src/ui/scroll-area.stories.tsx +52 -0
  89. package/src/ui/scroll-area.tsx +48 -0
  90. package/src/ui/select.stories.tsx +56 -0
  91. package/src/ui/select.tsx +159 -0
  92. package/src/ui/sheet.stories.tsx +63 -0
  93. package/src/ui/sheet.tsx +141 -0
  94. package/src/ui/skeleton.stories.tsx +40 -0
  95. package/src/ui/skeleton.tsx +15 -0
  96. package/src/ui/sonner.stories.tsx +62 -0
  97. package/src/ui/sonner.tsx +34 -0
  98. package/src/ui/switch.stories.tsx +41 -0
  99. package/src/ui/switch.tsx +29 -0
  100. package/src/ui/table.stories.tsx +88 -0
  101. package/src/ui/table.tsx +132 -0
  102. package/src/ui/tabs.stories.tsx +63 -0
  103. package/src/ui/tabs.tsx +55 -0
  104. package/src/ui/textarea.stories.tsx +45 -0
  105. package/src/ui/textarea.tsx +22 -0
  106. package/src/ui/tooltip.stories.tsx +48 -0
  107. package/src/ui/tooltip.tsx +32 -0
@@ -0,0 +1,141 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import * as SheetPrimitive from '@radix-ui/react-dialog';
5
+ import { cva, type VariantProps } from 'class-variance-authority';
6
+ import { X } from 'lucide-react';
7
+
8
+ import { cn } from '../lib/utils';
9
+
10
+ const Sheet = SheetPrimitive.Root;
11
+
12
+ const SheetTrigger = SheetPrimitive.Trigger;
13
+
14
+ const SheetClose = SheetPrimitive.Close;
15
+
16
+ const SheetPortal = SheetPrimitive.Portal;
17
+
18
+ const SheetOverlay = React.forwardRef<
19
+ React.ElementRef<typeof SheetPrimitive.Overlay>,
20
+ React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
21
+ >(({ className, ...props }, ref) => (
22
+ <SheetPrimitive.Overlay
23
+ className={cn(
24
+ 'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80',
25
+ className
26
+ )}
27
+ {...props}
28
+ ref={ref}
29
+ />
30
+ ));
31
+ SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
32
+
33
+ const sheetVariants = cva(
34
+ 'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out',
35
+ {
36
+ variants: {
37
+ side: {
38
+ top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
39
+ bottom:
40
+ 'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
41
+ left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
42
+ right:
43
+ 'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',
44
+ },
45
+ },
46
+ defaultVariants: {
47
+ side: 'right',
48
+ },
49
+ }
50
+ );
51
+
52
+ interface SheetContentProps
53
+ extends
54
+ React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
55
+ VariantProps<typeof sheetVariants> {}
56
+
57
+ const SheetContent = React.forwardRef<
58
+ React.ElementRef<typeof SheetPrimitive.Content>,
59
+ SheetContentProps
60
+ >(({ side = 'right', className, children, ...props }, ref) => (
61
+ <SheetPortal>
62
+ <SheetOverlay />
63
+ <SheetPrimitive.Content
64
+ ref={ref}
65
+ className={cn(sheetVariants({ side }), className)}
66
+ {...props}
67
+ >
68
+ <SheetPrimitive.Close className='ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:pointer-events-none'>
69
+ <X className='h-4 w-4' />
70
+ <span className='sr-only'>Close</span>
71
+ </SheetPrimitive.Close>
72
+ {children}
73
+ </SheetPrimitive.Content>
74
+ </SheetPortal>
75
+ ));
76
+ SheetContent.displayName = SheetPrimitive.Content.displayName;
77
+
78
+ const SheetHeader = ({
79
+ className,
80
+ ...props
81
+ }: React.HTMLAttributes<HTMLDivElement>) => (
82
+ <div
83
+ className={cn(
84
+ 'flex flex-col space-y-2 text-center sm:text-left',
85
+ className
86
+ )}
87
+ {...props}
88
+ />
89
+ );
90
+ SheetHeader.displayName = 'SheetHeader';
91
+
92
+ const SheetFooter = ({
93
+ className,
94
+ ...props
95
+ }: React.HTMLAttributes<HTMLDivElement>) => (
96
+ <div
97
+ className={cn(
98
+ 'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
99
+ className
100
+ )}
101
+ {...props}
102
+ />
103
+ );
104
+ SheetFooter.displayName = 'SheetFooter';
105
+
106
+ const SheetTitle = React.forwardRef<
107
+ React.ElementRef<typeof SheetPrimitive.Title>,
108
+ React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
109
+ >(({ className, ...props }, ref) => (
110
+ <SheetPrimitive.Title
111
+ ref={ref}
112
+ className={cn('text-foreground text-lg font-semibold', className)}
113
+ {...props}
114
+ />
115
+ ));
116
+ SheetTitle.displayName = SheetPrimitive.Title.displayName;
117
+
118
+ const SheetDescription = React.forwardRef<
119
+ React.ElementRef<typeof SheetPrimitive.Description>,
120
+ React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
121
+ >(({ className, ...props }, ref) => (
122
+ <SheetPrimitive.Description
123
+ ref={ref}
124
+ className={cn('text-muted-foreground text-sm', className)}
125
+ {...props}
126
+ />
127
+ ));
128
+ SheetDescription.displayName = SheetPrimitive.Description.displayName;
129
+
130
+ export {
131
+ Sheet,
132
+ SheetPortal,
133
+ SheetOverlay,
134
+ SheetTrigger,
135
+ SheetClose,
136
+ SheetContent,
137
+ SheetHeader,
138
+ SheetFooter,
139
+ SheetTitle,
140
+ SheetDescription,
141
+ };
@@ -0,0 +1,40 @@
1
+ import type { Meta, StoryObj } from '../story-types';
2
+
3
+ import { Skeleton } from './skeleton';
4
+
5
+ const meta = {
6
+ title: 'Components/Skeleton',
7
+ component: Skeleton,
8
+ tags: ['autodocs'],
9
+ // Skeleton is a bare styled div (`React.HTMLAttributes<HTMLDivElement>`) — it
10
+ // has NO domain prop of its own: size/shape are driven purely by className,
11
+ // which must never be exposed (§6). Nothing was fabricated; the only real,
12
+ // forwarded scalar worth editing is the accessible loading label. It renders
13
+ // no composed content → no slot (§4).
14
+ argTypes: {
15
+ 'aria-label': {
16
+ control: 'text',
17
+ description: 'Accessible label announced while content loads',
18
+ },
19
+ },
20
+ args: { 'aria-label': 'Loading' },
21
+ } satisfies Meta<typeof Skeleton>;
22
+
23
+ export default meta;
24
+ type Story = StoryObj<typeof meta>;
25
+
26
+ export const Default: Story = {
27
+ render: (args) => <Skeleton className='h-6 w-48' {...args} />,
28
+ };
29
+
30
+ export const Card: Story = {
31
+ render: () => (
32
+ <div className='flex w-80 items-center gap-4'>
33
+ <Skeleton className='h-12 w-12 rounded-full' />
34
+ <div className='flex-1 space-y-2'>
35
+ <Skeleton className='h-4 w-3/4' />
36
+ <Skeleton className='h-4 w-1/2' />
37
+ </div>
38
+ </div>
39
+ ),
40
+ };
@@ -0,0 +1,15 @@
1
+ import { cn } from '../lib/utils';
2
+
3
+ function Skeleton({
4
+ className,
5
+ ...props
6
+ }: React.HTMLAttributes<HTMLDivElement>) {
7
+ return (
8
+ <div
9
+ className={cn('bg-primary/10 animate-pulse rounded-md', className)}
10
+ {...props}
11
+ />
12
+ );
13
+ }
14
+
15
+ export { Skeleton };
@@ -0,0 +1,62 @@
1
+ import type { Meta, StoryObj } from '../story-types';
2
+ import { toast } from 'sonner';
3
+
4
+ import { Toaster } from './sonner';
5
+ import { Button } from './button';
6
+
7
+ const meta = {
8
+ // Title leaf must match the real named export — the component is `Toaster`,
9
+ // so the old 'Components/Sonner (Toast)' leaf was invalid (§7).
10
+ title: 'Components/Toaster',
11
+ component: Toaster,
12
+ tags: ['autodocs'],
13
+ // Leaf: sonner's ToasterProps. It renders the toast viewport, never composed
14
+ // children → no slot (§4). `className`/`style` are excluded (§6).
15
+ argTypes: {
16
+ position: {
17
+ control: 'select',
18
+ options: [
19
+ 'top-left',
20
+ 'top-center',
21
+ 'top-right',
22
+ 'bottom-left',
23
+ 'bottom-center',
24
+ 'bottom-right',
25
+ ],
26
+ },
27
+ theme: { control: 'inline-radio', options: ['light', 'dark', 'system'] },
28
+ richColors: { control: 'boolean' },
29
+ closeButton: { control: 'boolean' },
30
+ expand: { control: 'boolean' },
31
+ duration: { control: 'number', description: 'Toast lifetime in ms' },
32
+ },
33
+ args: { position: 'bottom-right', richColors: false, closeButton: false },
34
+ } satisfies Meta<typeof Toaster>;
35
+
36
+ export default meta;
37
+ type Story = StoryObj<typeof meta>;
38
+
39
+ export const Default: Story = {
40
+ render: (args) => (
41
+ <div className='flex flex-wrap gap-3'>
42
+ <Button onClick={() => toast('Booking saved')}>Default</Button>
43
+ <Button
44
+ variant='outline'
45
+ onClick={() =>
46
+ toast.success('Appointment confirmed', {
47
+ description: 'Mon, 9:00–9:30 AM with Dr. Lopez',
48
+ })
49
+ }
50
+ >
51
+ Success
52
+ </Button>
53
+ <Button
54
+ variant='destructive'
55
+ onClick={() => toast.error('Sync failed — retrying')}
56
+ >
57
+ Error
58
+ </Button>
59
+ <Toaster {...args} />
60
+ </div>
61
+ ),
62
+ };
@@ -0,0 +1,34 @@
1
+ 'use client';
2
+
3
+ import { useTheme } from 'next-themes';
4
+ import { Toaster as Sonner } from 'sonner';
5
+
6
+ type ToasterProps = React.ComponentProps<typeof Sonner>;
7
+
8
+ const Toaster = ({ ...props }: ToasterProps) => {
9
+ const { theme = 'system' } = useTheme();
10
+
11
+ return (
12
+ <Sonner
13
+ theme={theme as ToasterProps['theme']}
14
+ className='toaster group'
15
+ toastOptions={{
16
+ classNames: {
17
+ toast:
18
+ 'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg',
19
+ description: 'group-[.toast]:text-muted-foreground',
20
+ actionButton:
21
+ 'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',
22
+ cancelButton:
23
+ 'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground',
24
+ },
25
+ }}
26
+ {...props}
27
+ />
28
+ );
29
+ };
30
+ // Explicit: bundlers rename this binding (it collides with sonner's own
31
+ // `Toaster`), so the inferred function name cannot be relied on for resolution.
32
+ Toaster.displayName = 'Toaster';
33
+
34
+ export { Toaster };
@@ -0,0 +1,41 @@
1
+ import type { Meta, StoryObj } from '../story-types';
2
+
3
+ import { Switch } from './switch';
4
+ import { Label } from './label';
5
+
6
+ const meta = {
7
+ title: 'Components/Switch',
8
+ component: Switch,
9
+ tags: ['autodocs'],
10
+ // Leaf control: Radix `Switch.Root` renders its own Thumb, so it takes no
11
+ // composed content — NO slot. Real Radix Root props only (§6).
12
+ argTypes: {
13
+ defaultChecked: { control: 'boolean' },
14
+ disabled: { control: 'boolean' },
15
+ required: { control: 'boolean' },
16
+ name: { control: 'text', description: 'Name submitted with the form' },
17
+ value: { control: 'text', description: 'Value submitted when checked' },
18
+ },
19
+ args: { defaultChecked: true },
20
+ } satisfies Meta<typeof Switch>;
21
+
22
+ export default meta;
23
+ type Story = StoryObj<typeof meta>;
24
+
25
+ export const Default: Story = {
26
+ render: (args) => (
27
+ <div className='flex items-center gap-2'>
28
+ <Switch id='avail' {...args} />
29
+ <Label htmlFor='avail'>Accepting bookings</Label>
30
+ </div>
31
+ ),
32
+ };
33
+
34
+ export const Disabled: Story = {
35
+ render: () => (
36
+ <div className='flex items-center gap-2'>
37
+ <Switch id='dis' disabled />
38
+ <Label htmlFor='dis'>Disabled</Label>
39
+ </div>
40
+ ),
41
+ };
@@ -0,0 +1,29 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import * as SwitchPrimitives from '@radix-ui/react-switch';
5
+
6
+ import { cn } from '../lib/utils';
7
+
8
+ const Switch = React.forwardRef<
9
+ React.ElementRef<typeof SwitchPrimitives.Root>,
10
+ React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
11
+ >(({ className, ...props }, ref) => (
12
+ <SwitchPrimitives.Root
13
+ className={cn(
14
+ 'peer focus-visible:ring-ring focus-visible:ring-offset-background data-[state=checked]:bg-primary data-[state=unchecked]:bg-input inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',
15
+ className
16
+ )}
17
+ {...props}
18
+ ref={ref}
19
+ >
20
+ <SwitchPrimitives.Thumb
21
+ className={cn(
22
+ 'bg-background pointer-events-none block h-4 w-4 rounded-full shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0'
23
+ )}
24
+ />
25
+ </SwitchPrimitives.Root>
26
+ ));
27
+ Switch.displayName = SwitchPrimitives.Root.displayName;
28
+
29
+ export { Switch };
@@ -0,0 +1,88 @@
1
+ import type { Meta, StoryObj } from '../story-types';
2
+
3
+ import {
4
+ Table,
5
+ TableHeader,
6
+ TableBody,
7
+ TableRow,
8
+ TableHead,
9
+ TableCell,
10
+ TableCaption,
11
+ TableFooter,
12
+ } from './table';
13
+
14
+ const meta = {
15
+ // Leaf normalised to the exported component name and to the 'Components/*'
16
+ // namespace the rest of the package uses (was 'UI/Table').
17
+ title: 'Components/Table',
18
+ component: Table,
19
+ tags: ['autodocs'],
20
+ // Table is a plain <table> (`React.HTMLAttributes<HTMLTableElement>`) — it has
21
+ // no design-system prop of its own. Nothing was invented: the curated keys are
22
+ // real, forwarded HTML attributes. The rows/headers are `children` → slot (§4).
23
+ argTypes: {
24
+ 'aria-label': {
25
+ control: 'text',
26
+ description: 'Accessible name for the table',
27
+ },
28
+ id: { control: 'text', description: 'DOM id on the <table> element' },
29
+ },
30
+ parameters: { puck: { slots: ['children'] } },
31
+ } satisfies Meta<typeof Table>;
32
+
33
+ export default meta;
34
+ type Story = StoryObj<typeof meta>;
35
+
36
+ export const Default: Story = {
37
+ args: { 'aria-label': 'Recent invoices' },
38
+ render: (args) => (
39
+ <div className='p-6'>
40
+ <Table {...args}>
41
+ <TableCaption>A list of recent invoices.</TableCaption>
42
+ <TableHeader>
43
+ <TableRow>
44
+ <TableHead>Invoice</TableHead>
45
+ <TableHead>Status</TableHead>
46
+ <TableHead>Method</TableHead>
47
+ <TableHead className='text-right'>Amount</TableHead>
48
+ </TableRow>
49
+ </TableHeader>
50
+ <TableBody>
51
+ {[
52
+ {
53
+ id: 'INV001',
54
+ status: 'Paid',
55
+ method: 'Credit Card',
56
+ amount: '$250.00',
57
+ },
58
+ {
59
+ id: 'INV002',
60
+ status: 'Pending',
61
+ method: 'PayPal',
62
+ amount: '$150.00',
63
+ },
64
+ {
65
+ id: 'INV003',
66
+ status: 'Unpaid',
67
+ method: 'Bank Transfer',
68
+ amount: '$350.00',
69
+ },
70
+ ].map((invoice) => (
71
+ <TableRow key={invoice.id}>
72
+ <TableCell className='font-medium'>{invoice.id}</TableCell>
73
+ <TableCell>{invoice.status}</TableCell>
74
+ <TableCell>{invoice.method}</TableCell>
75
+ <TableCell className='text-right'>{invoice.amount}</TableCell>
76
+ </TableRow>
77
+ ))}
78
+ </TableBody>
79
+ <TableFooter>
80
+ <TableRow>
81
+ <TableCell colSpan={3}>Total</TableCell>
82
+ <TableCell className='text-right'>$750.00</TableCell>
83
+ </TableRow>
84
+ </TableFooter>
85
+ </Table>
86
+ </div>
87
+ ),
88
+ };
@@ -0,0 +1,132 @@
1
+ import * as React from 'react';
2
+
3
+ import { cn } from '../lib/utils';
4
+
5
+ const Table = React.forwardRef<
6
+ HTMLTableElement,
7
+ React.HTMLAttributes<HTMLTableElement>
8
+ >(({ className, ...props }, ref) => (
9
+ <div data-slot='table-wrapper' className='relative w-full overflow-auto'>
10
+ <table
11
+ ref={ref}
12
+ data-slot='table'
13
+ className={cn('w-full caption-bottom text-sm', className)}
14
+ {...props}
15
+ />
16
+ </div>
17
+ ));
18
+ Table.displayName = 'Table';
19
+
20
+ const TableHeader = React.forwardRef<
21
+ HTMLTableSectionElement,
22
+ React.HTMLAttributes<HTMLTableSectionElement>
23
+ >(({ className, ...props }, ref) => (
24
+ <thead
25
+ ref={ref}
26
+ data-slot='table-header'
27
+ className={cn('[&_tr]:border-b', className)}
28
+ {...props}
29
+ />
30
+ ));
31
+ TableHeader.displayName = 'TableHeader';
32
+
33
+ const TableBody = React.forwardRef<
34
+ HTMLTableSectionElement,
35
+ React.HTMLAttributes<HTMLTableSectionElement>
36
+ >(({ className, ...props }, ref) => (
37
+ <tbody
38
+ ref={ref}
39
+ data-slot='table-body'
40
+ className={cn('[&_tr:last-child]:border-0', className)}
41
+ {...props}
42
+ />
43
+ ));
44
+ TableBody.displayName = 'TableBody';
45
+
46
+ const TableFooter = React.forwardRef<
47
+ HTMLTableSectionElement,
48
+ React.HTMLAttributes<HTMLTableSectionElement>
49
+ >(({ className, ...props }, ref) => (
50
+ <tfoot
51
+ ref={ref}
52
+ data-slot='table-footer'
53
+ className={cn(
54
+ 'bg-muted/50 border-t font-medium [&>tr]:last:border-b-0',
55
+ className
56
+ )}
57
+ {...props}
58
+ />
59
+ ));
60
+ TableFooter.displayName = 'TableFooter';
61
+
62
+ const TableRow = React.forwardRef<
63
+ HTMLTableRowElement,
64
+ React.HTMLAttributes<HTMLTableRowElement>
65
+ >(({ className, ...props }, ref) => (
66
+ <tr
67
+ ref={ref}
68
+ data-slot='table-row'
69
+ className={cn(
70
+ 'hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors',
71
+ className
72
+ )}
73
+ {...props}
74
+ />
75
+ ));
76
+ TableRow.displayName = 'TableRow';
77
+
78
+ const TableHead = React.forwardRef<
79
+ HTMLTableCellElement,
80
+ React.ThHTMLAttributes<HTMLTableCellElement>
81
+ >(({ className, ...props }, ref) => (
82
+ <th
83
+ ref={ref}
84
+ data-slot='table-head'
85
+ className={cn(
86
+ 'text-muted-foreground h-10 px-2 text-left align-middle font-medium [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
87
+ className
88
+ )}
89
+ {...props}
90
+ />
91
+ ));
92
+ TableHead.displayName = 'TableHead';
93
+
94
+ const TableCell = React.forwardRef<
95
+ HTMLTableCellElement,
96
+ React.TdHTMLAttributes<HTMLTableCellElement>
97
+ >(({ className, ...props }, ref) => (
98
+ <td
99
+ ref={ref}
100
+ data-slot='table-cell'
101
+ className={cn(
102
+ 'p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
103
+ className
104
+ )}
105
+ {...props}
106
+ />
107
+ ));
108
+ TableCell.displayName = 'TableCell';
109
+
110
+ const TableCaption = React.forwardRef<
111
+ HTMLTableCaptionElement,
112
+ React.HTMLAttributes<HTMLTableCaptionElement>
113
+ >(({ className, ...props }, ref) => (
114
+ <caption
115
+ ref={ref}
116
+ data-slot='table-caption'
117
+ className={cn('text-muted-foreground mt-4 text-sm', className)}
118
+ {...props}
119
+ />
120
+ ));
121
+ TableCaption.displayName = 'TableCaption';
122
+
123
+ export {
124
+ Table,
125
+ TableHeader,
126
+ TableBody,
127
+ TableFooter,
128
+ TableRow,
129
+ TableHead,
130
+ TableCell,
131
+ TableCaption,
132
+ };
@@ -0,0 +1,63 @@
1
+ import type { Meta, StoryObj } from '../story-types';
2
+
3
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from './tabs';
4
+
5
+ const meta = {
6
+ title: 'Components/Tabs',
7
+ component: Tabs,
8
+ tags: ['autodocs'],
9
+ // Radix Tabs.Root scalars (`value` / `onValueChange` are controlled-mode
10
+ // only). `children` (TabsList + TabsContent) is composed content → a slot.
11
+ argTypes: {
12
+ defaultValue: {
13
+ control: 'text',
14
+ description: 'Value of the tab selected on first render',
15
+ },
16
+ orientation: {
17
+ control: 'inline-radio',
18
+ options: ['horizontal', 'vertical'],
19
+ },
20
+ dir: { control: 'inline-radio', options: ['ltr', 'rtl'] },
21
+ activationMode: {
22
+ control: 'inline-radio',
23
+ options: ['automatic', 'manual'],
24
+ description: 'Select a tab on focus, or only on click/Enter',
25
+ },
26
+ },
27
+ args: {
28
+ defaultValue: 'upcoming',
29
+ orientation: 'horizontal',
30
+ activationMode: 'automatic',
31
+ },
32
+ parameters: { puck: { slots: ['children'] } },
33
+ } satisfies Meta<typeof Tabs>;
34
+
35
+ export default meta;
36
+ type Story = StoryObj<typeof meta>;
37
+
38
+ export const Default: Story = {
39
+ render: (args) => (
40
+ <Tabs {...args} className='w-96'>
41
+ <TabsList className='grid w-full grid-cols-3'>
42
+ <TabsTrigger value='upcoming'>Upcoming</TabsTrigger>
43
+ <TabsTrigger value='past'>Past</TabsTrigger>
44
+ <TabsTrigger value='canceled'>Canceled</TabsTrigger>
45
+ </TabsList>
46
+ <TabsContent
47
+ value='upcoming'
48
+ className='text-muted-foreground pt-3 text-sm'
49
+ >
50
+ 3 upcoming appointments this week.
51
+ </TabsContent>
52
+ <TabsContent value='past' className='text-muted-foreground pt-3 text-sm'>
53
+ 12 appointments in the last 30 days.
54
+ </TabsContent>
55
+ <TabsContent
56
+ value='canceled'
57
+ className='text-muted-foreground pt-3 text-sm'
58
+ >
59
+ No cancellations. Nice.
60
+ </TabsContent>
61
+ </Tabs>
62
+ ),
63
+ };