@trycompai/design-system 1.0.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/README.md +110 -0
- package/components.json +21 -0
- package/hooks/use-mobile.tsx +19 -0
- package/lib/utils.ts +6 -0
- package/package.json +103 -0
- package/postcss.config.mjs +8 -0
- package/src/components/ui/accordion.tsx +60 -0
- package/src/components/ui/alert-dialog.tsx +161 -0
- package/src/components/ui/alert.tsx +109 -0
- package/src/components/ui/aspect-ratio.tsx +21 -0
- package/src/components/ui/avatar.tsx +74 -0
- package/src/components/ui/badge.tsx +48 -0
- package/src/components/ui/breadcrumb.tsx +254 -0
- package/src/components/ui/button-group.tsx +89 -0
- package/src/components/ui/button.tsx +122 -0
- package/src/components/ui/calendar.tsx +190 -0
- package/src/components/ui/card.tsx +155 -0
- package/src/components/ui/carousel.tsx +216 -0
- package/src/components/ui/chart.tsx +325 -0
- package/src/components/ui/checkbox.tsx +22 -0
- package/src/components/ui/collapsible.tsx +17 -0
- package/src/components/ui/combobox.tsx +248 -0
- package/src/components/ui/command.tsx +189 -0
- package/src/components/ui/container.tsx +34 -0
- package/src/components/ui/context-menu.tsx +235 -0
- package/src/components/ui/dialog.tsx +122 -0
- package/src/components/ui/drawer.tsx +102 -0
- package/src/components/ui/dropdown-menu.tsx +242 -0
- package/src/components/ui/empty.tsx +94 -0
- package/src/components/ui/field.tsx +215 -0
- package/src/components/ui/grid.tsx +135 -0
- package/src/components/ui/heading.tsx +56 -0
- package/src/components/ui/hover-card.tsx +46 -0
- package/src/components/ui/index.ts +61 -0
- package/src/components/ui/input-group.tsx +128 -0
- package/src/components/ui/input-otp.tsx +84 -0
- package/src/components/ui/input.tsx +15 -0
- package/src/components/ui/item.tsx +188 -0
- package/src/components/ui/kbd.tsx +26 -0
- package/src/components/ui/label.tsx +15 -0
- package/src/components/ui/menubar.tsx +163 -0
- package/src/components/ui/navigation-menu.tsx +147 -0
- package/src/components/ui/page-header.tsx +51 -0
- package/src/components/ui/page-layout.tsx +65 -0
- package/src/components/ui/pagination.tsx +104 -0
- package/src/components/ui/popover.tsx +57 -0
- package/src/components/ui/progress.tsx +61 -0
- package/src/components/ui/radio-group.tsx +37 -0
- package/src/components/ui/resizable.tsx +41 -0
- package/src/components/ui/scroll-area.tsx +48 -0
- package/src/components/ui/section.tsx +64 -0
- package/src/components/ui/select.tsx +166 -0
- package/src/components/ui/separator.tsx +17 -0
- package/src/components/ui/sheet.tsx +104 -0
- package/src/components/ui/sidebar.tsx +707 -0
- package/src/components/ui/skeleton.tsx +5 -0
- package/src/components/ui/slider.tsx +51 -0
- package/src/components/ui/sonner.tsx +43 -0
- package/src/components/ui/spinner.tsx +14 -0
- package/src/components/ui/stack.tsx +72 -0
- package/src/components/ui/switch.tsx +26 -0
- package/src/components/ui/table.tsx +65 -0
- package/src/components/ui/tabs.tsx +69 -0
- package/src/components/ui/text.tsx +59 -0
- package/src/components/ui/textarea.tsx +13 -0
- package/src/components/ui/toggle-group.tsx +87 -0
- package/src/components/ui/toggle.tsx +42 -0
- package/src/components/ui/tooltip.tsx +52 -0
- package/src/index.ts +3 -0
- package/src/styles/globals.css +122 -0
- package/tailwind.config.ts +59 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import {
|
|
5
|
+
DayPicker,
|
|
6
|
+
getDefaultClassNames,
|
|
7
|
+
type DayButton,
|
|
8
|
+
type DayPickerProps,
|
|
9
|
+
} from 'react-day-picker';
|
|
10
|
+
|
|
11
|
+
import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';
|
|
12
|
+
import { cn } from '../../../lib/utils';
|
|
13
|
+
import { Button, buttonVariants } from './button';
|
|
14
|
+
|
|
15
|
+
type CalendarButtonVariant = 'default' | 'outline' | 'secondary' | 'ghost' | 'destructive' | 'link';
|
|
16
|
+
|
|
17
|
+
interface CalendarBaseProps {
|
|
18
|
+
buttonVariant?: CalendarButtonVariant;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Calendar accepts all DayPickerProps except className/classNames (we control those internally)
|
|
22
|
+
type CalendarProps = CalendarBaseProps & DayPickerProps;
|
|
23
|
+
|
|
24
|
+
function Calendar({
|
|
25
|
+
showOutsideDays = true,
|
|
26
|
+
captionLayout = 'label',
|
|
27
|
+
buttonVariant = 'ghost',
|
|
28
|
+
formatters,
|
|
29
|
+
components,
|
|
30
|
+
className: _className,
|
|
31
|
+
classNames: _classNames,
|
|
32
|
+
...props
|
|
33
|
+
}: CalendarProps) {
|
|
34
|
+
const defaultClassNames = getDefaultClassNames();
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<DayPicker
|
|
38
|
+
showOutsideDays={showOutsideDays}
|
|
39
|
+
className={cn(
|
|
40
|
+
'p-3 [--cell-radius:var(--radius-md)] [--cell-size:--spacing(8)] bg-background group/calendar [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent',
|
|
41
|
+
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
|
|
42
|
+
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
|
|
43
|
+
)}
|
|
44
|
+
captionLayout={captionLayout}
|
|
45
|
+
formatters={{
|
|
46
|
+
formatMonthDropdown: (date) => date.toLocaleString('default', { month: 'short' }),
|
|
47
|
+
...formatters,
|
|
48
|
+
}}
|
|
49
|
+
classNames={{
|
|
50
|
+
root: cn('w-fit', defaultClassNames.root),
|
|
51
|
+
months: cn('flex gap-4 flex-col md:flex-row relative', defaultClassNames.months),
|
|
52
|
+
month: cn('flex flex-col w-full gap-4', defaultClassNames.month),
|
|
53
|
+
nav: cn(
|
|
54
|
+
'flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between',
|
|
55
|
+
defaultClassNames.nav,
|
|
56
|
+
),
|
|
57
|
+
button_previous: cn(
|
|
58
|
+
buttonVariants({ variant: buttonVariant }),
|
|
59
|
+
'size-(--cell-size) aria-disabled:opacity-50 p-0 select-none',
|
|
60
|
+
defaultClassNames.button_previous,
|
|
61
|
+
),
|
|
62
|
+
button_next: cn(
|
|
63
|
+
buttonVariants({ variant: buttonVariant }),
|
|
64
|
+
'size-(--cell-size) aria-disabled:opacity-50 p-0 select-none',
|
|
65
|
+
defaultClassNames.button_next,
|
|
66
|
+
),
|
|
67
|
+
month_caption: cn(
|
|
68
|
+
'flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)',
|
|
69
|
+
defaultClassNames.month_caption,
|
|
70
|
+
),
|
|
71
|
+
dropdowns: cn(
|
|
72
|
+
'w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5',
|
|
73
|
+
defaultClassNames.dropdowns,
|
|
74
|
+
),
|
|
75
|
+
dropdown_root: cn(
|
|
76
|
+
'relative cn-calendar-dropdown-root rounded-(--cell-radius)',
|
|
77
|
+
defaultClassNames.dropdown_root,
|
|
78
|
+
),
|
|
79
|
+
dropdown: cn('absolute bg-popover inset-0 opacity-0', defaultClassNames.dropdown),
|
|
80
|
+
caption_label: cn(
|
|
81
|
+
'select-none font-medium',
|
|
82
|
+
captionLayout === 'label'
|
|
83
|
+
? 'text-sm'
|
|
84
|
+
: 'cn-calendar-caption-label rounded-(--cell-radius) flex items-center gap-1 text-sm [&>svg]:text-muted-foreground [&>svg]:size-3.5',
|
|
85
|
+
defaultClassNames.caption_label,
|
|
86
|
+
),
|
|
87
|
+
table: 'w-full border-collapse',
|
|
88
|
+
weekdays: cn('flex', defaultClassNames.weekdays),
|
|
89
|
+
weekday: cn(
|
|
90
|
+
'text-muted-foreground rounded-(--cell-radius) flex-1 font-normal text-[0.8rem] select-none',
|
|
91
|
+
defaultClassNames.weekday,
|
|
92
|
+
),
|
|
93
|
+
week: cn('flex w-full mt-2', defaultClassNames.week),
|
|
94
|
+
week_number_header: cn('select-none w-(--cell-size)', defaultClassNames.week_number_header),
|
|
95
|
+
week_number: cn(
|
|
96
|
+
'text-[0.8rem] select-none text-muted-foreground',
|
|
97
|
+
defaultClassNames.week_number,
|
|
98
|
+
),
|
|
99
|
+
day: cn(
|
|
100
|
+
'relative w-full rounded-(--cell-radius) h-full p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-(--cell-radius) group/day aspect-square select-none',
|
|
101
|
+
props.showWeekNumber
|
|
102
|
+
? '[&:nth-child(2)[data-selected=true]_button]:rounded-l-(--cell-radius)'
|
|
103
|
+
: '[&:first-child[data-selected=true]_button]:rounded-l-(--cell-radius)',
|
|
104
|
+
defaultClassNames.day,
|
|
105
|
+
),
|
|
106
|
+
range_start: cn(
|
|
107
|
+
'rounded-l-(--cell-radius) bg-muted relative after:bg-muted after:absolute after:inset-y-0 after:w-4 after:right-0 -z-0 isolate',
|
|
108
|
+
defaultClassNames.range_start,
|
|
109
|
+
),
|
|
110
|
+
range_middle: cn('rounded-none', defaultClassNames.range_middle),
|
|
111
|
+
range_end: cn(
|
|
112
|
+
'rounded-r-(--cell-radius) bg-muted relative after:bg-muted after:absolute after:inset-y-0 after:w-4 after:left-0 -z-0 isolate',
|
|
113
|
+
defaultClassNames.range_end,
|
|
114
|
+
),
|
|
115
|
+
today: cn(
|
|
116
|
+
'bg-muted text-foreground rounded-(--cell-radius) data-[selected=true]:rounded-none',
|
|
117
|
+
defaultClassNames.today,
|
|
118
|
+
),
|
|
119
|
+
outside: cn(
|
|
120
|
+
'text-muted-foreground aria-selected:text-muted-foreground',
|
|
121
|
+
defaultClassNames.outside,
|
|
122
|
+
),
|
|
123
|
+
disabled: cn('text-muted-foreground opacity-50', defaultClassNames.disabled),
|
|
124
|
+
hidden: cn('invisible', defaultClassNames.hidden),
|
|
125
|
+
}}
|
|
126
|
+
components={{
|
|
127
|
+
Root: ({ rootRef, ...componentProps }) => {
|
|
128
|
+
return <div data-slot="calendar" ref={rootRef} {...componentProps} />;
|
|
129
|
+
},
|
|
130
|
+
Chevron: ({ orientation, ...componentProps }) => {
|
|
131
|
+
if (orientation === 'left') {
|
|
132
|
+
return <ChevronLeftIcon className="size-4" {...componentProps} />;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (orientation === 'right') {
|
|
136
|
+
return <ChevronRightIcon className="size-4" {...componentProps} />;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return <ChevronDownIcon className="size-4" {...componentProps} />;
|
|
140
|
+
},
|
|
141
|
+
DayButton: CalendarDayButton,
|
|
142
|
+
WeekNumber: ({ children, ...componentProps }) => {
|
|
143
|
+
return (
|
|
144
|
+
<td {...componentProps}>
|
|
145
|
+
<div className="flex size-(--cell-size) items-center justify-center text-center">
|
|
146
|
+
{children}
|
|
147
|
+
</div>
|
|
148
|
+
</td>
|
|
149
|
+
);
|
|
150
|
+
},
|
|
151
|
+
...components,
|
|
152
|
+
}}
|
|
153
|
+
{...props}
|
|
154
|
+
/>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function CalendarDayButton({
|
|
159
|
+
day,
|
|
160
|
+
modifiers,
|
|
161
|
+
className: _className,
|
|
162
|
+
...props
|
|
163
|
+
}: React.ComponentProps<typeof DayButton>) {
|
|
164
|
+
const ref = React.useRef<HTMLButtonElement>(null);
|
|
165
|
+
React.useEffect(() => {
|
|
166
|
+
if (modifiers.focused) ref.current?.focus();
|
|
167
|
+
}, [modifiers.focused]);
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<Button
|
|
171
|
+
ref={ref}
|
|
172
|
+
variant="ghost"
|
|
173
|
+
size="calendar-day"
|
|
174
|
+
data-day={day.date.toLocaleDateString()}
|
|
175
|
+
data-selected-single={
|
|
176
|
+
modifiers.selected &&
|
|
177
|
+
!modifiers.range_start &&
|
|
178
|
+
!modifiers.range_end &&
|
|
179
|
+
!modifiers.range_middle
|
|
180
|
+
}
|
|
181
|
+
data-range-start={modifiers.range_start}
|
|
182
|
+
data-range-end={modifiers.range_end}
|
|
183
|
+
data-range-middle={modifiers.range_middle}
|
|
184
|
+
{...props}
|
|
185
|
+
/>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export { Calendar, CalendarDayButton };
|
|
190
|
+
export type { CalendarProps };
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
|
|
4
|
+
const cardVariants = cva(
|
|
5
|
+
'ring-foreground/10 bg-card text-card-foreground gap-4 overflow-hidden rounded-xl py-4 text-sm ring-1 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl group/card flex flex-col',
|
|
6
|
+
{
|
|
7
|
+
variants: {
|
|
8
|
+
width: {
|
|
9
|
+
auto: '', // shrink to content (default behavior)
|
|
10
|
+
full: 'w-full', // fill parent container
|
|
11
|
+
sm: 'w-sm', // 24rem (384px)
|
|
12
|
+
md: 'w-md', // 28rem (448px)
|
|
13
|
+
lg: 'w-lg', // 32rem (512px)
|
|
14
|
+
xl: 'w-xl', // 36rem (576px)
|
|
15
|
+
'2xl': 'w-2xl', // 42rem (672px)
|
|
16
|
+
'3xl': 'w-3xl', // 48rem (768px)
|
|
17
|
+
},
|
|
18
|
+
maxWidth: {
|
|
19
|
+
sm: 'max-w-sm',
|
|
20
|
+
md: 'max-w-md',
|
|
21
|
+
lg: 'max-w-lg',
|
|
22
|
+
xl: 'max-w-xl',
|
|
23
|
+
'2xl': 'max-w-2xl',
|
|
24
|
+
'3xl': 'max-w-3xl',
|
|
25
|
+
full: 'max-w-full',
|
|
26
|
+
},
|
|
27
|
+
spacing: {
|
|
28
|
+
default: '',
|
|
29
|
+
tight: 'gap-3',
|
|
30
|
+
relaxed: 'gap-6',
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
defaultVariants: {
|
|
34
|
+
width: 'auto',
|
|
35
|
+
spacing: 'default',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
interface CardProps
|
|
41
|
+
extends
|
|
42
|
+
Omit<React.ComponentProps<'div'>, 'className' | 'title'>,
|
|
43
|
+
VariantProps<typeof cardVariants> {
|
|
44
|
+
size?: 'default' | 'sm';
|
|
45
|
+
/** Card title - renders in CardHeader */
|
|
46
|
+
title?: React.ReactNode;
|
|
47
|
+
/** Card description - renders below title in CardHeader */
|
|
48
|
+
description?: React.ReactNode;
|
|
49
|
+
/** Action element(s) in the header (e.g., buttons, badges) */
|
|
50
|
+
headerAction?: React.ReactNode;
|
|
51
|
+
/** Footer content */
|
|
52
|
+
footer?: React.ReactNode;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function Card({
|
|
56
|
+
size = 'default',
|
|
57
|
+
width,
|
|
58
|
+
maxWidth,
|
|
59
|
+
title,
|
|
60
|
+
description,
|
|
61
|
+
headerAction,
|
|
62
|
+
footer,
|
|
63
|
+
children,
|
|
64
|
+
...props
|
|
65
|
+
}: CardProps) {
|
|
66
|
+
const hasHeader = title || description || headerAction;
|
|
67
|
+
|
|
68
|
+
// Check if children contain compound components (have data-slot)
|
|
69
|
+
const hasCompoundChildren = React.Children.toArray(children).some((child) => {
|
|
70
|
+
if (React.isValidElement(child)) {
|
|
71
|
+
const props = child.props as Record<string, unknown>;
|
|
72
|
+
return (
|
|
73
|
+
props['data-slot'] === 'card-header' ||
|
|
74
|
+
props['data-slot'] === 'card-content' ||
|
|
75
|
+
props['data-slot'] === 'card-footer'
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div data-slot="card" data-size={size} className={cardVariants({ width, maxWidth })} {...props}>
|
|
83
|
+
{hasHeader && (
|
|
84
|
+
<CardHeader>
|
|
85
|
+
{title && <CardTitle>{title}</CardTitle>}
|
|
86
|
+
{description && <CardDescription>{description}</CardDescription>}
|
|
87
|
+
{headerAction && <CardAction>{headerAction}</CardAction>}
|
|
88
|
+
</CardHeader>
|
|
89
|
+
)}
|
|
90
|
+
{hasCompoundChildren ? children : children && <CardContent>{children}</CardContent>}
|
|
91
|
+
{footer && <CardFooter>{footer}</CardFooter>}
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function CardHeader({ ...props }: Omit<React.ComponentProps<'div'>, 'className'>) {
|
|
97
|
+
return (
|
|
98
|
+
<div
|
|
99
|
+
data-slot="card-header"
|
|
100
|
+
className="gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3 group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]"
|
|
101
|
+
{...props}
|
|
102
|
+
/>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function CardTitle({ ...props }: Omit<React.ComponentProps<'div'>, 'className'>) {
|
|
107
|
+
return (
|
|
108
|
+
<div
|
|
109
|
+
data-slot="card-title"
|
|
110
|
+
className="text-base leading-snug font-medium group-data-[size=sm]/card:text-sm"
|
|
111
|
+
{...props}
|
|
112
|
+
/>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function CardDescription({ ...props }: Omit<React.ComponentProps<'div'>, 'className'>) {
|
|
117
|
+
return <div data-slot="card-description" className="text-muted-foreground text-sm" {...props} />;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function CardAction({ ...props }: Omit<React.ComponentProps<'div'>, 'className'>) {
|
|
121
|
+
return (
|
|
122
|
+
<div
|
|
123
|
+
data-slot="card-action"
|
|
124
|
+
className="col-start-2 row-span-2 row-start-1 self-start justify-self-end"
|
|
125
|
+
{...props}
|
|
126
|
+
/>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function CardContent({ ...props }: Omit<React.ComponentProps<'div'>, 'className'>) {
|
|
131
|
+
return (
|
|
132
|
+
<div data-slot="card-content" className="px-4 group-data-[size=sm]/card:px-3" {...props} />
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function CardFooter({ ...props }: Omit<React.ComponentProps<'div'>, 'className'>) {
|
|
137
|
+
return (
|
|
138
|
+
<div
|
|
139
|
+
data-slot="card-footer"
|
|
140
|
+
className="bg-muted/50 rounded-b-xl border-t p-4 group-data-[size=sm]/card:p-3 flex items-center"
|
|
141
|
+
{...props}
|
|
142
|
+
/>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export {
|
|
147
|
+
Card,
|
|
148
|
+
CardAction,
|
|
149
|
+
CardContent,
|
|
150
|
+
CardDescription,
|
|
151
|
+
CardFooter,
|
|
152
|
+
CardHeader,
|
|
153
|
+
CardTitle,
|
|
154
|
+
cardVariants,
|
|
155
|
+
};
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import useEmblaCarousel, { type UseEmblaCarouselType } from 'embla-carousel-react';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
|
|
4
|
+
import { Button as ButtonPrimitive } from '@base-ui/react/button';
|
|
5
|
+
import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';
|
|
6
|
+
import { cn } from '../../../lib/utils';
|
|
7
|
+
import { buttonVariants } from './button';
|
|
8
|
+
|
|
9
|
+
type CarouselApi = UseEmblaCarouselType[1];
|
|
10
|
+
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
|
|
11
|
+
type CarouselOptions = UseCarouselParameters[0];
|
|
12
|
+
type CarouselPlugin = UseCarouselParameters[1];
|
|
13
|
+
|
|
14
|
+
type CarouselProps = {
|
|
15
|
+
opts?: CarouselOptions;
|
|
16
|
+
plugins?: CarouselPlugin;
|
|
17
|
+
orientation?: 'horizontal' | 'vertical';
|
|
18
|
+
setApi?: (api: CarouselApi) => void;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type CarouselContextProps = {
|
|
22
|
+
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
|
|
23
|
+
api: ReturnType<typeof useEmblaCarousel>[1];
|
|
24
|
+
scrollPrev: () => void;
|
|
25
|
+
scrollNext: () => void;
|
|
26
|
+
canScrollPrev: boolean;
|
|
27
|
+
canScrollNext: boolean;
|
|
28
|
+
} & CarouselProps;
|
|
29
|
+
|
|
30
|
+
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
|
|
31
|
+
|
|
32
|
+
function useCarousel() {
|
|
33
|
+
const context = React.useContext(CarouselContext);
|
|
34
|
+
|
|
35
|
+
if (!context) {
|
|
36
|
+
throw new Error('useCarousel must be used within a <Carousel />');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return context;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function Carousel({
|
|
43
|
+
orientation = 'horizontal',
|
|
44
|
+
opts,
|
|
45
|
+
setApi,
|
|
46
|
+
plugins,
|
|
47
|
+
className,
|
|
48
|
+
children,
|
|
49
|
+
...props
|
|
50
|
+
}: React.ComponentProps<'div'> & CarouselProps) {
|
|
51
|
+
const [carouselRef, api] = useEmblaCarousel(
|
|
52
|
+
{
|
|
53
|
+
...opts,
|
|
54
|
+
axis: orientation === 'horizontal' ? 'x' : 'y',
|
|
55
|
+
},
|
|
56
|
+
plugins,
|
|
57
|
+
);
|
|
58
|
+
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
|
|
59
|
+
const [canScrollNext, setCanScrollNext] = React.useState(false);
|
|
60
|
+
|
|
61
|
+
const onSelect = React.useCallback((api: CarouselApi) => {
|
|
62
|
+
if (!api) return;
|
|
63
|
+
setCanScrollPrev(api.canScrollPrev());
|
|
64
|
+
setCanScrollNext(api.canScrollNext());
|
|
65
|
+
}, []);
|
|
66
|
+
|
|
67
|
+
const scrollPrev = React.useCallback(() => {
|
|
68
|
+
api?.scrollPrev();
|
|
69
|
+
}, [api]);
|
|
70
|
+
|
|
71
|
+
const scrollNext = React.useCallback(() => {
|
|
72
|
+
api?.scrollNext();
|
|
73
|
+
}, [api]);
|
|
74
|
+
|
|
75
|
+
const handleKeyDown = React.useCallback(
|
|
76
|
+
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
77
|
+
if (event.key === 'ArrowLeft') {
|
|
78
|
+
event.preventDefault();
|
|
79
|
+
scrollPrev();
|
|
80
|
+
} else if (event.key === 'ArrowRight') {
|
|
81
|
+
event.preventDefault();
|
|
82
|
+
scrollNext();
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
[scrollPrev, scrollNext],
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
React.useEffect(() => {
|
|
89
|
+
if (!api || !setApi) return;
|
|
90
|
+
setApi(api);
|
|
91
|
+
}, [api, setApi]);
|
|
92
|
+
|
|
93
|
+
React.useEffect(() => {
|
|
94
|
+
if (!api) return;
|
|
95
|
+
onSelect(api);
|
|
96
|
+
api.on('reInit', onSelect);
|
|
97
|
+
api.on('select', onSelect);
|
|
98
|
+
|
|
99
|
+
return () => {
|
|
100
|
+
api?.off('select', onSelect);
|
|
101
|
+
};
|
|
102
|
+
}, [api, onSelect]);
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<CarouselContext.Provider
|
|
106
|
+
value={{
|
|
107
|
+
carouselRef,
|
|
108
|
+
api: api,
|
|
109
|
+
opts,
|
|
110
|
+
orientation: orientation || (opts?.axis === 'y' ? 'vertical' : 'horizontal'),
|
|
111
|
+
scrollPrev,
|
|
112
|
+
scrollNext,
|
|
113
|
+
canScrollPrev,
|
|
114
|
+
canScrollNext,
|
|
115
|
+
}}
|
|
116
|
+
>
|
|
117
|
+
<div
|
|
118
|
+
onKeyDownCapture={handleKeyDown}
|
|
119
|
+
className={cn('relative', className)}
|
|
120
|
+
role="region"
|
|
121
|
+
aria-roledescription="carousel"
|
|
122
|
+
data-slot="carousel"
|
|
123
|
+
{...props}
|
|
124
|
+
>
|
|
125
|
+
{children}
|
|
126
|
+
</div>
|
|
127
|
+
</CarouselContext.Provider>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function CarouselContent({ className, ...props }: React.ComponentProps<'div'>) {
|
|
132
|
+
const { carouselRef, orientation } = useCarousel();
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<div ref={carouselRef} className="overflow-hidden" data-slot="carousel-content">
|
|
136
|
+
<div
|
|
137
|
+
className={cn('flex', orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col', className)}
|
|
138
|
+
{...props}
|
|
139
|
+
/>
|
|
140
|
+
</div>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function CarouselItem({ className, ...props }: React.ComponentProps<'div'>) {
|
|
145
|
+
const { orientation } = useCarousel();
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<div
|
|
149
|
+
role="group"
|
|
150
|
+
aria-roledescription="slide"
|
|
151
|
+
data-slot="carousel-item"
|
|
152
|
+
className={cn(
|
|
153
|
+
'min-w-0 shrink-0 grow-0 basis-full',
|
|
154
|
+
orientation === 'horizontal' ? 'pl-4' : 'pt-4',
|
|
155
|
+
className,
|
|
156
|
+
)}
|
|
157
|
+
{...props}
|
|
158
|
+
/>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function CarouselPrevious({ ...props }: Omit<ButtonPrimitive.Props, 'className'>) {
|
|
163
|
+
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<ButtonPrimitive
|
|
167
|
+
data-slot="carousel-previous"
|
|
168
|
+
className={cn(
|
|
169
|
+
buttonVariants({ variant: 'outline', size: 'icon-sm' }),
|
|
170
|
+
'rounded-full absolute touch-manipulation',
|
|
171
|
+
orientation === 'horizontal'
|
|
172
|
+
? 'top-1/2 -left-12 -translate-y-1/2'
|
|
173
|
+
: '-top-12 left-1/2 -translate-x-1/2 rotate-90',
|
|
174
|
+
)}
|
|
175
|
+
disabled={!canScrollPrev}
|
|
176
|
+
onClick={scrollPrev}
|
|
177
|
+
{...props}
|
|
178
|
+
>
|
|
179
|
+
<ChevronLeftIcon />
|
|
180
|
+
<span className="sr-only">Previous slide</span>
|
|
181
|
+
</ButtonPrimitive>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function CarouselNext({ ...props }: Omit<ButtonPrimitive.Props, 'className'>) {
|
|
186
|
+
const { orientation, scrollNext, canScrollNext } = useCarousel();
|
|
187
|
+
|
|
188
|
+
return (
|
|
189
|
+
<ButtonPrimitive
|
|
190
|
+
data-slot="carousel-next"
|
|
191
|
+
className={cn(
|
|
192
|
+
buttonVariants({ variant: 'outline', size: 'icon-sm' }),
|
|
193
|
+
'rounded-full absolute touch-manipulation',
|
|
194
|
+
orientation === 'horizontal'
|
|
195
|
+
? 'top-1/2 -right-12 -translate-y-1/2'
|
|
196
|
+
: '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
|
|
197
|
+
)}
|
|
198
|
+
disabled={!canScrollNext}
|
|
199
|
+
onClick={scrollNext}
|
|
200
|
+
{...props}
|
|
201
|
+
>
|
|
202
|
+
<ChevronRightIcon />
|
|
203
|
+
<span className="sr-only">Next slide</span>
|
|
204
|
+
</ButtonPrimitive>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export {
|
|
209
|
+
Carousel,
|
|
210
|
+
CarouselContent,
|
|
211
|
+
CarouselItem,
|
|
212
|
+
CarouselNext,
|
|
213
|
+
CarouselPrevious,
|
|
214
|
+
useCarousel,
|
|
215
|
+
type CarouselApi,
|
|
216
|
+
};
|