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.
- package/components.json +21 -0
- package/dist/styles.css +3452 -0
- package/package.json +103 -0
- package/src/layout/app-shell.tsx +115 -0
- package/src/layout/app-topbar.stories.tsx +74 -0
- package/src/layout/app-topbar.tsx +145 -0
- package/src/layout/auth-layout.tsx +45 -0
- package/src/layout/box.stories.tsx +86 -0
- package/src/layout/box.tsx +34 -0
- package/src/layout/center.tsx +24 -0
- package/src/layout/container.stories.tsx +44 -0
- package/src/layout/container.tsx +39 -0
- package/src/layout/divider.tsx +64 -0
- package/src/layout/flex.stories.tsx +109 -0
- package/src/layout/flex.test.tsx +71 -0
- package/src/layout/flex.tsx +112 -0
- package/src/layout/grid.stories.tsx +91 -0
- package/src/layout/grid.tsx +132 -0
- package/src/layout/heading.tsx +53 -0
- package/src/layout/index.ts +47 -0
- package/src/layout/layout-style.ts +291 -0
- package/src/layout/link.stories.tsx +54 -0
- package/src/layout/link.test.tsx +75 -0
- package/src/layout/link.tsx +100 -0
- package/src/layout/navbar.stories.tsx +70 -0
- package/src/layout/navbar.tsx +128 -0
- package/src/layout/page-header.stories.tsx +80 -0
- package/src/layout/page-header.tsx +59 -0
- package/src/layout/section.tsx +22 -0
- package/src/layout/spacer.tsx +21 -0
- package/src/layout/stack.stories.tsx +58 -0
- package/src/layout/stack.tsx +19 -0
- package/src/layout/text.tsx +139 -0
- package/src/layout/typography.stories.tsx +112 -0
- package/src/lib/utils.ts +6 -0
- package/src/story-types.ts +80 -0
- package/src/styles/build.css +33 -0
- package/src/styles/globals.css +9 -0
- package/src/styles/tokens.css +281 -0
- package/src/ui/accordion.stories.tsx +84 -0
- package/src/ui/accordion.tsx +57 -0
- package/src/ui/alert-dialog.stories.tsx +57 -0
- package/src/ui/alert-dialog.tsx +141 -0
- package/src/ui/alert.stories.tsx +46 -0
- package/src/ui/alert.tsx +59 -0
- package/src/ui/avatar.stories.tsx +56 -0
- package/src/ui/avatar.tsx +50 -0
- package/src/ui/badge.stories.tsx +105 -0
- package/src/ui/badge.tsx +57 -0
- package/src/ui/breadcrumb.stories.tsx +69 -0
- package/src/ui/breadcrumb.tsx +140 -0
- package/src/ui/button.stories.tsx +85 -0
- package/src/ui/button.tsx +60 -0
- package/src/ui/card.stories.tsx +62 -0
- package/src/ui/card.tsx +96 -0
- package/src/ui/carousel.stories.tsx +55 -0
- package/src/ui/carousel.tsx +262 -0
- package/src/ui/checkbox.stories.tsx +52 -0
- package/src/ui/checkbox.tsx +30 -0
- package/src/ui/combobox.stories.tsx +138 -0
- package/src/ui/combobox.tsx +217 -0
- package/src/ui/command.tsx +184 -0
- package/src/ui/dialog.stories.tsx +66 -0
- package/src/ui/dialog.tsx +163 -0
- package/src/ui/drawer.stories.tsx +80 -0
- package/src/ui/drawer.tsx +118 -0
- package/src/ui/dropdown-menu.stories.tsx +57 -0
- package/src/ui/dropdown-menu.tsx +201 -0
- package/src/ui/form.stories.tsx +70 -0
- package/src/ui/form.tsx +181 -0
- package/src/ui/input-otp.stories.tsx +52 -0
- package/src/ui/input-otp.tsx +71 -0
- package/src/ui/input.stories.tsx +68 -0
- package/src/ui/input.tsx +22 -0
- package/src/ui/label.stories.tsx +35 -0
- package/src/ui/label.tsx +26 -0
- package/src/ui/navigation-menu.stories.tsx +79 -0
- package/src/ui/navigation-menu.tsx +128 -0
- package/src/ui/pagination.stories.tsx +62 -0
- package/src/ui/pagination.tsx +137 -0
- package/src/ui/popover.tsx +89 -0
- package/src/ui/progress.stories.tsx +33 -0
- package/src/ui/progress.tsx +28 -0
- package/src/ui/radio-group.stories.tsx +52 -0
- package/src/ui/radio-group.tsx +44 -0
- package/src/ui/scheduling-chip.stories.tsx +117 -0
- package/src/ui/scheduling-chip.tsx +62 -0
- package/src/ui/scroll-area.stories.tsx +52 -0
- package/src/ui/scroll-area.tsx +48 -0
- package/src/ui/select.stories.tsx +56 -0
- package/src/ui/select.tsx +159 -0
- package/src/ui/sheet.stories.tsx +63 -0
- package/src/ui/sheet.tsx +141 -0
- package/src/ui/skeleton.stories.tsx +40 -0
- package/src/ui/skeleton.tsx +15 -0
- package/src/ui/sonner.stories.tsx +62 -0
- package/src/ui/sonner.tsx +34 -0
- package/src/ui/switch.stories.tsx +41 -0
- package/src/ui/switch.tsx +29 -0
- package/src/ui/table.stories.tsx +88 -0
- package/src/ui/table.tsx +132 -0
- package/src/ui/tabs.stories.tsx +63 -0
- package/src/ui/tabs.tsx +55 -0
- package/src/ui/textarea.stories.tsx +45 -0
- package/src/ui/textarea.tsx +22 -0
- package/src/ui/tooltip.stories.tsx +48 -0
- package/src/ui/tooltip.tsx +32 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Slot } from '@radix-ui/react-slot';
|
|
3
|
+
import { ChevronRight, MoreHorizontal } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
import { cn } from '../lib/utils';
|
|
6
|
+
|
|
7
|
+
/** Glyph rendered between crumbs by a childless `BreadcrumbSeparator`. */
|
|
8
|
+
export type BreadcrumbSeparatorVariant = 'chevron' | 'slash' | 'dot';
|
|
9
|
+
|
|
10
|
+
const SeparatorContext =
|
|
11
|
+
React.createContext<BreadcrumbSeparatorVariant>('chevron');
|
|
12
|
+
|
|
13
|
+
const SEPARATOR_GLYPH: Record<BreadcrumbSeparatorVariant, React.ReactNode> = {
|
|
14
|
+
chevron: <ChevronRight />,
|
|
15
|
+
slash: <span>/</span>,
|
|
16
|
+
dot: <span>·</span>,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export interface BreadcrumbProps extends React.ComponentPropsWithoutRef<'nav'> {
|
|
20
|
+
/**
|
|
21
|
+
* Separator glyph used by every `BreadcrumbSeparator` in the trail that does
|
|
22
|
+
* not pass its own children. Defaults to a chevron.
|
|
23
|
+
*/
|
|
24
|
+
separator?: BreadcrumbSeparatorVariant;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const Breadcrumb = React.forwardRef<HTMLElement, BreadcrumbProps>(
|
|
28
|
+
({ separator = 'chevron', ...props }, ref) => (
|
|
29
|
+
<SeparatorContext.Provider value={separator}>
|
|
30
|
+
<nav ref={ref} aria-label='breadcrumb' {...props} />
|
|
31
|
+
</SeparatorContext.Provider>
|
|
32
|
+
)
|
|
33
|
+
);
|
|
34
|
+
Breadcrumb.displayName = 'Breadcrumb';
|
|
35
|
+
|
|
36
|
+
const BreadcrumbList = React.forwardRef<
|
|
37
|
+
HTMLOListElement,
|
|
38
|
+
React.ComponentPropsWithoutRef<'ol'>
|
|
39
|
+
>(({ className, ...props }, ref) => (
|
|
40
|
+
<ol
|
|
41
|
+
ref={ref}
|
|
42
|
+
className={cn(
|
|
43
|
+
'text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5',
|
|
44
|
+
className
|
|
45
|
+
)}
|
|
46
|
+
{...props}
|
|
47
|
+
/>
|
|
48
|
+
));
|
|
49
|
+
BreadcrumbList.displayName = 'BreadcrumbList';
|
|
50
|
+
|
|
51
|
+
const BreadcrumbItem = React.forwardRef<
|
|
52
|
+
HTMLLIElement,
|
|
53
|
+
React.ComponentPropsWithoutRef<'li'>
|
|
54
|
+
>(({ className, ...props }, ref) => (
|
|
55
|
+
<li
|
|
56
|
+
ref={ref}
|
|
57
|
+
className={cn('inline-flex items-center gap-1.5', className)}
|
|
58
|
+
{...props}
|
|
59
|
+
/>
|
|
60
|
+
));
|
|
61
|
+
BreadcrumbItem.displayName = 'BreadcrumbItem';
|
|
62
|
+
|
|
63
|
+
const BreadcrumbLink = React.forwardRef<
|
|
64
|
+
HTMLAnchorElement,
|
|
65
|
+
React.ComponentPropsWithoutRef<'a'> & {
|
|
66
|
+
asChild?: boolean;
|
|
67
|
+
}
|
|
68
|
+
>(({ asChild, className, ...props }, ref) => {
|
|
69
|
+
const Comp = asChild ? Slot : 'a';
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<Comp
|
|
73
|
+
ref={ref}
|
|
74
|
+
className={cn('hover:text-foreground transition-colors', className)}
|
|
75
|
+
{...props}
|
|
76
|
+
/>
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
BreadcrumbLink.displayName = 'BreadcrumbLink';
|
|
80
|
+
|
|
81
|
+
const BreadcrumbPage = React.forwardRef<
|
|
82
|
+
HTMLSpanElement,
|
|
83
|
+
React.ComponentPropsWithoutRef<'span'>
|
|
84
|
+
>(({ className, ...props }, ref) => (
|
|
85
|
+
<span
|
|
86
|
+
ref={ref}
|
|
87
|
+
role='link'
|
|
88
|
+
aria-disabled='true'
|
|
89
|
+
aria-current='page'
|
|
90
|
+
className={cn('text-foreground font-normal', className)}
|
|
91
|
+
{...props}
|
|
92
|
+
/>
|
|
93
|
+
));
|
|
94
|
+
BreadcrumbPage.displayName = 'BreadcrumbPage';
|
|
95
|
+
|
|
96
|
+
const BreadcrumbSeparator = ({
|
|
97
|
+
children,
|
|
98
|
+
className,
|
|
99
|
+
...props
|
|
100
|
+
}: React.ComponentProps<'li'>) => {
|
|
101
|
+
const variant = React.useContext(SeparatorContext);
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<li
|
|
105
|
+
role='presentation'
|
|
106
|
+
aria-hidden='true'
|
|
107
|
+
className={cn('[&>svg]:h-3.5 [&>svg]:w-3.5', className)}
|
|
108
|
+
{...props}
|
|
109
|
+
>
|
|
110
|
+
{children ?? SEPARATOR_GLYPH[variant]}
|
|
111
|
+
</li>
|
|
112
|
+
);
|
|
113
|
+
};
|
|
114
|
+
BreadcrumbSeparator.displayName = 'BreadcrumbSeparator';
|
|
115
|
+
|
|
116
|
+
const BreadcrumbEllipsis = ({
|
|
117
|
+
className,
|
|
118
|
+
...props
|
|
119
|
+
}: React.ComponentProps<'span'>) => (
|
|
120
|
+
<span
|
|
121
|
+
role='presentation'
|
|
122
|
+
aria-hidden='true'
|
|
123
|
+
className={cn('flex h-9 w-9 items-center justify-center', className)}
|
|
124
|
+
{...props}
|
|
125
|
+
>
|
|
126
|
+
<MoreHorizontal className='h-4 w-4' />
|
|
127
|
+
<span className='sr-only'>More</span>
|
|
128
|
+
</span>
|
|
129
|
+
);
|
|
130
|
+
BreadcrumbEllipsis.displayName = 'BreadcrumbElipssis';
|
|
131
|
+
|
|
132
|
+
export {
|
|
133
|
+
Breadcrumb,
|
|
134
|
+
BreadcrumbList,
|
|
135
|
+
BreadcrumbItem,
|
|
136
|
+
BreadcrumbLink,
|
|
137
|
+
BreadcrumbPage,
|
|
138
|
+
BreadcrumbSeparator,
|
|
139
|
+
BreadcrumbEllipsis,
|
|
140
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '../story-types';
|
|
2
|
+
import { CalendarPlus, Loader2 } from 'lucide-react';
|
|
3
|
+
|
|
4
|
+
import { Button } from './button';
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: 'Components/Button',
|
|
8
|
+
component: Button,
|
|
9
|
+
tags: ['autodocs'],
|
|
10
|
+
argTypes: {
|
|
11
|
+
// `children` is the button label — a plain editable string here (a leaf,
|
|
12
|
+
// not a slot). `className`/`style` are deliberately NOT exposed (§6).
|
|
13
|
+
children: { control: 'text', description: 'Button label' },
|
|
14
|
+
variant: {
|
|
15
|
+
control: 'select',
|
|
16
|
+
options: [
|
|
17
|
+
'default',
|
|
18
|
+
'secondary',
|
|
19
|
+
'destructive',
|
|
20
|
+
'outline',
|
|
21
|
+
'ghost',
|
|
22
|
+
'link',
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
size: {
|
|
26
|
+
control: 'select',
|
|
27
|
+
options: ['xs', 'sm', 'default', 'lg', 'xl', 'icon'],
|
|
28
|
+
},
|
|
29
|
+
disabled: { control: 'boolean' },
|
|
30
|
+
},
|
|
31
|
+
args: { children: 'Book appointment', variant: 'default', size: 'default' },
|
|
32
|
+
} satisfies Meta<typeof Button>;
|
|
33
|
+
|
|
34
|
+
export default meta;
|
|
35
|
+
type Story = StoryObj<typeof meta>;
|
|
36
|
+
|
|
37
|
+
export const Default: Story = {};
|
|
38
|
+
|
|
39
|
+
export const Variants: Story = {
|
|
40
|
+
render: () => (
|
|
41
|
+
<div className='flex flex-wrap items-center gap-3'>
|
|
42
|
+
<Button>Default</Button>
|
|
43
|
+
<Button variant='secondary'>Secondary</Button>
|
|
44
|
+
<Button variant='destructive'>Destructive</Button>
|
|
45
|
+
<Button variant='outline'>Outline</Button>
|
|
46
|
+
<Button variant='ghost'>Ghost</Button>
|
|
47
|
+
<Button variant='link'>Link</Button>
|
|
48
|
+
</div>
|
|
49
|
+
),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const Sizes: Story = {
|
|
53
|
+
render: () => (
|
|
54
|
+
<div className='flex flex-wrap items-center gap-3'>
|
|
55
|
+
<Button size='xs'>Extra small</Button>
|
|
56
|
+
<Button size='sm'>Small</Button>
|
|
57
|
+
<Button size='default'>Default</Button>
|
|
58
|
+
<Button size='lg'>Large</Button>
|
|
59
|
+
<Button size='xl'>Extra large</Button>
|
|
60
|
+
<Button size='icon' aria-label='Add'>
|
|
61
|
+
<CalendarPlus />
|
|
62
|
+
</Button>
|
|
63
|
+
</div>
|
|
64
|
+
),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const WithIcon: Story = {
|
|
68
|
+
render: () => (
|
|
69
|
+
<Button>
|
|
70
|
+
<CalendarPlus />
|
|
71
|
+
New booking
|
|
72
|
+
</Button>
|
|
73
|
+
),
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const Loading: Story = {
|
|
77
|
+
render: () => (
|
|
78
|
+
<Button disabled>
|
|
79
|
+
<Loader2 className='animate-spin' />
|
|
80
|
+
Syncing…
|
|
81
|
+
</Button>
|
|
82
|
+
),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const Disabled: Story = { args: { disabled: true } };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Slot } from '@radix-ui/react-slot';
|
|
3
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
4
|
+
|
|
5
|
+
import { cn } from '../lib/utils';
|
|
6
|
+
|
|
7
|
+
const buttonVariants = cva(
|
|
8
|
+
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default:
|
|
13
|
+
'bg-primary text-primary-foreground shadow hover:bg-primary/90',
|
|
14
|
+
destructive:
|
|
15
|
+
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
|
|
16
|
+
outline:
|
|
17
|
+
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
|
|
18
|
+
secondary:
|
|
19
|
+
'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
|
|
20
|
+
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
|
21
|
+
link: 'text-primary underline-offset-4 hover:underline',
|
|
22
|
+
},
|
|
23
|
+
size: {
|
|
24
|
+
xs: 'h-7 gap-1.5 rounded-sm px-2.5 text-xs [&_svg]:size-3.5',
|
|
25
|
+
sm: 'h-8 rounded-md px-3 text-xs',
|
|
26
|
+
default: 'h-9 px-4 py-2',
|
|
27
|
+
lg: 'h-10 rounded-md px-8',
|
|
28
|
+
xl: 'h-13 gap-2.5 rounded-lg px-6 text-base [&_svg]:size-5',
|
|
29
|
+
icon: 'h-9 w-9',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
defaultVariants: {
|
|
33
|
+
variant: 'default',
|
|
34
|
+
size: 'default',
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
export interface ButtonProps
|
|
40
|
+
extends
|
|
41
|
+
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
42
|
+
VariantProps<typeof buttonVariants> {
|
|
43
|
+
asChild?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
47
|
+
({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
48
|
+
const Comp = asChild ? Slot : 'button';
|
|
49
|
+
return (
|
|
50
|
+
<Comp
|
|
51
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
52
|
+
ref={ref}
|
|
53
|
+
{...props}
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
Button.displayName = 'Button';
|
|
59
|
+
|
|
60
|
+
export { Button, buttonVariants };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '../story-types';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Card,
|
|
5
|
+
CardContent,
|
|
6
|
+
CardDescription,
|
|
7
|
+
CardFooter,
|
|
8
|
+
CardHeader,
|
|
9
|
+
CardTitle,
|
|
10
|
+
} from './card';
|
|
11
|
+
import { Button } from './button';
|
|
12
|
+
import { Badge } from './badge';
|
|
13
|
+
|
|
14
|
+
const meta = {
|
|
15
|
+
title: 'Components/Card',
|
|
16
|
+
component: Card,
|
|
17
|
+
tags: ['autodocs'],
|
|
18
|
+
// A container still curates scalars: the emptiness check counts argTypes keys
|
|
19
|
+
// and runs BEFORE slots are read, so slots alone would throw
|
|
20
|
+
// AUTO_INFERRED_ARGTYPES_ONLY. `padding` is a real prop (not a disabled-key
|
|
21
|
+
// placeholder) and must not collide with a slot name (SLOT_ARGTYPE_COLLISION).
|
|
22
|
+
argTypes: {
|
|
23
|
+
padding: {
|
|
24
|
+
control: 'select',
|
|
25
|
+
options: [0, 2, 4, 6, 8, 10, 12],
|
|
26
|
+
description: 'Padding on the card surface (4px token scale)',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
parameters: { puck: { slots: ['children'] } },
|
|
30
|
+
} satisfies Meta<typeof Card>;
|
|
31
|
+
|
|
32
|
+
export default meta;
|
|
33
|
+
type Story = StoryObj<typeof meta>;
|
|
34
|
+
|
|
35
|
+
export const Default: Story = {
|
|
36
|
+
render: () => (
|
|
37
|
+
<Card className='w-80'>
|
|
38
|
+
<CardHeader>
|
|
39
|
+
<CardTitle>Annual checkup</CardTitle>
|
|
40
|
+
<CardDescription>Dr. Lopez · Cardiology</CardDescription>
|
|
41
|
+
</CardHeader>
|
|
42
|
+
<CardContent className='space-y-2 text-sm'>
|
|
43
|
+
<div className='flex justify-between'>
|
|
44
|
+
<span className='text-muted-foreground'>When</span>
|
|
45
|
+
<span className='font-mono'>Mon, 9:00–9:30 AM</span>
|
|
46
|
+
</div>
|
|
47
|
+
<div className='flex justify-between'>
|
|
48
|
+
<span className='text-muted-foreground'>Status</span>
|
|
49
|
+
<Badge className='bg-success border-transparent text-white'>
|
|
50
|
+
Confirmed
|
|
51
|
+
</Badge>
|
|
52
|
+
</div>
|
|
53
|
+
</CardContent>
|
|
54
|
+
<CardFooter className='gap-2'>
|
|
55
|
+
<Button className='flex-1'>Reschedule</Button>
|
|
56
|
+
<Button variant='outline' className='flex-1'>
|
|
57
|
+
Cancel
|
|
58
|
+
</Button>
|
|
59
|
+
</CardFooter>
|
|
60
|
+
</Card>
|
|
61
|
+
),
|
|
62
|
+
};
|
package/src/ui/card.tsx
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
import { cn } from '../lib/utils';
|
|
4
|
+
import { resolveSpace, type Space } from '../layout/layout-style';
|
|
5
|
+
|
|
6
|
+
export interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
7
|
+
/**
|
|
8
|
+
* Padding on the card surface, on the 4px token scale. Omit when composing
|
|
9
|
+
* CardHeader/CardContent/CardFooter — those bring their own p-6. Useful when
|
|
10
|
+
* arbitrary content is placed directly in the card (e.g. the composer drops
|
|
11
|
+
* components into the `children` slot).
|
|
12
|
+
*/
|
|
13
|
+
padding?: Space;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const Card = React.forwardRef<HTMLDivElement, CardProps>(
|
|
17
|
+
({ className, padding, style, ...props }, ref) => (
|
|
18
|
+
<div
|
|
19
|
+
ref={ref}
|
|
20
|
+
className={cn(
|
|
21
|
+
'bg-card text-card-foreground rounded-xl border shadow',
|
|
22
|
+
className
|
|
23
|
+
)}
|
|
24
|
+
style={
|
|
25
|
+
padding != null ? { padding: resolveSpace(padding), ...style } : style
|
|
26
|
+
}
|
|
27
|
+
{...props}
|
|
28
|
+
/>
|
|
29
|
+
)
|
|
30
|
+
);
|
|
31
|
+
Card.displayName = 'Card';
|
|
32
|
+
|
|
33
|
+
const CardHeader = React.forwardRef<
|
|
34
|
+
HTMLDivElement,
|
|
35
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
36
|
+
>(({ className, ...props }, ref) => (
|
|
37
|
+
<div
|
|
38
|
+
ref={ref}
|
|
39
|
+
className={cn('flex flex-col space-y-1.5 p-6', className)}
|
|
40
|
+
{...props}
|
|
41
|
+
/>
|
|
42
|
+
));
|
|
43
|
+
CardHeader.displayName = 'CardHeader';
|
|
44
|
+
|
|
45
|
+
const CardTitle = React.forwardRef<
|
|
46
|
+
HTMLDivElement,
|
|
47
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
48
|
+
>(({ className, ...props }, ref) => (
|
|
49
|
+
<div
|
|
50
|
+
ref={ref}
|
|
51
|
+
className={cn('leading-none font-semibold tracking-tight', className)}
|
|
52
|
+
{...props}
|
|
53
|
+
/>
|
|
54
|
+
));
|
|
55
|
+
CardTitle.displayName = 'CardTitle';
|
|
56
|
+
|
|
57
|
+
const CardDescription = React.forwardRef<
|
|
58
|
+
HTMLDivElement,
|
|
59
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
60
|
+
>(({ className, ...props }, ref) => (
|
|
61
|
+
<div
|
|
62
|
+
ref={ref}
|
|
63
|
+
className={cn('text-muted-foreground text-sm', className)}
|
|
64
|
+
{...props}
|
|
65
|
+
/>
|
|
66
|
+
));
|
|
67
|
+
CardDescription.displayName = 'CardDescription';
|
|
68
|
+
|
|
69
|
+
const CardContent = React.forwardRef<
|
|
70
|
+
HTMLDivElement,
|
|
71
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
72
|
+
>(({ className, ...props }, ref) => (
|
|
73
|
+
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
|
|
74
|
+
));
|
|
75
|
+
CardContent.displayName = 'CardContent';
|
|
76
|
+
|
|
77
|
+
const CardFooter = React.forwardRef<
|
|
78
|
+
HTMLDivElement,
|
|
79
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
80
|
+
>(({ className, ...props }, ref) => (
|
|
81
|
+
<div
|
|
82
|
+
ref={ref}
|
|
83
|
+
className={cn('flex items-center p-6 pt-0', className)}
|
|
84
|
+
{...props}
|
|
85
|
+
/>
|
|
86
|
+
));
|
|
87
|
+
CardFooter.displayName = 'CardFooter';
|
|
88
|
+
|
|
89
|
+
export {
|
|
90
|
+
Card,
|
|
91
|
+
CardHeader,
|
|
92
|
+
CardFooter,
|
|
93
|
+
CardTitle,
|
|
94
|
+
CardDescription,
|
|
95
|
+
CardContent,
|
|
96
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '../story-types';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Carousel,
|
|
5
|
+
CarouselContent,
|
|
6
|
+
CarouselItem,
|
|
7
|
+
CarouselNext,
|
|
8
|
+
CarouselPrevious,
|
|
9
|
+
} from './carousel';
|
|
10
|
+
import { Card, CardContent } from './card';
|
|
11
|
+
|
|
12
|
+
const meta = {
|
|
13
|
+
title: 'Components/Carousel',
|
|
14
|
+
component: Carousel,
|
|
15
|
+
tags: ['autodocs'],
|
|
16
|
+
// `orientation` and `opts` are the real editable props on CarouselProps
|
|
17
|
+
// (`plugins`/`setApi` are functions/instances — not curatable). `children` is
|
|
18
|
+
// the composed slide content → slot (§4), never also an argType (§5).
|
|
19
|
+
argTypes: {
|
|
20
|
+
orientation: {
|
|
21
|
+
control: 'inline-radio',
|
|
22
|
+
options: ['horizontal', 'vertical'],
|
|
23
|
+
},
|
|
24
|
+
opts: {
|
|
25
|
+
control: 'object',
|
|
26
|
+
description:
|
|
27
|
+
'Embla carousel options (e.g. { loop: true, align: "start" })',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
args: { orientation: 'horizontal' },
|
|
31
|
+
parameters: { puck: { slots: ['children'] } },
|
|
32
|
+
} satisfies Meta<typeof Carousel>;
|
|
33
|
+
|
|
34
|
+
export default meta;
|
|
35
|
+
type Story = StoryObj<typeof meta>;
|
|
36
|
+
|
|
37
|
+
export const Default: Story = {
|
|
38
|
+
render: (args) => (
|
|
39
|
+
<Carousel className='w-64' {...args}>
|
|
40
|
+
<CarouselContent>
|
|
41
|
+
{Array.from({ length: 5 }).map((_, i) => (
|
|
42
|
+
<CarouselItem key={i}>
|
|
43
|
+
<Card>
|
|
44
|
+
<CardContent className='flex h-32 items-center justify-center p-6 text-3xl font-semibold'>
|
|
45
|
+
{i + 1}
|
|
46
|
+
</CardContent>
|
|
47
|
+
</Card>
|
|
48
|
+
</CarouselItem>
|
|
49
|
+
))}
|
|
50
|
+
</CarouselContent>
|
|
51
|
+
<CarouselPrevious />
|
|
52
|
+
<CarouselNext />
|
|
53
|
+
</Carousel>
|
|
54
|
+
),
|
|
55
|
+
};
|