Skip to content

Commit ed9a71b

Browse files
authored
Merge pull request #103 from Logging-Studio/new-com-carousel
carousal component added
2 parents 5977e3e + c803dd4 commit ed9a71b

File tree

12 files changed

+778
-96
lines changed

12 files changed

+778
-96
lines changed

components/TopNav.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,8 @@ export default function TopNav() {
7878
target="_blank"
7979
rel="noopener noreferrer"
8080
>
81-
<Button variant="secondary" size="sm">
82-
<GithubIcon size="14" className="mr-2" />
83-
Star on GitHub
81+
<Button variant="secondary" size="icon">
82+
<GithubIcon size="14"/>
8483
</Button>
8584
</Link>
8685
<Button variant="secondary" size="icon" onClick={toggleTheme}>

components/retroui/Carousel.tsx

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
"use client"
2+
3+
import * as React from "react"
4+
import useEmblaCarousel, {
5+
type UseEmblaCarouselType,
6+
} from "embla-carousel-react"
7+
import { ArrowLeft, ArrowRight } from "lucide-react"
8+
9+
import { cn } from "@/lib/utils"
10+
import { Button } from "@/components/retroui/Button"
11+
12+
type CarouselApi = UseEmblaCarouselType[1]
13+
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
14+
type CarouselOptions = UseCarouselParameters[0]
15+
type CarouselPlugin = UseCarouselParameters[1]
16+
17+
type CarouselProps = {
18+
opts?: CarouselOptions
19+
plugins?: CarouselPlugin
20+
orientation?: "horizontal" | "vertical"
21+
setApi?: (api: CarouselApi) => void
22+
}
23+
24+
type CarouselContextProps = {
25+
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
26+
api: ReturnType<typeof useEmblaCarousel>[1]
27+
scrollPrev: () => void
28+
scrollNext: () => void
29+
canScrollPrev: boolean
30+
canScrollNext: boolean
31+
} & CarouselProps
32+
33+
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
34+
35+
function useCarousel() {
36+
const context = React.useContext(CarouselContext)
37+
38+
if (!context) {
39+
throw new Error("useCarousel must be used within a <Carousel />")
40+
}
41+
42+
return context
43+
}
44+
45+
function Carousel({
46+
orientation = "horizontal",
47+
opts,
48+
setApi,
49+
plugins,
50+
className,
51+
children,
52+
...props
53+
}: React.ComponentProps<"div"> & CarouselProps) {
54+
const [carouselRef, api] = useEmblaCarousel(
55+
{
56+
...opts,
57+
axis: orientation === "horizontal" ? "x" : "y",
58+
},
59+
plugins
60+
)
61+
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
62+
const [canScrollNext, setCanScrollNext] = React.useState(false)
63+
64+
const onSelect = React.useCallback((api: CarouselApi) => {
65+
if (!api) return
66+
setCanScrollPrev(api.canScrollPrev())
67+
setCanScrollNext(api.canScrollNext())
68+
}, [])
69+
70+
const scrollPrev = React.useCallback(() => {
71+
api?.scrollPrev()
72+
}, [api])
73+
74+
const scrollNext = React.useCallback(() => {
75+
api?.scrollNext()
76+
}, [api])
77+
78+
const handleKeyDown = React.useCallback(
79+
(event: React.KeyboardEvent<HTMLDivElement>) => {
80+
if (event.key === "ArrowLeft") {
81+
event.preventDefault()
82+
scrollPrev()
83+
} else if (event.key === "ArrowRight") {
84+
event.preventDefault()
85+
scrollNext()
86+
}
87+
},
88+
[scrollPrev, scrollNext]
89+
)
90+
91+
React.useEffect(() => {
92+
if (!api || !setApi) return
93+
setApi(api)
94+
}, [api, setApi])
95+
96+
React.useEffect(() => {
97+
if (!api) return
98+
onSelect(api)
99+
api.on("reInit", onSelect)
100+
api.on("select", onSelect)
101+
102+
return () => {
103+
api?.off("select", onSelect)
104+
}
105+
}, [api, onSelect])
106+
107+
return (
108+
<CarouselContext.Provider
109+
value={{
110+
carouselRef,
111+
api: api,
112+
opts,
113+
orientation:
114+
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
115+
scrollPrev,
116+
scrollNext,
117+
canScrollPrev,
118+
canScrollNext,
119+
}}
120+
>
121+
<div
122+
onKeyDownCapture={handleKeyDown}
123+
className={cn("relative", className)}
124+
role="region"
125+
aria-roledescription="carousel"
126+
data-slot="carousel"
127+
{...props}
128+
>
129+
{children}
130+
</div>
131+
</CarouselContext.Provider>
132+
)
133+
}
134+
135+
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
136+
const { carouselRef, orientation } = useCarousel()
137+
138+
return (
139+
<div
140+
ref={carouselRef}
141+
className="overflow-hidden"
142+
data-slot="carousel-content"
143+
>
144+
<div
145+
className={cn(
146+
"flex",
147+
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
148+
className
149+
)}
150+
{...props}
151+
/>
152+
</div>
153+
)
154+
}
155+
156+
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
157+
const { orientation } = useCarousel()
158+
159+
return (
160+
<div
161+
role="group"
162+
aria-roledescription="slide"
163+
data-slot="carousel-item"
164+
className={cn(
165+
"min-w-0 shrink-0 grow-0 basis-full",
166+
orientation === "horizontal" ? "pl-4" : "pt-4",
167+
className
168+
)}
169+
{...props}
170+
/>
171+
)
172+
}
173+
174+
function CarouselPrevious({
175+
className,
176+
variant = "outline",
177+
size = "icon",
178+
...props
179+
}: React.ComponentProps<typeof Button>) {
180+
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
181+
182+
return (
183+
<Button
184+
data-slot="carousel-previous"
185+
variant={variant}
186+
size={size}
187+
className={cn(
188+
"absolute size-8 rounded",
189+
orientation === "horizontal"
190+
? "top-1/2 -left-12 -translate-y-1/2 hover:-translate-y-[calc(50%-2px)] active:-translate-y-[calc(50%-4px)]"
191+
: "-top-12 left-1/2 -translate-x-1/2 rotate-90 hover:-translate-x-[calc(50%-2px)] active:-translate-x-[calc(50%-4px)]",
192+
className
193+
)}
194+
disabled={!canScrollPrev}
195+
onClick={scrollPrev}
196+
{...props}
197+
>
198+
<ArrowLeft />
199+
<span className="sr-only">Previous slide</span>
200+
</Button>
201+
)
202+
}
203+
204+
function CarouselNext({
205+
className,
206+
variant = "outline",
207+
size = "icon",
208+
...props
209+
}: React.ComponentProps<typeof Button>) {
210+
const { orientation, scrollNext, canScrollNext } = useCarousel()
211+
212+
return (
213+
<Button
214+
data-slot="carousel-next"
215+
variant={variant}
216+
size={size}
217+
className={cn(
218+
"absolute size-8 rounded",
219+
orientation === "horizontal"
220+
? "top-1/2 -right-12 -translate-y-1/2 hover:-translate-y-[calc(50%-2px)] active:-translate-y-[calc(50%-4px)]"
221+
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90 hover:-translate-x-[calc(50%-2px)] active:-translate-x-[calc(50%-4px)]",
222+
className
223+
)}
224+
disabled={!canScrollNext}
225+
onClick={scrollNext}
226+
{...props}
227+
>
228+
<ArrowRight />
229+
<span className="sr-only">Next slide</span>
230+
</Button>
231+
)
232+
}
233+
234+
const CarouselObject = Object.assign(Carousel, {
235+
Content: CarouselContent,
236+
Item: CarouselItem,
237+
Previous: CarouselPrevious,
238+
Next: CarouselNext,
239+
})
240+
241+
export { CarouselObject as Carousel }

