Skip to content

Commit ec315c7

Browse files
committed
feat: add component carousel
1 parent bdc6944 commit ec315c7

File tree

18 files changed

+792
-0
lines changed

18 files changed

+792
-0
lines changed

packages/ui-variants/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export * from './variants/breadcrumb';
1111
export * from './variants/button';
1212
export * from './variants/button-group';
1313
export * from './variants/card';
14+
export * from './variants/carousel';
1415
export * from './variants/divider';
1516
export * from './variants/scroll-area';
1617
export * from './variants/tabs';
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import { tv } from 'tailwind-variants';
2+
3+
export const carouselVariants = tv({
4+
compoundVariants: [
5+
{
6+
class: {
7+
content: '-ml-3',
8+
item: 'pl-3',
9+
next: '-right-10',
10+
previous: '-left-10'
11+
},
12+
orientation: 'horizontal',
13+
size: 'xs'
14+
},
15+
{
16+
class: {
17+
content: '-ml-3.5',
18+
item: 'pl-3.5',
19+
next: '-right-11',
20+
previous: '-left-11'
21+
},
22+
orientation: 'horizontal',
23+
size: 'sm'
24+
},
25+
{
26+
class: {
27+
content: '-ml-4.5',
28+
item: 'pl-4.5',
29+
next: '-right-13',
30+
previous: '-left-13'
31+
},
32+
orientation: 'horizontal',
33+
size: 'lg'
34+
},
35+
{
36+
class: {
37+
content: '-ml-5',
38+
item: 'pl-5',
39+
next: '-right-14',
40+
previous: '-left-14'
41+
},
42+
orientation: 'horizontal',
43+
size: 'xl'
44+
},
45+
{
46+
class: {
47+
content: '-ml-6',
48+
item: 'pl-6',
49+
next: '-right-16',
50+
previous: '-left-16'
51+
},
52+
orientation: 'horizontal',
53+
size: '2xl'
54+
},
55+
{
56+
class: {
57+
content: '-mt-3',
58+
item: 'pt-3',
59+
next: '-bottom-10',
60+
previous: '-top-10'
61+
},
62+
orientation: 'vertical',
63+
size: 'xs'
64+
},
65+
{
66+
class: {
67+
content: '-mt-3.5',
68+
item: 'pt-3.5',
69+
next: '-bottom-11',
70+
previous: '-top-11'
71+
},
72+
orientation: 'vertical',
73+
size: 'sm'
74+
},
75+
{
76+
class: {
77+
content: '-mt-4.5',
78+
item: 'pt-4.5',
79+
next: '-bottom-13',
80+
previous: '-top-13'
81+
},
82+
orientation: 'vertical',
83+
size: 'lg'
84+
},
85+
{
86+
class: {
87+
content: '-mt-5',
88+
item: 'pt-5',
89+
next: '-bottom-14',
90+
previous: '-top-14'
91+
},
92+
orientation: 'vertical',
93+
size: 'xl'
94+
},
95+
{
96+
class: {
97+
content: '-mt-6',
98+
item: 'pt-6',
99+
next: '-bottom-16',
100+
previous: '-top-16'
101+
},
102+
orientation: 'vertical',
103+
size: '2xl'
104+
}
105+
],
106+
defaultVariants: {
107+
orientation: 'horizontal'
108+
},
109+
slots: {
110+
content: 'flex',
111+
contentWrapper: 'overflow-hidden',
112+
item: 'min-w-0 shrink-0 grow-0 basis-full',
113+
next: 'touch-manipulation absolute',
114+
previous: 'touch-manipulation absolute',
115+
root: 'relative'
116+
},
117+
variants: {
118+
orientation: {
119+
horizontal: {
120+
content: '-ml-4',
121+
item: 'pl-4',
122+
next: '-right-12 top-1/2 -translate-y-1/2',
123+
previous: '-left-12 top-1/2 -translate-y-1/2'
124+
},
125+
vertical: {
126+
content: 'flex-col -mt-4',
127+
item: 'pt-4',
128+
next: '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
129+
previous: '-top-12 left-1/2 -translate-x-1/2 rotate-90'
130+
}
131+
},
132+
size: {
133+
'2xl': {
134+
root: 'text-xl'
135+
},
136+
lg: {
137+
root: 'text-base'
138+
},
139+
md: {
140+
root: 'text-sm'
141+
},
142+
sm: {
143+
root: 'text-xs'
144+
},
145+
xl: {
146+
root: 'text-lg'
147+
},
148+
xs: {
149+
root: 'text-2xs'
150+
}
151+
}
152+
}
153+
});
154+
155+
export type CarouselSlots = keyof typeof carouselVariants.slots;

