+
+
+
diff --git a/docs/components/content/examples/CarouselExampleSlotsIndicator.vue b/docs/components/content/examples/CarouselExampleSlotsIndicator.vue
new file mode 100644
index 0000000000..52e0768ddb
--- /dev/null
+++ b/docs/components/content/examples/CarouselExampleSlotsIndicator.vue
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/components/content/examples/CarouselExampleSlotsPrevNext.vue b/docs/components/content/examples/CarouselExampleSlotsPrevNext.vue
new file mode 100644
index 0000000000..6461c34f8f
--- /dev/null
+++ b/docs/components/content/examples/CarouselExampleSlotsPrevNext.vue
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/components/content/examples/CarouselExampleSnapEnd.vue b/docs/components/content/examples/CarouselExampleSnapEnd.vue
new file mode 100644
index 0000000000..5eda03f5ab
--- /dev/null
+++ b/docs/components/content/examples/CarouselExampleSnapEnd.vue
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
diff --git a/docs/components/content/examples/CarouselExampleSnapStart.vue b/docs/components/content/examples/CarouselExampleSnapStart.vue
new file mode 100644
index 0000000000..5fb27e5280
--- /dev/null
+++ b/docs/components/content/examples/CarouselExampleSnapStart.vue
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
diff --git a/docs/content/1.getting-started/3.theming.md b/docs/content/1.getting-started/3.theming.md
index a77e0dc62c..4f7f52e719 100644
--- a/docs/content/1.getting-started/3.theming.md
+++ b/docs/content/1.getting-started/3.theming.md
@@ -122,7 +122,7 @@ export default defineAppConfig({
Thanks to [tailwind-merge](https://github.com/dcastil/tailwind-merge), the `app.config.ts` is smartly merged with the default config. This means you don't have to rewrite everything.
-You can change this behaviour by setting `strategy` to `override` in your `app.config.ts`:
+You can change this behavior by setting `strategy` to `override` in your `app.config.ts`:
```ts [app.config.ts]
export default defineAppConfig({
@@ -175,7 +175,7 @@ To change the font of the `label`, you only need to write:
This will smartly replace the `font-medium` by `font-semibold` and prevent any class duplication and any class priority issue.
-You can change this behaviour by setting `strategy` to `override` inside the `ui` prop:
+You can change this behavior by setting `strategy` to `override` inside the `ui` prop:
```vue
+
+
+
+
+```
+::
+
+#### Snap to end
+
+::component-example
+---
+component: 'carousel-example-snap-end'
+---
+
+#code
+```vue
+
+
+
+
+
+```
+::
+
+::callout{icon="i-heroicons-light-bulb" to="https://tailwindcss.com/docs/scroll-snap-align" target="_blank"}
+Learn more about the `scroll-snap-align` property on the Tailwind CSS documentation.
+::
+
+### Size
+
+Each item will take its own size by default in the carousel. You can use the `basis` / `width` utility classes to change this behavior using the `ui` prop:
+
+:component-example{component="carousel-example-size"}
+
+In this example, we used `basis-1/3` to display 3 items at a time but you can also use this to make the carousel full width using `basis-full` and display only one item at a time:
+
+:component-example{component="carousel-example-size-full"}
+
+You can also set a width on the container to center the carousel:
+
+:component-example{component="carousel-example-size-center"}
+
+::callout{icon="i-heroicons-light-bulb" to="https://tailwindcss.com/docs/flex-basis" target="_blank"}
+Learn more about the `flex-basis` property on the Tailwind CSS documentation.
+::
+
+## Navigation
+
+### Arrows
+
+Use the `arrows` prop to enable prev and next buttons, they will be automatically disabled when the carousel reaches the first or last item.
+
+:component-example{component="carousel-example-arrows"}
+
+You can also customize the prev and next buttons using the `prev-button` and `next-button` props:
+
+:component-example{component="carousel-example-arrows-center"}
+
+In this example, we move the buttons outside of the carousel container. You can also change this globally in `ui.carousel.default.prevButton` and `ui.carousel.default.nextButton`.
+
+### Indicators
+
+Use the `indicators` prop to display a list of buttons at the bottom of the carousel to navigate between items.
+
+:component-example{component="carousel-example-indicators"}
+
+The number of indicators will be automatically generated based on the number of items:
+
+:component-example{component="carousel-example-indicators-size"}
+
+## Slots
+
+### `default`
+
+You can put anything inside the default slot, not only images. You will have access to the `item` and `index` properties in the slot scope.
+
+:component-example{component="carousel-example-slots-default"}
+
+### `prev` / `next`
+
+With the `arrows` prop enabled, use the `#prev` and `#next` slots to set the content of the previous and next buttons. You will have access to the `disabled` property and `on-click` method in the slot scope.
+
+:component-example{component="carousel-example-slots-prev-next"}
+
+::callout{icon="i-heroicons-light-bulb"}
+You can customize the position of the buttons through `ui.arrows.wrapper`.
+::
+
+### `indicator`
+
+With the `indicators` prop enabled, use the `#indicator` slot to set the content of the indicators. You will have access to the `active`, `index` properties and `on-click` method in the slot scope.
+
+:component-example{component="carousel-example-slots-indicator"}
+
+::callout{icon="i-heroicons-light-bulb"}
+You can customize the position of the buttons through `ui.indicators.wrapper`.
+::
+
+## Props
+
+:component-props
+
+## Config
+
+:component-preset
diff --git a/src/runtime/components/elements/Carousel.vue b/src/runtime/components/elements/Carousel.vue
new file mode 100644
index 0000000000..905039e59b
--- /dev/null
+++ b/src/runtime/components/elements/Carousel.vue
@@ -0,0 +1,176 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/runtime/components/forms/Input.vue b/src/runtime/components/forms/Input.vue
index f9d4a444e6..333b2ad476 100644
--- a/src/runtime/components/forms/Input.vue
+++ b/src/runtime/components/forms/Input.vue
@@ -211,7 +211,7 @@ export default defineComponent({
updateInput(value)
}
- // Update trimmed input so that it has same behaviour as native input https://github.com/vuejs/core/blob/5ea8a8a4fab4e19a71e123e4d27d051f5e927172/packages/runtime-dom/src/directives/vModel.ts#L63
+ // Update trimmed input so that it has same behavior as native input https://github.com/vuejs/core/blob/5ea8a8a4fab4e19a71e123e4d27d051f5e927172/packages/runtime-dom/src/directives/vModel.ts#L63
if (modelModifiers.value.trim) {
(event.target as HTMLInputElement).value = value.trim()
}
diff --git a/src/runtime/components/forms/Textarea.vue b/src/runtime/components/forms/Textarea.vue
index 0d30b357ed..3e51f80fd0 100644
--- a/src/runtime/components/forms/Textarea.vue
+++ b/src/runtime/components/forms/Textarea.vue
@@ -193,7 +193,7 @@ export default defineComponent({
updateInput(value)
}
- // Update trimmed input so that it has same behaviour as native input
+ // Update trimmed input so that it has same behavior as native input
if (modelModifiers.value.trim) {
(event.target as HTMLInputElement).value = value.trim()
}
diff --git a/src/runtime/composables/useCarouselScroll.ts b/src/runtime/composables/useCarouselScroll.ts
new file mode 100644
index 0000000000..69290f22ca
--- /dev/null
+++ b/src/runtime/composables/useCarouselScroll.ts
@@ -0,0 +1,49 @@
+import { ref, type Ref, onMounted, onUnmounted } from 'vue'
+
+export const useCarouselScroll = (el: Ref) => {
+ const x = ref(0)
+
+ function onMouseDown (e) {
+ el.value.style.scrollSnapType = 'none'
+ el.value.style.scrollBehavior = 'auto'
+
+ x.value = e.pageX
+
+ window.addEventListener('mousemove', onMouseMove)
+ window.addEventListener('mouseup', onMouseUp)
+ }
+
+ function onMouseUp () {
+ el.value.style.removeProperty('scroll-behavior')
+ el.value.style.removeProperty('scroll-snap-type')
+
+ window.removeEventListener('mousemove', onMouseMove)
+ window.removeEventListener('mouseup', onMouseUp)
+ }
+
+ function onMouseMove (e) {
+ e.preventDefault()
+
+ const delta = e.pageX - x.value
+
+ x.value = e.pageX
+
+ el.value.scrollBy(-delta, 0)
+ }
+
+ onMounted(() => {
+ if (!el.value) {
+ return
+ }
+
+ el.value.addEventListener('mousedown', onMouseDown)
+ })
+
+ onUnmounted(() => {
+ if (!el.value) {
+ return
+ }
+
+ el.value.removeEventListener('mousedown', onMouseDown)
+ })
+}
diff --git a/src/runtime/ui.config/elements/carousel.ts b/src/runtime/ui.config/elements/carousel.ts
new file mode 100644
index 0000000000..297af98c9d
--- /dev/null
+++ b/src/runtime/ui.config/elements/carousel.ts
@@ -0,0 +1,26 @@
+export default {
+ wrapper: 'relative',
+ container: 'relative w-full flex overflow-x-auto snap-x snap-mandatory scroll-smooth',
+ item: 'flex flex-none snap-center',
+ arrows: {
+ wrapper: 'flex items-center justify-between'
+ },
+ indicators: {
+ wrapper: 'absolute flex items-center justify-center gap-3 bottom-4 inset-x-0',
+ base: 'rounded-full h-3 w-3',
+ active: 'bg-primary-500 dark:bg-primary-400',
+ inactive: 'bg-gray-100 dark:bg-gray-800 mix-blend-overlay'
+ },
+ default: {
+ prevButton: {
+ color: 'black' as const,
+ class: 'rtl:[&_span:first-child]:rotate-180 absolute left-4 top-1/2 transform -translate-y-1/2 rounded-full',
+ icon: 'i-heroicons-chevron-left-20-solid'
+ },
+ nextButton: {
+ color: 'black' as const,
+ class: 'rtl:[&_span:last-child]:rotate-180 absolute right-4 top-1/2 transform -translate-y-1/2 rounded-full',
+ icon: 'i-heroicons-chevron-right-20-solid '
+ }
+ }
+}
diff --git a/src/runtime/ui.config/index.ts b/src/runtime/ui.config/index.ts
index cf4203be36..4255a09642 100644
--- a/src/runtime/ui.config/index.ts
+++ b/src/runtime/ui.config/index.ts
@@ -15,6 +15,7 @@ export { default as kbd } from './elements/kbd'
export { default as progress } from './elements/progress'
export { default as meter } from './elements/meter'
export { default as meterGroup } from './elements/meterGroup'
+export { default as carousel } from './elements/carousel'
// Forms
export { default as input } from './forms/input'