config/components.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ export const componentConfig: {
6060
name: "barChart",
6161
filePath: "components/retroui/charts/BarChart.tsx",
6262
},
63+
carousel: {
64+
name: "carousel",
65+
filePath: "components/retroui/Carousel.tsx",
66+
},
6367
checkbox: {
6468
name: "checkbox",
6569
filePath: "components/retroui/Checkbox.tsx",
@@ -295,6 +299,21 @@ export const componentConfig: {
295299
filePath: "preview/components/calendar-style-default.tsx",
296300
preview: lazy(() => import("@/preview/components/calendar-style-default")),
297301
},
302+
"carousel-style-default": {
303+
name: "carousel-style-default",
304+
filePath: "preview/components/carousel-style-default.tsx",
305+
preview: lazy(() => import("@/preview/components/carousel-style-default")),
306+
},
307+
"carousel-style-sizes": {
308+
name: "carousel-style-sizes",
309+
filePath: "preview/components/carousel-style-sizes.tsx",
310+
preview: lazy(() => import("@/preview/components/carousel-style-sizes")),
311+
},
312+
"carousel-style-vertical": {
313+
name: "carousel-style-vertical",
314+
filePath: "preview/components/carousel-style-vertical.tsx",
315+
preview: lazy(() => import("@/preview/components/carousel-style-vertical")),
316+
},
298317
"card-style-default": {
299318
name: "card-style-default",
300319
filePath: "preview/components/card-style-default.tsx",

config/navigation.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export const navConfig: INavigationConfig = {
5252
{ title: "Button", href: `${componentsRoute}/button` },
5353
{ title: "Card", href: `${componentsRoute}/card` },
5454
{ title: "Calendar", href: `${componentsRoute}/calendar`, tag: "New" },
55+
{ title: "Carousel", href: `${componentsRoute}/carousel`, tag: "New" },
5556
{ title: "Checkbox", href: `${componentsRoute}/checkbox` },
5657
{ title: "Command", href: `${componentsRoute}/command` },
5758
{ title: "Dialog", href: `${componentsRoute}/dialog` },
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
title: Carousel
3+
description: Let your users select a date to cancel subscription.
4+
lastUpdated: 14 Nov, 2025
5+
---
6+
7+
<ComponentShowcase name="carousel-style-default" />
8+
<br />
9+
<br />
10+
11+
## Installation
12+
13+
<ComponentInstall>
14+
<ComponentInstall.Cli npmCommand="npx shadcn@latest add @retroui/carousel" />
15+
<ComponentInstall.Manual>
16+
17+
#### 1. Install dependencies:
18+
19+
```sh
20+
npm install react-day-picker lucide-react
21+
```
22+
23+
<br />
24+
25+
#### 2. Copy the code 👇 into your project:
26+
27+
<ComponentSource name="carousel" />
28+
29+
</ComponentInstall.Manual>
30+
</ComponentInstall>
31+
32+
<br />
33+
<br />
34+
35+
## Examples
36+
37+
### Default
38+
39+
<ComponentShowcase name="carousel-style-default" />
40+
41+
<br />
42+
<br />
43+
44+
### Sizes
45+
46+
<ComponentShowcase name="carousel-style-sizes" />
47+
48+
<br />
49+
<br />
50+
51+
### Vertical
52+
53+
<ComponentShowcase name="carousel-style-vertical" />
54+

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"cmdk": "^1.1.1",
3737
"contentlayer": "^0.3.4",
3838
"date-fns": "^4.1.0",
39+
"embla-carousel-react": "^8.6.0",
3940
"lucide-react": "^0.445.0",
4041
"mdast-util-toc": "^7.1.0",
4142
"next": "14.2.7",

0 commit comments

Comments
 (0)