packages/ui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"@radix-ui/react-slot": "1.2.3",
2222
"@radix-ui/react-tabs": "1.1.12",
2323
"@soybean-react-ui/variants": "workspace:*",
24+
"embla-carousel-react": "^8.6.0",
2425
"lucide-react": "0.525.0",
2526
"next-themes": "^0.4.6",
2627
"react": "19.1.0",
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
'use client';
2+
3+
import { createContext, useContext } from 'react';
4+
5+
import type { CarouselContextProps } from './types';
6+
7+
export const CarouselContext = createContext<CarouselContextProps | null>(null);
8+
9+
export function useCarousel() {
10+
const context = useContext(CarouselContext);
11+
12+
if (!context) {
13+
throw new Error('useCarousel must be used within a <Carousel />');
14+
}
15+
16+
return context;
17+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export { default as Carousel } from './source/Carousel';
2+
export { default as CarouselContent } from './source/CarouselContent';
3+
export { default as CarouselItem } from './source/CarouselItem';
4+
export { default as CarouselNext } from './source/CarouselNext';
5+
export { default as CarouselPrevious } from './source/CarouselPrevious';
6+
export { default as CarouselRoot } from './source/CarouselRoot';
7+
8+
export * from './types';
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { Children, forwardRef } from 'react';
2+
3+
import { isFunction } from '../../../utils';
4+
import type { CarouselProps } from '../types';
5+
6+
import CarouselContent from './CarouselContent';
7+
import CarouselItem from './CarouselItem';
8+
import CarouselNext from './CarouselNext';
9+
import CarouselPrevious from './CarouselPrevious';
10+
import CarouselRoot from './CarouselRoot';
11+
12+
const Carousel = forwardRef<HTMLDivElement, CarouselProps>((props, ref) => {
13+
const { children, className, classNames, counts, nextProps, previousProps, size, ...rest } = props;
14+
15+
return (
16+
<CarouselRoot
17+
className={className || classNames?.root}
18+
ref={ref}
19+
size={size}
20+
{...rest}
21+
>
22+
<CarouselContent
23+
classNames={classNames}
24+
size={size}
25+
>
26+
{counts &&
27+
Array.from({ length: counts }).map((_, index) => (
28+
<CarouselItem
29+
className={classNames?.item}
30+
key={index}
31+
size={size}
32+
>
33+
{isFunction(children) ? children(index) : Children.toArray(children)[index]}
34+
</CarouselItem>
35+
))}
36+
</CarouselContent>
37+
38+
<CarouselNext
39+
className={classNames?.next}
40+
size={size}
41+
{...nextProps}
42+
/>
43+
44+
<CarouselPrevious
45+
className={classNames?.previous}
46+
size={size}
47+
{...previousProps}
48+
/>
49+
</CarouselRoot>
50+
);
51+
});
52+
53+
Carousel.displayName = 'Carousel';
54+
55+
export default Carousel;
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
'use client';
2+
3+
import { carouselVariants, cn } from '@soybean-react-ui/variants';
4+
import { forwardRef } from 'react';
5+
6+
import { useCarousel } from '../context';
7+
import type { CarouselContentProps } from '../types';
8+
9+
const CarouselContent = forwardRef<HTMLDivElement, CarouselContentProps>((props, ref) => {
10+
const { className, classNames, size, ...rest } = props;
11+
12+
const { carouselRef, orientation } = useCarousel();
13+
14+
const { content, contentWrapper } = carouselVariants({ orientation, size });
15+
16+
const contentClassName = cn(content(), classNames?.content);
17+
18+
const contentWrapperClassName = cn(contentWrapper(), className || classNames?.contentWrapper);
19+
20+
return (
21+
<div
22+
className={contentWrapperClassName}
23+
ref={carouselRef}
24+
>
25+
<div
26+
className={contentClassName}
27+
ref={ref}
28+
{...rest}
29+
/>
30+
</div>
31+
);
32+
});
33+
CarouselContent.displayName = 'CarouselContent';
34+
35+
export default CarouselContent;
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
'use client';
2+
3+
import { carouselVariants, cn } from '@soybean-react-ui/variants';
4+
import { forwardRef } from 'react';
5+
6+
import { useCarousel } from '../context';
7+
import type { CarouselItemProps } from '../types';
8+
9+
const CarouselItem = forwardRef<HTMLDivElement, CarouselItemProps>((props, ref) => {
10+
const { className, size, ...rest } = props;
11+
12+
const { orientation } = useCarousel();
13+
14+
const { item } = carouselVariants({ orientation, size });
15+
16+
const itemClassName = cn(item(), className);
17+
18+
return (
19+
<div
20+
aria-roledescription="slide"
21+
className={itemClassName}
22+
ref={ref}
23+
role="group"
24+
{...rest}
25+
/>
26+
);
27+
});
28+
29+
CarouselItem.displayName = 'CarouselItem';
30+
31+
export default CarouselItem;
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
'use client';
2+
3+
import { carouselVariants, cn } from '@soybean-react-ui/variants';
4+
import { ChevronRight } from 'lucide-react';
5+
import { forwardRef } from 'react';
6+
7+
import { Button } from '../../button';
8+
import { useCarousel } from '../context';
9+
import type { CarouselNextProps } from '../types';
10+
11+
const CarouselNext = forwardRef<HTMLButtonElement, CarouselNextProps>((props, ref) => {
12+
const { children, className, disabled, shape = 'circle', size, variant = 'pure', ...rest } = props;
13+
14+
const { canScrollNext, orientation, scrollNext } = useCarousel();
15+
16+
const { next } = carouselVariants({ orientation, size });
17+
18+
const nextClassName = cn(next(), className);
19+
20+
return (
21+
<Button
22+
className={nextClassName}
23+
disabled={!canScrollNext || disabled}
24+
ref={ref}
25+
shape={shape}
26+
size={size}
27+
variant={variant}
28+
onClick={scrollNext}
29+
{...rest}
30+
>
31+
{children || <ChevronRight />}
32+
</Button>
33+
);
34+
});
35+
36+
CarouselNext.displayName = 'CarouselNext';
37+
38+
export default CarouselNext;

0 commit comments

Comments
 (0)