feat: add carousel component (#227)
* feat: create new carousel component with embala-carousel * feat: create demos for the carousel component * feat: add the default carousel component to the docs * feat: add new-york styling for carousels * feat: add more examples for spacing, size and options * refactor: change ways to better pass the data to parent * feat: add examples for carousel api handling * feat: add example for using embla plugin * chore: add carousel component doc to the table of contents * feat: add focusability on carousel element * fix: update docs * chore: add docs for slot props * feat: expose api for the parent component * chore: include missing filenames * chore: update embla carousel dependency versions * chore: fix typescript error by getting the types from core package * chore: prevent duplicate classes by using class as prop * feat: use slot fallback content so user could change navigation button icons * fix: change attribute inheritance element * chore: update www package.json `scripts` update tsconfig exclude for the strict registry build * refactor: fix embla-carousel types after v8.0.0-rc18 update embla deps * chore: update @vue/tsconfig * chore: run registry * refactor: remove uneended ref * fix: dependencies for embla missing * docs: update carousel for optional plugin installation --------- Co-authored-by: sadeghbarati <sadeghbaratiwork@gmail.com> Co-authored-by: zernonia <zernonia@gmail.com>
This commit is contained in:
parent
4214134e18
commit
97c7417352
|
|
@ -168,6 +168,12 @@ export const docsConfig: DocsConfig = {
|
||||||
href: '/docs/components/card',
|
href: '/docs/components/card',
|
||||||
items: [],
|
items: [],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Carousel',
|
||||||
|
href: '/docs/components/carousel',
|
||||||
|
label: 'New',
|
||||||
|
items: [],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'Checkbox',
|
title: 'Checkbox',
|
||||||
href: '/docs/components/checkbox',
|
href: '/docs/components/checkbox',
|
||||||
|
|
|
||||||
|
|
@ -184,6 +184,48 @@ export const Index = {
|
||||||
component: () => import('../src/lib/registry/default/example/CardWithForm.vue').then(m => m.default),
|
component: () => import('../src/lib/registry/default/example/CardWithForm.vue').then(m => m.default),
|
||||||
files: ['../src/lib/registry/default/example/CardWithForm.vue'],
|
files: ['../src/lib/registry/default/example/CardWithForm.vue'],
|
||||||
},
|
},
|
||||||
|
CarouselApi: {
|
||||||
|
name: 'CarouselApi',
|
||||||
|
type: 'components:example',
|
||||||
|
registryDependencies: ['carousel', 'card'],
|
||||||
|
component: () => import('../src/lib/registry/default/example/CarouselApi.vue').then(m => m.default),
|
||||||
|
files: ['../src/lib/registry/default/example/CarouselApi.vue'],
|
||||||
|
},
|
||||||
|
CarouselDemo: {
|
||||||
|
name: 'CarouselDemo',
|
||||||
|
type: 'components:example',
|
||||||
|
registryDependencies: ['carousel', 'card'],
|
||||||
|
component: () => import('../src/lib/registry/default/example/CarouselDemo.vue').then(m => m.default),
|
||||||
|
files: ['../src/lib/registry/default/example/CarouselDemo.vue'],
|
||||||
|
},
|
||||||
|
CarouselOrientation: {
|
||||||
|
name: 'CarouselOrientation',
|
||||||
|
type: 'components:example',
|
||||||
|
registryDependencies: ['carousel', 'card'],
|
||||||
|
component: () => import('../src/lib/registry/default/example/CarouselOrientation.vue').then(m => m.default),
|
||||||
|
files: ['../src/lib/registry/default/example/CarouselOrientation.vue'],
|
||||||
|
},
|
||||||
|
CarouselPlugin: {
|
||||||
|
name: 'CarouselPlugin',
|
||||||
|
type: 'components:example',
|
||||||
|
registryDependencies: ['carousel', 'card'],
|
||||||
|
component: () => import('../src/lib/registry/default/example/CarouselPlugin.vue').then(m => m.default),
|
||||||
|
files: ['../src/lib/registry/default/example/CarouselPlugin.vue'],
|
||||||
|
},
|
||||||
|
CarouselSize: {
|
||||||
|
name: 'CarouselSize',
|
||||||
|
type: 'components:example',
|
||||||
|
registryDependencies: ['carousel', 'card'],
|
||||||
|
component: () => import('../src/lib/registry/default/example/CarouselSize.vue').then(m => m.default),
|
||||||
|
files: ['../src/lib/registry/default/example/CarouselSize.vue'],
|
||||||
|
},
|
||||||
|
CarouselSpacing: {
|
||||||
|
name: 'CarouselSpacing',
|
||||||
|
type: 'components:example',
|
||||||
|
registryDependencies: ['carousel', 'card'],
|
||||||
|
component: () => import('../src/lib/registry/default/example/CarouselSpacing.vue').then(m => m.default),
|
||||||
|
files: ['../src/lib/registry/default/example/CarouselSpacing.vue'],
|
||||||
|
},
|
||||||
CheckboxDemo: {
|
CheckboxDemo: {
|
||||||
name: 'CheckboxDemo',
|
name: 'CheckboxDemo',
|
||||||
type: 'components:example',
|
type: 'components:example',
|
||||||
|
|
@ -977,6 +1019,48 @@ export const Index = {
|
||||||
component: () => import('../src/lib/registry/new-york/example/CardWithForm.vue').then(m => m.default),
|
component: () => import('../src/lib/registry/new-york/example/CardWithForm.vue').then(m => m.default),
|
||||||
files: ['../src/lib/registry/new-york/example/CardWithForm.vue'],
|
files: ['../src/lib/registry/new-york/example/CardWithForm.vue'],
|
||||||
},
|
},
|
||||||
|
CarouselApi: {
|
||||||
|
name: 'CarouselApi',
|
||||||
|
type: 'components:example',
|
||||||
|
registryDependencies: ['carousel', 'card'],
|
||||||
|
component: () => import('../src/lib/registry/new-york/example/CarouselApi.vue').then(m => m.default),
|
||||||
|
files: ['../src/lib/registry/new-york/example/CarouselApi.vue'],
|
||||||
|
},
|
||||||
|
CarouselDemo: {
|
||||||
|
name: 'CarouselDemo',
|
||||||
|
type: 'components:example',
|
||||||
|
registryDependencies: ['carousel', 'card'],
|
||||||
|
component: () => import('../src/lib/registry/new-york/example/CarouselDemo.vue').then(m => m.default),
|
||||||
|
files: ['../src/lib/registry/new-york/example/CarouselDemo.vue'],
|
||||||
|
},
|
||||||
|
CarouselOrientation: {
|
||||||
|
name: 'CarouselOrientation',
|
||||||
|
type: 'components:example',
|
||||||
|
registryDependencies: ['carousel', 'card'],
|
||||||
|
component: () => import('../src/lib/registry/new-york/example/CarouselOrientation.vue').then(m => m.default),
|
||||||
|
files: ['../src/lib/registry/new-york/example/CarouselOrientation.vue'],
|
||||||
|
},
|
||||||
|
CarouselPlugin: {
|
||||||
|
name: 'CarouselPlugin',
|
||||||
|
type: 'components:example',
|
||||||
|
registryDependencies: ['carousel', 'card'],
|
||||||
|
component: () => import('../src/lib/registry/new-york/example/CarouselPlugin.vue').then(m => m.default),
|
||||||
|
files: ['../src/lib/registry/new-york/example/CarouselPlugin.vue'],
|
||||||
|
},
|
||||||
|
CarouselSize: {
|
||||||
|
name: 'CarouselSize',
|
||||||
|
type: 'components:example',
|
||||||
|
registryDependencies: ['carousel', 'card'],
|
||||||
|
component: () => import('../src/lib/registry/new-york/example/CarouselSize.vue').then(m => m.default),
|
||||||
|
files: ['../src/lib/registry/new-york/example/CarouselSize.vue'],
|
||||||
|
},
|
||||||
|
CarouselSpacing: {
|
||||||
|
name: 'CarouselSpacing',
|
||||||
|
type: 'components:example',
|
||||||
|
registryDependencies: ['carousel', 'card'],
|
||||||
|
component: () => import('../src/lib/registry/new-york/example/CarouselSpacing.vue').then(m => m.default),
|
||||||
|
files: ['../src/lib/registry/new-york/example/CarouselSpacing.vue'],
|
||||||
|
},
|
||||||
CheckboxDemo: {
|
CheckboxDemo: {
|
||||||
name: 'CheckboxDemo',
|
name: 'CheckboxDemo',
|
||||||
type: 'components:example',
|
type: 'components:example',
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,10 @@
|
||||||
"dev": "vitepress dev",
|
"dev": "vitepress dev",
|
||||||
"build": "vitepress build",
|
"build": "vitepress build",
|
||||||
"preview": "vitepress preview",
|
"preview": "vitepress preview",
|
||||||
"typecheck": "vue-tsc --noEmit",
|
"typecheck": "vue-tsc",
|
||||||
"typecheck:registry": "vue-tsc --noEmit -p tsconfig.registry.json",
|
"typecheck:registry": "vue-tsc -p tsconfig.registry.json",
|
||||||
"build:registry": "pnpm typecheck:registry && tsx ./scripts/build-registry.ts"
|
"build:registry": "tsx ./scripts/build-registry.ts",
|
||||||
|
"build:registry-strict": "pnpm typecheck:registry && tsx ./scripts/build-registry.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formkit/auto-animate": "^0.8.0",
|
"@formkit/auto-animate": "^0.8.0",
|
||||||
|
|
@ -27,6 +28,9 @@
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"codesandbox": "^2.2.3",
|
"codesandbox": "^2.2.3",
|
||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
|
"embla-carousel": "8.0.0-rc19",
|
||||||
|
"embla-carousel-autoplay": "8.0.0-rc19",
|
||||||
|
"embla-carousel-vue": "8.0.0-rc19",
|
||||||
"lucide-vue-next": "^0.276.0",
|
"lucide-vue-next": "^0.276.0",
|
||||||
"radix-vue": "^1.2.5",
|
"radix-vue": "^1.2.5",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
|
@ -47,7 +51,7 @@
|
||||||
"@vitejs/plugin-vue-jsx": "^3.0.2",
|
"@vitejs/plugin-vue-jsx": "^3.0.2",
|
||||||
"@vue/compiler-core": "^3.3.7",
|
"@vue/compiler-core": "^3.3.7",
|
||||||
"@vue/compiler-dom": "^3.3.7",
|
"@vue/compiler-dom": "^3.3.7",
|
||||||
"@vue/tsconfig": "^0.4.0",
|
"@vue/tsconfig": "^0.5.1",
|
||||||
"autoprefixer": "^10.4.16",
|
"autoprefixer": "^10.4.16",
|
||||||
"lodash.template": "^4.5.0",
|
"lodash.template": "^4.5.0",
|
||||||
"pathe": "^1.1.1",
|
"pathe": "^1.1.1",
|
||||||
|
|
@ -59,6 +63,6 @@
|
||||||
"unplugin-icons": "^0.17.1",
|
"unplugin-icons": "^0.17.1",
|
||||||
"vite": "^4.5.0",
|
"vite": "^4.5.0",
|
||||||
"vitepress": "^1.0.0-rc.24",
|
"vitepress": "^1.0.0-rc.24",
|
||||||
"vue-tsc": "^1.8.25"
|
"vue-tsc": "^1.8.27"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
283
apps/www/src/content/docs/components/carousel.md
Normal file
283
apps/www/src/content/docs/components/carousel.md
Normal file
|
|
@ -0,0 +1,283 @@
|
||||||
|
---
|
||||||
|
title: Carousel
|
||||||
|
description: A carousel with motion and swipe built using Embla.
|
||||||
|
source: apps/www/src/lib/registry/default/ui/carousel
|
||||||
|
primitive: https://www.embla-carousel.com/api
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
<ComponentPreview name="CarouselDemo" />
|
||||||
|
|
||||||
|
|
||||||
|
## About
|
||||||
|
|
||||||
|
The carousel component is built using the [Embla Carousel](https://www.embla-carousel.com/) library.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx shadcn-vue@latest add carousel
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
Carousel,
|
||||||
|
CarouselContent,
|
||||||
|
CarouselItem,
|
||||||
|
CarouselNext,
|
||||||
|
CarouselPrevious,
|
||||||
|
} from '@/components/ui/carousel'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Carousel>
|
||||||
|
<CarouselContent>
|
||||||
|
<CarouselItem>...</CarouselItem>
|
||||||
|
<CarouselItem>...</CarouselItem>
|
||||||
|
<CarouselItem>...</CarouselItem>
|
||||||
|
</CarouselContent>
|
||||||
|
<CarouselPrevious />
|
||||||
|
<CarouselNext />
|
||||||
|
</Carousel>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Orientation
|
||||||
|
|
||||||
|
Use the `orientation` prop to set the orientation of the carousel.
|
||||||
|
|
||||||
|
<ComponentPreview name="CarouselOrientation" />
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<Carousel orientation="vertical | horizontal">
|
||||||
|
...
|
||||||
|
</Carousel>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sizes
|
||||||
|
|
||||||
|
To set the size of the items, you can use the `basis` utility class on the `<CarouselItem />`.
|
||||||
|
|
||||||
|
<ComponentPreview name="CarouselSize" />
|
||||||
|
|
||||||
|
|
||||||
|
Example
|
||||||
|
|
||||||
|
```vue title="Example" showLineNumbers {4-6}
|
||||||
|
// 33% of the carousel width.
|
||||||
|
<Carousel>
|
||||||
|
<CarouselContent>
|
||||||
|
<CarouselItem class="basis-1/3">...</CarouselItem>
|
||||||
|
<CarouselItem class="basis-1/3">...</CarouselItem>
|
||||||
|
<CarouselItem class="basis-1/3">...</CarouselItem>
|
||||||
|
</CarouselContent>
|
||||||
|
</Carousel>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Responsive
|
||||||
|
|
||||||
|
```vue title="Responsive" showLineNumbers {4-6}
|
||||||
|
// 50% on small screens and 33% on larger screens.
|
||||||
|
<Carousel>
|
||||||
|
<CarouselContent>
|
||||||
|
<CarouselItem class="md:basis-1/2 lg:basis-1/3">...</CarouselItem>
|
||||||
|
<CarouselItem class="md:basis-1/2 lg:basis-1/3">...</CarouselItem>
|
||||||
|
<CarouselItem class="md:basis-1/2 lg:basis-1/3">...</CarouselItem>
|
||||||
|
</CarouselContent>
|
||||||
|
</Carousel>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Spacing
|
||||||
|
|
||||||
|
To set the spacing between the items, we use a `pl-[VALUE]` utility on the `<CarouselItem />` and a negative `-ml-[VALUE]` on the `<CarouselContent />`.
|
||||||
|
|
||||||
|
<Callout class="mt-6">
|
||||||
|
|
||||||
|
**Why:** I tried to use the `gap` property or a `grid` layout on the `
|
||||||
|
CarouselContent` but it required a lot of math and mental effort to get the
|
||||||
|
spacing right. I found `pl-[VALUE]` and `-ml-[VALUE]` utilities much easier to
|
||||||
|
use.
|
||||||
|
<br/><br/>
|
||||||
|
You can always adjust this in your own project if you need to.
|
||||||
|
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
<ComponentPreview name="CarouselSpacing" />
|
||||||
|
|
||||||
|
Example
|
||||||
|
|
||||||
|
```vue showLineNumbers /-ml-4/ /pl-4/
|
||||||
|
<template>
|
||||||
|
<Carousel>
|
||||||
|
<CarouselContent class="-ml-4">
|
||||||
|
<CarouselItem class="pl-4">
|
||||||
|
...
|
||||||
|
</CarouselItem>
|
||||||
|
<CarouselItem class="pl-4">
|
||||||
|
...
|
||||||
|
</CarouselItem>
|
||||||
|
<CarouselItem class="pl-4">
|
||||||
|
...
|
||||||
|
</CarouselItem>
|
||||||
|
</CarouselContent>
|
||||||
|
</Carousel>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
Responsive
|
||||||
|
|
||||||
|
```vue showLineNumbers /-ml-2/ /pl-2/ /md:-ml-4/ /md:pl-4/
|
||||||
|
<template>
|
||||||
|
<Carousel>
|
||||||
|
<CarouselContent class="-ml-2 md:-ml-4">
|
||||||
|
<CarouselItem class="pl-2 md:pl-4">
|
||||||
|
...
|
||||||
|
</CarouselItem>
|
||||||
|
<CarouselItem class="pl-2 md:pl-4">
|
||||||
|
...
|
||||||
|
</CarouselItem>
|
||||||
|
<CarouselItem class="pl-2 md:pl-4">
|
||||||
|
...
|
||||||
|
</CarouselItem>
|
||||||
|
</CarouselContent>
|
||||||
|
</Carousel>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
```vue showLineNumbers {3-6}
|
||||||
|
<template>
|
||||||
|
<Carousel
|
||||||
|
:opts="{
|
||||||
|
align: 'start',
|
||||||
|
loop: true,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<CarouselContent>
|
||||||
|
<CarouselItem>...</CarouselItem>
|
||||||
|
<CarouselItem>...</CarouselItem>
|
||||||
|
<CarouselItem>...</CarouselItem>
|
||||||
|
</CarouselContent>
|
||||||
|
</Carousel>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### Method 1
|
||||||
|
|
||||||
|
Use the `@init-api` emit method on `<Carousel />` component to set the instance of the API.
|
||||||
|
|
||||||
|
<ComponentPreview name="CarouselApi" />
|
||||||
|
|
||||||
|
### Method 2
|
||||||
|
|
||||||
|
You can access it through setting a template ref on the `<Carousel />` component.
|
||||||
|
|
||||||
|
```vue showLineNumbers {2,5,9}
|
||||||
|
<script setup>
|
||||||
|
const carouselContainerRef = ref<InstanceType<typeof Carousel> | null>(null)
|
||||||
|
|
||||||
|
function accessApi() {
|
||||||
|
carouselContainerRef.value?.carouselApi.on('select', () => {})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Carousel ref="carouselContainerRef">
|
||||||
|
...
|
||||||
|
</Carousel>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Events
|
||||||
|
|
||||||
|
You can listen to events using the API. To get the API instance use the `@init-api` emit method on the `<Carousel />` component
|
||||||
|
|
||||||
|
```vue showLineNumbers {5,7-9,25}
|
||||||
|
<script setup>
|
||||||
|
import { nextTick, ref, watch } from 'vue'
|
||||||
|
import { useCarousel } from '@/components/ui/carousel'
|
||||||
|
|
||||||
|
const api = ref<CarouselApi>()
|
||||||
|
|
||||||
|
function setApi(val: CarouselApi) {
|
||||||
|
api.value = val
|
||||||
|
}
|
||||||
|
|
||||||
|
const stop = watch(api, (api) => {
|
||||||
|
if (!api)
|
||||||
|
return
|
||||||
|
|
||||||
|
// Watch only once or use watchOnce() in @vueuse/core
|
||||||
|
nextTick(() => stop())
|
||||||
|
|
||||||
|
api.on('select', () => {
|
||||||
|
// Do something on select.
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Carousel @init-api="setApi">
|
||||||
|
...
|
||||||
|
</Carousel>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
See the [Embla Carousel docs](https://www.embla-carousel.com/api/events/) for more information on using events.
|
||||||
|
|
||||||
|
## Slot Props
|
||||||
|
|
||||||
|
You can get the reactive slot props like `carouselRef, canScrollNext..Prev, scrollNext..Prev` using the `v-slot` directive in the `<Carousel v-slot="slotProps" />` component to extend the functionality.
|
||||||
|
|
||||||
|
```vue showLineNumbers {2}
|
||||||
|
<template>
|
||||||
|
<Carousel v-slot="{ canScrollNext, canScrollPrev }">
|
||||||
|
...
|
||||||
|
<CarouselPrevious v-if="canScrollPrev" />
|
||||||
|
<CarouselNext v-if="canScrollNext" />
|
||||||
|
</Carousel>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Plugins
|
||||||
|
|
||||||
|
You can use the `plugins` prop to add plugins to the carousel.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm i embla-carousel-autoplay
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
```vue showLineNumbers {2,8-10}
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Autoplay from 'embla-carousel-autoplay'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Carousel
|
||||||
|
class="w-full max-w-xs"
|
||||||
|
:plugins="[Autoplay({
|
||||||
|
delay: 2000,
|
||||||
|
})]"
|
||||||
|
>
|
||||||
|
...
|
||||||
|
</Carousel>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
<ComponentPreview name="CarouselPlugin" />
|
||||||
|
|
||||||
|
See the [Embla Carousel docs](https://www.embla-carousel.com/api/plugins/) for more information on using plugins.
|
||||||
51
apps/www/src/lib/registry/default/example/CarouselApi.vue
Normal file
51
apps/www/src/lib/registry/default/example/CarouselApi.vue
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { watchOnce } from '@vueuse/core'
|
||||||
|
import type { CarouselApi } from '@/lib/registry/default/ui/carousel'
|
||||||
|
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from '@/lib/registry/default/ui/carousel'
|
||||||
|
import { Card, CardContent } from '@/lib/registry/default/ui/card'
|
||||||
|
|
||||||
|
const api = ref<CarouselApi>()
|
||||||
|
const totalCount = ref(0)
|
||||||
|
const current = ref(0)
|
||||||
|
|
||||||
|
function setApi(val: CarouselApi) {
|
||||||
|
api.value = val
|
||||||
|
}
|
||||||
|
|
||||||
|
watchOnce(api, (api) => {
|
||||||
|
if (!api)
|
||||||
|
return
|
||||||
|
|
||||||
|
totalCount.value = api.scrollSnapList().length
|
||||||
|
current.value = api.selectedScrollSnap() + 1
|
||||||
|
|
||||||
|
api.on('select', () => {
|
||||||
|
current.value = api.selectedScrollSnap() + 1
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col items-center space-x-2">
|
||||||
|
<Carousel class="w-full max-w-xs" @init-api="setApi">
|
||||||
|
<CarouselContent>
|
||||||
|
<CarouselItem v-for="(_, index) in 5" :key="index">
|
||||||
|
<div class="p-1">
|
||||||
|
<Card>
|
||||||
|
<CardContent class="flex aspect-square items-center justify-center p-6">
|
||||||
|
<span class="text-4xl font-semibold">{{ index + 1 }}</span>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</CarouselItem>
|
||||||
|
</CarouselContent>
|
||||||
|
<CarouselPrevious />
|
||||||
|
<CarouselNext />
|
||||||
|
</Carousel>
|
||||||
|
|
||||||
|
<div class="py-2 text-center text-sm text-muted-foreground">
|
||||||
|
Slide {{ current }} of {{ totalCount }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
24
apps/www/src/lib/registry/default/example/CarouselDemo.vue
Normal file
24
apps/www/src/lib/registry/default/example/CarouselDemo.vue
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from '@/lib/registry/default/ui/carousel'
|
||||||
|
import { Card, CardContent } from '@/lib/registry/default/ui/card'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<Carousel class="w-full max-w-xs">
|
||||||
|
<CarouselContent>
|
||||||
|
<CarouselItem v-for="(_, index) in 5" :key="index">
|
||||||
|
<div class="p-1">
|
||||||
|
<Card>
|
||||||
|
<CardContent class="flex aspect-square items-center justify-center p-6">
|
||||||
|
<span class="text-4xl font-semibold">{{ index + 1 }}</span>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</CarouselItem>
|
||||||
|
</CarouselContent>
|
||||||
|
<CarouselPrevious />
|
||||||
|
<CarouselNext />
|
||||||
|
</Carousel>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from '@/lib/registry/default/ui/carousel'
|
||||||
|
import { Card, CardContent } from '@/lib/registry/default/ui/card'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-1/2">
|
||||||
|
<Carousel
|
||||||
|
orientation="vertical"
|
||||||
|
class="w-full max-w-xs"
|
||||||
|
:opts="{
|
||||||
|
align: 'start',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<CarouselContent class="-mt-1 h-[200px]">
|
||||||
|
<CarouselItem v-for="(_, index) in 5" :key="index" class="p-1 md:basis-1/2">
|
||||||
|
<div class="p-1">
|
||||||
|
<Card>
|
||||||
|
<CardContent class="flex items-center justify-center p-6">
|
||||||
|
<span class="text-3xl font-semibold">{{ index + 1 }}</span>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</CarouselItem>
|
||||||
|
</CarouselContent>
|
||||||
|
<CarouselPrevious />
|
||||||
|
<CarouselNext />
|
||||||
|
</Carousel>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
36
apps/www/src/lib/registry/default/example/CarouselPlugin.vue
Normal file
36
apps/www/src/lib/registry/default/example/CarouselPlugin.vue
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Autoplay from 'embla-carousel-autoplay'
|
||||||
|
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from '@/lib/registry/default/ui/carousel'
|
||||||
|
import { Card, CardContent } from '@/lib/registry/default/ui/card'
|
||||||
|
|
||||||
|
const plugin = Autoplay({
|
||||||
|
delay: 2000,
|
||||||
|
stopOnMouseEnter: true,
|
||||||
|
stopOnInteraction: false,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<Carousel
|
||||||
|
class="w-full max-w-xs"
|
||||||
|
:plugins="[plugin]"
|
||||||
|
@mouseenter="plugin.stop"
|
||||||
|
@mouseleave="[plugin.reset(), plugin.play(), console.log('Runing')];"
|
||||||
|
>
|
||||||
|
<CarouselContent>
|
||||||
|
<CarouselItem v-for="(_, index) in 5" :key="index">
|
||||||
|
<div class="p-1">
|
||||||
|
<Card>
|
||||||
|
<CardContent class="flex aspect-square items-center justify-center p-6">
|
||||||
|
<span class="text-4xl font-semibold">{{ index + 1 }}</span>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</CarouselItem>
|
||||||
|
</CarouselContent>
|
||||||
|
<CarouselPrevious />
|
||||||
|
<CarouselNext />
|
||||||
|
</Carousel>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
29
apps/www/src/lib/registry/default/example/CarouselSize.vue
Normal file
29
apps/www/src/lib/registry/default/example/CarouselSize.vue
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from '@/lib/registry/default/ui/carousel'
|
||||||
|
import { Card, CardContent } from '@/lib/registry/default/ui/card'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<Carousel
|
||||||
|
class="w-full max-w-xs"
|
||||||
|
:opts="{
|
||||||
|
align: 'start',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<CarouselContent>
|
||||||
|
<CarouselItem v-for="(_, index) in 5" :key="index" class="md:basis-1/2 lg:basis-1/3">
|
||||||
|
<div class="p-1">
|
||||||
|
<Card>
|
||||||
|
<CardContent class="flex aspect-square items-center justify-center p-6">
|
||||||
|
<span class="text-3xl font-semibold">{{ index + 1 }}</span>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</CarouselItem>
|
||||||
|
</CarouselContent>
|
||||||
|
<CarouselPrevious />
|
||||||
|
<CarouselNext />
|
||||||
|
</Carousel>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from '@/lib/registry/default/ui/carousel'
|
||||||
|
import { Card, CardContent } from '@/lib/registry/default/ui/card'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<Carousel
|
||||||
|
class="w-full max-w-sm"
|
||||||
|
:opts="{
|
||||||
|
align: 'start',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<CarouselContent class="-ml-1">
|
||||||
|
<CarouselItem v-for="(_, index) in 5" :key="index" class="pl-1 md:basis-1/2 lg:basis-1/3">
|
||||||
|
<div class="p-1">
|
||||||
|
<Card>
|
||||||
|
<CardContent class="flex aspect-square items-center justify-center p-6">
|
||||||
|
<span class="text-2xl font-semibold">{{ index + 1 }}</span>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</CarouselItem>
|
||||||
|
</CarouselContent>
|
||||||
|
<CarouselPrevious />
|
||||||
|
<CarouselNext />
|
||||||
|
</Carousel>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
45
apps/www/src/lib/registry/default/ui/carousel/Carousel.vue
Normal file
45
apps/www/src/lib/registry/default/ui/carousel/Carousel.vue
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import emblaCarouselVue from 'embla-carousel-vue'
|
||||||
|
import { useProvideCarousel } from './useCarousel'
|
||||||
|
import type { CarouselEmits, CarouselProps, WithClassAsProps } from './interface'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<CarouselProps & WithClassAsProps>(), {
|
||||||
|
orientation: 'horizontal',
|
||||||
|
})
|
||||||
|
|
||||||
|
const emits = defineEmits<CarouselEmits>()
|
||||||
|
|
||||||
|
const carouselArgs = useProvideCarousel(props, emits)
|
||||||
|
|
||||||
|
defineExpose(carouselArgs)
|
||||||
|
|
||||||
|
function onKeyDown(event: KeyboardEvent) {
|
||||||
|
const prevKey = props.orientation === 'vertical' ? 'ArrowUp' : 'ArrowLeft'
|
||||||
|
const nextKey = props.orientation === 'vertical' ? 'ArrowDown' : 'ArrowRight'
|
||||||
|
|
||||||
|
if (event.key === prevKey) {
|
||||||
|
event.preventDefault()
|
||||||
|
carouselArgs.scrollPrev()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === nextKey) {
|
||||||
|
event.preventDefault()
|
||||||
|
carouselArgs.scrollNext()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:class="cn('relative', props.class)"
|
||||||
|
role="region"
|
||||||
|
aria-roledescription="carousel"
|
||||||
|
tabindex="0"
|
||||||
|
@keydown="onKeyDown"
|
||||||
|
>
|
||||||
|
<slot v-bind="carouselArgs" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { WithClassAsProps } from './interface'
|
||||||
|
import { useCarousel } from './useCarousel'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps<WithClassAsProps>()
|
||||||
|
|
||||||
|
const { carouselRef, orientation } = useCarousel()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div ref="carouselRef" class="overflow-hidden">
|
||||||
|
<div
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'flex',
|
||||||
|
orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col',
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
v-bind="$attrs"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { WithClassAsProps } from './interface'
|
||||||
|
import { useCarousel } from './useCarousel'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<WithClassAsProps>()
|
||||||
|
|
||||||
|
const { orientation } = useCarousel()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
role="group"
|
||||||
|
aria-roledescription="slide"
|
||||||
|
:class="cn(
|
||||||
|
'min-w-0 shrink-0 grow-0 basis-full',
|
||||||
|
orientation === 'horizontal' ? 'pl-4' : 'pt-4',
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ChevronRight } from 'lucide-vue-next'
|
||||||
|
import { useCarousel } from './useCarousel'
|
||||||
|
import type { WithClassAsProps } from './interface'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { Button } from '@/lib/registry/default/ui/button'
|
||||||
|
|
||||||
|
const props = defineProps<WithClassAsProps>()
|
||||||
|
|
||||||
|
const { orientation, canScrollNext, scrollNext } = useCarousel()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Button
|
||||||
|
:disabled="!canScrollNext"
|
||||||
|
:class="cn(
|
||||||
|
'absolute h-10 w-10 rounded-full p-0',
|
||||||
|
orientation === 'horizontal'
|
||||||
|
? '-right-12 top-1/2 -translate-y-1/2'
|
||||||
|
: '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
variant="outline"
|
||||||
|
@click="scrollNext"
|
||||||
|
>
|
||||||
|
<slot>
|
||||||
|
<ChevronRight class="h-4 w-4 text-current" />
|
||||||
|
</slot>
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ChevronLeft } from 'lucide-vue-next'
|
||||||
|
import { useCarousel } from './useCarousel'
|
||||||
|
import type { WithClassAsProps } from './interface'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { Button } from '@/lib/registry/default/ui/button'
|
||||||
|
|
||||||
|
const props = defineProps<WithClassAsProps>()
|
||||||
|
|
||||||
|
const { orientation, canScrollPrev, scrollPrev } = useCarousel()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Button
|
||||||
|
:disabled="!canScrollPrev"
|
||||||
|
:class="cn(
|
||||||
|
'absolute h-10 w-10 rounded-full p-0',
|
||||||
|
orientation === 'horizontal'
|
||||||
|
? '-left-12 top-1/2 -translate-y-1/2'
|
||||||
|
: '-top-12 left-1/2 -translate-x-1/2 rotate-90',
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
variant="outline"
|
||||||
|
@click="scrollPrev"
|
||||||
|
>
|
||||||
|
<slot>
|
||||||
|
<ChevronLeft class="h-4 w-4 text-current" />
|
||||||
|
</slot>
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
10
apps/www/src/lib/registry/default/ui/carousel/index.ts
Normal file
10
apps/www/src/lib/registry/default/ui/carousel/index.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
export { default as Carousel } from './Carousel.vue'
|
||||||
|
export { default as CarouselContent } from './CarouselContent.vue'
|
||||||
|
export { default as CarouselItem } from './CarouselItem.vue'
|
||||||
|
export { default as CarouselPrevious } from './CarouselPrevious.vue'
|
||||||
|
export { default as CarouselNext } from './CarouselNext.vue'
|
||||||
|
export { useCarousel } from './useCarousel'
|
||||||
|
|
||||||
|
export type {
|
||||||
|
EmblaCarouselType as CarouselApi,
|
||||||
|
} from 'embla-carousel'
|
||||||
20
apps/www/src/lib/registry/default/ui/carousel/interface.ts
Normal file
20
apps/www/src/lib/registry/default/ui/carousel/interface.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import type {
|
||||||
|
EmblaCarouselType as CarouselApi,
|
||||||
|
EmblaOptionsType as CarouselOptions,
|
||||||
|
EmblaPluginType as CarouselPlugin,
|
||||||
|
} from 'embla-carousel'
|
||||||
|
import type { HTMLAttributes, Ref } from 'vue'
|
||||||
|
|
||||||
|
export interface CarouselProps {
|
||||||
|
opts?: CarouselOptions | Ref<CarouselOptions>
|
||||||
|
plugins?: CarouselPlugin[] | Ref<CarouselPlugin[]>
|
||||||
|
orientation?: 'horizontal' | 'vertical'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CarouselEmits {
|
||||||
|
(e: 'init-api', payload: CarouselApi): void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WithClassAsProps {
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}
|
||||||
57
apps/www/src/lib/registry/default/ui/carousel/useCarousel.ts
Normal file
57
apps/www/src/lib/registry/default/ui/carousel/useCarousel.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { createInjectionState } from '@vueuse/core'
|
||||||
|
import emblaCarouselVue from 'embla-carousel-vue'
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import type {
|
||||||
|
EmblaCarouselType as CarouselApi,
|
||||||
|
} from 'embla-carousel'
|
||||||
|
import type { CarouselEmits, CarouselProps } from './interface'
|
||||||
|
|
||||||
|
const [useProvideCarousel, useInjectCarousel] = createInjectionState(
|
||||||
|
({
|
||||||
|
opts, orientation, plugins,
|
||||||
|
}: CarouselProps, emits: CarouselEmits) => {
|
||||||
|
const [emblaNode, emblaApi] = emblaCarouselVue({
|
||||||
|
...opts,
|
||||||
|
axis: orientation === 'horizontal' ? 'x' : 'y',
|
||||||
|
}, plugins)
|
||||||
|
|
||||||
|
function scrollPrev() {
|
||||||
|
emblaApi.value?.scrollPrev()
|
||||||
|
}
|
||||||
|
function scrollNext() {
|
||||||
|
emblaApi.value?.scrollNext()
|
||||||
|
}
|
||||||
|
|
||||||
|
const canScrollNext = ref(true)
|
||||||
|
const canScrollPrev = ref(true)
|
||||||
|
|
||||||
|
function onSelect(api: CarouselApi) {
|
||||||
|
canScrollNext.value = api.canScrollNext()
|
||||||
|
canScrollPrev.value = api.canScrollPrev()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!emblaApi.value)
|
||||||
|
return
|
||||||
|
|
||||||
|
emblaApi.value?.on('init', onSelect)
|
||||||
|
emblaApi.value?.on('reInit', onSelect)
|
||||||
|
emblaApi.value?.on('select', onSelect)
|
||||||
|
|
||||||
|
emits('init-api', emblaApi.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
return { carouselRef: emblaNode, carouselApi: emblaApi, canScrollPrev, canScrollNext, scrollPrev, scrollNext, orientation }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
function useCarousel() {
|
||||||
|
const carouselState = useInjectCarousel()
|
||||||
|
|
||||||
|
if (!carouselState)
|
||||||
|
throw new Error('useCarousel must be used within a <Carousel />')
|
||||||
|
|
||||||
|
return carouselState
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useCarousel, useProvideCarousel }
|
||||||
51
apps/www/src/lib/registry/new-york/example/CarouselApi.vue
Normal file
51
apps/www/src/lib/registry/new-york/example/CarouselApi.vue
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { watchOnce } from '@vueuse/core'
|
||||||
|
import type { CarouselApi } from '@/lib/registry/new-york/ui/carousel'
|
||||||
|
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from '@/lib/registry/new-york/ui/carousel'
|
||||||
|
import { Card, CardContent } from '@/lib/registry/new-york/ui/card'
|
||||||
|
|
||||||
|
const api = ref<CarouselApi>()
|
||||||
|
const totalCount = ref(0)
|
||||||
|
const current = ref(0)
|
||||||
|
|
||||||
|
function setApi(val: CarouselApi) {
|
||||||
|
api.value = val
|
||||||
|
}
|
||||||
|
|
||||||
|
watchOnce(api, (api) => {
|
||||||
|
if (!api)
|
||||||
|
return
|
||||||
|
|
||||||
|
totalCount.value = api.scrollSnapList().length
|
||||||
|
current.value = api.selectedScrollSnap() + 1
|
||||||
|
|
||||||
|
api.on('select', () => {
|
||||||
|
current.value = api.selectedScrollSnap() + 1
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col items-center space-x-2">
|
||||||
|
<Carousel class="w-full max-w-xs" @init-api="setApi">
|
||||||
|
<CarouselContent>
|
||||||
|
<CarouselItem v-for="(_, index) in 5" :key="index">
|
||||||
|
<div class="p-1">
|
||||||
|
<Card>
|
||||||
|
<CardContent class="flex aspect-square items-center justify-center p-6">
|
||||||
|
<span class="text-4xl font-semibold">{{ index + 1 }}</span>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</CarouselItem>
|
||||||
|
</CarouselContent>
|
||||||
|
<CarouselPrevious />
|
||||||
|
<CarouselNext />
|
||||||
|
</Carousel>
|
||||||
|
|
||||||
|
<div class="py-2 text-center text-sm text-muted-foreground">
|
||||||
|
Slide {{ current }} of {{ totalCount }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
24
apps/www/src/lib/registry/new-york/example/CarouselDemo.vue
Normal file
24
apps/www/src/lib/registry/new-york/example/CarouselDemo.vue
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from '@/lib/registry/new-york/ui/carousel'
|
||||||
|
import { Card, CardContent } from '@/lib/registry/new-york/ui/card'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<Carousel class="w-full max-w-xs">
|
||||||
|
<CarouselContent>
|
||||||
|
<CarouselItem v-for="(_, index) in 5" :key="index">
|
||||||
|
<div class="p-1">
|
||||||
|
<Card>
|
||||||
|
<CardContent class="flex aspect-square items-center justify-center p-6">
|
||||||
|
<span class="text-4xl font-semibold">{{ index + 1 }}</span>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</CarouselItem>
|
||||||
|
</CarouselContent>
|
||||||
|
<CarouselPrevious />
|
||||||
|
<CarouselNext />
|
||||||
|
</Carousel>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from '@/lib/registry/new-york/ui/carousel'
|
||||||
|
import { Card, CardContent } from '@/lib/registry/new-york/ui/card'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-1/2">
|
||||||
|
<Carousel
|
||||||
|
orientation="vertical"
|
||||||
|
class="w-full max-w-xs"
|
||||||
|
:opts="{
|
||||||
|
align: 'start',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<CarouselContent class="-mt-1 h-[200px]">
|
||||||
|
<CarouselItem v-for="(_, index) in 5" :key="index" class="p-1 md:basis-1/2">
|
||||||
|
<div class="p-1">
|
||||||
|
<Card>
|
||||||
|
<CardContent class="flex items-center justify-center p-6">
|
||||||
|
<span class="text-3xl font-semibold">{{ index + 1 }}</span>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</CarouselItem>
|
||||||
|
</CarouselContent>
|
||||||
|
<CarouselPrevious />
|
||||||
|
<CarouselNext />
|
||||||
|
</Carousel>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Autoplay from 'embla-carousel-autoplay'
|
||||||
|
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from '@/lib/registry/new-york/ui/carousel'
|
||||||
|
import { Card, CardContent } from '@/lib/registry/new-york/ui/card'
|
||||||
|
|
||||||
|
const plugin = Autoplay({
|
||||||
|
delay: 2000,
|
||||||
|
stopOnMouseEnter: true,
|
||||||
|
stopOnInteraction: false,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<Carousel
|
||||||
|
class="w-full max-w-xs"
|
||||||
|
:plugins="[plugin]"
|
||||||
|
@mouseenter="plugin.stop"
|
||||||
|
@mouseleave="[plugin.reset(), plugin.play(), console.log('Runing')];"
|
||||||
|
>
|
||||||
|
<CarouselContent>
|
||||||
|
<CarouselItem v-for="(_, index) in 5" :key="index">
|
||||||
|
<div class="p-1">
|
||||||
|
<Card>
|
||||||
|
<CardContent class="flex aspect-square items-center justify-center p-6">
|
||||||
|
<span class="text-4xl font-semibold">{{ index + 1 }}</span>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</CarouselItem>
|
||||||
|
</CarouselContent>
|
||||||
|
<CarouselPrevious />
|
||||||
|
<CarouselNext />
|
||||||
|
</Carousel>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
29
apps/www/src/lib/registry/new-york/example/CarouselSize.vue
Normal file
29
apps/www/src/lib/registry/new-york/example/CarouselSize.vue
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from '@/lib/registry/new-york/ui/carousel'
|
||||||
|
import { Card, CardContent } from '@/lib/registry/new-york/ui/card'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<Carousel
|
||||||
|
class="w-full max-w-xs"
|
||||||
|
:opts="{
|
||||||
|
align: 'start',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<CarouselContent>
|
||||||
|
<CarouselItem v-for="(_, index) in 5" :key="index" class="md:basis-1/2 lg:basis-1/3">
|
||||||
|
<div class="p-1">
|
||||||
|
<Card>
|
||||||
|
<CardContent class="flex aspect-square items-center justify-center p-6">
|
||||||
|
<span class="text-3xl font-semibold">{{ index + 1 }}</span>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</CarouselItem>
|
||||||
|
</CarouselContent>
|
||||||
|
<CarouselPrevious />
|
||||||
|
<CarouselNext />
|
||||||
|
</Carousel>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from '@/lib/registry/new-york/ui/carousel'
|
||||||
|
import { Card, CardContent } from '@/lib/registry/new-york/ui/card'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<Carousel
|
||||||
|
class="w-full max-w-sm"
|
||||||
|
:opts="{
|
||||||
|
align: 'start',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<CarouselContent class="-ml-1">
|
||||||
|
<CarouselItem v-for="(_, index) in 5" :key="index" class="pl-1 md:basis-1/2 lg:basis-1/3">
|
||||||
|
<div class="p-1">
|
||||||
|
<Card>
|
||||||
|
<CardContent class="flex aspect-square items-center justify-center p-6">
|
||||||
|
<span class="text-2xl font-semibold">{{ index + 1 }}</span>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</CarouselItem>
|
||||||
|
</CarouselContent>
|
||||||
|
<CarouselPrevious />
|
||||||
|
<CarouselNext />
|
||||||
|
</Carousel>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
45
apps/www/src/lib/registry/new-york/ui/carousel/Carousel.vue
Normal file
45
apps/www/src/lib/registry/new-york/ui/carousel/Carousel.vue
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import emblaCarouselVue from 'embla-carousel-vue'
|
||||||
|
import { useProvideCarousel } from './useCarousel'
|
||||||
|
import type { CarouselEmits, CarouselProps, WithClassAsProps } from './interface'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<CarouselProps & WithClassAsProps>(), {
|
||||||
|
orientation: 'horizontal',
|
||||||
|
})
|
||||||
|
|
||||||
|
const emits = defineEmits<CarouselEmits>()
|
||||||
|
|
||||||
|
const carouselArgs = useProvideCarousel(props, emits)
|
||||||
|
|
||||||
|
defineExpose(carouselArgs)
|
||||||
|
|
||||||
|
function onKeyDown(event: KeyboardEvent) {
|
||||||
|
const prevKey = props.orientation === 'vertical' ? 'ArrowUp' : 'ArrowLeft'
|
||||||
|
const nextKey = props.orientation === 'vertical' ? 'ArrowDown' : 'ArrowRight'
|
||||||
|
|
||||||
|
if (event.key === prevKey) {
|
||||||
|
event.preventDefault()
|
||||||
|
carouselArgs.scrollPrev()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === nextKey) {
|
||||||
|
event.preventDefault()
|
||||||
|
carouselArgs.scrollNext()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:class="cn('relative', props.class)"
|
||||||
|
role="region"
|
||||||
|
aria-roledescription="carousel"
|
||||||
|
tabindex="0"
|
||||||
|
@keydown="onKeyDown"
|
||||||
|
>
|
||||||
|
<slot v-bind="carouselArgs" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useCarousel } from './useCarousel'
|
||||||
|
import type { WithClassAsProps } from './interface'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps<WithClassAsProps>()
|
||||||
|
|
||||||
|
const { carouselRef, orientation } = useCarousel()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div ref="carouselRef" class="overflow-hidden">
|
||||||
|
<div
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'flex',
|
||||||
|
orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col',
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
v-bind="$attrs"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useCarousel } from './useCarousel'
|
||||||
|
import type { WithClassAsProps } from './interface'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<WithClassAsProps>()
|
||||||
|
|
||||||
|
const { orientation } = useCarousel()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
role="group"
|
||||||
|
aria-roledescription="slide"
|
||||||
|
:class="cn(
|
||||||
|
'min-w-0 shrink-0 grow-0 basis-full',
|
||||||
|
orientation === 'horizontal' ? 'pl-4' : 'pt-4',
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ChevronRightIcon } from '@radix-icons/vue'
|
||||||
|
import { useCarousel } from './useCarousel'
|
||||||
|
import type { WithClassAsProps } from './interface'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { Button } from '@/lib/registry/new-york/ui/button'
|
||||||
|
|
||||||
|
const props = defineProps<WithClassAsProps>()
|
||||||
|
|
||||||
|
const { orientation, canScrollNext, scrollNext } = useCarousel()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Button
|
||||||
|
:disabled="!canScrollNext"
|
||||||
|
:class="cn(
|
||||||
|
'absolute h-10 w-10 rounded-full p-0',
|
||||||
|
orientation === 'horizontal'
|
||||||
|
? '-right-12 top-1/2 -translate-y-1/2'
|
||||||
|
: '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
variant="outline"
|
||||||
|
@click="scrollNext"
|
||||||
|
>
|
||||||
|
<slot>
|
||||||
|
<ChevronRightIcon class="h-4 w-4 text-current" />
|
||||||
|
</slot>
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ChevronLeftIcon } from '@radix-icons/vue'
|
||||||
|
import { useCarousel } from './useCarousel'
|
||||||
|
import type { WithClassAsProps } from './interface'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { Button } from '@/lib/registry/new-york/ui/button'
|
||||||
|
|
||||||
|
const props = defineProps<WithClassAsProps>()
|
||||||
|
|
||||||
|
const { orientation, canScrollPrev, scrollPrev } = useCarousel()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Button
|
||||||
|
:disabled="!canScrollPrev"
|
||||||
|
:class="cn(
|
||||||
|
'absolute h-10 w-10 rounded-full p-0',
|
||||||
|
orientation === 'horizontal'
|
||||||
|
? '-left-12 top-1/2 -translate-y-1/2'
|
||||||
|
: '-top-12 left-1/2 -translate-x-1/2 rotate-90',
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
variant="outline"
|
||||||
|
@click="scrollPrev"
|
||||||
|
>
|
||||||
|
<slot>
|
||||||
|
<ChevronLeftIcon class="h-4 w-4 text-current" />
|
||||||
|
</slot>
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
10
apps/www/src/lib/registry/new-york/ui/carousel/index.ts
Normal file
10
apps/www/src/lib/registry/new-york/ui/carousel/index.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
export { default as Carousel } from './Carousel.vue'
|
||||||
|
export { default as CarouselContent } from './CarouselContent.vue'
|
||||||
|
export { default as CarouselItem } from './CarouselItem.vue'
|
||||||
|
export { default as CarouselPrevious } from './CarouselPrevious.vue'
|
||||||
|
export { default as CarouselNext } from './CarouselNext.vue'
|
||||||
|
export { useCarousel } from './useCarousel'
|
||||||
|
|
||||||
|
export type {
|
||||||
|
EmblaCarouselType as CarouselApi,
|
||||||
|
} from 'embla-carousel'
|
||||||
20
apps/www/src/lib/registry/new-york/ui/carousel/interface.ts
Normal file
20
apps/www/src/lib/registry/new-york/ui/carousel/interface.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import type {
|
||||||
|
EmblaCarouselType as CarouselApi,
|
||||||
|
EmblaOptionsType as CarouselOptions,
|
||||||
|
EmblaPluginType as CarouselPlugin,
|
||||||
|
} from 'embla-carousel'
|
||||||
|
import type { HTMLAttributes, Ref } from 'vue'
|
||||||
|
|
||||||
|
export interface CarouselProps {
|
||||||
|
opts?: CarouselOptions | Ref<CarouselOptions>
|
||||||
|
plugins?: CarouselPlugin[] | Ref<CarouselPlugin[]>
|
||||||
|
orientation?: 'horizontal' | 'vertical'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CarouselEmits {
|
||||||
|
(e: 'init-api', payload: CarouselApi): void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WithClassAsProps {
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { createInjectionState } from '@vueuse/core'
|
||||||
|
import emblaCarouselVue from 'embla-carousel-vue'
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import type {
|
||||||
|
EmblaCarouselType as CarouselApi,
|
||||||
|
} from 'embla-carousel'
|
||||||
|
import type { CarouselEmits, CarouselProps } from './interface'
|
||||||
|
|
||||||
|
const [useProvideCarousel, useInjectCarousel] = createInjectionState(
|
||||||
|
({
|
||||||
|
opts, orientation, plugins,
|
||||||
|
}: CarouselProps, emits: CarouselEmits) => {
|
||||||
|
const [emblaNode, emblaApi] = emblaCarouselVue({
|
||||||
|
...opts,
|
||||||
|
axis: orientation === 'horizontal' ? 'x' : 'y',
|
||||||
|
}, plugins)
|
||||||
|
|
||||||
|
function scrollPrev() {
|
||||||
|
emblaApi.value?.scrollPrev()
|
||||||
|
}
|
||||||
|
function scrollNext() {
|
||||||
|
emblaApi.value?.scrollNext()
|
||||||
|
}
|
||||||
|
|
||||||
|
const canScrollNext = ref(true)
|
||||||
|
const canScrollPrev = ref(true)
|
||||||
|
|
||||||
|
function onSelect(api: CarouselApi) {
|
||||||
|
canScrollNext.value = api.canScrollNext()
|
||||||
|
canScrollPrev.value = api.canScrollPrev()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!emblaApi.value)
|
||||||
|
return
|
||||||
|
|
||||||
|
emblaApi.value?.on('init', onSelect)
|
||||||
|
emblaApi.value?.on('reInit', onSelect)
|
||||||
|
emblaApi.value?.on('select', onSelect)
|
||||||
|
|
||||||
|
emits('init-api', emblaApi.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
return { carouselRef: emblaNode, carouselApi: emblaApi, canScrollPrev, canScrollNext, scrollPrev, scrollNext, orientation }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
function useCarousel() {
|
||||||
|
const carouselState = useInjectCarousel()
|
||||||
|
|
||||||
|
if (!carouselState)
|
||||||
|
throw new Error('useCarousel must be used within a <Carousel />')
|
||||||
|
|
||||||
|
return carouselState
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useCarousel, useProvideCarousel }
|
||||||
|
|
@ -8,6 +8,7 @@ const DEPENDENCIES = new Map<string, string[]>([
|
||||||
['@vueuse/core', []],
|
['@vueuse/core', []],
|
||||||
['v-calendar', []],
|
['v-calendar', []],
|
||||||
['@tanstack/vue-table', []],
|
['@tanstack/vue-table', []],
|
||||||
|
['embla-carousel-vue', ['embla-carousel']],
|
||||||
['vee-validate', ['@vee-validate/zod', 'zod']],
|
['vee-validate', ['@vee-validate/zod', 'zod']],
|
||||||
])
|
])
|
||||||
// Some dependencies latest tag were not compatible with Vue3.
|
// Some dependencies latest tag were not compatible with Vue3.
|
||||||
|
|
@ -133,7 +134,7 @@ async function buildUIRegistry(componentPath: string, componentName: string) {
|
||||||
|
|
||||||
async function getDependencies(filename: string) {
|
async function getDependencies(filename: string) {
|
||||||
const code = await readFile(filename, { encoding: 'utf8' })
|
const code = await readFile(filename, { encoding: 'utf8' })
|
||||||
const parsed = parse(code)
|
const parsed = parse(code, { filename })
|
||||||
|
|
||||||
const registryDependencies = new Set<string>()
|
const registryDependencies = new Set<string>()
|
||||||
const dependencies = new Set<string>()
|
const dependencies = new Set<string>()
|
||||||
|
|
|
||||||
|
|
@ -130,6 +130,28 @@
|
||||||
],
|
],
|
||||||
"type": "components:ui"
|
"type": "components:ui"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "carousel",
|
||||||
|
"dependencies": [
|
||||||
|
"embla-carousel-vue",
|
||||||
|
"embla-carousel"
|
||||||
|
],
|
||||||
|
"registryDependencies": [
|
||||||
|
"utils",
|
||||||
|
"button"
|
||||||
|
],
|
||||||
|
"files": [
|
||||||
|
"ui/carousel/Carousel.vue",
|
||||||
|
"ui/carousel/CarouselContent.vue",
|
||||||
|
"ui/carousel/CarouselItem.vue",
|
||||||
|
"ui/carousel/CarouselNext.vue",
|
||||||
|
"ui/carousel/CarouselPrevious.vue",
|
||||||
|
"ui/carousel/index.ts",
|
||||||
|
"ui/carousel/interface.ts",
|
||||||
|
"ui/carousel/useCarousel.ts"
|
||||||
|
],
|
||||||
|
"type": "components:ui"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "checkbox",
|
"name": "checkbox",
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
|
|
|
||||||
46
apps/www/src/public/registry/styles/default/carousel.json
Normal file
46
apps/www/src/public/registry/styles/default/carousel.json
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
{
|
||||||
|
"name": "carousel",
|
||||||
|
"dependencies": [
|
||||||
|
"embla-carousel-vue",
|
||||||
|
"embla-carousel"
|
||||||
|
],
|
||||||
|
"registryDependencies": [
|
||||||
|
"utils",
|
||||||
|
"button"
|
||||||
|
],
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"name": "Carousel.vue",
|
||||||
|
"content": "<script setup lang=\"ts\">\nimport emblaCarouselVue from 'embla-carousel-vue'\nimport { useProvideCarousel } from './useCarousel'\nimport type { CarouselEmits, CarouselProps, WithClassAsProps } from './interface'\nimport { cn } from '@/lib/utils'\n\nconst props = withDefaults(defineProps<CarouselProps & WithClassAsProps>(), {\n orientation: 'horizontal',\n})\n\nconst emits = defineEmits<CarouselEmits>()\n\nconst carouselArgs = useProvideCarousel(props, emits)\n\ndefineExpose(carouselArgs)\n\nfunction onKeyDown(event: KeyboardEvent) {\n const prevKey = props.orientation === 'vertical' ? 'ArrowUp' : 'ArrowLeft'\n const nextKey = props.orientation === 'vertical' ? 'ArrowDown' : 'ArrowRight'\n\n if (event.key === prevKey) {\n event.preventDefault()\n carouselArgs.scrollPrev()\n\n return\n }\n\n if (event.key === nextKey) {\n event.preventDefault()\n carouselArgs.scrollNext()\n }\n}\n</script>\n\n<template>\n <div\n :class=\"cn('relative', props.class)\"\n role=\"region\"\n aria-roledescription=\"carousel\"\n tabindex=\"0\"\n @keydown=\"onKeyDown\"\n >\n <slot v-bind=\"carouselArgs\" />\n </div>\n</template>\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "CarouselContent.vue",
|
||||||
|
"content": "<script setup lang=\"ts\">\nimport type { WithClassAsProps } from './interface'\nimport { useCarousel } from './useCarousel'\nimport { cn } from '@/lib/utils'\n\ndefineOptions({\n inheritAttrs: false,\n})\n\nconst props = defineProps<WithClassAsProps>()\n\nconst { carouselRef, orientation } = useCarousel()\n</script>\n\n<template>\n <div ref=\"carouselRef\" class=\"overflow-hidden\">\n <div\n :class=\"\n cn(\n 'flex',\n orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col',\n props.class,\n )\"\n v-bind=\"$attrs\"\n >\n <slot />\n </div>\n </div>\n</template>\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "CarouselItem.vue",
|
||||||
|
"content": "<script setup lang=\"ts\">\nimport type { WithClassAsProps } from './interface'\nimport { useCarousel } from './useCarousel'\nimport { cn } from '@/lib/utils'\n\nconst props = defineProps<WithClassAsProps>()\n\nconst { orientation } = useCarousel()\n</script>\n\n<template>\n <div\n role=\"group\"\n aria-roledescription=\"slide\"\n :class=\"cn(\n 'min-w-0 shrink-0 grow-0 basis-full',\n orientation === 'horizontal' ? 'pl-4' : 'pt-4',\n props.class,\n )\"\n >\n <slot />\n </div>\n</template>\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "CarouselNext.vue",
|
||||||
|
"content": "<script setup lang=\"ts\">\nimport { ChevronRight } from 'lucide-vue-next'\nimport { useCarousel } from './useCarousel'\nimport type { WithClassAsProps } from './interface'\nimport { cn } from '@/lib/utils'\nimport { Button } from '@/lib/registry/default/ui/button'\n\nconst props = defineProps<WithClassAsProps>()\n\nconst { orientation, canScrollNext, scrollNext } = useCarousel()\n</script>\n\n<template>\n <Button\n :disabled=\"!canScrollNext\"\n :class=\"cn(\n 'absolute h-10 w-10 rounded-full p-0',\n orientation === 'horizontal'\n ? '-right-12 top-1/2 -translate-y-1/2'\n : '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',\n props.class,\n )\"\n variant=\"outline\"\n @click=\"scrollNext\"\n >\n <slot>\n <ChevronRight class=\"h-4 w-4 text-current\" />\n </slot>\n </Button>\n</template>\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "CarouselPrevious.vue",
|
||||||
|
"content": "<script setup lang=\"ts\">\nimport { ChevronLeft } from 'lucide-vue-next'\nimport { useCarousel } from './useCarousel'\nimport type { WithClassAsProps } from './interface'\nimport { cn } from '@/lib/utils'\nimport { Button } from '@/lib/registry/default/ui/button'\n\nconst props = defineProps<WithClassAsProps>()\n\nconst { orientation, canScrollPrev, scrollPrev } = useCarousel()\n</script>\n\n<template>\n <Button\n :disabled=\"!canScrollPrev\"\n :class=\"cn(\n 'absolute h-10 w-10 rounded-full p-0',\n orientation === 'horizontal'\n ? '-left-12 top-1/2 -translate-y-1/2'\n : '-top-12 left-1/2 -translate-x-1/2 rotate-90',\n props.class,\n )\"\n variant=\"outline\"\n @click=\"scrollPrev\"\n >\n <slot>\n <ChevronLeft class=\"h-4 w-4 text-current\" />\n </slot>\n </Button>\n</template>\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index.ts",
|
||||||
|
"content": "export { default as Carousel } from './Carousel.vue'\nexport { default as CarouselContent } from './CarouselContent.vue'\nexport { default as CarouselItem } from './CarouselItem.vue'\nexport { default as CarouselPrevious } from './CarouselPrevious.vue'\nexport { default as CarouselNext } from './CarouselNext.vue'\nexport { useCarousel } from './useCarousel'\n\nexport type {\n EmblaCarouselType as CarouselApi,\n} from 'embla-carousel'\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "interface.ts",
|
||||||
|
"content": "import type {\n EmblaCarouselType as CarouselApi,\n EmblaOptionsType as CarouselOptions,\n EmblaPluginType as CarouselPlugin,\n} from 'embla-carousel'\nimport type { HTMLAttributes, Ref } from 'vue'\n\nexport interface CarouselProps {\n opts?: CarouselOptions | Ref<CarouselOptions>\n plugins?: CarouselPlugin[] | Ref<CarouselPlugin[]>\n orientation?: 'horizontal' | 'vertical'\n}\n\nexport interface CarouselEmits {\n (e: 'init-api', payload: CarouselApi): void\n}\n\nexport interface WithClassAsProps {\n class?: HTMLAttributes['class']\n}\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "useCarousel.ts",
|
||||||
|
"content": "import { createInjectionState } from '@vueuse/core'\nimport emblaCarouselVue from 'embla-carousel-vue'\nimport { onMounted, ref } from 'vue'\nimport type {\n EmblaCarouselType as CarouselApi,\n} from 'embla-carousel'\nimport type { CarouselEmits, CarouselProps } from './interface'\n\nconst [useProvideCarousel, useInjectCarousel] = createInjectionState(\n ({\n opts, orientation, plugins,\n }: CarouselProps, emits: CarouselEmits) => {\n const [emblaNode, emblaApi] = emblaCarouselVue({\n ...opts,\n axis: orientation === 'horizontal' ? 'x' : 'y',\n }, plugins)\n\n function scrollPrev() {\n emblaApi.value?.scrollPrev()\n }\n function scrollNext() {\n emblaApi.value?.scrollNext()\n }\n\n const canScrollNext = ref(true)\n const canScrollPrev = ref(true)\n\n function onSelect(api: CarouselApi) {\n canScrollNext.value = api.canScrollNext()\n canScrollPrev.value = api.canScrollPrev()\n }\n\n onMounted(() => {\n if (!emblaApi.value)\n return\n\n emblaApi.value?.on('init', onSelect)\n emblaApi.value?.on('reInit', onSelect)\n emblaApi.value?.on('select', onSelect)\n\n emits('init-api', emblaApi.value)\n })\n\n return { carouselRef: emblaNode, carouselApi: emblaApi, canScrollPrev, canScrollNext, scrollPrev, scrollNext, orientation }\n },\n)\n\nfunction useCarousel() {\n const carouselState = useInjectCarousel()\n\n if (!carouselState)\n throw new Error('useCarousel must be used within a <Carousel />')\n\n return carouselState\n}\n\nexport { useCarousel, useProvideCarousel }\n"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "components:ui"
|
||||||
|
}
|
||||||
46
apps/www/src/public/registry/styles/new-york/carousel.json
Normal file
46
apps/www/src/public/registry/styles/new-york/carousel.json
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
{
|
||||||
|
"name": "carousel",
|
||||||
|
"dependencies": [
|
||||||
|
"embla-carousel-vue",
|
||||||
|
"embla-carousel"
|
||||||
|
],
|
||||||
|
"registryDependencies": [
|
||||||
|
"utils",
|
||||||
|
"button"
|
||||||
|
],
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"name": "Carousel.vue",
|
||||||
|
"content": "<script setup lang=\"ts\">\nimport emblaCarouselVue from 'embla-carousel-vue'\nimport { useProvideCarousel } from './useCarousel'\nimport type { CarouselEmits, CarouselProps, WithClassAsProps } from './interface'\nimport { cn } from '@/lib/utils'\n\nconst props = withDefaults(defineProps<CarouselProps & WithClassAsProps>(), {\n orientation: 'horizontal',\n})\n\nconst emits = defineEmits<CarouselEmits>()\n\nconst carouselArgs = useProvideCarousel(props, emits)\n\ndefineExpose(carouselArgs)\n\nfunction onKeyDown(event: KeyboardEvent) {\n const prevKey = props.orientation === 'vertical' ? 'ArrowUp' : 'ArrowLeft'\n const nextKey = props.orientation === 'vertical' ? 'ArrowDown' : 'ArrowRight'\n\n if (event.key === prevKey) {\n event.preventDefault()\n carouselArgs.scrollPrev()\n\n return\n }\n\n if (event.key === nextKey) {\n event.preventDefault()\n carouselArgs.scrollNext()\n }\n}\n</script>\n\n<template>\n <div\n :class=\"cn('relative', props.class)\"\n role=\"region\"\n aria-roledescription=\"carousel\"\n tabindex=\"0\"\n @keydown=\"onKeyDown\"\n >\n <slot v-bind=\"carouselArgs\" />\n </div>\n</template>\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "CarouselContent.vue",
|
||||||
|
"content": "<script setup lang=\"ts\">\nimport { useCarousel } from './useCarousel'\nimport type { WithClassAsProps } from './interface'\nimport { cn } from '@/lib/utils'\n\ndefineOptions({\n inheritAttrs: false,\n})\n\nconst props = defineProps<WithClassAsProps>()\n\nconst { carouselRef, orientation } = useCarousel()\n</script>\n\n<template>\n <div ref=\"carouselRef\" class=\"overflow-hidden\">\n <div\n :class=\"\n cn(\n 'flex',\n orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col',\n props.class,\n )\"\n v-bind=\"$attrs\"\n >\n <slot />\n </div>\n </div>\n</template>\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "CarouselItem.vue",
|
||||||
|
"content": "<script setup lang=\"ts\">\nimport { useCarousel } from './useCarousel'\nimport type { WithClassAsProps } from './interface'\nimport { cn } from '@/lib/utils'\n\nconst props = defineProps<WithClassAsProps>()\n\nconst { orientation } = useCarousel()\n</script>\n\n<template>\n <div\n role=\"group\"\n aria-roledescription=\"slide\"\n :class=\"cn(\n 'min-w-0 shrink-0 grow-0 basis-full',\n orientation === 'horizontal' ? 'pl-4' : 'pt-4',\n props.class,\n )\"\n >\n <slot />\n </div>\n</template>\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "CarouselNext.vue",
|
||||||
|
"content": "<script setup lang=\"ts\">\nimport { ChevronRightIcon } from '@radix-icons/vue'\nimport { useCarousel } from './useCarousel'\nimport type { WithClassAsProps } from './interface'\nimport { cn } from '@/lib/utils'\nimport { Button } from '@/lib/registry/new-york/ui/button'\n\nconst props = defineProps<WithClassAsProps>()\n\nconst { orientation, canScrollNext, scrollNext } = useCarousel()\n</script>\n\n<template>\n <Button\n :disabled=\"!canScrollNext\"\n :class=\"cn(\n 'absolute h-10 w-10 rounded-full p-0',\n orientation === 'horizontal'\n ? '-right-12 top-1/2 -translate-y-1/2'\n : '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',\n props.class,\n )\"\n variant=\"outline\"\n @click=\"scrollNext\"\n >\n <slot>\n <ChevronRightIcon class=\"h-4 w-4 text-current\" />\n </slot>\n </Button>\n</template>\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "CarouselPrevious.vue",
|
||||||
|
"content": "<script setup lang=\"ts\">\nimport { ChevronLeftIcon } from '@radix-icons/vue'\nimport { useCarousel } from './useCarousel'\nimport type { WithClassAsProps } from './interface'\nimport { cn } from '@/lib/utils'\nimport { Button } from '@/lib/registry/new-york/ui/button'\n\nconst props = defineProps<WithClassAsProps>()\n\nconst { orientation, canScrollPrev, scrollPrev } = useCarousel()\n</script>\n\n<template>\n <Button\n :disabled=\"!canScrollPrev\"\n :class=\"cn(\n 'absolute h-10 w-10 rounded-full p-0',\n orientation === 'horizontal'\n ? '-left-12 top-1/2 -translate-y-1/2'\n : '-top-12 left-1/2 -translate-x-1/2 rotate-90',\n props.class,\n )\"\n variant=\"outline\"\n @click=\"scrollPrev\"\n >\n <slot>\n <ChevronLeftIcon class=\"h-4 w-4 text-current\" />\n </slot>\n </Button>\n</template>\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index.ts",
|
||||||
|
"content": "export { default as Carousel } from './Carousel.vue'\nexport { default as CarouselContent } from './CarouselContent.vue'\nexport { default as CarouselItem } from './CarouselItem.vue'\nexport { default as CarouselPrevious } from './CarouselPrevious.vue'\nexport { default as CarouselNext } from './CarouselNext.vue'\nexport { useCarousel } from './useCarousel'\n\nexport type {\n EmblaCarouselType as CarouselApi,\n} from 'embla-carousel'\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "interface.ts",
|
||||||
|
"content": "import type {\n EmblaCarouselType as CarouselApi,\n EmblaOptionsType as CarouselOptions,\n EmblaPluginType as CarouselPlugin,\n} from 'embla-carousel'\nimport type { HTMLAttributes, Ref } from 'vue'\n\nexport interface CarouselProps {\n opts?: CarouselOptions | Ref<CarouselOptions>\n plugins?: CarouselPlugin[] | Ref<CarouselPlugin[]>\n orientation?: 'horizontal' | 'vertical'\n}\n\nexport interface CarouselEmits {\n (e: 'init-api', payload: CarouselApi): void\n}\n\nexport interface WithClassAsProps {\n class?: HTMLAttributes['class']\n}\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "useCarousel.ts",
|
||||||
|
"content": "import { createInjectionState } from '@vueuse/core'\nimport emblaCarouselVue from 'embla-carousel-vue'\nimport { onMounted, ref } from 'vue'\nimport type {\n EmblaCarouselType as CarouselApi,\n} from 'embla-carousel'\nimport type { CarouselEmits, CarouselProps } from './interface'\n\nconst [useProvideCarousel, useInjectCarousel] = createInjectionState(\n ({\n opts, orientation, plugins,\n }: CarouselProps, emits: CarouselEmits) => {\n const [emblaNode, emblaApi] = emblaCarouselVue({\n ...opts,\n axis: orientation === 'horizontal' ? 'x' : 'y',\n }, plugins)\n\n function scrollPrev() {\n emblaApi.value?.scrollPrev()\n }\n function scrollNext() {\n emblaApi.value?.scrollNext()\n }\n\n const canScrollNext = ref(true)\n const canScrollPrev = ref(true)\n\n function onSelect(api: CarouselApi) {\n canScrollNext.value = api.canScrollNext()\n canScrollPrev.value = api.canScrollPrev()\n }\n\n onMounted(() => {\n if (!emblaApi.value)\n return\n\n emblaApi.value?.on('init', onSelect)\n emblaApi.value?.on('reInit', onSelect)\n emblaApi.value?.on('select', onSelect)\n\n emits('init-api', emblaApi.value)\n })\n\n return { carouselRef: emblaNode, carouselApi: emblaApi, canScrollPrev, canScrollNext, scrollPrev, scrollNext, orientation }\n },\n)\n\nfunction useCarousel() {\n const carouselState = useInjectCarousel()\n\n if (!carouselState)\n throw new Error('useCarousel must be used within a <Carousel />')\n\n return carouselState\n}\n\nexport { useCarousel, useProvideCarousel }\n"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "components:ui"
|
||||||
|
}
|
||||||
648
pnpm-lock.yaml
648
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user