From 1c6f588a7ed5787c5bb438885b74c380394fce26 Mon Sep 17 00:00:00 2001 From: Kevin Abatan Date: Thu, 25 Dec 2025 22:12:17 +0100 Subject: [PATCH 1/5] chore: add FRONTEND_PORT env var to allow worktrees --- apps/docs/app.config.ts | 5 ++++- apps/docs/tsconfig.json | 2 -- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/docs/app.config.ts b/apps/docs/app.config.ts index b836b7e6..fa4254d6 100644 --- a/apps/docs/app.config.ts +++ b/apps/docs/app.config.ts @@ -20,7 +20,10 @@ export default defineConfig( } }, vite: { - plugins: [tailwindcss()] + plugins: [tailwindcss()], + server: { + port: parseInt(process.env.FRONTEND_PORT || "5173", 10) + } } }, { diff --git a/apps/docs/tsconfig.json b/apps/docs/tsconfig.json index 047b6294..cbd39639 100644 --- a/apps/docs/tsconfig.json +++ b/apps/docs/tsconfig.json @@ -22,10 +22,8 @@ "noFallthroughCasesInSwitch": true, "noUncheckedIndexedAccess": true, "noImplicitOverride": true, - // Some stricter flags (disabled by default) "noUnusedLocals": true, "noUnusedParameters": true, - "noPropertyAccessFromIndexSignature": true, "baseUrl": ".", "paths": { "~/*": ["./src/*"] From 395c6ea4988546b6ee6f22457443184d450948ab Mon Sep 17 00:00:00 2001 From: Kevin Abatan Date: Fri, 26 Dec 2025 04:19:30 +0100 Subject: [PATCH 2/5] chore: adding trees to gitignore --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index dc1b5dd4..ad08f192 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,10 @@ Thumbs.db # Claude .claude logs +trees/ + +# Worktree environment configs +**/.ports.env app.config.timestamp* From 3a8509c946cfe3a5216ba02e16ae1ae1b7e7b3b9 Mon Sep 17 00:00:00 2001 From: Kevin Abatan Date: Fri, 26 Dec 2025 23:49:47 +0100 Subject: [PATCH 3/5] feat(carousel): add carousel component with embla-carousel-solid MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext components - Add examples: demo, size, spacing, orientation, api, plugin - Add documentation with usage examples - Update UI and examples registries - Add embla-carousel-solid, embla-carousel, embla-carousel-autoplay dependencies 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- apps/docs/package.json | 7 +- apps/docs/src/config/docs.ts | 5 + apps/docs/src/registry/__index__.tsx | 98 ++++++ apps/docs/src/registry/examples/_registry.ts | 67 ++++ .../src/registry/examples/carousel-api.tsx | 55 +++ .../src/registry/examples/carousel-demo.tsx | 34 ++ .../examples/carousel-orientation.tsx | 40 +++ .../src/registry/examples/carousel-plugin.tsx | 42 +++ .../src/registry/examples/carousel-size.tsx | 39 +++ .../registry/examples/carousel-spacing.tsx | 34 ++ apps/docs/src/registry/ui/_registry.ts | 12 + apps/docs/src/registry/ui/carousel.tsx | 266 +++++++++++++++ .../src/routes/docs/components/carousel.mdx | 319 ++++++++++++++++++ pnpm-lock.yaml | 39 +++ 14 files changed, 1055 insertions(+), 2 deletions(-) create mode 100644 apps/docs/src/registry/examples/carousel-api.tsx create mode 100644 apps/docs/src/registry/examples/carousel-demo.tsx create mode 100644 apps/docs/src/registry/examples/carousel-orientation.tsx create mode 100644 apps/docs/src/registry/examples/carousel-plugin.tsx create mode 100644 apps/docs/src/registry/examples/carousel-size.tsx create mode 100644 apps/docs/src/registry/examples/carousel-spacing.tsx create mode 100644 apps/docs/src/registry/ui/carousel.tsx create mode 100644 apps/docs/src/routes/docs/components/carousel.mdx diff --git a/apps/docs/package.json b/apps/docs/package.json index a5fc76b8..b23ab906 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -23,6 +23,9 @@ "@solidjs/start": "^1.2.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "embla-carousel": "^8.6.0", + "embla-carousel-autoplay": "^8.6.0", + "embla-carousel-solid": "^8.6.0", "lucide-solid": "^0.562.0", "rimraf": "^6.0.1", "solid-js": "^1.9.9", @@ -38,7 +41,7 @@ "tailwindcss": "^4.1.14", "tsx": "^4.20.6", "tw-animate-css": "^1.4.0", - "vite": "^6.3.5", - "typescript": "^5.9.3" + "typescript": "^5.9.3", + "vite": "^6.3.5" } } diff --git a/apps/docs/src/config/docs.ts b/apps/docs/src/config/docs.ts index a893e129..c994750f 100644 --- a/apps/docs/src/config/docs.ts +++ b/apps/docs/src/config/docs.ts @@ -47,6 +47,11 @@ export const docsConfig: Config = { title: "Button", href: "/docs/components/button" }, + { + title: "Carousel", + href: "/docs/components/carousel", + status: "new" + }, { title: "Checkbox", href: "/docs/components/checkbox" diff --git a/apps/docs/src/registry/__index__.tsx b/apps/docs/src/registry/__index__.tsx index 7eddc1d9..1491fa61 100644 --- a/apps/docs/src/registry/__index__.tsx +++ b/apps/docs/src/registry/__index__.tsx @@ -74,6 +74,20 @@ export const Index: Record = { categories: undefined, meta: undefined, }, + "carousel": { + name: "carousel", + description: "", + type: "registry:ui", + registryDependencies: ["button"], + component: lazy(() => import("~/registry/ui/carousel.tsx")), + files: [{ + path: "registry/ui/carousel.tsx", + type: "registry:ui", + target: "" + }], + categories: undefined, + meta: undefined, + }, "checkbox": { name: "checkbox", description: "", @@ -242,6 +256,90 @@ export const Index: Record = { categories: undefined, meta: undefined, }, + "carousel-demo": { + name: "carousel-demo", + description: "", + type: "registry:example", + registryDependencies: ["carousel","card"], + component: lazy(() => import("~/registry/examples/carousel-demo.tsx")), + files: [{ + path: "registry/examples/carousel-demo.tsx", + type: "registry:example", + target: "" + }], + categories: undefined, + meta: undefined, + }, + "carousel-size": { + name: "carousel-size", + description: "", + type: "registry:example", + registryDependencies: ["carousel","card"], + component: lazy(() => import("~/registry/examples/carousel-size.tsx")), + files: [{ + path: "registry/examples/carousel-size.tsx", + type: "registry:example", + target: "" + }], + categories: undefined, + meta: undefined, + }, + "carousel-spacing": { + name: "carousel-spacing", + description: "", + type: "registry:example", + registryDependencies: ["carousel","card"], + component: lazy(() => import("~/registry/examples/carousel-spacing.tsx")), + files: [{ + path: "registry/examples/carousel-spacing.tsx", + type: "registry:example", + target: "" + }], + categories: undefined, + meta: undefined, + }, + "carousel-orientation": { + name: "carousel-orientation", + description: "", + type: "registry:example", + registryDependencies: ["carousel","card"], + component: lazy(() => import("~/registry/examples/carousel-orientation.tsx")), + files: [{ + path: "registry/examples/carousel-orientation.tsx", + type: "registry:example", + target: "" + }], + categories: undefined, + meta: undefined, + }, + "carousel-api": { + name: "carousel-api", + description: "", + type: "registry:example", + registryDependencies: ["carousel","card"], + component: lazy(() => import("~/registry/examples/carousel-api.tsx")), + files: [{ + path: "registry/examples/carousel-api.tsx", + type: "registry:example", + target: "" + }], + categories: undefined, + meta: undefined, + }, + "carousel-plugin": { + name: "carousel-plugin", + description: "", + type: "registry:example", + registryDependencies: ["carousel","card"], + component: lazy(() => import("~/registry/examples/carousel-plugin.tsx")), + files: [{ + path: "registry/examples/carousel-plugin.tsx", + type: "registry:example", + target: "" + }], + categories: undefined, + meta: undefined, + }, "button-default": { name: "button-default", description: "", diff --git a/apps/docs/src/registry/examples/_registry.ts b/apps/docs/src/registry/examples/_registry.ts index 6cd90966..54a986ae 100644 --- a/apps/docs/src/registry/examples/_registry.ts +++ b/apps/docs/src/registry/examples/_registry.ts @@ -23,6 +23,73 @@ export const examples: Registry["items"] = [ } ] }, + { + name: "carousel-demo", + type: "registry:example", + registryDependencies: ["carousel", "card"], + files: [ + { + path: "examples/carousel-demo.tsx", + type: "registry:example" + } + ] + }, + { + name: "carousel-size", + type: "registry:example", + registryDependencies: ["carousel", "card"], + files: [ + { + path: "examples/carousel-size.tsx", + type: "registry:example" + } + ] + }, + { + name: "carousel-spacing", + type: "registry:example", + registryDependencies: ["carousel", "card"], + files: [ + { + path: "examples/carousel-spacing.tsx", + type: "registry:example" + } + ] + }, + { + name: "carousel-orientation", + type: "registry:example", + registryDependencies: ["carousel", "card"], + files: [ + { + path: "examples/carousel-orientation.tsx", + type: "registry:example" + } + ] + }, + { + name: "carousel-api", + type: "registry:example", + registryDependencies: ["carousel", "card"], + files: [ + { + path: "examples/carousel-api.tsx", + type: "registry:example" + } + ] + }, + { + name: "carousel-plugin", + type: "registry:example", + dependencies: ["embla-carousel-autoplay"], + registryDependencies: ["carousel", "card"], + files: [ + { + path: "examples/carousel-plugin.tsx", + type: "registry:example" + } + ] + }, { name: "button-default", type: "registry:example", diff --git a/apps/docs/src/registry/examples/carousel-api.tsx b/apps/docs/src/registry/examples/carousel-api.tsx new file mode 100644 index 00000000..d0325573 --- /dev/null +++ b/apps/docs/src/registry/examples/carousel-api.tsx @@ -0,0 +1,55 @@ +import { createEffect, createSignal, For, on } from "solid-js" + +import { Card, CardContent } from "~/registry/ui/card" +import { + Carousel, + CarouselContent, + CarouselItem, + CarouselNext, + CarouselPrevious, + type CarouselApi +} from "~/registry/ui/carousel" + +export default function CarouselApiDemo() { + const [api, setApi] = createSignal() + const [current, setCurrent] = createSignal(0) + const [count, setCount] = createSignal(0) + + createEffect( + on(api, (emblaApi) => { + if (!emblaApi) return + + setCount(emblaApi.scrollSnapList().length) + setCurrent(emblaApi.selectedScrollSnap() + 1) + + emblaApi.on("select", () => { + setCurrent(emblaApi.selectedScrollSnap() + 1) + }) + }) + ) + + return ( +
+ + + + {(_, index) => ( + + + + {index() + 1} + + + + )} + + + + + +
+ Slide {current()} of {count()} +
+
+ ) +} diff --git a/apps/docs/src/registry/examples/carousel-demo.tsx b/apps/docs/src/registry/examples/carousel-demo.tsx new file mode 100644 index 00000000..d74623e1 --- /dev/null +++ b/apps/docs/src/registry/examples/carousel-demo.tsx @@ -0,0 +1,34 @@ +import { For } from "solid-js" + +import { Card, CardContent } from "~/registry/ui/card" +import { + Carousel, + CarouselContent, + CarouselItem, + CarouselNext, + CarouselPrevious +} from "~/registry/ui/carousel" + +export default function CarouselDemo() { + return ( + + + + {(_, index) => ( + +
+ + + {index() + 1} + + +
+
+ )} +
+
+ + +
+ ) +} diff --git a/apps/docs/src/registry/examples/carousel-orientation.tsx b/apps/docs/src/registry/examples/carousel-orientation.tsx new file mode 100644 index 00000000..7c824028 --- /dev/null +++ b/apps/docs/src/registry/examples/carousel-orientation.tsx @@ -0,0 +1,40 @@ +import { For } from "solid-js" + +import { Card, CardContent } from "~/registry/ui/card" +import { + Carousel, + CarouselContent, + CarouselItem, + CarouselNext, + CarouselPrevious +} from "~/registry/ui/carousel" + +export default function CarouselOrientation() { + return ( + + + + {(_, index) => ( + +
+ + + {index() + 1} + + +
+
+ )} +
+
+ + +
+ ) +} diff --git a/apps/docs/src/registry/examples/carousel-plugin.tsx b/apps/docs/src/registry/examples/carousel-plugin.tsx new file mode 100644 index 00000000..68feea01 --- /dev/null +++ b/apps/docs/src/registry/examples/carousel-plugin.tsx @@ -0,0 +1,42 @@ +import { For } from "solid-js" +import Autoplay from "embla-carousel-autoplay" + +import { Card, CardContent } from "~/registry/ui/card" +import { + Carousel, + CarouselContent, + CarouselItem, + CarouselNext, + CarouselPrevious +} from "~/registry/ui/carousel" + +export default function CarouselPlugin() { + const plugin = Autoplay({ delay: 2000, stopOnInteraction: true }) + + return ( + plugin.play()} + > + + + {(_, index) => ( + +
+ + + {index() + 1} + + +
+
+ )} +
+
+ + +
+ ) +} diff --git a/apps/docs/src/registry/examples/carousel-size.tsx b/apps/docs/src/registry/examples/carousel-size.tsx new file mode 100644 index 00000000..f146839c --- /dev/null +++ b/apps/docs/src/registry/examples/carousel-size.tsx @@ -0,0 +1,39 @@ +import { For } from "solid-js" + +import { Card, CardContent } from "~/registry/ui/card" +import { + Carousel, + CarouselContent, + CarouselItem, + CarouselNext, + CarouselPrevious +} from "~/registry/ui/carousel" + +export default function CarouselSize() { + return ( + + + + {(_, index) => ( + +
+ + + {index() + 1} + + +
+
+ )} +
+
+ + +
+ ) +} diff --git a/apps/docs/src/registry/examples/carousel-spacing.tsx b/apps/docs/src/registry/examples/carousel-spacing.tsx new file mode 100644 index 00000000..b64ed357 --- /dev/null +++ b/apps/docs/src/registry/examples/carousel-spacing.tsx @@ -0,0 +1,34 @@ +import { For } from "solid-js" + +import { Card, CardContent } from "~/registry/ui/card" +import { + Carousel, + CarouselContent, + CarouselItem, + CarouselNext, + CarouselPrevious +} from "~/registry/ui/carousel" + +export default function CarouselSpacing() { + return ( + + + + {(_, index) => ( + +
+ + + {index() + 1} + + +
+
+ )} +
+
+ + +
+ ) +} diff --git a/apps/docs/src/registry/ui/_registry.ts b/apps/docs/src/registry/ui/_registry.ts index d4758d03..b828aa90 100644 --- a/apps/docs/src/registry/ui/_registry.ts +++ b/apps/docs/src/registry/ui/_registry.ts @@ -46,6 +46,18 @@ export const ui: Registry["items"] = [ } ] }, + { + name: "carousel", + type: "registry:ui", + dependencies: ["embla-carousel-solid", "lucide-solid"], + registryDependencies: ["button"], + files: [ + { + path: "ui/carousel.tsx", + type: "registry:ui" + } + ] + }, { name: "checkbox", type: "registry:ui", diff --git a/apps/docs/src/registry/ui/carousel.tsx b/apps/docs/src/registry/ui/carousel.tsx new file mode 100644 index 00000000..66cd310c --- /dev/null +++ b/apps/docs/src/registry/ui/carousel.tsx @@ -0,0 +1,266 @@ +import type { Accessor, ComponentProps, JSX, ValidComponent } from "solid-js" +import { createContext, createEffect, createSignal, mergeProps, onCleanup, splitProps, useContext } from "solid-js" + +import type { PolymorphicProps } from "@kobalte/core/polymorphic" +import createEmblaCarousel from "embla-carousel-solid" +import type { EmblaCarouselType, EmblaOptionsType, EmblaPluginType } from "embla-carousel" +import { ChevronLeft, ChevronRight } from "lucide-solid" + +import { cn } from "~/lib/utils" +import { Button, type ButtonProps } from "~/registry/ui/button" + +type CarouselApi = EmblaCarouselType | undefined +type CarouselOptions = EmblaOptionsType +type CarouselPlugin = EmblaPluginType + +type CarouselProps = { + opts?: CarouselOptions + plugins?: CarouselPlugin[] + orientation?: "horizontal" | "vertical" + setApi?: (api: CarouselApi) => void +} + +type CarouselContextProps = { + carouselRef: ReturnType[0] + api: ReturnType[1] + scrollPrev: () => void + scrollNext: () => void + canScrollPrev: Accessor + canScrollNext: Accessor +} & CarouselProps + +const CarouselContext = createContext(null) + +function useCarousel() { + const context = useContext(CarouselContext) + + if (!context) { + throw new Error("useCarousel must be used within a ") + } + + return context +} + +type CarouselRootProps = ComponentProps & + CarouselProps & { + class?: string | undefined + children?: JSX.Element + } + +const Carousel = ( + rawProps: PolymorphicProps> +) => { + const props = mergeProps({ orientation: "horizontal" as const }, rawProps) + const [local, others] = splitProps(props as CarouselRootProps, [ + "class", + "children", + "opts", + "plugins", + "orientation", + "setApi" + ]) + + const [carouselRef, api] = createEmblaCarousel( + () => ({ + ...local.opts, + axis: local.orientation === "horizontal" ? "x" : "y" + }), + () => local.plugins ?? [] + ) + + const [canScrollPrev, setCanScrollPrev] = createSignal(false) + const [canScrollNext, setCanScrollNext] = createSignal(false) + + const onSelect = (emblaApi: EmblaCarouselType) => { + setCanScrollPrev(emblaApi.canScrollPrev()) + setCanScrollNext(emblaApi.canScrollNext()) + } + + const scrollPrev = () => { + api()?.scrollPrev() + } + + const scrollNext = () => { + api()?.scrollNext() + } + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "ArrowLeft") { + event.preventDefault() + scrollPrev() + } else if (event.key === "ArrowRight") { + event.preventDefault() + scrollNext() + } + } + + createEffect(() => { + const emblaApi = api() + if (!emblaApi || !local.setApi) return + local.setApi(emblaApi) + }) + + createEffect(() => { + const emblaApi = api() + if (!emblaApi) return + + onSelect(emblaApi) + emblaApi.on("reInit", onSelect) + emblaApi.on("select", onSelect) + + onCleanup(() => { + emblaApi.off("select", onSelect) + }) + }) + + return ( + +
+ {local.children} +
+
+ ) +} + +type CarouselContentProps = ComponentProps & { + class?: string | undefined +} + +const CarouselContent = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as CarouselContentProps, ["class"]) + const { carouselRef, orientation } = useCarousel() + + return ( +
+
+
+ ) +} + +type CarouselItemProps = ComponentProps & { + class?: string | undefined +} + +const CarouselItem = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as CarouselItemProps, ["class"]) + const { orientation } = useCarousel() + + return ( +
+ ) +} + +type CarouselPreviousProps = ButtonProps & { + class?: string | undefined +} + +const CarouselPrevious = ( + rawProps: PolymorphicProps> +) => { + const props = mergeProps({ variant: "outline" as const, size: "icon-sm" as const }, rawProps) + const [local, others] = splitProps(props as CarouselPreviousProps, ["class", "variant", "size"]) + const { orientation, scrollPrev, canScrollPrev } = useCarousel() + + return ( + + ) +} + +type CarouselNextProps = ButtonProps & { + class?: string | undefined +} + +const CarouselNext = ( + rawProps: PolymorphicProps> +) => { + const props = mergeProps({ variant: "outline" as const, size: "icon-sm" as const }, rawProps) + const [local, others] = splitProps(props as CarouselNextProps, ["class", "variant", "size"]) + const { orientation, scrollNext, canScrollNext } = useCarousel() + + return ( + + ) +} + +export { + type CarouselApi, + Carousel, + CarouselContent, + CarouselItem, + CarouselPrevious, + CarouselNext, + useCarousel +} diff --git a/apps/docs/src/routes/docs/components/carousel.mdx b/apps/docs/src/routes/docs/components/carousel.mdx new file mode 100644 index 00000000..c9c2bd7d --- /dev/null +++ b/apps/docs/src/routes/docs/components/carousel.mdx @@ -0,0 +1,319 @@ +--- +title: Carousel +description: A carousel with motion and swipe built using Embla. +links: + doc: https://www.embla-carousel.com/get-started/solid + api: https://www.embla-carousel.com/api +--- + +::::tab-group[preview] +:::tab[Preview] + + + +::: +:::tab[Code] + +```file="~/registry/examples/carousel-demo" frame=none showLineNumbers + +``` + +::: +:::: + +## About + +The carousel component is built using the [Embla Carousel](https://www.embla-carousel.com/) library. + +## Installation + +### CLI + +```package-exec +solidui-cli@latest add carousel +``` + +### Manual + +Install the following dependencies: + +```package-install +embla-carousel-solid lucide-solid +``` + +Copy and paste the following code into your project. + +```file="~/registry/ui/carousel.tsx" showLineNumbers + +``` + +## Usage + +```tsx +import { + Carousel, + CarouselContent, + CarouselItem, + CarouselNext, + CarouselPrevious +} from "~/components/ui/carousel"; +``` + +```tsx + + + ... + ... + ... + + + + +``` + +## Examples + +### Sizes + +To set the size of the items, you can use the `basis` utility class on the ``. + +::::tab-group[sizes] +:::tab[Preview] + + + +::: +:::tab[Code] + +```file="~/registry/examples/carousel-size" frame=none showLineNumbers + +``` + +::: +:::: + +```tsx +// 33% of the carousel width. + + + ... + ... + ... + + +``` + +```tsx +// 50% on small screens and 33% on larger screens. + + + ... + ... + ... + + +``` + +### Spacing + +To set the spacing between the items, we use a `pl-[VALUE]` utility on the `` and a negative `-ml-[VALUE]` on the ``. + +::::tab-group[spacing] +:::tab[Preview] + + + +::: +:::tab[Code] + +```file="~/registry/examples/carousel-spacing" frame=none showLineNumbers + +``` + +::: +:::: + +```tsx + + + ... + ... + ... + + +``` + +### Orientation + +Use the `orientation` prop to set the orientation of the carousel. + +::::tab-group[orientation] +:::tab[Preview] + + + +::: +:::tab[Code] + +```file="~/registry/examples/carousel-orientation" frame=none showLineNumbers + +``` + +::: +:::: + +```tsx + + + ... + ... + ... + + +``` + +## Options + +You can pass options to the carousel using the `opts` prop. See the [Embla Carousel docs](https://www.embla-carousel.com/api/options/) for more information. + +```tsx + + + ... + ... + ... + + +``` + +## API + +Use a signal and the `setApi` prop to get an instance of the carousel API. + +::::tab-group[api] +:::tab[Preview] + + + +::: +:::tab[Code] + +```file="~/registry/examples/carousel-api" frame=none showLineNumbers + +``` + +::: +:::: + +```tsx +import { createEffect, createSignal, on } from "solid-js"; +import { type CarouselApi } from "~/components/ui/carousel"; + +export function Example() { + const [api, setApi] = createSignal(); + const [current, setCurrent] = createSignal(0); + const [count, setCount] = createSignal(0); + + createEffect( + on(api, (emblaApi) => { + if (!emblaApi) return; + + setCount(emblaApi.scrollSnapList().length); + setCurrent(emblaApi.selectedScrollSnap() + 1); + + emblaApi.on("select", () => { + setCurrent(emblaApi.selectedScrollSnap() + 1); + }); + }) + ); + + return ( + + + ... + ... + ... + + + ); +} +``` + +## Events + +You can listen to events using the api instance from `setApi`. + +```tsx +import { createEffect, createSignal, on } from "solid-js"; +import { type CarouselApi } from "~/components/ui/carousel"; + +export function Example() { + const [api, setApi] = createSignal(); + + createEffect( + on(api, (emblaApi) => { + if (!emblaApi) return; + + emblaApi.on("select", () => { + // Do something on select. + }); + }) + ); + + return ( + + + ... + ... + ... + + + ); +} +``` + +See the [Embla Carousel docs](https://www.embla-carousel.com/api/events/) for more information on using events. + +## Plugins + +You can use the `plugins` prop to add plugins to the carousel. + +```tsx +import Autoplay from "embla-carousel-autoplay"; + +export function Example() { + return ( + + // ... + + ); +} +``` + +::::tab-group[plugin] +:::tab[Preview] + + + +::: +:::tab[Code] + +```file="~/registry/examples/carousel-plugin" frame=none showLineNumbers + +``` + +::: +:::: + +See the [Embla Carousel docs](https://www.embla-carousel.com/api/plugins/) for more information on using plugins. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 93557fe3..90bb678e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,6 +59,15 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + embla-carousel: + specifier: ^8.6.0 + version: 8.6.0 + embla-carousel-autoplay: + specifier: ^8.6.0 + version: 8.6.0(embla-carousel@8.6.0) + embla-carousel-solid: + specifier: ^8.6.0 + version: 8.6.0(solid-js@1.9.9) lucide-solid: specifier: ^0.562.0 version: 0.562.0(solid-js@1.9.9) @@ -4558,6 +4567,36 @@ packages: resolution: {integrity: sha512-/5UMj9IiGDMOFBnN4i7/Ry5onJrAGSbOGo3s9FEKmwobGq6xw832ccET0CE3CkkMBZ8GJSlUIesZofpyurqDXw==} dev: false + /embla-carousel-autoplay@8.6.0(embla-carousel@8.6.0): + resolution: {integrity: sha512-OBu5G3nwaSXkZCo1A6LTaFMZ8EpkYbwIaH+bPqdBnDGQ2fh4+NbzjXjs2SktoPNKCtflfVMc75njaDHOYXcrsA==} + peerDependencies: + embla-carousel: 8.6.0 + dependencies: + embla-carousel: 8.6.0 + dev: false + + /embla-carousel-reactive-utils@8.6.0(embla-carousel@8.6.0): + resolution: {integrity: sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==} + peerDependencies: + embla-carousel: 8.6.0 + dependencies: + embla-carousel: 8.6.0 + dev: false + + /embla-carousel-solid@8.6.0(solid-js@1.9.9): + resolution: {integrity: sha512-xQQjPZL+CQ4n2KemoeTu0BD9mk8tkVuSMOe/GEnzKM0lAoXY/akSYBNZ4dvJDE7TMeUCAuI6D9742TTLglGq/A==} + peerDependencies: + solid-js: ^1.0.0 + dependencies: + embla-carousel: 8.6.0 + embla-carousel-reactive-utils: 8.6.0(embla-carousel@8.6.0) + solid-js: 1.9.9 + dev: false + + /embla-carousel@8.6.0: + resolution: {integrity: sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==} + dev: false + /emoji-regex-xs@1.0.0: resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==} dev: false From c084057a370e5ba52cd3af7078643ac8fedc224b Mon Sep 17 00:00:00 2001 From: Stefan Karger Date: Tue, 6 Jan 2026 13:12:00 +0100 Subject: [PATCH 4/5] attributes & imports --- apps/docs/src/registry/ui/carousel.tsx | 50 ++++++++++++++------------ 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/apps/docs/src/registry/ui/carousel.tsx b/apps/docs/src/registry/ui/carousel.tsx index 66cd310c..2ad47864 100644 --- a/apps/docs/src/registry/ui/carousel.tsx +++ b/apps/docs/src/registry/ui/carousel.tsx @@ -1,9 +1,17 @@ import type { Accessor, ComponentProps, JSX, ValidComponent } from "solid-js" -import { createContext, createEffect, createSignal, mergeProps, onCleanup, splitProps, useContext } from "solid-js" +import { + createContext, + createEffect, + createSignal, + mergeProps, + onCleanup, + splitProps, + useContext +} from "solid-js" import type { PolymorphicProps } from "@kobalte/core/polymorphic" -import createEmblaCarousel from "embla-carousel-solid" import type { EmblaCarouselType, EmblaOptionsType, EmblaPluginType } from "embla-carousel" +import createEmblaCarousel from "embla-carousel-solid" import { ChevronLeft, ChevronRight } from "lucide-solid" import { cn } from "~/lib/utils" @@ -127,11 +135,11 @@ const Carousel = ( }} >
{local.children} @@ -151,13 +159,9 @@ const CarouselContent = ( const { carouselRef, orientation } = useCarousel() return ( -
+
@@ -176,14 +180,14 @@ const CarouselItem = ( return (
) @@ -202,18 +206,18 @@ const CarouselPrevious = ( return (