diff --git a/docs/components/content/examples/CarouselExample.vue b/docs/components/content/examples/CarouselExample.vue new file mode 100644 index 0000000000..031c810772 --- /dev/null +++ b/docs/components/content/examples/CarouselExample.vue @@ -0,0 +1,16 @@ + + + diff --git a/docs/components/content/examples/CarouselExampleArrows.vue b/docs/components/content/examples/CarouselExampleArrows.vue new file mode 100644 index 0000000000..f064fe1b1d --- /dev/null +++ b/docs/components/content/examples/CarouselExampleArrows.vue @@ -0,0 +1,16 @@ + + + diff --git a/docs/components/content/examples/CarouselExampleArrowsCenter.vue b/docs/components/content/examples/CarouselExampleArrowsCenter.vue new file mode 100644 index 0000000000..1ad2cac399 --- /dev/null +++ b/docs/components/content/examples/CarouselExampleArrowsCenter.vue @@ -0,0 +1,35 @@ + + + diff --git a/docs/components/content/examples/CarouselExampleIndicators.vue b/docs/components/content/examples/CarouselExampleIndicators.vue new file mode 100644 index 0000000000..cd86bf8e34 --- /dev/null +++ b/docs/components/content/examples/CarouselExampleIndicators.vue @@ -0,0 +1,16 @@ + + + diff --git a/docs/components/content/examples/CarouselExampleIndicatorsSize.vue b/docs/components/content/examples/CarouselExampleIndicatorsSize.vue new file mode 100644 index 0000000000..64a583c934 --- /dev/null +++ b/docs/components/content/examples/CarouselExampleIndicatorsSize.vue @@ -0,0 +1,16 @@ + + + diff --git a/docs/components/content/examples/CarouselExampleSize.vue b/docs/components/content/examples/CarouselExampleSize.vue new file mode 100644 index 0000000000..82981b663a --- /dev/null +++ b/docs/components/content/examples/CarouselExampleSize.vue @@ -0,0 +1,16 @@ + + + diff --git a/docs/components/content/examples/CarouselExampleSizeCenter.vue b/docs/components/content/examples/CarouselExampleSizeCenter.vue new file mode 100644 index 0000000000..cd74b265a5 --- /dev/null +++ b/docs/components/content/examples/CarouselExampleSizeCenter.vue @@ -0,0 +1,16 @@ + + + diff --git a/docs/components/content/examples/CarouselExampleSizeFull.vue b/docs/components/content/examples/CarouselExampleSizeFull.vue new file mode 100644 index 0000000000..1e09b45cbe --- /dev/null +++ b/docs/components/content/examples/CarouselExampleSizeFull.vue @@ -0,0 +1,16 @@ + + + diff --git a/docs/components/content/examples/CarouselExampleSlotsDefault.vue b/docs/components/content/examples/CarouselExampleSlotsDefault.vue new file mode 100644 index 0000000000..47bf6b5a6a --- /dev/null +++ b/docs/components/content/examples/CarouselExampleSlotsDefault.vue @@ -0,0 +1,33 @@ + + + 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'