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,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
+ };
@@ -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
+ };