Compare commits

..

2 Commits

Author SHA1 Message Date
HugoRCD
ac6ce7418e Merge remote-tracking branch 'origin/v3' into feat/3827 2025-05-26 11:40:41 +02:00
HugoRCD
5caa566e2a feat(CommandPalette): support for avatars with chips 2025-05-26 10:50:31 +02:00
158 changed files with 6393 additions and 11742 deletions

1
.github/CODEOWNERS vendored
View File

@@ -1 +0,0 @@
* @benjamincanac

View File

@@ -10,7 +10,7 @@ body:
id: env
attributes:
label: Environment
description: You can use `npx nuxt info` to fill this section
description: You can use `npx nuxi info` to fill this section
placeholder: |
- Operating System: `Darwin`
- Node Version: `v18.16.0`

View File

@@ -10,7 +10,7 @@ body:
id: env
attributes:
label: Environment
description: You can use `npx nuxt info` to fill this section
description: You can use `npx nuxi info` to fill this section
placeholder: |
- Operating System: `Darwin`
- Node Version: `v18.16.0`

View File

@@ -6,6 +6,10 @@ jobs:
deploy:
runs-on: ${{ matrix.os }}
environment:
name: ${{ github.ref == 'refs/heads/v3' && 'production' || 'preview' }}
url: ${{ steps.deploy.outputs.deployment-url }}
permissions:
contents: read
id-token: write
@@ -36,10 +40,14 @@ jobs:
- name: Prepare build
run: pnpm run dev:prepare
- name: Deploy to NuxtHub
uses: nuxt-hub/action@v2
- name: Build application
run: pnpm run docs:build
env:
NODE_OPTIONS: '--max-old-space-size=8192'
- name: Deploy to NuxtHub
uses: nuxt-hub/action@v1
id: deploy
with:
project-key: ui-7eg3
directory: docs
directory: docs/dist

View File

@@ -93,7 +93,7 @@ jobs:
- name: Store commit SHA
run: |
echo "COMMIT_SHA=$(echo ${{ github.event.pull_request.head.sha || github.sha }} | cut -c1-7)" >> $GITHUB_ENV
echo "COMMIT_SHA=$(echo ${{ github.workflow_sha }} | cut -c1-7)" >> $GITHUB_ENV
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -111,7 +111,7 @@ jobs:
run: pnpm install --ignore-workspace
- name: Prepare
run: pnpm nuxt prepare
run: pnpm nuxi prepare
- name: Typecheck
run: pnpm run typecheck
@@ -138,7 +138,7 @@ jobs:
- name: Store commit SHA
run: |
echo "COMMIT_SHA=$(echo ${{ github.event.pull_request.head.sha || github.sha }} | cut -c1-7)" >> $GITHUB_ENV
echo "COMMIT_SHA=$(echo ${{ github.workflow_sha }} | cut -c1-7)" >> $GITHUB_ENV
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -183,7 +183,7 @@ jobs:
- name: Store commit SHA
run: |
echo "COMMIT_SHA=$(echo ${{ github.event.pull_request.head.sha || github.sha }} | cut -c1-7)" >> $GITHUB_ENV
echo "COMMIT_SHA=$(echo ${{ github.workflow_sha }} | cut -c1-7)" >> $GITHUB_ENV
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -235,7 +235,7 @@ jobs:
- name: Store commit SHA
run: |
echo "COMMIT_SHA=$(echo ${{ github.event.pull_request.head.sha || github.sha }} | cut -c1-7)" >> $GITHUB_ENV
echo "COMMIT_SHA=$(echo ${{ github.workflow_sha }} | cut -c1-7)" >> $GITHUB_ENV
- name: Install pnpm
uses: pnpm/action-setup@v4

View File

@@ -9,6 +9,10 @@ jobs:
deploy:
runs-on: ${{ matrix.os }}
environment:
name: ${{ github.ref == 'refs/heads/v3' && 'production' || 'preview' }}
url: ${{ steps.deploy.outputs.deployment-url }}
permissions:
contents: read
id-token: write
@@ -36,10 +40,14 @@ jobs:
- name: Prepare build
run: pnpm run dev:prepare
- name: Deploy to NuxtHub
uses: nuxt-hub/action@v2
- name: Build application
run: pnpm run dev:build
env:
NODE_OPTIONS: '--max-old-space-size=8192'
NITRO_PRESET: cloudflare-pages
- name: Deploy to NuxtHub
uses: nuxt-hub/action@v1
id: deploy
with:
project-key: ui3-playground-pb9b
directory: playground
directory: playground/dist

View File

@@ -1,27 +0,0 @@
name: reproduction
on:
workflow_dispatch:
schedule:
- cron: '30 1 * * *'
jobs:
reproduction:
runs-on: ubuntu-latest
permissions:
actions: write
issues: write
steps:
- uses: actions/stale@v9
with:
days-before-stale: -1 # Issues and PR will never be flagged stale automatically.
stale-issue-label: 'needs reproduction' # Label that flags an issue as stale.
only-labels: 'needs reproduction' # Only process these issues
days-before-issue-close: 7
ignore-updates: true
remove-stale-when-updated: false
close-issue-message: This issue was closed because it was open for 7 days without a reproduction.
close-issue-label: closed-by-bot
operations-per-run: 300 #default 30

View File

@@ -1,7 +1,6 @@
name: stale
on:
workflow_dispatch:
schedule:
- cron: '30 1 * * *'
@@ -10,28 +9,17 @@ jobs:
runs-on: ubuntu-latest
permissions:
actions: write
issues: write
steps:
- uses: actions/stale@4c023f01d613e60293d8004f251a18bfb9bbd71d
- uses: actions/stale@v9
with:
days-before-pr-stale: -1
days-before-stale: 60
days-before-close: 7
stale-issue-label: 'stale'
close-issue-label: 'closed-by-bot'
close-issue-message: |
Hi! 👋
This issue has been automatically **closed** due to prolonged inactivity.
We're a small team and can't address every report, but we appreciate your feedback and contributions.
If this issue is still relevant with the latest version of Nuxt UI, please feel free to reopen or create a new issue with updated details.
Thank you for your understanding and support!
— Nuxt UI Team
exempt-issue-labels: 'feature,announcement'
operations-per-run: 300
days-before-stale: -1 # Issues and PR will never be flagged stale automatically.
stale-issue-label: 'needs reproduction' # Label that flags an issue as stale.
only-labels: 'needs reproduction' # Only process these issues
days-before-issue-close: 7
ignore-updates: true
remove-stale-when-updated: false
close-issue-message: This issue was closed because it was open for 7 days without a reproduction.
close-issue-label: closed-by-bot
operations-per-run: 300 #default 30

View File

@@ -1,40 +1,5 @@
# Changelog
## [3.1.3](https://github.com/nuxt/ui/compare/v3.1.2...v3.1.3) (2025-05-26)
### ⚠ BREAKING CHANGES
* **NavigationMenu:** revert new `collapsible` field
### Features
* **locale:** add Kyrgyz language ([#4189](https://github.com/nuxt/ui/issues/4189)) ([4053047](https://github.com/nuxt/ui/commit/405304775e4b2b4e8b37a2364f3e5ee34b46036e))
* **locale:** add Lithuanian language ([#4171](https://github.com/nuxt/ui/issues/4171)) ([d86956e](https://github.com/nuxt/ui/commit/d86956e1d57482b3e98eef2d34bff13544284b0b))
* **locale:** add Malay language ([#4160](https://github.com/nuxt/ui/issues/4160)) ([c00f6e8](https://github.com/nuxt/ui/commit/c00f6e8cdfd88eeba58812b78d94a2326c13f164))
* **locale:** add Mongolian language ([#4214](https://github.com/nuxt/ui/issues/4214)) ([44ea02c](https://github.com/nuxt/ui/commit/44ea02c0d64322ef0cfda63b234369c00d3d0180))
* **Modal/Slideover:** add `after:enter` event ([#4187](https://github.com/nuxt/ui/issues/4187)) ([d9e9fea](https://github.com/nuxt/ui/commit/d9e9fea35e4b22d68324c9e85b3aa221a7987d0f))
* **NavigationMenu:** add `tooltip` and `popover` props ([f2682fd](https://github.com/nuxt/ui/commit/f2682fd2ae8abb7807977727fc22ef34cb5752e5)), closes [#4186](https://github.com/nuxt/ui/issues/4186)
* **NavigationMenu:** add `trigger` type in items ([9cf9f25](https://github.com/nuxt/ui/commit/9cf9f25f4424447691e03e9034155d1541badd43))
* **NavigationMenu:** handle `vertical` orientation with Accordion instead of Collapsible ([1e2a10b](https://github.com/nuxt/ui/commit/1e2a10b4bdebaef12316ac60f98a956dad21c1ec)), closes [#4072](https://github.com/nuxt/ui/issues/4072) [#3911](https://github.com/nuxt/ui/issues/3911)
* **Popover:** add `anchor` slot ([#4119](https://github.com/nuxt/ui/issues/4119)) ([473513c](https://github.com/nuxt/ui/commit/473513c2460d4329d7d2e0a0ea69bf1310a072d1))
### Bug Fixes
* **CheckboxGroup/RadioGroup:** variant `table` borders in RTL mode ([#4192](https://github.com/nuxt/ui/issues/4192)) ([43d281f](https://github.com/nuxt/ui/commit/43d281f6d1d8b0017ed61d929c5e311fb5b03447))
* **CommandPalette:** add `presentation` role to viewport ([2ba94db](https://github.com/nuxt/ui/commit/2ba94db09e1ba86020d5d289f1ca1e24ef706299))
* **ContextMenu/DropdownMenu:** wrap groups in a viewport ([dcf34a7](https://github.com/nuxt/ui/commit/dcf34a7ac236b96b1302ec2eae155b8f2d3784ef)), closes [#3315](https://github.com/nuxt/ui/issues/3315)
* **Drawer:** improve title & description accessibility ([41087d4](https://github.com/nuxt/ui/commit/41087d4c9569eb00c04bd748e055cd151c2f762c)), closes [#4199](https://github.com/nuxt/ui/issues/4199)
* **icons:** update `loading` icon ([#4163](https://github.com/nuxt/ui/issues/4163)) ([fe4e1f8](https://github.com/nuxt/ui/commit/fe4e1f859d42aa3c32bb7b75302e84a280abe525))
* **Input/Textarea:** define model modifiers types ([#4195](https://github.com/nuxt/ui/issues/4195)) ([3243fb8](https://github.com/nuxt/ui/commit/3243fb88f71c5475824bfdc4d7c4f303b2d6790b))
* **InputMenu/Select/SelectMenu:** manual viewport to display scrollbars ([f95abf8](https://github.com/nuxt/ui/commit/f95abf8d1d7b9149e400d7dc6f96f93f5154da7a)), closes [#4069](https://github.com/nuxt/ui/issues/4069)
* **NavigationMenu:** incorrect hover when disabled and active ([d0be599](https://github.com/nuxt/ui/commit/d0be59946bfe30c79a6f75476385ab8538aa51b8))
* **NavigationMenu:** only display `tooltip` when collapsed ([44f536f](https://github.com/nuxt/ui/commit/44f536fd0034facb3550d910fae71d4f9442ed19))
* **NavigationMenu:** remove `font-medium` in popover children ([0236399](https://github.com/nuxt/ui/commit/02363994d66d3c2d11b9913f31167fa25f5c5de2))
* **NavigationMenu:** revert new `collapsible` field ([3c78e2f](https://github.com/nuxt/ui/commit/3c78e2fd983f19b5cec65b4a94a8a8b14e548e5e))
* **Textarea:** missing imports ([#4207](https://github.com/nuxt/ui/issues/4207)) ([6aab62e](https://github.com/nuxt/ui/commit/6aab62ec30e266c5f0da0cd24aefbb7c53f447ac))
* **theme:** define `old-neutral` color as static ([#4193](https://github.com/nuxt/ui/issues/4193)) ([dae9f0b](https://github.com/nuxt/ui/commit/dae9f0b8631b3b9fb60ef47753f7aded0c36c4a2))
* **Tooltip:** increase padding for consistency ([0634a75](https://github.com/nuxt/ui/commit/0634a756a496f5131841abafd218ae7e4aaa61e5))
## [3.1.2](https://github.com/nuxt/ui/compare/v3.1.1...v3.1.2) (2025-05-15)
### Features

View File

@@ -22,7 +22,9 @@ onMounted(() => {
const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
const githubLink = computed(() => `https://github.com/nuxt/${value.value}`)
const githubLink = computed(() => {
return `https://github.com/nuxt/${value.value}`
})
const desktopLinks = computed(() => props.links.map(({ icon, ...link }) => link))
const mobileLinks = computed(() => [
@@ -34,16 +36,6 @@ const mobileLinks = computed(() => [
target: '_blank'
}
])
const items = computed(() => {
const ui2 = { label: 'v2.22.0', to: 'https://ui2.nuxt.com' }
const uiPro1 = { label: 'v1.8.0', to: 'https://ui2.nuxt.com/pro' }
return [
{ label: `v${config.version}`, active: true, color: 'primary' as const, checked: true, type: 'checkbox' as const },
route.path === '/' ? ui2 : route.path.startsWith('/pro') ? uiPro1 : module.value === 'ui-pro' ? uiPro1 : ui2
]
})
</script>
<template>
@@ -61,7 +53,7 @@ const items = computed(() => {
<UDropdownMenu
v-slot="{ open }"
:modal="false"
:items="items"
:items="[{ label: `v${config.version}`, active: true, color: 'primary', checked: true, type: 'checkbox' }, { label: module === 'ui-pro' ? 'v1.7.1' : 'v2.21.1', to: module === 'ui-pro' ? 'https://ui2.nuxt.com/pro' : 'https://ui2.nuxt.com' }]"
:ui="{ content: 'w-(--reka-dropdown-menu-trigger-width) min-w-0' }"
size="xs"
>

View File

@@ -26,7 +26,6 @@ function getEmojiFlag(locale: string): string {
km: 'kh', // Khmer -> Cambodia
ko: 'kr', // Korean -> South Korea
ky: 'kg', // Kyrgyz -> Kyrgyzstan
lb: 'lu', // Luxembourgish -> Luxembourg
ms: 'my', // Malay -> Malaysia
nb: 'no', // Norwegian Bokmål -> Norway
sl: 'si', // Slovenian -> Slovenia

View File

@@ -15,9 +15,6 @@ const schema = z.object({
select: z.string().refine(value => value === 'option-2', {
message: 'Select Option 2'
}),
selectMultiple: z.array(z.string()).refine(values => values.includes('option-2'), {
message: 'Include Option 2'
}),
selectMenu: z.any().refine(option => option?.value === 'option-2', {
message: 'Select Option 2'
}),
@@ -84,10 +81,6 @@ async function onSubmit(event: FormSubmitEvent<Schema>) {
<USelect v-model="state.select" :items="items" class="w-full" />
</UFormField>
<UFormField name="selectMultiple" label="Select (Multiple)">
<USelect v-model="state.selectMultiple" multiple :items="items" class="w-full" />
</UFormField>
<UFormField name="selectMenu" label="Select Menu">
<USelectMenu v-model="state.selectMenu" :items="items" class="w-full" />
</UFormField>

View File

@@ -1,9 +0,0 @@
<script setup lang="ts">
const tags = ref(['Vue'])
</script>
<template>
<UFormField label="Tags" required>
<UInputTags v-model="tags" placeholder="Enter tags..." />
</UFormField>
</template>

View File

@@ -10,7 +10,7 @@ const domain = ref(domains[0])
v-model="value"
placeholder="nuxt"
:ui="{
base: 'pl-14.5',
base: 'pl-[57px]',
leading: 'pointer-events-none'
}"
>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
const value = ref('npx nuxt module add ui')
const value = ref('npx nuxi module add ui')
const copied = ref(false)
function copy() {

View File

@@ -1,14 +0,0 @@
<script setup lang="ts">
import { vMaska } from 'maska/vue'
</script>
<template>
<div class="flex flex-col gap-2">
<UInput v-maska="'#### #### #### ####'" placeholder="4242 4242 4242 4242" icon="i-lucide-credit-card" />
<div class="flex items-center gap-2">
<UInput v-maska="'##/##'" placeholder="MM/YY" icon="i-lucide-calendar" />
<UInput v-maska="'###'" placeholder="CVC" />
</div>
</div>
</template>

View File

@@ -10,8 +10,8 @@ const open = ref(false)
<Placeholder class="h-48" />
</template>
<template #footer="{ close }">
<UButton label="Cancel" color="neutral" variant="outline" @click="close" />
<template #footer>
<UButton label="Cancel" color="neutral" variant="outline" @click="open = false" />
<UButton label="Submit" color="neutral" />
</template>
</UModal>

View File

@@ -10,8 +10,8 @@ const open = ref(false)
<Placeholder class="h-full" />
</template>
<template #footer="{ close }">
<UButton label="Cancel" color="neutral" variant="outline" @click="close" />
<template #footer>
<UButton label="Cancel" color="neutral" variant="outline" @click="open = false" />
<UButton label="Submit" color="neutral" />
</template>
</USlideover>

View File

@@ -1,34 +0,0 @@
<script setup lang="ts">
import type { TimelineItem } from '@nuxt/ui'
const items: TimelineItem[] = [{
date: 'Mar 15, 2025',
title: 'Project Kickoff',
icon: 'i-lucide-rocket',
value: 'kickoff'
}, {
date: 'Mar 22, 2025',
title: 'Design Phase',
icon: 'i-lucide-palette',
value: 'design'
}, {
date: 'Mar 29, 2025',
title: 'Development Sprint',
icon: 'i-lucide-code',
value: 'development'
}, {
date: 'Apr 5, 2025',
title: 'Testing & Deployment',
icon: 'i-lucide-check-circle',
value: 'deployment'
}]
</script>
<template>
<UTimeline
:items="items"
:ui="{ item: 'even:flex-row-reverse even:-translate-x-[calc(100%-2rem)] even:text-right' }"
:default-value="2"
class="translate-x-[calc(50%-1rem)]"
/>
</template>

View File

@@ -1,52 +0,0 @@
<script setup lang="ts">
import type { TimelineItem } from '@nuxt/ui'
const items = [{
date: 'Mar 15, 2025',
title: 'Project Kickoff',
subtitle: 'Project Initiation',
description: 'Kicked off the project with team alignment. Set up project milestones and allocated resources.',
icon: 'i-lucide-rocket',
value: 'kickoff'
}, {
date: 'Mar 22, 2025',
title: 'Design Phase',
description: 'User research and design workshops. Created wireframes and prototypes for user testing.',
icon: 'i-lucide-palette',
value: 'design'
}, {
date: 'Mar 29, 2025',
title: 'Development Sprint',
description: 'Frontend and backend development. Implemented core features and integrated with APIs.',
icon: 'i-lucide-code',
value: 'development',
slot: 'development' as const,
developers: [
{
src: 'https://github.com/J-Michalek.png'
}, {
src: 'https://github.com/benjamincanac.png'
}
]
}, {
date: 'Apr 5, 2025',
title: 'Testing & Deployment',
description: 'QA testing and performance optimization. Deployed the application to production.',
icon: 'i-lucide-check-circle',
value: 'deployment'
}] satisfies TimelineItem[]
</script>
<template>
<UTimeline :items="items" :default-value="2" class="w-96">
<template #development-title="{ item }">
<div class="flex items-center gap-1">
<span>{{ item.title }}</span>
<UAvatarGroup size="2xs">
<UAvatar v-for="(developer, index) of item.developers" :key="index" v-bind="developer" />
</UAvatarGroup>
</div>
</template>
</UTimeline>
</template>

View File

@@ -1,42 +0,0 @@
<script setup lang="ts">
import type { TimelineItem } from '@nuxt/ui'
const items: TimelineItem[] = [{
date: 'Mar 15, 2025',
title: 'Project Kickoff',
description: 'Kicked off the project with team alignment. Set up project milestones and allocated resources.',
icon: 'i-lucide-rocket',
value: 'kickoff'
}, {
date: 'Mar 22, 2025',
title: 'Design Phase',
description: 'User research and design workshops. Created wireframes and prototypes for user testing.',
icon: 'i-lucide-palette',
value: 'design'
}, {
date: 'Mar 29, 2025',
title: 'Development Sprint',
description: 'Frontend and backend development. Implemented core features and integrated with APIs.',
icon: 'i-lucide-code',
value: 'development'
}, {
date: 'Apr 5, 2025',
title: 'Testing & Deployment',
description: 'QA testing and performance optimization. Deployed the application to production.',
icon: 'i-lucide-check-circle',
value: 'deployment'
}]
const active = ref(0)
// Note: This is for demonstration purposes only. Don't do this at home.
onMounted(() => {
setInterval(() => {
active.value = (active.value + 1) % items.length
}, 2000)
})
</script>
<template>
<UTimeline v-model="active" :items="items" class="w-96" />
</template>

View File

@@ -1,60 +0,0 @@
<script lang="ts" setup>
import type { TimelineItem } from '@nuxt/ui'
import { useTimeAgo } from '@vueuse/core'
const items = [{
username: 'J-Michalek',
date: '2025-05-24T14:58:55Z',
action: 'opened this',
avatar: {
src: 'https://github.com/J-Michalek.png'
}
}, {
username: 'J-Michalek',
date: '2025-05-26T19:30:14+02:00',
action: 'marked this pull request as ready for review',
icon: 'i-lucide-check-circle'
}, {
username: 'benjamincanac',
date: '2025-05-27T11:01:20Z',
action: 'commented on this',
description: 'I\'ve made a few changes, let me know what you think! Basically I updated the design, removed unnecessary divs, used Avatar component for the indicator since it supports icon already.',
avatar: {
src: 'https://github.com/benjamincanac.png'
}
}, {
username: 'J-Michalek',
date: '2025-05-27T11:01:20Z',
action: 'commented on this',
description: 'Looks great! Good job on cleaning it up.',
avatar: {
src: 'https://github.com/J-Michalek.png'
}
}, {
username: 'benjamincanac',
date: '2025-05-27T11:01:20Z',
action: 'merged this',
icon: 'i-lucide-git-merge'
}] satisfies TimelineItem[]
</script>
<template>
<UTimeline
:items="items"
size="xs"
class="w-96"
:ui="{
date: 'float-end ms-1',
description: 'px-3 py-2 ring ring-default mt-2 rounded-md text-default'
}"
>
<template #title="{ item }">
<span>{{ item.username }}</span>
<span class="font-normal text-muted">&nbsp;{{ item.action }}</span>
</template>
<template #date="{ item }">
{{ useTimeAgo(new Date(item.date)) }}
</template>
</UTimeline>
</template>

View File

@@ -84,7 +84,7 @@ You can play with Nuxt UI components as well as your app components directly fro
Install the module to your Nuxt application with one command:
```bash [Terminal]
npx nuxt module add compodium
npx nuxi module add compodium
```
::

View File

@@ -115,7 +115,7 @@ Start your project using the [nuxt/starter#ui](https://github.com/nuxt/starter/t
Create a new project locally by running the following command:
```bash [Terminal]
npm create nuxt@latest -- -t ui
npx nuxi init -t ui <my-app>
```
::note

View File

@@ -78,22 +78,6 @@ components.d.ts
::
::tip
Internally, Nuxt UI relies on custom alias to resolve the theme types. If you're using TypeScript, you should add an alias to your `tsconfig` to enable auto-completion in your `vite.config.ts`.
```json [tsconfig.node.json]
{
"compilerOptions": {
"paths": {
"#build/ui": [
"./node_modules/@nuxt/ui/.nuxt/ui"
]
}
}
}
```
::
#### Use the Nuxt UI Vue plugin in your `main.ts`
```ts [main.ts]{3,14}
@@ -195,7 +179,7 @@ Start your project using the [nuxtlabs/nuxt-ui-vue-starter](https://github.com/n
Create a new project locally by running the following command:
```bash [Terminal]
npm create nuxt@latest -- -t github:nuxtlabs/nuxt-ui-vue-starter
npx nuxi init -t github:nuxtlabs/nuxt-ui-vue-starter <my-app>
```
::note

View File

@@ -60,7 +60,7 @@ import { fr } from '@nuxt/ui-pro/locale'
### Custom locale
You can create your own locale using the `defineLocale` composable:
You also have the option to add your own locale using `defineLocale`:
::module-only
@@ -125,65 +125,6 @@ Look at the `code` parameter, there you need to pass the iso code of the languag
::
### Extend locale :badge{label="Soon" class="align-text-top"}
You can customize an existing locale by overriding its `messages` or `code` using the `extendLocale` composable:
::module-only
#ui
:::div
```vue [app.vue]
<script setup lang="ts">
import { en } from '@nuxt/ui/locale'
const locale = extendLocale(en, {
code: 'en-GB',
messages: {
commandPalette: {
placeholder: 'Search a component...'
}
}
})
</script>
<template>
<UApp :locale="locale">
<NuxtPage />
</UApp>
</template>
```
:::
#ui-pro
:::div
```vue [app.vue]
<script setup lang="ts">
import { en } from '@nuxt/ui-pro/locale'
const locale = extendLocale(en, {
code: 'en-GB',
messages: {
commandPalette: {
placeholder: 'Search a component...'
}
}
})
</script>
<template>
<UApp :locale="locale">
<NuxtPage />
</UApp>
</template>
```
:::
::
### Dynamic locale
To dynamically switch between languages, you can use the [Nuxt I18n](https://i18n.nuxtjs.org/) module.

View File

@@ -60,7 +60,7 @@ import { fr } from '@nuxt/ui-pro/locale'
### Custom locale
You can create your own locale using the `defineLocale` composable:
You also have the option to add your locale using `defineLocale`:
::module-only
@@ -127,67 +127,6 @@ Look at the `code` parameter, there you need to pass the iso code of the languag
::
### Extend locale :badge{label="Soon" class="align-text-top"}
You can customize an existing locale by overriding its `messages` or `code` using the `extendLocale` composable:
::module-only
#ui
:::div
```vue [App.vue]
<script setup lang="ts">
import { en } from '@nuxt/ui/locale'
import { extendLocale } from '@nuxt/ui/composables/defineLocale.js'
const locale = extendLocale(en, {
code: 'en-GB',
messages: {
commandPalette: {
placeholder: 'Search a component...'
}
}
})
</script>
<template>
<UApp :locale="locale">
<RouterView />
</UApp>
</template>
```
:::
#ui-pro
:::div
```vue [App.vue]
<script setup lang="ts">
import { en } from '@nuxt/ui-pro/locale'
import { extendLocale } from '@nuxt/ui/composables/defineLocale.js'
const locale = extendLocale(en, {
code: 'en-GB',
messages: {
commandPalette: {
placeholder: 'Search a component...'
}
}
})
</script>
<template>
<UApp :locale="locale">
<RouterView />
</UApp>
</template>
```
:::
::
### Dynamic locale
To dynamically switch between languages, you can use the [Vue I18n](https://vue-i18n.intlify.dev/) plugin.

View File

@@ -1,6 +1,6 @@
---
title: useOverlay
description: 'A composable to programmatically control overlays.'
description: "A composable to programmatically control overlays."
---
## Usage
@@ -9,11 +9,9 @@ Use the auto-imported `useOverlay` composable to programmatically control [Modal
```vue
<script setup lang="ts">
import { LazyModalExample } from '#components'
const overlay = useOverlay()
const modal = overlay.create(LazyModalExample)
const modal = overlay.create(MyModal)
async function openModal() {
modal.open()
@@ -31,73 +29,71 @@ In order to return a value from the overlay, the `overlay.open().instance.result
### `create(component: T, options: OverlayOptions): OverlayInstance`
Create an overlay, and return a factory instance.
Creates an overlay, and returns a factory instance
- Parameters:
- `component`: The overlay component.
- `options`:
- `defaultOpen?: boolean` Open the overlay immediately after being created. Defaults to `false`.
- `component`: The overlay component
- `options` The overlay options
- `defaultOpen?: boolean` Opens the overlay immediately after being created `default: false`
- `props?: ComponentProps`: An optional object of props to pass to the rendered component.
- `destroyOnClose?: boolean` Removes the overlay from memory when closed. Defaults to `false`.
- `destroyOnClose?: boolean` Removes the overlay from memory when closed `default: false`
### `open(id: symbol, props?: ComponentProps<T>): OpenedOverlay<T>`
Open an overlay by its `id`.
Opens the overlay using its `id`
- Parameters:
- `id`: The identifier of the overlay.
- `id`: The identifier of the overlay
- `props`: An optional object of props to pass to the rendered component.
### `close(id: symbol, value?: any): void`
Close an overlay by its `id`.
Close an overlay using its `id`
- Parameters:
- `id`: The identifier of the overlay.
- `value`: A value to resolve the overlay promise with.
- `id`: The identifier of the overlay
- `value`: A value to resolve the overlay promise with
### `patch(id: symbol, props: ComponentProps<T>): void`
Update an overlay by its `id`.
Update an overlay using its `id`
- Parameters:
- `id`: The identifier of the overlay.
- `id`: The identifier of the overlay
- `props`: An object of props to update on the rendered component.
### `unmount(id: symbol): void`
### `unMount(id: symbol): void`
Remove an overlay from the DOM by its `id`.
Removes the overlay from the DOM using its `id`
- Parameters:
- `id`: The identifier of the overlay.
- `id`: The identifier of the overlay
### `isOpen(id: symbol): boolean`
Check if an overlay is open using its `id`.
Checks if an overlay its open using its `id`
- Parameters:
- `id`: The identifier of the overlay.
- `id`: The identifier of the overlay
### `overlays: Overlay[]`
In-memory list of all overlays that were created.
In-memory list of overlays that were created
## Instance API
## Overlay Instance API
### `open(props?: ComponentProps<T>): Promise<OpenedOverlay<T>>`
Open the overlay.
Opens the overlay
- Parameters:
- `props`: An optional object of props to pass to the rendered component.
```vue
<script setup lang="ts">
import { LazyModalExample } from '#components'
const overlay = useOverlay()
const modal = overlay.create(LazyModalExample)
const modal = overlay.create(MyModalContent)
function openModal() {
modal.open({
@@ -109,25 +105,23 @@ function openModal() {
### `close(value?: any): void`
Close the overlay.
Close the overlay
- Parameters:
- `value`: A value to resolve the overlay promise with.
- `value`: A value to resolve the overlay promise with
### `patch(props: ComponentProps<T>)`
Update the props of the overlay.
Updates the props of the overlay.
- Parameters:
- `props`: An object of props to update on the rendered component.
```vue
<script setup lang="ts">
import { LazyModalExample } from '#components'
const overlay = useOverlay()
const modal = overlay.create(LazyModalExample, {
const modal = overlay.create(MyModal, {
title: 'Welcome'
})
@@ -147,8 +141,6 @@ Here's a complete example of how to use the `useOverlay` composable:
```vue
<script setup lang="ts">
import { ModalA, ModalB, SlideoverA } from '#components'
const overlay = useOverlay()
// Create with default props
@@ -158,7 +150,7 @@ const modalB = overlay.create(ModalB)
const slideoverA = overlay.create(SlideoverA)
const openModalA = () => {
// Open modalA, but override the title prop
// Open Modal A, but override the title prop
modalA.open({ title: 'Hello' })
}
@@ -168,37 +160,16 @@ const openModalB = async () => {
const input = await modalBInstance.result
// Pass the result from modalB to the slideover, and open it
// Pass the result from modalB to the slideover, and open it.
slideoverA.open({ input })
}
</script>
<template>
<button @click="openModalA">Open Modal</button>
<div>
<button @click="openModal">Open Modal</button>
</div>
</template>
```
In this example, we're using the `useOverlay` composable to control multiple modals and slideovers.
## Caveats
### Provide / Inject
When opening overlays programmatically (e.g. modals, slideovers, etc), the overlay component can only access injected values from the component containing `UApp` (typically `app.vue` or layout components). This is because overlays are mounted outside of the page context by the `UApp` component.
As such, using `provide()` in pages or parent components isn't supported directly. To pass provided values to overlays, the recommended approach is to use props instead:
```vue
<script setup lang="ts">
import { LazyModalExample } from '#components'
const providedValue = inject('valueProvidedInPage')
const modal = overlay.create(LazyModalExample, {
props: {
providedValue,
otherData: someValue
}
})
</script>
```

View File

@@ -135,7 +135,7 @@ props:
### Multiple
Use the `multiple` prop to allow multiple selections, the selected items will be displayed as tags.
Use the `multiple` prop to allow multiple selections, the selected items will be displayed as badges.
::component-code
---
@@ -166,7 +166,7 @@ Ensure to pass an array to the `default-value` prop or the `v-model` directive.
### Delete Icon
With `multiple`, use the `delete-icon` prop to customize the delete [Icon](/components/icon) in the tags. Defaults to `i-lucide-x`.
With `multiple`, use the `delete-icon` prop to customize the delete [Icon](/components/icon) in the badges. Defaults to `i-lucide-x`.
::component-code
---

View File

@@ -1,284 +0,0 @@
---
title: InputTags
description: An input element that displays interactive tags.
links:
- label: InputTags
icon: i-custom-reka-ui
to: https://reka-ui.com/docs/components/tags-input
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxt/ui/tree/v3/src/runtime/components/InputTags.vue
navigation.badge: Soon
---
## Usage
Use the `v-model` directive to control the value of the InputTags.
::component-code
---
prettier: true
ignore:
- modelValue
external:
- modelValue
props:
modelValue: ['Vue']
---
::
Use the `default-value` prop to set the initial value when you do not need to control its state.
::component-code
---
prettier: true
ignore:
- defaultValue
props:
defaultValue: ['Vue']
---
::
### Placeholder
Use the `placeholder` prop to set a placeholder text.
::component-code
---
props:
placeholder: 'Enter tags...'
---
::
### Color
Use the `color` prop to change the ring color when the InputTags is focused.
::component-code
---
prettier: true
ignore:
- modelValue
external:
- modelValue
props:
modelValue: ['Vue']
color: neutral
highlight: true
---
::
::note
The `highlight` prop is used here to show the focus state. It's used internally when a validation error occurs.
::
### Variants
Use the `variant` prop to change the appearance of the InputTags.
::component-code
---
prettier: true
ignore:
- modelValue
external:
- modelValue
props:
modelValue: ['Vue']
variant: subtle
color: neutral
highlight: false
---
::
### Sizes
Use the `size` prop to adjust the size of the InputTags.
::component-code
---
prettier: true
ignore:
- modelValue
external:
- modelValue
props:
modelValue: ['Vue']
size: xl
---
::
### Icon
Use the `icon` prop to show an [Icon](/components/icon) inside the InputTags.
::component-code
---
prettier: true
ignore:
- modelValue
external:
- modelValue
props:
modelValue: ['Vue']
icon: 'i-lucide-search'
size: md
variant: outline
---
::
::note
Use the `leading` and `trailing` props to set the icon position or the `leading-icon` and `trailing-icon` props to set a different icon for each position.
::
### Avatar
Use the `avatar` prop to show an [Avatar](/components/avatar) inside the InputTags.
::component-code
---
prettier: true
ignore:
- modelValue
external:
- modelValue
props:
modelValue: ['Vue']
avatar:
src: 'https://github.com/vuejs.png'
size: md
variant: outline
---
::
### Delete Icon
Use the `delete-icon` prop to customize the delete [Icon](/components/icon) in the tags. Defaults to `i-lucide-x`.
::component-code
---
prettier: true
ignore:
- modelValue
external:
- modelValue
props:
modelValue: ['Vue']
deleteIcon: 'i-lucide-trash'
---
::
::framework-only
#nuxt
:::tip{to="/getting-started/icons/nuxt#theme"}
You can customize this icon globally in your `app.config.ts` under `ui.icons.close` key.
:::
#vue
:::tip{to="/getting-started/icons/vue#theme"}
You can customize this icon globally in your `vite.config.ts` under `ui.icons.close` key.
:::
::
### Loading
Use the `loading` prop to show a loading icon on the InputTags.
::component-code
---
prettier: true
ignore:
- modelValue
external:
- modelValue
props:
modelValue: ['Vue']
loading: true
trailing: false
---
::
### Loading Icon
Use the `loading-icon` prop to customize the loading icon. Defaults to `i-lucide-loader-circle`.
::component-code
---
prettier: true
ignore:
- modelValue
external:
- modelValue
props:
modelValue: ['Vue']
loading: true
loadingIcon: 'i-lucide-loader'
---
::
::framework-only
#nuxt
:::tip{to="/getting-started/icons/nuxt#theme"}
You can customize this icon globally in your `app.config.ts` under `ui.icons.loading` key.
:::
#vue
:::tip{to="/getting-started/icons/vue#theme"}
You can customize this icon globally in your `vite.config.ts` under `ui.icons.loading` key.
:::
::
### Disabled
Use the `disabled` prop to disable the InputTags.
::component-code
---
prettier: true
ignore:
- modelValue
external:
- modelValue
props:
modelValue: ['Vue']
disabled: true
---
::
## Examples
### Within a FormField
You can use the InputTags within a [FormField](/components/form-field) component to display a label, help text, required indicator, etc.
::component-example
---
name: 'input-tags-form-field-example'
---
::
## API
### Props
:component-props
### Slots
:component-slots
### Emits
:component-emits
### Expose
When accessing the component via a template ref, you can use the following:
| Name | Type |
|----------------------------|-------------------------------------------------|
| `inputRef`{lang="ts-type"} | `Ref<HTMLInputElement \| null>`{lang="ts-type"} |
## Theme
:component-theme

View File

@@ -278,16 +278,6 @@ name: 'input-kbd-example'
This example uses the `defineShortcuts` composable to focus the Input when the :kbd{value="/"} key is pressed.
::
### With mask
There's no built-in support for masks, but you can use librairies like [maska](https://github.com/beholdr/maska) to mask the Input.
::component-example
---
name: 'input-mask-example'
---
::
### With floating label
You can use the `#default` slot to add a floating label to the Input.

View File

@@ -305,13 +305,13 @@ slots:
### Programmatic usage
You can use the [`useOverlay`](/composables/use-overlay) composable to open a Modal programmatically.
You can use the [`useOverlay`](/composables/use-overlay) composable to open a Modal programatically.
::warning
Make sure to wrap your app with the [`App`](/components/app) component which uses the [`OverlayProvider`](https://github.com/nuxt/ui/blob/v3/src/runtime/components/OverlayProvider.vue) component.
::
First, create a modal component that will be opened programmatically:
First, create a modal component that will be opened programatically:
::component-example
---

View File

@@ -23,7 +23,7 @@ Use the `items` prop as an array of objects with the following properties:
- `badge?: string | number | BadgeProps`{lang="ts-type"}
- `tooltip?: TooltipProps`{lang="ts-type"}
- `trailingIcon?: string`{lang="ts-type"}
- `type?: 'label' | 'trigger' | 'link'`{lang="ts-type"}
- `type?: 'label' | 'link'`{lang="ts-type"}
- `defaultOpen?: boolean`{lang="ts-type"}
- `open?: boolean`{lang="ts-type"}
- `value?: string`{lang="ts-type"}
@@ -994,7 +994,7 @@ props:
---
::
### With popover in items :badge{label="New" class="align-text-top"}
### With popover in items :badge{label="Soon" class="align-text-top"}
When orientation is `vertical` and the menu is `collapsed`, you can set the `popover` prop to `true` to display a [Popover](/components/popover) around items with their children but you can also use the `popover` property on each item to override the default popover.

View File

@@ -202,7 +202,7 @@ name: 'popover-command-palette-example'
---
::
### With anchor slot :badge{label="New" class="align-text-top"}
### With anchor slot
You can use the `#anchor` slot to position the Popover against a custom element.

View File

@@ -304,13 +304,13 @@ slots:
### Programmatic usage
You can use the [`useOverlay`](/composables/use-overlay) composable to open a Slideover programmatically.
You can use the [`useOverlay`](/composables/use-overlay) composable to open a Slideover programatically.
::warning
Make sure to wrap your app with the [`App`](/components/app) component which uses the [`OverlayProvider`](https://github.com/nuxt/ui/blob/v3/src/runtime/components/OverlayProvider.vue) component.
::
First, create a slideover component that will be opened programmatically:
First, create a slideover component that will be opened programatically:
::component-example
---

View File

@@ -200,10 +200,6 @@ Use the `#content` slot to customize the content of each item.
Use the `slot` property to customize a specific item.
You will have access to the following slots:
- `#{{ item.slot }}`{lang="ts-type"}
:component-example{name="stepper-custom-slot-example"}
## API

View File

@@ -222,10 +222,6 @@ Use the `#content` slot to customize the content of each item.
Use the `slot` property to customize a specific item.
You will have access to the following slots:
- `#{{ item.slot }}`{lang="ts-type"}
:component-example{name="tabs-custom-slot-example"}
## API

View File

@@ -1,229 +0,0 @@
---
title: Timeline
description: 'A component that displays a sequence of events with dates, titles, icons or avatars.'
category: data
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxt/ui/tree/v3/src/runtime/components/Timeline.vue
navigation.badge: Soon
---
## Usage
### Items
Use the `items` prop as an array of objects with the following properties:
- `date?: string`{lang="ts-type"}
- `title?: string`{lang="ts-type"}
- `description?: AvatarProps`{lang="ts-type"}
- `icon?: string`{lang="ts-type"}
- `avatar?: AvatarProps`{lang="ts-type"}
- `value?: string | number`{lang="ts-type"}
- [`slot?: string`{lang="ts-type"}](#with-custom-slot)
- `class?: any`{lang="ts-type"}
- `ui?: { item?: ClassNameValue, container?: ClassNameValue, indicator?: ClassNameValue, separator?: ClassNameValue, wrapper?: ClassNameValue, date?: ClassNameValue, title?: ClassNameValue, description?: ClassNameValue }`{lang="ts-type"}
::component-code
---
ignore:
- items
- class
- defaultValue
external:
- items
externalTypes:
- TimelineItem[]
props:
defaultValue: 2
items:
- date: 'Mar 15, 2025'
title: 'Project Kickoff'
description: 'Kicked off the project with team alignment. Set up project milestones and allocated resources.'
icon: 'i-lucide-rocket'
- date: 'Mar 22 2025'
title: 'Design Phase'
description: 'User research and design workshops. Created wireframes and prototypes for user testing.'
icon: 'i-lucide-palette'
- date: 'Mar 29 2025'
title: 'Development Sprint'
description: 'Frontend and backend development. Implemented core features and integrated with APIs.'
icon: 'i-lucide-code'
- date: 'Apr 5 2025'
title: 'Testing & Deployment'
description: 'QA testing and performance optimization. Deployed the application to production.'
icon: 'i-lucide-check-circle'
class: 'w-96'
---
::
### Color
Use the `color` prop to change the color of the active items in a Timeline.
::component-code
---
ignore:
- items
- class
- defaultValue
external:
- items
externalTypes:
- TimelineItem[]
props:
color: neutral
defaultValue: 2
items:
- date: 'Mar 15, 2025'
title: 'Project Kickoff'
description: 'Kicked off the project with team alignment. Set up project milestones and allocated resources.'
icon: 'i-lucide-rocket'
- date: 'Mar 22 2025'
title: 'Design Phase'
description: 'User research and design workshops. Created wireframes and prototypes for user testing.'
icon: 'i-lucide-palette'
- date: 'Mar 29 2025'
title: 'Development Sprint'
description: 'Frontend and backend development. Implemented core features and integrated with APIs.'
icon: 'i-lucide-code'
- date: 'Apr 5 2025'
title: 'Testing & Deployment'
description: 'QA testing and performance optimization. Deployed the application to production.'
icon: 'i-lucide-check-circle'
class: 'w-96'
---
::
### Size
Use the `size` prop to change the size of the Timeline.
::component-code
---
ignore:
- items
- class
- defaultValue
external:
- items
externalTypes:
- TimelineItem[]
props:
size: xs
defaultValue: 2
items:
- date: 'Mar 15, 2025'
title: 'Project Kickoff'
description: 'Kicked off the project with team alignment. Set up project milestones and allocated resources.'
icon: 'i-lucide-rocket'
- date: 'Mar 22 2025'
title: 'Design Phase'
description: 'User research and design workshops. Created wireframes and prototypes for user testing.'
icon: 'i-lucide-palette'
- date: 'Mar 29 2025'
title: 'Development Sprint'
description: 'Frontend and backend development. Implemented core features and integrated with APIs.'
icon: 'i-lucide-code'
- date: 'Apr 5 2025'
title: 'Testing & Deployment'
description: 'QA testing and performance optimization. Deployed the application to production.'
icon: 'i-lucide-check-circle'
class: 'w-96'
---
::
### Orientation
Use the `orientation` prop to change the orientation of the Timeline. Defaults to `vertical`.
::component-code
---
ignore:
- items
- class
- defaultValue
external:
- items
externalTypes:
- TimelineItem[]
props:
orientation: 'horizontal'
defaultValue: 2
items:
- date: 'Mar 15, 2025'
title: 'Project Kickoff'
description: 'Kicked off the project with team alignment.'
icon: 'i-lucide-rocket'
- date: 'Mar 22 2025'
title: 'Design Phase'
description: 'User research and design workshops.'
icon: 'i-lucide-palette'
- date: 'Mar 29 2025'
title: 'Development Sprint'
description: 'Frontend and backend development.'
icon: 'i-lucide-code'
- date: 'Apr 5 2025'
title: 'Testing & Deployment'
description: 'QA testing and performance optimization.'
icon: 'i-lucide-check-circle'
class: 'w-full'
class: 'overflow-x-auto'
---
::
## Examples
### Control active item
You can control the active item by using the `default-value` prop or the `v-model` directive with the index of the item.
:component-example{name="timeline-model-value-example" prettier}
::tip
You can also pass the `value` of one of the items if provided.
::
### With alternating layout
Use the `ui` prop to create a Timeline with alternating layout.
:component-example{name="timeline-alternating-layout-example" prettier}
### With custom slot
Use the `slot` property to customize a specific item.
You will have access to the following slots:
- `#{{ item.slot }}-indicator`{lang="ts-type"}
- `#{{ item.slot }}-date`{lang="ts-type"}
- `#{{ item.slot }}-title`{lang="ts-type"}
- `#{{ item.slot }}-description`{lang="ts-type"}
:component-example{name="timeline-custom-slot-example" prettier}
### With slots
Use the available slots to create a more complex Timeline.
:component-example{name="timeline-slots-example" prettier}
## API
### Props
:component-props
### Slots
:component-slots
### Emits
:component-emits
## Theme
:component-theme

View File

@@ -407,14 +407,7 @@ This lets you select a parent item without expanding or collapsing its children.
### With custom slot
Use the `slot` property to customize a specific item.
You will have access to the following slots:
- `#{{ item.slot }}`{lang="ts-type"}
- `#{{ item.slot }}-leading`{lang="ts-type"}
- `#{{ item.slot }}-label`{lang="ts-type"}
- `#{{ item.slot }}-trailing`{lang="ts-type"}
Use the `item.slot` property to customize a specific item.
::component-example
---

View File

@@ -33,7 +33,7 @@ export default defineNuxtModule((_, nuxt) => {
}
const name = template.name.toLowerCase().replace(/\s/g, '-')
const filename = join(nuxt.options.rootDir, 'public/assets/showcase', `${name}.png`)
const filename = join(process.cwd(), 'docs/public/assets/showcase', `${name}.png`)
if (existsSync(filename)) {
continue

View File

@@ -2,48 +2,41 @@
"private": true,
"name": "@nuxt/ui-docs",
"type": "module",
"scripts": {
"dev": "nuxt dev",
"build": "nuxt build",
"generate": "nuxt generate",
"typecheck": "nuxt typecheck"
},
"dependencies": {
"@ai-sdk/vue": "^1.2.12",
"@iconify-json/logos": "^1.2.4",
"@iconify-json/lucide": "^1.2.47",
"@iconify-json/simple-icons": "^1.2.38",
"@iconify-json/vscode-icons": "^1.2.22",
"@nuxt/content": "^3.6.0",
"@iconify-json/lucide": "^1.2.44",
"@iconify-json/simple-icons": "^1.2.34",
"@iconify-json/vscode-icons": "^1.2.21",
"@nuxt/content": "^3.5.1",
"@nuxt/image": "^1.10.0",
"@nuxt/ui": "workspace:*",
"@nuxt/ui-pro": "https://pkg.pr.new/@nuxt/ui-pro@beebbd4",
"@nuxthub/core": "^0.9.0",
"@nuxt/ui": "latest",
"@nuxt/ui-pro": "https://pkg.pr.new/@nuxt/ui-pro@9038c43",
"@nuxthub/core": "^0.8.27",
"@nuxtjs/plausible": "^1.2.0",
"@octokit/rest": "^22.0.0",
"@octokit/rest": "^21.1.1",
"@rollup/plugin-yaml": "^4.1.2",
"@vueuse/integrations": "^13.3.0",
"@vueuse/nuxt": "^13.3.0",
"@vueuse/integrations": "^13.2.0",
"@vueuse/nuxt": "^13.2.0",
"ai": "^4.3.16",
"capture-website": "^4.2.0",
"joi": "^17.13.3",
"maska": "^3.1.1",
"motion-v": "^1.2.1",
"nuxt": "^3.17.5",
"motion-v": "^1.0.2",
"nuxt": "^3.17.4",
"nuxt-component-meta": "^0.11.0",
"nuxt-llms": "^0.1.3",
"nuxt-og-image": "^5.1.6",
"nuxt-llms": "^0.1.2",
"nuxt-og-image": "^5.1.3",
"prettier": "^3.5.3",
"shiki-transformer-color-highlight": "^1.0.0",
"sortablejs": "^1.15.6",
"superstruct": "^2.0.2",
"ufo": "^1.6.1",
"valibot": "^1.1.0",
"workers-ai-provider": "^0.6.0",
"workers-ai-provider": "^0.4.1",
"yup": "^1.6.1",
"zod": "^3.25.57"
"zod": "^3.24.4"
},
"devDependencies": {
"wrangler": "^4.19.1"
"wrangler": "^4.15.1"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -1,8 +1,8 @@
{
"name": "@nuxt/ui",
"description": "A UI Library for Modern Web Apps, powered by Vue & Tailwind CSS.",
"version": "3.1.3",
"packageManager": "pnpm@10.12.1",
"version": "3.1.2",
"packageManager": "pnpm@10.11.0",
"repository": {
"type": "git",
"url": "git+https://github.com/nuxt/ui.git"
@@ -96,37 +96,38 @@
"scripts": {
"build": "nuxt-module-build build",
"prepack": "pnpm build",
"dev": "nuxt dev playground --uiDev",
"dev:build": "nuxt build playground",
"dev": "nuxi dev playground --uiDev",
"dev:build": "nuxi build playground",
"dev:vue": "vite playground-vue -- --uiDev",
"dev:vue:build": "vite build playground-vue",
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt prepare playground && nuxt prepare docs && vite build playground-vue",
"docs": "nuxt dev docs --uiDev",
"docs:build": "nuxt build docs",
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground && nuxi prepare docs && vite build playground-vue",
"docs": "nuxi dev docs --uiDev",
"docs:build": "nuxi build docs",
"docs:prepare": "nuxt-component-meta docs",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"typecheck": "vue-tsc --noEmit && nuxt typecheck playground && nuxt typecheck docs && cd playground-vue && vue-tsc --noEmit",
"typecheck": "vue-tsc --noEmit && nuxi typecheck playground && nuxi typecheck docs && cd playground-vue && vue-tsc --noEmit",
"test": "vitest",
"test:vue": "vitest -c vitest.vue.config.ts",
"release": "release-it"
},
"dependencies": {
"@iconify/vue": "^5.0.0",
"@internationalized/date": "^3.8.2",
"@internationalized/number": "^3.6.3",
"@internationalized/date": "^3.8.0",
"@internationalized/number": "^3.6.1",
"@nuxt/fonts": "^0.11.4",
"@nuxt/icon": "^1.13.0",
"@nuxt/kit": "^3.17.5",
"@nuxt/schema": "^3.17.5",
"@nuxt/kit": "^3.17.4",
"@nuxt/schema": "^3.17.4",
"@nuxtjs/color-mode": "^3.5.2",
"@standard-schema/spec": "^1.0.0",
"@tailwindcss/postcss": "^4.1.10",
"@tailwindcss/vite": "^4.1.10",
"@tailwindcss/postcss": "^4.1.7",
"@tailwindcss/vite": "^4.1.7",
"@tanstack/vue-table": "^8.21.3",
"@unhead/vue": "^2.0.10",
"@vueuse/core": "^13.3.0",
"@vueuse/integrations": "^13.3.0",
"colortranslator": "^5.0.0",
"@unhead/vue": "^2.0.9",
"@vueuse/core": "^13.2.0",
"@vueuse/integrations": "^13.2.0",
"colortranslator": "^4.1.0",
"consola": "^3.4.2",
"defu": "^6.1.4",
"embla-carousel-auto-height": "^8.6.0",
@@ -143,29 +144,29 @@
"mlly": "^1.7.4",
"ohash": "^2.0.11",
"pathe": "^2.0.3",
"reka-ui": "2.3.1",
"reka-ui": "^2.2.1",
"scule": "^1.3.0",
"tailwind-variants": "^1.0.0",
"tailwindcss": "^4.1.10",
"tinyglobby": "^0.2.14",
"unplugin": "^2.3.5",
"unplugin-auto-import": "^19.3.0",
"unplugin-vue-components": "^28.7.0",
"vaul-vue": "0.4.1",
"tailwindcss": "^4.1.7",
"tinyglobby": "^0.2.13",
"unplugin": "^2.3.4",
"unplugin-auto-import": "^19.2.0",
"unplugin-vue-components": "^28.5.0",
"vaul-vue": "^0.4.1",
"vue-component-type-helpers": "^2.2.10"
},
"devDependencies": {
"@nuxt/eslint-config": "^1.4.1",
"@nuxt/eslint-config": "^1.4.0",
"@nuxt/module-builder": "^1.0.1",
"@nuxt/test-utils": "^3.19.1",
"@nuxt/test-utils": "^3.19.0",
"@release-it/conventional-changelog": "^10.0.1",
"@vue/test-utils": "^2.4.6",
"embla-carousel": "^8.6.0",
"eslint": "^9.28.0",
"happy-dom": "^17.6.3",
"nuxt": "^3.17.5",
"release-it": "^19.0.3",
"vitest": "^3.2.3",
"eslint": "^9.27.0",
"happy-dom": "^17.4.7",
"nuxt": "^3.17.4",
"release-it": "^19.0.2",
"vitest": "^3.1.3",
"vitest-environment-nuxt": "^1.0.1",
"vue-tsc": "^2.2.10"
},
@@ -208,7 +209,7 @@
"debug": "4.3.7",
"rollup": "4.34.9",
"unimport": "4.1.1",
"unplugin": "^2.3.5"
"unplugin": "^2.3.4"
},
"pnpm": {
"onlyBuiltDependencies": [

View File

@@ -10,10 +10,10 @@
"typecheck": "vue-tsc -p ./tsconfig.app.json"
},
"dependencies": {
"@nuxt/ui": "workspace:*",
"vue": "^3.5.16",
"@nuxt/ui": "latest",
"vue": "^3.5.14",
"vue-router": "^4.5.1",
"zod": "^3.25.57"
"zod": "^3.24.4"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.4",

View File

@@ -40,7 +40,6 @@ const components = [
'input',
'input-menu',
'input-number',
'input-tags',
'kbd',
'link',
'modal',
@@ -62,7 +61,6 @@ const components = [
'tabs',
'table',
'textarea',
'timeline',
'toast',
'tooltip',
'tree'

View File

@@ -40,7 +40,6 @@ const components = [
'input',
'input-menu',
'input-number',
'input-tags',
'kbd',
'link',
'modal',
@@ -62,7 +61,6 @@ const components = [
'tabs',
'table',
'textarea',
'timeline',
'toast',
'tooltip',
'tree'

View File

@@ -2,23 +2,19 @@
import { CalendarDate } from '@internationalized/date'
const singleValue = shallowRef(new CalendarDate(2022, 1, 10))
const multipleValue = shallowRef([new CalendarDate(2022, 1, 10), new CalendarDate(2022, 1, 20)])
const rangeValue = shallowRef({
const multipleValue = shallowRef({
start: new CalendarDate(2022, 1, 10),
end: new CalendarDate(2022, 1, 20)
})
</script>
<template>
<div class="flex gap-4">
<div class="flex flex-col gap-4">
<div class="flex justify-center gap-2">
<UCalendar v-model="singleValue" />
</div>
<div class="flex justify-center gap-2">
<UCalendar v-model="multipleValue" multiple />
</div>
<div class="flex justify-center gap-2">
<UCalendar v-model="rangeValue" range />
<UCalendar v-model="multipleValue" range />
</div>
</div>
</template>

View File

@@ -145,18 +145,5 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
class="w-48"
/>
</div>
<div class="flex items-center gap-4">
<UInputMenu
v-for="variant in variants"
:key="variant"
:items="items"
:model-value="[fruits[0]!]"
multiple
icon="i-lucide-search"
placeholder="Search..."
:variant="variant"
class="w-48"
/>
</div>
</div>
</template>

View File

@@ -1,87 +0,0 @@
<script setup lang="ts">
import { upperFirst } from 'scule'
import theme from '#build/ui/input-tags'
const sizes = Object.keys(theme.variants.size) as Array<keyof typeof theme.variants.size>
const variants = Object.keys(theme.variants.variant) as Array<keyof typeof theme.variants.variant>
const tags = ref(['Vue', 'Nuxt'])
</script>
<template>
<div class="flex flex-col items-center gap-4">
<div class="flex flex-col gap-4 w-48">
<UInputTags
v-model="tags"
placeholder="Enter tags..."
autofocus
/>
</div>
<div class="flex items-center gap-2">
<UInputTags
v-for="variant in variants"
:key="variant"
:placeholder="upperFirst(variant)"
:variant="variant"
class="w-48"
/>
</div>
<div class="flex items-center gap-2">
<UInputTags
v-for="variant in variants"
:key="variant"
:placeholder="upperFirst(variant)"
:variant="variant"
color="neutral"
class="w-48"
/>
</div>
<div class="flex items-center gap-2">
<UInputTags
v-for="variant in variants"
:key="variant"
:placeholder="upperFirst(variant)"
:variant="variant"
color="error"
highlight
class="w-48"
/>
</div>
<div class="flex flex-col gap-4 w-48">
<UInputTags placeholder="Disabled" disabled />
<UInputTags placeholder="Required" required />
<UInputTags loading placeholder="Loading..." />
<UInputTags loading trailing placeholder="Loading..." />
</div>
<div class="flex items-center gap-4">
<UInputTags
v-for="size in sizes"
:key="size"
:size="size"
:placeholder="upperFirst(size)"
class="w-48"
/>
</div>
<div class="flex items-center gap-4">
<UInputTags
v-for="size in sizes"
:key="size"
icon="i-lucide-search"
placeholder="Search..."
:size="size"
class="w-48"
/>
</div>
<div class="flex items-center gap-4">
<UInputTags
v-for="size in sizes"
:key="size"
icon="i-lucide-search"
trailing
placeholder="Search..."
:size="size"
class="w-48"
/>
</div>
</div>
</template>

View File

@@ -69,13 +69,5 @@ function openModal() {
</UModal>
<UButton label="Open programmatically" color="neutral" variant="outline" @click="openModal" />
<UModal title="First modal">
<UButton color="neutral" variant="outline" label="Close with scoped slot close" />
<template #footer="{ close }">
<UButton label="Close with scoped slot close" @click="close" />
</template>
</UModal>
</div>
</template>

View File

@@ -5,7 +5,7 @@ const sizes = Object.keys(theme.variants.size) as Array<keyof typeof theme.varia
const variants = Object.keys(theme.variants.variant) as Array<keyof typeof theme.variants.variant>
const onComplete = (e: string[]) => {
console.log(e)
alert(e.join(''))
}
</script>

View File

@@ -125,21 +125,5 @@ function openSlideover() {
</USlideover>
<UButton label="Open programmatically" color="neutral" variant="outline" @click="openSlideover" />
<USlideover title="Slideover with scoped slot close" description="This slideover has a scoped slot close that can be used to close the slideover from within the content.">
<UButton color="neutral" variant="subtle" label="Open with scoped slot close" />
<template #header="{ close }">
<UButton label="Close with scoped slot close" @click="close" />
</template>
<template #body="{ close }">
<UButton label="Close with scoped slot close" @click="close" />
</template>
<template #footer="{ close }">
<UButton label="Close with scoped slot close" @click="close" />
</template>
</USlideover>
</div>
</template>

View File

@@ -1,60 +0,0 @@
<script lang="ts" setup>
import type { TimelineItem } from '@nuxt/ui'
import theme from '#build/ui/timeline'
const sizes = Object.keys(theme.variants.size)
const colors = Object.keys(theme.variants.color)
const orientations = Object.keys(theme.variants.orientation)
const orientation = ref('vertical' as const)
const color = ref('primary' as const)
const size = ref('md' as const)
const items = [{
date: 'Mar 15, 2025',
title: 'Project Kickoff',
description: 'Kicked off the project with team alignment. Set up project milestones and allocated resources.',
icon: 'i-lucide-rocket',
value: 'kickoff'
}, {
date: 'Mar 22, 2025',
title: 'Design Phase',
description: 'User research and design workshops. Created wireframes and prototypes for user testing',
icon: 'i-lucide-palette',
value: 'design'
}, {
date: 'Mar 29, 2025',
title: 'Development Sprint',
description: 'Frontend and backend development. Implemented core features and integrated with APIs.',
icon: 'i-lucide-code',
value: 'development'
}, {
date: 'Apr 5, 2025',
title: 'Testing & Deployment',
description: 'QA testing and performance optimization. Deployed the application to production.',
icon: 'i-lucide-check-circle',
value: 'deployment'
}] satisfies TimelineItem[]
const value = ref('kickoff')
</script>
<template>
<div class="flex flex-col gap-10">
<div class="flex items-center justify-center gap-2">
<USelect v-model="color" :items="colors" placeholder="Color" />
<USelect v-model="orientation" :items="orientations" placeholder="Orientation" />
<USelect v-model="size" :items="sizes" placeholder="Size" />
<USelect v-model="value" :items="items.map(item => item.value)" placeholder="Value" />
</div>
<UTimeline
v-model="value"
:color="color"
:orientation="orientation"
:size="size"
:items="items"
class="data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-96"
/>
</div>
</template>

View File

@@ -3,19 +3,18 @@
"name": "@nuxt/ui-playground",
"type": "module",
"scripts": {
"dev": "nuxt dev",
"build": "nuxt build",
"generate": "nuxt generate",
"dev": "nuxi dev",
"build": "nuxi build",
"generate": "nuxi generate",
"typecheck": "nuxt typecheck"
},
"dependencies": {
"@iconify-json/lucide": "^1.2.47",
"@iconify-json/simple-icons": "^1.2.38",
"@internationalized/date": "^3.8.2",
"@nuxt/ui": "workspace:*",
"@nuxthub/core": "^0.9.0",
"nuxt": "^3.17.5",
"zod": "^3.25.57"
"@iconify-json/lucide": "^1.2.44",
"@iconify-json/simple-icons": "^1.2.34",
"@nuxt/ui": "latest",
"@nuxthub/core": "^0.8.27",
"nuxt": "^3.17.4",
"zod": "^3.24.4"
},
"devDependencies": {
"typescript": "^5.8.3",

3651
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -19,8 +19,7 @@
}, {
"groupName": "reka-ui",
"matchPackageNames": [
"reka-ui",
"vaul-vue"
"reka-ui"
]
}, {
"matchDepTypes": ["peerDependencies"],

View File

@@ -1,7 +1,6 @@
<script lang="ts">
import type { AppConfig } from '@nuxt/schema'
import theme from '#build/ui/avatar'
import type { ChipProps } from '../types'
import type { ComponentConfig } from '../types/utils'
type Avatar = ComponentConfig<typeof theme, AppConfig, 'avatar'>
@@ -23,7 +22,6 @@ export interface AvatarProps {
* @defaultValue 'md'
*/
size?: Avatar['variants']['size']
chip?: boolean | ChipProps
class?: any
style?: any
ui?: Avatar['slots']
@@ -42,7 +40,6 @@ import ImageComponent from '#build/ui-image-component'
import { useAvatarGroup } from '../composables/useAvatarGroup'
import { tv } from '../utils/tv'
import UIcon from './Icon.vue'
import UChip from './Chip.vue'
defineOptions({ inheritAttrs: false })
@@ -84,13 +81,7 @@ function onError() {
</script>
<template>
<component
:is="props.chip ? UChip : Primitive"
:as="as"
v-bind="props.chip ? (typeof props.chip === 'object' ? { inset: true, ...props.chip } : { inset: true }) : {}"
:class="ui.root({ class: [props.ui?.root, props.class] })"
:style="props.style"
>
<Primitive :as="as" :class="ui.root({ class: [props.ui?.root, props.class] })" :style="props.style">
<component
:is="ImageComponent"
v-if="src && !error"
@@ -110,5 +101,5 @@ function onError() {
<span v-else :class="ui.fallback({ class: props.ui?.fallback })">{{ fallback || '&nbsp;' }}</span>
</slot>
</Slot>
</component>
</Primitive>
</template>

View File

@@ -153,8 +153,8 @@ const Calendar = computed(() => props.range ? RangeCalendar : SingleCalendar)
<Calendar.Root
v-slot="{ weekDays, grid }"
v-bind="rootProps"
:model-value="(modelValue as DateValue | DateValue[])"
:default-value="(defaultValue as DateValue)"
:model-value="modelValue"
:default-value="defaultValue"
:locale="locale"
:dir="dir"
:class="ui.root({ class: [props.ui?.root, props.class] })"

View File

@@ -336,7 +336,6 @@ defineExpose({
<button
:aria-label="t('carousel.goto', { slide: index + 1 })"
:class="ui.dot({ class: props.ui?.dot, active: selectedIndex === index })"
:data-state="selectedIndex === index ? 'active' : undefined"
@click="scrollTo(index)"
/>
</template>

View File

@@ -19,7 +19,7 @@ export interface CommandPaletteItem extends Omit<LinkProps, 'type' | 'raw' | 'cu
* @IconifyIcon
*/
icon?: string
avatar?: AvatarProps
avatar?: AvatarProps & { chip?: boolean | ChipProps }
chip?: ChipProps
kbds?: KbdProps['value'][] | KbdProps[]
active?: boolean
@@ -300,6 +300,9 @@ const groups = computed(() => {
<slot :name="((item.slot ? `${item.slot}-leading` : group.slot ? `${group.slot}-leading` : `item-leading`) as keyof CommandPaletteSlots<G, T>)" :item="(item as any)" :index="index">
<UIcon v-if="item.loading" :name="loadingIcon || appConfig.ui.icons.loading" :class="ui.itemLeadingIcon({ class: [props.ui?.itemLeadingIcon, item.ui?.itemLeadingIcon], loading: true })" />
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: [props.ui?.itemLeadingIcon, item.ui?.itemLeadingIcon], active: active || item.active })" />
<UChip v-else-if="item.avatar?.chip" v-bind="typeof item.avatar.chip === 'boolean' ? {} : item.avatar.chip" :size="((item.ui?.itemLeadingChipSize || props.ui?.itemLeadingChipSize || ui.itemLeadingChipSize()) as ChipProps['size'])" :class="ui.itemLeadingChip({ class: [props.ui?.itemLeadingChip, item.ui?.itemLeadingChip], active: active || item.active })">
<UAvatar :size="((item.ui?.itemLeadingAvatarSize || props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="omit(item.avatar, ['chip'])" :class="ui.itemLeadingAvatar({ class: [props.ui?.itemLeadingAvatar, item.ui?.itemLeadingAvatar], active: active || item.active })" />
</UChip>
<UAvatar v-else-if="item.avatar" :size="((item.ui?.itemLeadingAvatarSize || props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: [props.ui?.itemLeadingAvatar, item.ui?.itemLeadingAvatar], active: active || item.active })" />
<UChip
v-else-if="item.chip"

View File

@@ -2,12 +2,12 @@
import type { DeepReadonly } from 'vue'
import type { AppConfig } from '@nuxt/schema'
import theme from '#build/ui/form'
import type { FormSchema, FormError, FormInputEvents, FormErrorEvent, FormSubmitEvent, FormEvent, Form, FormErrorWithId, InferInput, InferOutput, FormData } from '../types/form'
import type { FormSchema, FormError, FormInputEvents, FormErrorEvent, FormSubmitEvent, FormEvent, Form, FormErrorWithId, InferInput, InferOutput } from '../types/form'
import type { ComponentConfig } from '../types/utils'
type FormConfig = ComponentConfig<typeof theme, AppConfig, 'form'>
export interface FormProps<S extends FormSchema, T extends boolean = true> {
export interface FormProps<S extends FormSchema> {
id?: string | number
/** Schema to validate the form state. Supports Standard Schema objects, Yup, Joi, and Superstructs. */
schema?: S
@@ -35,7 +35,7 @@ export interface FormProps<S extends FormSchema, T extends boolean = true> {
* If true, schema transformations will be applied to the state on submit.
* @defaultValue `true`
*/
transform?: T
transform?: boolean
/**
* If true, this form will attach to its parent Form (if any) and validate at the same time.
@@ -50,20 +50,20 @@ export interface FormProps<S extends FormSchema, T extends boolean = true> {
*/
loadingAuto?: boolean
class?: any
onSubmit?: ((event: FormSubmitEvent<FormData<S, T>>) => void | Promise<void>) | (() => void | Promise<void>)
onSubmit?: ((event: FormSubmitEvent<InferOutput<S>>) => void | Promise<void>) | (() => void | Promise<void>)
}
export interface FormEmits<S extends FormSchema, T extends boolean = true> {
(e: 'submit', payload: FormSubmitEvent<FormData<S, T>>): void
export interface FormEmits<S extends FormSchema> {
(e: 'submit', payload: FormSubmitEvent<InferOutput<S>>): void
(e: 'error', payload: FormErrorEvent): void
}
export interface FormSlots {
default(props?: { errors: FormError[], loading: boolean }): any
default(props?: { errors: FormError[] }): any
}
</script>
<script lang="ts" setup generic="S extends FormSchema, T extends boolean = true">
<script lang="ts" setup generic="S extends FormSchema">
import { provide, inject, nextTick, ref, onUnmounted, onMounted, computed, useId, readonly } from 'vue'
import { useEventBus } from '@vueuse/core'
import { useAppConfig } from '#imports'
@@ -75,17 +75,17 @@ import { FormValidationException } from '../types/form'
type I = InferInput<S>
type O = InferOutput<S>
const props = withDefaults(defineProps<FormProps<S, T>>(), {
const props = withDefaults(defineProps<FormProps<S>>(), {
validateOn() {
return ['input', 'blur', 'change'] as FormInputEvents[]
},
validateOnInputDelay: 300,
attach: true,
transform: () => true as T,
transform: true,
loadingAuto: true
})
const emits = defineEmits<FormEmits<S, T>>()
const emits = defineEmits<FormEmits<S>>()
defineSlots<FormSlots>()
const appConfig = useAppConfig() as FormConfig['AppConfig']
@@ -183,10 +183,10 @@ async function getErrors(): Promise<FormErrorWithId[]> {
return resolveErrorIds(errs)
}
type ValidateOpts<Silent extends boolean, Transform extends boolean> = { name?: keyof I | (keyof I)[], silent?: Silent, nested?: boolean, transform?: Transform }
async function _validate<T extends boolean>(opts: ValidateOpts<false, T>): Promise<FormData<S, T>>
async function _validate<T extends boolean>(opts: ValidateOpts<true, T>): Promise<FormData<S, T> | false>
async function _validate<T extends boolean>(opts: ValidateOpts<boolean, boolean> = { silent: false, nested: true, transform: false }): Promise<FormData<S, T> | false> {
type ValidateOpts<Silent extends boolean> = { name?: keyof I | (keyof I)[], silent?: Silent, nested?: boolean, transform?: boolean }
async function _validate(opts: ValidateOpts<false>): Promise<O>
async function _validate(opts: ValidateOpts<true>): Promise<O | false>
async function _validate(opts: ValidateOpts<boolean> = { silent: false, nested: true, transform: false }): Promise<O | false> {
const names = opts.name && !Array.isArray(opts.name) ? [opts.name] : opts.name as (keyof O)[]
const nestedValidatePromises = !names && opts.nested
@@ -227,7 +227,7 @@ async function _validate<T extends boolean>(opts: ValidateOpts<boolean, boolean>
Object.assign(props.state, transformedState.value)
}
return props.state as FormData<S, T>
return props.state as O
}
const loading = ref(false)
@@ -236,7 +236,7 @@ provide(formLoadingInjectionKey, readonly(loading))
async function onSubmitWrapper(payload: Event) {
loading.value = props.loadingAuto && true
const event = payload as FormSubmitEvent<FormData<S, T>>
const event = payload as FormSubmitEvent<O>
try {
event.data = await _validate({ nested: true, transform: props.transform })
@@ -265,7 +265,7 @@ provide(formOptionsInjectionKey, computed(() => ({
validateOnInputDelay: props.validateOnInputDelay
})))
defineExpose<Form<S>>({
defineExpose<Form<I>>({
validate: _validate,
errors,
@@ -315,6 +315,6 @@ defineExpose<Form<S>>({
:class="ui({ class: props.class })"
@submit.prevent="onSubmitWrapper"
>
<slot :errors="errors" :loading="loading" />
<slot :errors="errors" />
</component>
</template>

View File

@@ -379,7 +379,6 @@ function onSelect(e: Event, item: InputMenuItem) {
function isInputItem(item: InputMenuItem): item is _InputMenuItem {
return typeof item === 'object' && item !== null
}
defineExpose({
inputRef
})

View File

@@ -7,7 +7,7 @@ import type { ComponentConfig } from '../types/utils'
type InputNumber = ComponentConfig<typeof theme, AppConfig, 'inputNumber'>
export interface InputNumberProps extends Pick<NumberFieldRootProps, 'modelValue' | 'defaultValue' | 'min' | 'max' | 'step' | 'stepSnapping' | 'disabled' | 'required' | 'id' | 'name' | 'formatOptions' | 'disableWheelChange' | 'invertWheelChange'> {
export interface InputNumberProps extends Pick<NumberFieldRootProps, 'modelValue' | 'defaultValue' | 'min' | 'max' | 'step' | 'stepSnapping' | 'disabled' | 'required' | 'id' | 'name' | 'formatOptions' | 'disableWheelChange'> {
/**
* The element or component this component should render as.
* @defaultValue 'div'
@@ -98,7 +98,7 @@ defineSlots<InputNumberSlots>()
const { t, code: codeLocale } = useLocale()
const appConfig = useAppConfig() as InputNumber['AppConfig']
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'min', 'max', 'step', 'stepSnapping', 'formatOptions', 'disableWheelChange', 'invertWheelChange'), emits)
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'min', 'max', 'step', 'stepSnapping', 'formatOptions', 'disableWheelChange'), emits)
const { emitFormBlur, emitFormFocus, emitFormChange, emitFormInput, id, color, size: formGroupSize, name, highlight, disabled, ariaAttrs } = useFormField<InputNumberProps>(props)
const { orientation, size: buttonGroupSize } = useButtonGroup<InputNumberProps>(props)

View File

@@ -1,203 +0,0 @@
<script lang="ts">
import type { AppConfig } from '@nuxt/schema'
import type { TagsInputRootProps, AcceptableInputValue } from 'reka-ui'
import theme from '#build/ui/input-tags'
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
import type { AvatarProps } from '../types'
import type { ComponentConfig } from '../types/utils'
type InputTags = ComponentConfig<typeof theme, AppConfig, 'inputTags'>
export type InputTagItem = AcceptableInputValue
export interface InputTagsProps<T extends InputTagItem = InputTagItem> extends Pick<TagsInputRootProps<T>, 'modelValue' | 'defaultValue' | 'addOnPaste' | 'addOnTab' | 'addOnBlur' | 'duplicate' | 'disabled' | 'delimiter' | 'max' | 'id' | 'convertValue' | 'displayValue' | 'name' | 'required'>, UseComponentIconsProps {
/**
* The element or component this component should render as.
* @defaultValue 'div'
*/
as?: any
/** The placeholder text when the input is empty. */
placeholder?: string
/**
* @defaultValue 'primary'
*/
color?: InputTags['variants']['color']
/**
* @defaultValue 'outline'
*/
variant?: InputTags['variants']['variant']
/**
* @defaultValue 'md'
*/
size?: InputTags['variants']['size']
autofocus?: boolean
autofocusDelay?: number
/**
* The icon displayed to delete a tag.
* @defaultValue appConfig.ui.icons.close
* @IconifyIcon
*/
deleteIcon?: string
/** Highlight the ring color like a focus state. */
highlight?: boolean
class?: any
ui?: InputTags['slots']
}
export type InputTagsEmits<T extends InputTagItem> = {
'update:modelValue': [payload: T[]]
'change': [payload: Event]
'blur': [payload: FocusEvent]
'focus': [payload: FocusEvent]
}
type SlotProps<T extends InputTagItem> = (props: { item: T, index: number }) => any
export interface InputTagsSlots<T extends InputTagItem = InputTagItem> {
'leading'(props?: {}): any
'default'(props?: {}): any
'trailing'(props?: {}): any
'item-text': SlotProps<T>
'item-delete': SlotProps<T>
}
</script>
<script setup lang="ts" generic="T extends InputTagItem">
import { computed, ref, onMounted, toRaw } from 'vue'
import { TagsInputRoot, TagsInputItem, TagsInputItemText, TagsInputItemDelete, TagsInputInput, useForwardPropsEmits } from 'reka-ui'
import { reactivePick } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { useButtonGroup } from '../composables/useButtonGroup'
import { useComponentIcons } from '../composables/useComponentIcons'
import { useFormField } from '../composables/useFormField'
import { tv } from '../utils/tv'
import UIcon from './Icon.vue'
defineOptions({ inheritAttrs: false })
const props = withDefaults(defineProps<InputTagsProps<T>>(), {
type: 'text',
autofocusDelay: 0
})
const emits = defineEmits<InputTagsEmits<T>>()
const slots = defineSlots<InputTagsSlots<T>>()
const appConfig = useAppConfig() as InputTags['AppConfig']
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'addOnPaste', 'addOnTab', 'addOnBlur', 'duplicate', 'delimiter', 'max', 'convertValue', 'displayValue', 'required'), emits)
const { emitFormBlur, emitFormFocus, emitFormChange, emitFormInput, size: formGroupSize, color, id, name, highlight, disabled, ariaAttrs } = useFormField<InputTagsProps>(props)
const { orientation, size: buttonGroupSize } = useButtonGroup<InputTagsProps>(props)
const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(props)
const inputSize = computed(() => buttonGroupSize.value || formGroupSize.value)
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.inputTags || {}) })({
color: color.value,
variant: props.variant,
size: inputSize?.value,
loading: props.loading,
highlight: highlight.value,
leading: isLeading.value || !!props.avatar || !!slots.leading,
trailing: isTrailing.value || !!slots.trailing,
buttonGroup: orientation.value
}))
const inputRef = ref<InstanceType<typeof TagsInputInput> | null>(null)
onMounted(() => {
setTimeout(() => {
autoFocus()
}, props.autofocusDelay)
})
function autoFocus() {
if (props.autofocus) {
inputRef.value?.$el?.focus()
}
}
function onUpdate(value: T[]) {
if (toRaw(props.modelValue) === value) {
return
}
// @ts-expect-error - 'target' does not exist in type 'EventInit'
const event = new Event('change', { target: { value } })
emits('change', event)
emitFormChange()
emitFormInput()
}
function onBlur(event: FocusEvent) {
emits('blur', event)
emitFormBlur()
}
function onFocus(event: FocusEvent) {
emits('focus', event)
emitFormFocus()
}
defineExpose({
inputRef
})
</script>
<!-- eslint-disable vue/no-template-shadow -->
<template>
<TagsInputRoot
:id="id"
v-slot="{ modelValue: tags }"
:model-value="modelValue"
:default-value="defaultValue"
:class="ui.root({ class: [ui.base({ class: props.ui?.base }), props.ui?.root, props.class] })"
v-bind="rootProps"
:name="name"
:disabled="disabled"
@update:model-value="onUpdate"
@blur="onBlur"
@focus="onFocus"
>
<TagsInputItem
v-for="(item, index) in tags"
:key="index"
:value="item"
:class="ui.item({ class: [props.ui?.item] })"
>
<TagsInputItemText :class="ui.itemText({ class: [props.ui?.itemText] })">
<slot v-if="!!slots['item-text']" name="item-text" :item="(item as T)" :index="index" />
</TagsInputItemText>
<TagsInputItemDelete
:class="ui.itemDelete({ class: [props.ui?.itemDelete] })"
:disabled="disabled"
>
<slot name="item-delete" :item="(item as T)" :index="index">
<UIcon :name="deleteIcon || appConfig.ui.icons.close" :class="ui.itemDeleteIcon({ class: [props.ui?.itemDeleteIcon] })" />
</slot>
</TagsInputItemDelete>
</TagsInputItem>
<TagsInputInput
ref="inputRef"
v-bind="{ ...$attrs, ...ariaAttrs }"
:placeholder="placeholder"
:class="ui.input({ class: props.ui?.input })"
/>
<slot />
<span v-if="isLeading || !!avatar || !!slots.leading" :class="ui.leading({ class: props.ui?.leading })">
<slot name="leading">
<UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
<UAvatar v-else-if="!!avatar" :size="((props.ui?.leadingAvatarSize || ui.leadingAvatarSize()) as AvatarProps['size'])" v-bind="avatar" :class="ui.leadingAvatar({ class: props.ui?.leadingAvatar })" />
</slot>
</span>
<span v-if="isTrailing || !!slots.trailing" :class="ui.trailing({ class: props.ui?.trailing })">
<slot name="trailing">
<UIcon v-if="trailingIconName" :name="trailingIconName" :class="ui.trailingIcon({ class: props.ui?.trailingIcon })" />
</slot>
</span>
</TagsInputRoot>
</template>

View File

@@ -61,13 +61,13 @@ export interface ModalEmits extends DialogRootEmits {
export interface ModalSlots {
default(props: { open: boolean }): any
content(props: { close: () => void }): any
header(props: { close: () => void }): any
content(props?: {}): any
header(props?: {}): any
title(props?: {}): any
description(props?: {}): any
close(props: { close: () => void, ui: { [K in keyof Required<Modal['slots']>]: (props?: Record<string, any>) => string } }): any
body(props: { close: () => void }): any
footer(props: { close: () => void }): any
close(props: { ui: { [K in keyof Required<Modal['slots']>]: (props?: Record<string, any>) => string } }): any
body(props?: {}): any
footer(props?: {}): any
}
</script>
@@ -104,15 +104,15 @@ const contentEvents = computed(() => {
}
if (!props.dismissible) {
const events = ['pointerDownOutside', 'interactOutside', 'escapeKeyDown']
const events = ['pointerDownOutside', 'interactOutside', 'escapeKeyDown', 'closeAutoFocus'] as const
type EventType = typeof events[number]
return events.reduce((acc, curr) => {
acc[curr] = (e: Event) => {
e.preventDefault()
emits('close:prevent')
}
return acc
}, defaultEvents as Record<typeof events[number] | keyof typeof defaultEvents, (e: Event) => void>)
}, {} as Record<EventType, (e: Event) => void>)
}
return defaultEvents
@@ -124,9 +124,8 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.modal || {})
}))
</script>
<!-- eslint-disable vue/no-template-shadow -->
<template>
<DialogRoot v-slot="{ open, close }" v-bind="rootProps">
<DialogRoot v-slot="{ open }" v-bind="rootProps">
<DialogTrigger v-if="!!slots.default" as-child :class="props.class">
<slot :open="open" />
</DialogTrigger>
@@ -149,9 +148,9 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.modal || {})
</DialogDescription>
</VisuallyHidden>
<slot name="content" :close="close">
<div v-if="!!slots.header || (title || !!slots.title) || (description || !!slots.description) || (props.close || !!slots.close)" :class="ui.header({ class: props.ui?.header })">
<slot name="header" :close="close">
<slot name="content">
<div v-if="!!slots.header || (title || !!slots.title) || (description || !!slots.description) || (close || !!slots.close)" :class="ui.header({ class: props.ui?.header })">
<slot name="header">
<div :class="ui.wrapper({ class: props.ui?.wrapper })">
<DialogTitle v-if="title || !!slots.title" :class="ui.title({ class: props.ui?.title })">
<slot name="title">
@@ -166,16 +165,16 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.modal || {})
</DialogDescription>
</div>
<DialogClose v-if="props.close || !!slots.close" as-child>
<slot name="close" :close="close" :ui="ui">
<DialogClose v-if="close || !!slots.close" as-child>
<slot name="close" :ui="ui">
<UButton
v-if="props.close"
v-if="close"
:icon="closeIcon || appConfig.ui.icons.close"
size="md"
color="neutral"
variant="ghost"
:aria-label="t('modal.close')"
v-bind="(typeof props.close === 'object' ? props.close as Partial<ButtonProps> : {})"
v-bind="(typeof close === 'object' ? close as Partial<ButtonProps> : {})"
:class="ui.close({ class: props.ui?.close })"
/>
</slot>
@@ -184,11 +183,11 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.modal || {})
</div>
<div v-if="!!slots.body" :class="ui.body({ class: props.ui?.body })">
<slot name="body" :close="close" />
<slot name="body" />
</div>
<div v-if="!!slots.footer" :class="ui.footer({ class: props.ui?.footer })">
<slot name="footer" :close="close" />
<slot name="footer" />
</div>
</slot>
</DialogContent>

View File

@@ -2,13 +2,13 @@
import { computed } from 'vue'
import { useOverlay, type Overlay } from '../composables/useOverlay'
const { overlays, unmount, close } = useOverlay()
const { overlays, unMount, close } = useOverlay()
const mountedOverlays = computed(() => overlays.filter((overlay: Overlay) => overlay.isMounted))
const onAfterLeave = (id: symbol) => {
close(id)
unmount(id)
unMount(id)
}
const onClose = (id: symbol, value: any) => {

View File

@@ -7,9 +7,7 @@ import type { ComponentConfig } from '../types/utils'
type PinInput = ComponentConfig<typeof theme, AppConfig, 'pinInput'>
type PinInputType = 'text' | 'number'
export interface PinInputProps<T extends PinInputType = 'text'> extends Pick<PinInputRootProps<T>, 'defaultValue' | 'disabled' | 'id' | 'mask' | 'modelValue' | 'name' | 'otp' | 'placeholder' | 'required' | 'type'> {
export interface PinInputProps extends Pick<PinInputRootProps, 'defaultValue' | 'disabled' | 'id' | 'mask' | 'modelValue' | 'name' | 'otp' | 'placeholder' | 'required' | 'type'> {
/**
* The element or component this component should render as.
* @defaultValue 'div'
@@ -39,14 +37,14 @@ export interface PinInputProps<T extends PinInputType = 'text'> extends Pick<Pin
ui?: PinInput['slots']
}
export type PinInputEmits<T extends PinInputType = 'text'> = PinInputRootEmits<T> & {
export type PinInputEmits = PinInputRootEmits & {
change: [payload: Event]
blur: [payload: Event]
}
</script>
<script setup lang="ts" generic="T extends PinInputType = 'text'">
<script setup lang="ts">
import type { ComponentPublicInstance } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { PinInputInput, PinInputRoot, useForwardPropsEmits } from 'reka-ui'
@@ -56,16 +54,16 @@ import { useFormField } from '../composables/useFormField'
import { looseToNumber } from '../utils'
import { tv } from '../utils/tv'
const props = withDefaults(defineProps<PinInputProps<T>>(), {
type: 'text' as never,
const props = withDefaults(defineProps<PinInputProps>(), {
type: 'text',
length: 5,
autofocusDelay: 0
})
const emits = defineEmits<PinInputEmits<T>>()
const emits = defineEmits<PinInputEmits>()
const appConfig = useAppConfig() as PinInput['AppConfig']
const rootProps = useForwardPropsEmits(reactivePick(props, 'defaultValue', 'disabled', 'id', 'mask', 'modelValue', 'name', 'otp', 'required', 'type'), emits)
const rootProps = useForwardPropsEmits(reactivePick(props, 'defaultValue', 'disabled', 'id', 'mask', 'modelValue', 'name', 'otp', 'placeholder', 'required', 'type'), emits)
const { emitFormInput, emitFormFocus, emitFormChange, emitFormBlur, size, color, id, name, highlight, disabled, ariaAttrs } = useFormField<PinInputProps>(props)
@@ -79,7 +77,7 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.pinInput ||
const inputsRef = ref<ComponentPublicInstance[]>([])
const completed = ref(false)
function onComplete(value: string[] | number[]) {
function onComplete(value: string[]) {
// @ts-expect-error - 'target' does not exist in type 'EventInit'
const event = new Event('change', { target: { value } })
emits('change', event)
@@ -115,7 +113,6 @@ defineExpose({
v-bind="{ ...rootProps, ...ariaAttrs }"
:id="id"
:name="name"
:placeholder="placeholder"
:class="ui.root({ class: [props.ui?.root, props.class] })"
@update:model-value="emitFormInput()"
@complete="onComplete"

View File

@@ -75,15 +75,15 @@ const portalProps = usePortal(toRef(() => props.portal))
const contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffset: 8, collisionPadding: 8 }) as PopoverContentProps)
const contentEvents = computed(() => {
if (!props.dismissible) {
const events = ['pointerDownOutside', 'interactOutside', 'escapeKeyDown']
const events = ['pointerDownOutside', 'interactOutside', 'escapeKeyDown'] as const
type EventType = typeof events[number]
return events.reduce((acc, curr) => {
acc[curr] = (e: Event) => {
e.preventDefault()
emits('close:prevent')
}
return acc
}, {} as Record<typeof events[number], (e: Event) => void>)
}, {} as Record<EventType, (e: Event) => void>)
}
return {}

View File

@@ -7,7 +7,7 @@ import type { ComponentConfig } from '../types/utils'
type Progress = ComponentConfig<typeof theme, AppConfig, 'progress'>
export interface ProgressProps extends Pick<ProgressRootProps, 'getValueLabel' | 'getValueText' | 'modelValue'> {
export interface ProgressProps extends Pick<ProgressRootProps, 'getValueLabel' | 'modelValue'> {
/**
* The element or component this component should render as.
* @defaultValue 'div'
@@ -70,7 +70,7 @@ const slots = defineSlots<ProgressSlots>()
const { dir } = useLocale()
const appConfig = useAppConfig() as Progress['AppConfig']
const rootProps = useForwardPropsEmits(reactivePick(props, 'getValueLabel', 'getValueText', 'modelValue'), emits)
const rootProps = useForwardPropsEmits(reactivePick(props, 'getValueLabel', 'modelValue'), emits)
const isIndeterminate = computed(() => rootProps.value.modelValue === null)
const hasSteps = computed(() => Array.isArray(props.max))

View File

@@ -92,8 +92,6 @@ export interface SelectProps<T extends ArrayOrNested<SelectItem> = ArrayOrNested
multiple?: M & boolean
/** Highlight the ring color like a focus state. */
highlight?: boolean
autofocus?: boolean
autofocusDelay?: number
class?: any
ui?: Select['slots']
}
@@ -136,7 +134,7 @@ export interface SelectSlots<
</script>
<script setup lang="ts" generic="T extends ArrayOrNested<SelectItem>, VK extends GetItemKeys<T> = 'value', M extends boolean = false">
import { ref, computed, onMounted, toRef } from 'vue'
import { computed, toRef } from 'vue'
import { SelectRoot, SelectArrow, SelectTrigger, SelectPortal, SelectContent, SelectLabel, SelectGroup, SelectItem, SelectItemIndicator, SelectItemText, SelectSeparator, useForwardPropsEmits } from 'reka-ui'
import { defu } from 'defu'
import { reactivePick } from '@vueuse/core'
@@ -156,8 +154,7 @@ defineOptions({ inheritAttrs: false })
const props = withDefaults(defineProps<SelectProps<T, VK, M>>(), {
valueKey: 'value' as never,
labelKey: 'label' as never,
portal: true,
autofocusDelay: 0
portal: true
})
const emits = defineEmits<SelectEmits<T, VK, M>>()
const slots = defineSlots<SelectSlots<T, VK, M>>()
@@ -196,32 +193,15 @@ const groups = computed<SelectItem[][]>(() =>
// eslint-disable-next-line vue/no-dupe-keys
const items = computed(() => groups.value.flatMap(group => group) as T[])
function displayValue(value: GetItemValue<T, VK> | GetItemValue<T, VK>[]): string | undefined {
function displayValue(value?: GetItemValue<T, VK> | GetItemValue<T, VK>[]): string {
if (props.multiple && Array.isArray(value)) {
const values = value.map(v => displayValue(v)).filter(Boolean)
return values?.length ? values.join(', ') : undefined
return value.map(v => displayValue(v)).filter(Boolean).join(', ')
}
const item = items.value.find(item => compare(typeof item === 'object' ? get(item as Record<string, any>, props.valueKey as string) : item, value))
return item && (typeof item === 'object' ? get(item, props.labelKey as string) : item)
}
const triggerRef = ref<InstanceType<typeof SelectTrigger> | null>(null)
function autoFocus() {
if (props.autofocus) {
triggerRef.value?.$el?.focus({
focusVisible: true
})
}
}
onMounted(() => {
setTimeout(() => {
autoFocus()
}, props.autofocusDelay)
})
function onUpdate(value: any) {
// @ts-expect-error - 'target' does not exist in type 'EventInit'
const event = new Event('change', { target: { value } })
@@ -260,7 +240,7 @@ function isSelectItem(item: SelectItem): item is SelectItemBase {
@update:model-value="onUpdate"
@update:open="onUpdateOpen"
>
<SelectTrigger :id="id" ref="triggerRef" :class="ui.base({ class: [props.ui?.base, props.class] })" v-bind="{ ...$attrs, ...ariaAttrs }">
<SelectTrigger :id="id" :class="ui.base({ class: [props.ui?.base, props.class] })" v-bind="{ ...$attrs, ...ariaAttrs }">
<span v-if="isLeading || !!avatar || !!slots.leading" :class="ui.leading({ class: props.ui?.leading })">
<slot name="leading" :model-value="(modelValue as GetModelValue<T, VK, M>)" :open="open" :ui="ui">
<UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
@@ -270,7 +250,7 @@ function isSelectItem(item: SelectItem): item is SelectItemBase {
<slot :model-value="(modelValue as GetModelValue<T, VK, M>)" :open="open">
<template v-for="displayedModelValue in [displayValue(modelValue as GetModelValue<T, VK, M>)]" :key="displayedModelValue">
<span v-if="displayedModelValue !== undefined && displayedModelValue !== null" :class="ui.value({ class: props.ui?.value })">
<span v-if="displayedModelValue" :class="ui.value({ class: props.ui?.value })">
{{ displayedModelValue }}
</span>
<span v-else :class="ui.placeholder({ class: props.ui?.placeholder })">

View File

@@ -115,8 +115,6 @@ export interface SelectMenuProps<T extends ArrayOrNested<SelectMenuItem> = Array
* @defaultValue false
*/
ignoreFilter?: boolean
autofocus?: boolean
autofocusDelay?: number
class?: any
ui?: SelectMenu['slots']
}
@@ -167,7 +165,7 @@ export interface SelectMenuSlots<
</script>
<script setup lang="ts" generic="T extends ArrayOrNested<SelectMenuItem>, VK extends GetItemKeys<T> | undefined = undefined, M extends boolean = false">
import { ref, computed, onMounted, toRef, toRaw } from 'vue'
import { computed, toRef, toRaw } from 'vue'
import { ComboboxRoot, ComboboxArrow, ComboboxAnchor, ComboboxInput, ComboboxTrigger, ComboboxPortal, ComboboxContent, ComboboxEmpty, ComboboxGroup, ComboboxLabel, ComboboxSeparator, ComboboxItem, ComboboxItemIndicator, FocusScope, useForwardPropsEmits, useFilter } from 'reka-ui'
import { defu } from 'defu'
import { reactivePick, createReusableTemplate } from '@vueuse/core'
@@ -191,8 +189,7 @@ const props = withDefaults(defineProps<SelectMenuProps<T, VK, M>>(), {
searchInput: true,
labelKey: 'label' as never,
resetSearchTermOnBlur: true,
resetSearchTermOnSelect: true,
autofocusDelay: 0
resetSearchTermOnSelect: true
})
const emits = defineEmits<SelectMenuEmits<T, VK, M>>()
const slots = defineSlots<SelectMenuSlots<T, VK, M>>()
@@ -228,10 +225,9 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.selectMenu |
buttonGroup: orientation.value
}))
function displayValue(value: GetItemValue<T, VK> | GetItemValue<T, VK>[]): string | undefined {
function displayValue(value: GetItemValue<T, VK> | GetItemValue<T, VK>[]): string {
if (props.multiple && Array.isArray(value)) {
const values = value.map(v => displayValue(v)).filter(Boolean)
return values?.length ? values.join(', ') : undefined
return value.map(v => displayValue(v)).filter(Boolean).join(', ')
}
if (!props.valueKey) {
@@ -290,22 +286,6 @@ const createItem = computed(() => {
})
const createItemPosition = computed(() => typeof props.createItem === 'object' ? props.createItem.position : 'bottom')
const triggerRef = ref<InstanceType<typeof ComboboxTrigger> | null>(null)
function autoFocus() {
if (props.autofocus) {
triggerRef.value?.$el?.focus({
focusVisible: true
})
}
}
onMounted(() => {
setTimeout(() => {
autoFocus()
}, props.autofocusDelay)
})
function onUpdate(value: any) {
if (toRaw(props.modelValue) === value) {
return
@@ -395,7 +375,7 @@ function isSelectItem(item: SelectMenuItem): item is _SelectMenuItem {
@update:open="onUpdateOpen"
>
<ComboboxAnchor as-child>
<ComboboxTrigger ref="triggerRef" :class="ui.base({ class: [props.ui?.base, props.class] })" tabindex="0">
<ComboboxTrigger :class="ui.base({ class: [props.ui?.base, props.class] })" tabindex="0">
<span v-if="isLeading || !!avatar || !!slots.leading" :class="ui.leading({ class: props.ui?.leading })">
<slot name="leading" :model-value="(modelValue as GetModelValue<T, VK, M>)" :open="open" :ui="ui">
<UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
@@ -405,7 +385,7 @@ function isSelectItem(item: SelectMenuItem): item is _SelectMenuItem {
<slot :model-value="(modelValue as GetModelValue<T, VK, M>)" :open="open">
<template v-for="displayedModelValue in [displayValue(modelValue as GetModelValue<T, VK, M>)]" :key="displayedModelValue">
<span v-if="displayedModelValue !== undefined && displayedModelValue !== null" :class="ui.value({ class: props.ui?.value })">
<span v-if="displayedModelValue" :class="ui.value({ class: props.ui?.value })">
{{ displayedModelValue }}
</span>
<span v-else :class="ui.placeholder({ class: props.ui?.placeholder })">

View File

@@ -61,13 +61,13 @@ export interface SlideoverEmits extends DialogRootEmits {
export interface SlideoverSlots {
default(props: { open: boolean }): any
content(props: { close: () => void }): any
header(props: { close: () => void }): any
content(props?: {}): any
header(props?: {}): any
title(props?: {}): any
description(props?: {}): any
close(props: { close: () => void, ui: { [K in keyof Required<Slideover['slots']>]: (props?: Record<string, any>) => string } }): any
body(props: { close: () => void }): any
footer(props: { close: () => void }): any
close(props: { ui: { [K in keyof Required<Slideover['slots']>]: (props?: Record<string, any>) => string } }): any
body(props?: {}): any
footer(props?: {}): any
}
</script>
@@ -103,17 +103,16 @@ const contentEvents = computed(() => {
const defaultEvents = {
closeAutoFocus: (e: Event) => e.preventDefault()
}
if (!props.dismissible) {
const events = ['pointerDownOutside', 'interactOutside', 'escapeKeyDown']
const events = ['pointerDownOutside', 'interactOutside', 'escapeKeyDown', 'closeAutoFocus'] as const
type EventType = typeof events[number]
return events.reduce((acc, curr) => {
acc[curr] = (e: Event) => {
e.preventDefault()
emits('close:prevent')
}
return acc
}, defaultEvents as Record<typeof events[number] | keyof typeof defaultEvents, (e: Event) => void>)
}, {} as Record<EventType, (e: Event) => void>)
}
return defaultEvents
@@ -125,9 +124,8 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.slideover ||
}))
</script>
<!-- eslint-disable vue/no-template-shadow -->
<template>
<DialogRoot v-slot="{ open, close }" v-bind="rootProps">
<DialogRoot v-slot="{ open }" v-bind="rootProps">
<DialogTrigger v-if="!!slots.default" as-child :class="props.class">
<slot :open="open" />
</DialogTrigger>
@@ -157,9 +155,9 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.slideover ||
</DialogDescription>
</VisuallyHidden>
<slot name="content" :close="close">
<div v-if="!!slots.header || (title || !!slots.title) || (description || !!slots.description) || (props.close || !!slots.close)" :class="ui.header({ class: props.ui?.header })">
<slot name="header" :close="close">
<slot name="content">
<div v-if="!!slots.header || (title || !!slots.title) || (description || !!slots.description) || (close || !!slots.close)" :class="ui.header({ class: props.ui?.header })">
<slot name="header">
<div :class="ui.wrapper({ class: props.ui?.wrapper })">
<DialogTitle v-if="title || !!slots.title" :class="ui.title({ class: props.ui?.title })">
<slot name="title">
@@ -174,16 +172,16 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.slideover ||
</DialogDescription>
</div>
<DialogClose v-if="props.close || !!slots.close" as-child>
<slot name="close" :close="close" :ui="ui">
<DialogClose v-if="close || !!slots.close" as-child>
<slot name="close" :ui="ui">
<UButton
v-if="props.close"
v-if="close"
:icon="closeIcon || appConfig.ui.icons.close"
size="md"
color="neutral"
variant="ghost"
:aria-label="t('slideover.close')"
v-bind="(typeof props.close === 'object' ? props.close as Partial<ButtonProps> : {})"
v-bind="(typeof close === 'object' ? close as Partial<ButtonProps> : {})"
:class="ui.close({ class: props.ui?.close })"
/>
</slot>
@@ -192,11 +190,11 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.slideover ||
</div>
<div :class="ui.body({ class: props.ui?.body })">
<slot name="body" :close="close" />
<slot name="body" />
</div>
<div v-if="!!slots.footer" :class="ui.footer({ class: props.ui?.footer })">
<slot name="footer" :close="close" />
<slot name="footer" />
</div>
</slot>
</DialogContent>

View File

@@ -3,12 +3,10 @@ import type { AppConfig } from '@nuxt/schema'
import theme from '#build/ui/textarea'
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
import type { AvatarProps } from '../types'
import type { ComponentConfig } from '../types/utils'
import type { AcceptableValue, ComponentConfig } from '../types/utils'
type Textarea = ComponentConfig<typeof theme, AppConfig, 'textarea'>
type TextareaValue = string | number | null
export interface TextareaProps extends UseComponentIconsProps {
/**
* The element or component this component should render as.
@@ -51,7 +49,7 @@ export interface TextareaProps extends UseComponentIconsProps {
ui?: Textarea['slots']
}
export interface TextareaEmits<T extends TextareaValue = TextareaValue> {
export interface TextareaEmits<T extends AcceptableValue = AcceptableValue> {
(e: 'update:modelValue', payload: T): void
(e: 'blur', event: FocusEvent): void
(e: 'change', event: Event): void
@@ -64,7 +62,7 @@ export interface TextareaSlots {
}
</script>
<script setup lang="ts" generic="T extends TextareaValue">
<script setup lang="ts" generic="T extends AcceptableValue">
import { ref, computed, onMounted, nextTick, watch } from 'vue'
import { Primitive } from 'reka-ui'
import { useAppConfig } from '#imports'

View File

@@ -1,129 +0,0 @@
<!-- eslint-disable vue/block-tag-newline -->
<script lang="ts">
import type { AppConfig } from '@nuxt/schema'
import theme from '#build/ui/timeline'
import type { AvatarProps } from '../types'
import type { DynamicSlots, ComponentConfig } from '../types/utils'
type Timeline = ComponentConfig<typeof theme, AppConfig, 'timeline'>
export interface TimelineItem {
date?: string
title?: string
description?: string
icon?: string
avatar?: AvatarProps
value?: string | number
slot?: string
class?: any
ui?: Pick<Timeline['slots'], 'item' | 'container' | 'indicator' | 'separator' | 'wrapper' | 'date' | 'title' | 'description'>
[key: string]: any
}
export interface TimelineProps<T extends TimelineItem = TimelineItem> {
/**
* The element or component this component should render as.
* @defaultValue 'div'
*/
as?: any
items: T[]
/**
* @defaultValue 'md'
*/
size?: Timeline['variants']['size']
/**
* @defaultValue 'primary'
*/
color?: Timeline['variants']['color']
/**
* The orientation of the Timeline.
* @defaultValue 'vertical'
*/
orientation?: Timeline['variants']['orientation']
defaultValue?: string | number
class?: any
ui?: Timeline['slots']
}
type SlotProps<T extends TimelineItem> = (props: { item: T }) => any
export type TimelineSlots<T extends TimelineItem = TimelineItem> = {
indicator: SlotProps<T>
date: SlotProps<T>
title: SlotProps<T>
description: SlotProps<T>
} & DynamicSlots<T, 'indicator' | 'date' | 'title' | 'description', { item: T }>
</script>
<script setup lang="ts" generic="T extends TimelineItem">
import { computed } from 'vue'
import { Primitive, Separator } from 'reka-ui'
import { useAppConfig } from '#imports'
import { tv } from '../utils/tv'
import UAvatar from './Avatar.vue'
const props = withDefaults(defineProps<TimelineProps<T>>(), {
orientation: 'vertical'
})
const slots = defineSlots<TimelineSlots<T>>()
const modelValue = defineModel<string | number>()
const appConfig = useAppConfig() as Timeline['AppConfig']
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.timeline || {}) })({
orientation: props.orientation,
size: props.size,
color: props.color
}))
const currentStepIndex = computed(() => {
const value = modelValue.value ?? props.defaultValue
return ((typeof value === 'string')
? props.items.findIndex(item => item.value === value)
: value) ?? -1
})
</script>
<template>
<Primitive :as="as" :data-orientation="orientation" :class="ui.root({ class: [props.ui?.root, props.class] })">
<div
v-for="(item, index) in items"
:key="item.value ?? index"
:class="ui.item({ class: [props.ui?.item, item.ui?.item, item.class] })"
:data-state="index < currentStepIndex ? 'completed' : index === currentStepIndex ? 'active' : undefined"
>
<div :class="ui.container({ class: [props.ui?.container, item.ui?.container] })">
<UAvatar :size="size" :icon="item.icon" v-bind="typeof item.avatar === 'object' ? item.avatar : {}" :class="ui.indicator({ class: [props.ui?.indicator, item.ui?.indicator] })" :ui="{ icon: 'text-inherit', fallback: 'text-inherit' }">
<slot :name="((item.slot ? `${item.slot}-indicator` : 'indicator') as keyof TimelineSlots<T>)" :item="(item as Extract<T, { slot: string; }>)" />
</UAvatar>
<Separator
v-if="index < items.length - 1"
:class="ui.separator({ class: [props.ui?.separator, item.ui?.separator] })"
:orientation="props.orientation"
/>
</div>
<div :class="ui.wrapper({ class: [props.ui?.wrapper, item.ui?.wrapper] })">
<div v-if="item.date" :class="ui.date({ class: [props.ui?.date, item.ui?.date] })">
<slot :name="((item.slot ? `${item.slot}-date` : 'date') as keyof TimelineSlots<T>)" :item="(item as Extract<T, { slot: string; }>)">
{{ item.date }}
</slot>
</div>
<div v-if="item.title || !!slots.title" :class="ui.title({ class: [props.ui?.title, item.ui?.title] })">
<slot :name="((item.slot ? `${item.slot}-title` : 'title') as keyof TimelineSlots<T>)" :item="(item as Extract<T, { slot: string; }>)">
{{ item.title }}
</slot>
</div>
<div v-if="item.description || !!slots.description" :class="ui.description({ class: [props.ui?.description, item.ui?.description] })">
<slot :name="((item.slot ? `${item.slot}-description` : 'description') as keyof TimelineSlots<T>)" :item="(item as Extract<T, { slot: string; }>)">
{{ item.description }}
</slot>
</div>
</div>
</div>
</Primitive>
</template>

View File

@@ -69,7 +69,7 @@ export interface ToastSlots {
</script>
<script setup lang="ts">
import { ref, computed, onMounted, nextTick } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { ToastRoot, ToastTitle, ToastDescription, ToastAction, ToastClose, useForwardPropsEmits } from 'reka-ui'
import { reactivePick } from '@vueuse/core'
import { useAppConfig } from '#imports'
@@ -106,9 +106,9 @@ onMounted(() => {
return
}
nextTick(() => {
height.value = el.value?.$el?.getBoundingClientRect()?.height
})
setTimeout(() => {
height.value = el.value.$el.getBoundingClientRect()?.height
}, 0)
})
defineExpose({

View File

@@ -29,7 +29,7 @@ export type TreeItem = {
[key: string]: any
}
export interface TreeProps<T extends TreeItem[] = TreeItem[], VK extends GetItemKeys<T> = 'value', M extends boolean = false> extends Pick<TreeRootProps<T>, 'expanded' | 'defaultExpanded' | 'selectionBehavior' | 'propagateSelect' | 'disabled' | 'bubbleSelect'> {
export interface TreeProps<T extends TreeItem[] = TreeItem[], VK extends GetItemKeys<T> = 'value', M extends boolean = false> extends Pick<TreeRootProps<T>, 'expanded' | 'defaultExpanded' | 'selectionBehavior' | 'propagateSelect' | 'disabled'> {
/**
* The element or component this component should render as.
* @defaultValue 'ul'
@@ -116,7 +116,7 @@ const slots = defineSlots<TreeSlots<T>>()
const appConfig = useAppConfig() as Tree['AppConfig']
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'items', 'multiple', 'expanded', 'disabled', 'propagateSelect', 'bubbleSelect'), emits)
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'items', 'multiple', 'expanded', 'disabled', 'propagateSelect'), emits)
const [DefineTreeTemplate, ReuseTreeTemplate] = createReusableTemplate<{ items?: TreeItem[], level: number }, TreeSlots<T>>()

View File

@@ -1,6 +1,5 @@
import { defu } from 'defu'
import type { Locale, Direction } from '../types/locale'
import type { DeepPartial } from '../types/utils'
interface DefineLocaleOptions<M> {
name: string
@@ -13,8 +12,3 @@ interface DefineLocaleOptions<M> {
export function defineLocale<M>(options: DefineLocaleOptions<M>): Locale<M> {
return defu<DefineLocaleOptions<M>, [{ dir: Direction }]>(options, { dir: 'ltr' })
}
/* @__NO_SIDE_EFFECTS__ */
export function extendLocale<M>(locale: Locale<M>, options: Partial<DefineLocaleOptions<DeepPartial<M>>>): Locale<M> {
return defu<Locale<M>, [DefineLocaleOptions<M>]>(options, locale)
}

View File

@@ -151,7 +151,7 @@ export function defineShortcuts(config: MaybeRef<ShortcutsConfig>, options: Shor
// Parse key and modifiers
let shortcut: Partial<Shortcut>
if (key.includes('-') && key !== '-' && !key.includes('_') && !key.match(chainedShortcutRegex)?.length) {
if (key.includes('-') && key !== '-' && !key.match(chainedShortcutRegex)?.length) {
console.trace(`[Shortcut] Invalid key: "${key}"`)
}
@@ -159,7 +159,7 @@ export function defineShortcuts(config: MaybeRef<ShortcutsConfig>, options: Shor
console.trace(`[Shortcut] Invalid key: "${key}"`)
}
const chained = key.includes('-') && key !== '-' && !key.includes('_')
const chained = key.includes('-') && key !== '-'
if (chained) {
shortcut = {
key: key.toLowerCase(),

View File

@@ -17,7 +17,6 @@ interface ManagedOverlayOptionsPrivate<T extends Component> {
id: symbol
isMounted: boolean
isOpen: boolean
originalProps?: ComponentProps<T>
resolvePromise?: (value: any) => void
}
export type Overlay = OverlayOptions<Component> & ManagedOverlayOptionsPrivate<Component>
@@ -27,6 +26,7 @@ type OverlayInstance<T extends Component> = Omit<ManagedOverlayOptionsPrivate<T>
open: (props?: ComponentProps<T>) => OpenedOverlay<T>
close: (value?: any) => void
patch: (props: Partial<ComponentProps<T>>) => void
}
type OpenedOverlay<T extends Component> = Omit<OverlayInstance<T>, 'open' | 'close' | 'patch' | 'modelValue' | 'resolvePromise'> & {
@@ -37,7 +37,7 @@ function _useOverlay() {
const overlays = shallowReactive<Overlay[]>([])
const create = <T extends Component>(component: T, _options?: OverlayOptions<ComponentProps<T>>): OverlayInstance<T> => {
const { props, defaultOpen, destroyOnClose } = _options || {}
const { props: props, defaultOpen, destroyOnClose } = _options || {}
const options = reactive<Overlay>({
id: Symbol(import.meta.dev ? 'useOverlay' : ''),
@@ -45,8 +45,7 @@ function _useOverlay() {
component: markRaw(component!),
isMounted: !!defaultOpen,
destroyOnClose: !!destroyOnClose,
originalProps: props || {},
props: { ...(props || {}) }
props: props || {}
})
overlays.push(options)
@@ -65,8 +64,6 @@ function _useOverlay() {
// If props are provided, update the overlay's props
if (props) {
patch(overlay.id, props)
} else {
patch(overlay.id, overlay.originalProps)
}
overlay.isOpen = true
@@ -96,7 +93,7 @@ function _useOverlay() {
overlays.forEach(overlay => close(overlay.id))
}
const unmount = (id: symbol): void => {
const unMount = (id: symbol): void => {
const overlay = getOverlay(id)
overlay.isMounted = false
@@ -110,7 +107,9 @@ function _useOverlay() {
const patch = <T extends Component>(id: symbol, props: Partial<ComponentProps<T>>): void => {
const overlay = getOverlay(id)
overlay.props = { ...props }
Object.entries(props!).forEach(([key, value]) => {
(overlay.props as any)[key] = value
})
}
const getOverlay = (id: symbol): Overlay => {
@@ -136,7 +135,7 @@ function _useOverlay() {
closeAll,
create,
patch,
unmount,
unMount,
isOpen
}
}

View File

@@ -25,7 +25,6 @@ export { default as kk } from './kk'
export { default as km } from './km'
export { default as ko } from './ko'
export { default as ky } from './ky'
export { default as lb } from './lb'
export { default as lt } from './lt'
export { default as mn } from './mn'
export { default as ms } from './ms'

View File

@@ -1,56 +0,0 @@
import type { Messages } from '../types'
import { defineLocale } from '../composables/defineLocale'
export default defineLocale<Messages>({
name: 'Lëtzebuergesch',
code: 'lb',
messages: {
inputMenu: {
noMatch: 'Keng entspriechend Donnéeën',
noData: 'Keng Donnéeën',
create: '"{label}" erstellen'
},
calendar: {
prevYear: 'Viregt Joer',
nextYear: 'Nächst Joer',
prevMonth: 'Virege Mount',
nextMonth: 'Nächste Mount'
},
inputNumber: {
increment: 'Inkrementéieren',
decrement: 'Dekrementéieren'
},
commandPalette: {
placeholder: 'Tippt e Befeel oder sicht...',
noMatch: 'Keng entspriechend Donnéeën',
noData: 'Keng Donnéeën',
close: 'Zoumaachen'
},
selectMenu: {
noMatch: 'Keng entspriechend Donnéeën',
noData: 'Keng Donnéeën',
create: '"{label}" erstellen',
search: 'Sichen..'
},
toast: {
close: 'Zoumaachen'
},
carousel: {
prev: 'Präz.',
next: 'Näch.',
goto: 'Gitt op d\'Slide {Slide}'
},
modal: {
close: 'Zoumaachen'
},
slideover: {
close: 'Zoumaachen'
},
alert: {
close: 'Zoumaachen'
},
table: {
noData: 'Keng Donnéeën'
}
}
})

View File

@@ -5,20 +5,20 @@ import type { ObjectSchema as YupObjectSchema } from 'yup'
import type { GetObjectField } from './utils'
import type { Struct as SuperstructSchema } from 'superstruct'
export interface Form<S extends FormSchema> {
validate<T extends boolean>(opts?: { name?: keyof FormData<S, false> | (keyof FormData<S, false>)[], silent?: boolean, nested?: boolean, transform?: T }): Promise<FormData<S, T> | false>
export interface Form<T extends object> {
validate (opts?: { name?: keyof T | (keyof T)[], silent?: boolean, nested?: boolean, transform?: boolean }): Promise<T | false>
clear (path?: string): void
errors: Ref<FormError[]>
setErrors (errs: FormError[], name?: keyof FormData<S, false>): void
getErrors (name?: keyof FormData<S, false>): FormError[]
setErrors (errs: FormError[], name?: keyof T): void
getErrors (name?: keyof T): FormError[]
submit (): Promise<void>
disabled: ComputedRef<boolean>
dirty: ComputedRef<boolean>
loading: Ref<boolean>
dirtyFields: DeepReadonly<Set<keyof FormData<S, false>>>
touchedFields: DeepReadonly<Set<keyof FormData<S, false>>>
blurredFields: DeepReadonly<Set<keyof FormData<S, false>>>
dirtyFields: DeepReadonly<Set<keyof T>>
touchedFields: DeepReadonly<Set<keyof T>>
blurredFields: DeepReadonly<Set<keyof T>>
}
export type FormSchema<I extends object = object, O extends object = I> =
@@ -42,8 +42,6 @@ export type InferOutput<Schema> = Schema extends StandardSchemaV1 ? StandardSche
: Schema extends SuperstructSchema<infer O, any> ? O
: never
export type FormData<S extends FormSchema, T extends boolean = true> = T extends true ? InferOutput<S> : InferInput<S>
export type FormInputEvents = 'input' | 'blur' | 'change' | 'focus'
export interface FormError<P extends string = string> {

View File

@@ -26,7 +26,6 @@ export * from '../components/Icon.vue'
export * from '../components/Input.vue'
export * from '../components/InputMenu.vue'
export * from '../components/InputNumber.vue'
export * from '../components/InputTags.vue'
export * from '../components/Kbd.vue'
export * from '../components/Link.vue'
export * from '../components/Modal.vue'
@@ -47,7 +46,6 @@ export * from '../components/Switch.vue'
export * from '../components/Table.vue'
export * from '../components/Tabs.vue'
export * from '../components/Textarea.vue'
export * from '../components/Timeline.vue'
export * from '../components/Toast.vue'
export * from '../components/Toaster.vue'
export * from '../components/Tooltip.vue'

View File

@@ -1,10 +1,6 @@
import type { VNode } from 'vue'
import type { AcceptableValue as _AcceptableValue } from 'reka-ui'
export type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P] | undefined
}
export type DynamicSlotsKeys<Name extends string | undefined, Suffix extends string | undefined = undefined> = (
Name extends string
? Suffix extends string

View File

@@ -1,6 +1,6 @@
export default {
slots: {
root: 'inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated',
root: 'inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-elevated',
image: 'h-full w-full rounded-[inherit] object-cover',
fallback: 'font-medium leading-none text-muted truncate',
icon: 'text-muted shrink-0'

View File

@@ -30,7 +30,7 @@ export default (options: Required<ModuleOptions>) => ({
},
active: {
true: {
dot: 'data-[state=active]:bg-inverted'
dot: 'bg-inverted'
}
}
}

View File

@@ -24,7 +24,6 @@ export { default as formField } from './form-field'
export { default as input } from './input'
export { default as inputMenu } from './input-menu'
export { default as inputNumber } from './input-number'
export { default as inputTags } from './input-tags'
export { default as kbd } from './kbd'
export { default as link } from './link'
export { default as modal } from './modal'
@@ -45,7 +44,6 @@ export { default as switch } from './switch'
export { default as table } from './table'
export { default as tabs } from './tabs'
export { default as textarea } from './textarea'
export { default as timeline } from './timeline'
export { default as toast } from './toast'
export { default as toaster } from './toaster'
export { default as tooltip } from './tooltip'

View File

@@ -26,13 +26,14 @@ export default (options: Required<ModuleOptions>) => {
tagsItem: 'px-1.5 py-0.5 rounded-sm font-medium inline-flex items-center gap-0.5 ring ring-inset ring-accented bg-elevated text-default data-disabled:cursor-not-allowed data-disabled:opacity-75',
tagsItemText: 'truncate',
tagsItemDelete: ['inline-flex items-center rounded-xs text-dimmed hover:text-default hover:bg-accented/75 disabled:pointer-events-none', options.theme.transitions && 'transition-colors'],
tagsItemDeleteIcon: 'shrink-0',
tagsInput: 'flex-1 border-0 bg-transparent placeholder:text-dimmed focus:outline-none disabled:cursor-not-allowed disabled:opacity-75'
tagsItemDeleteIcon: '',
tagsInput: ''
},
variants: {
multiple: {
true: {
root: 'flex-wrap'
root: 'flex-wrap',
tagsInput: 'border-0 bg-transparent placeholder:text-dimmed focus:outline-none disabled:cursor-not-allowed disabled:opacity-75'
},
false: {
base: 'w-full border-0 placeholder:text-dimmed focus:outline-none disabled:cursor-not-allowed disabled:opacity-75'
@@ -96,15 +97,7 @@ export default (options: Required<ModuleOptions>) => {
}
}
},
compoundVariants: [{
variant: 'soft',
multiple: true,
class: 'has-focus:bg-elevated'
}, {
variant: 'ghost',
multiple: true,
class: 'has-focus:bg-elevated'
}, ...(options.theme.colors || []).map((color: string) => ({
compoundVariants: [...(options.theme.colors || []).map((color: string) => ({
color,
multiple: true,
variant: ['outline', 'subtle'],

View File

@@ -1,54 +0,0 @@
import { defuFn } from 'defu'
import type { ModuleOptions } from '../module'
import input from './input'
export default (options: Required<ModuleOptions>) => {
return defuFn({
slots: {
root: (prev: string) => [prev, 'flex-wrap'],
base: () => ['rounded-md', options.theme.transitions && 'transition-colors'],
item: 'px-1.5 py-0.5 rounded-sm font-medium inline-flex items-center gap-0.5 ring ring-inset ring-accented bg-elevated text-default data-disabled:cursor-not-allowed data-disabled:opacity-75 wrap-anywhere data-[state="active"]:bg-accented',
itemText: '',
itemDelete: ['inline-flex items-center rounded-xs text-dimmed hover:text-default hover:bg-accented/75 disabled:pointer-events-none', options.theme.transitions && 'transition-colors'],
itemDeleteIcon: 'shrink-0',
input: 'flex-1 border-0 bg-transparent placeholder:text-dimmed focus:outline-none disabled:cursor-not-allowed disabled:opacity-75'
},
variants: {
size: {
xs: {
item: 'text-[10px]/3',
itemDeleteIcon: 'size-3'
},
sm: {
item: 'text-[10px]/3',
itemDeleteIcon: 'size-3'
},
md: {
item: 'text-xs',
itemDeleteIcon: 'size-3.5'
},
lg: {
item: 'text-xs',
itemDeleteIcon: 'size-3.5'
},
xl: {
item: 'text-sm',
itemDeleteIcon: 'size-4'
}
},
variant: (prev: Record<string, string>) => Object.fromEntries(
Object.entries(prev).map(([key, value]) => [key, replaceFocus(value)])
)
},
compoundVariants: (prev: Record<string, any>[]) => prev.map(item => ({
...item,
class: typeof item.class === 'string' ? replaceFocus(item.class) : item.class
}))
}, input(options))
}
function replaceFocus(str: string): string {
return str
.replace(/focus:/g, 'has-focus:')
.replace(/focus-visible:/g, 'has-focus-visible:')
}

View File

@@ -60,7 +60,7 @@ export default (options: Required<ModuleOptions>) => ({
childList: 'grid p-2',
childLink: 'px-3 py-2 gap-2 before:inset-x-px before:inset-y-0',
childLinkLabel: 'font-medium',
content: 'absolute top-0 left-0 w-full max-h-[70vh] overflow-y-auto'
content: 'absolute top-0 left-0 w-full'
},
vertical: {
root: 'flex-col',

View File

@@ -1,168 +0,0 @@
import type { ModuleOptions } from '../module'
export default (options: Required<ModuleOptions>) => ({
slots: {
root: 'flex gap-1.5',
item: 'group relative flex flex-1 gap-3',
container: 'relative flex items-center gap-1.5',
indicator: 'group-data-[state=completed]:text-inverted group-data-[state=active]:text-inverted text-muted',
separator: 'flex-1 rounded-full bg-elevated',
wrapper: 'w-full',
date: 'text-dimmed text-xs/5',
title: 'font-medium text-highlighted text-sm',
description: 'text-muted text-wrap text-sm'
},
variants: {
orientation: {
horizontal: {
root: 'flex-row w-full',
item: 'flex-col',
separator: 'h-0.5'
},
vertical: {
root: 'flex-col',
container: 'flex-col',
separator: 'w-0.5'
}
},
color: {
...Object.fromEntries((options.theme.colors || []).map((color: string) => [color, {
indicator: `group-data-[state=completed]:bg-${color} group-data-[state=active]:bg-${color}`,
separator: `group-data-[state=completed]:bg-${color}`
}])),
neutral: {
indicator: 'group-data-[state=completed]:bg-inverted group-data-[state=active]:bg-inverted',
separator: 'group-data-[state=completed]:bg-inverted'
}
},
size: {
'3xs': '',
'2xs': '',
'xs': '',
'sm': '',
'md': '',
'lg': '',
'xl': '',
'2xl': '',
'3xl': ''
}
},
compoundVariants: [{
orientation: 'horizontal',
size: '3xs',
class: {
wrapper: 'pe-4.5'
}
}, {
orientation: 'horizontal',
size: '2xs',
class: {
wrapper: 'pe-5'
}
}, {
orientation: 'horizontal',
size: 'xs',
class: {
wrapper: 'pe-5.5'
}
}, {
orientation: 'horizontal',
size: 'sm',
class: {
wrapper: 'pe-6'
}
}, {
orientation: 'horizontal',
size: 'md',
class: {
wrapper: 'pe-6.5'
}
}, {
orientation: 'horizontal',
size: 'lg',
class: {
wrapper: 'pe-7'
}
}, {
orientation: 'horizontal',
size: 'xl',
class: {
wrapper: 'pe-7.5'
}
}, {
orientation: 'horizontal',
size: '2xl',
class: {
wrapper: 'pe-8'
}
}, {
orientation: 'horizontal',
size: '3xl',
class: {
wrapper: 'pe-8.5'
}
}, {
orientation: 'vertical',
size: '3xs',
class: {
wrapper: '-mt-0.5 pb-4.5'
}
}, {
orientation: 'vertical',
size: '2xs',
class: {
wrapper: 'pb-5'
}
}, {
orientation: 'vertical',
size: 'xs',
class: {
wrapper: 'mt-0.5 pb-5.5'
}
}, {
orientation: 'vertical',
size: 'sm',
class: {
wrapper: 'mt-1 pb-6'
}
}, {
orientation: 'vertical',
size: 'md',
class: {
wrapper: 'mt-1.5 pb-6.5'
}
}, {
orientation: 'vertical',
size: 'lg',
class: {
wrapper: 'mt-2 pb-7'
}
}, {
orientation: 'vertical',
size: 'xl',
class: {
wrapper: 'mt-2.5 pb-7.5'
}
}, {
orientation: 'vertical',
size: '2xl',
class: {
wrapper: 'mt-3 pb-8'
}
}, {
orientation: 'vertical',
size: '3xl',
class: {
wrapper: 'mt-3.5 pb-8.5'
}
}],
defaultVariants: {
size: 'md',
color: 'primary'
}
})

View File

@@ -12,7 +12,6 @@ describe('Avatar', () => {
['with alt', { props: { alt: 'Benjamin Canac' } }],
['with text', { props: { text: '+1' } }],
['with icon', { props: { icon: 'i-lucide-image' } }],
['with chip', { props: { chip: { text: '1' } } }],
...sizes.map((size: string) => [`with size ${size}`, { props: { src: 'https://github.com/benjamincanac.png', size } }]),
['with as', { props: { as: 'section' } }],
['with class', { props: { class: 'bg-default' } }],

View File

@@ -42,8 +42,6 @@ describe('InputMenu', () => {
['with defaultValue', { props: { ...props, defaultValue: items[0] } }],
['with valueKey', { props: { ...props, valueKey: 'value' } }],
['with labelKey', { props: { ...props, labelKey: 'value' } }],
['with multiple', { props: { ...props, multiple: true } }],
['with multiple and modelValue', { props: { ...props, multiple: true, modelValue: [items[0], items[1]] } }],
['with id', { props: { ...props, id: 'id' } }],
['with name', { props: { ...props, name: 'name' } }],
['with placeholder', { props: { ...props, placeholder: 'Search...' } }],

View File

@@ -1,42 +0,0 @@
import { describe, it, expect } from 'vitest'
import theme from '#build/ui/input'
import InputTags, { type InputTagsProps, type InputTagsSlots } from '../../src/runtime/components/InputTags.vue'
import ComponentRender from '../component-render'
describe('InputTags', () => {
const sizes = Object.keys(theme.variants.size) as any
const variants = Object.keys(theme.variants.variant) as any
it.each([
// Props
['with modelValue', { props: { modelValue: ['test'] } }],
['with defaultValue', { props: { defaultValue: ['test'] } }],
['with id', { props: { id: 'id' } }],
['with name', { props: { name: 'name' } }],
['with placeholder', { props: { placeholder: 'Search...' } }],
['with disabled', { props: { disabled: true } }],
['with required', { props: { required: true } }],
['with icon', { props: { icon: 'i-lucide-search' } }],
['with trailing and icon', { props: { trailing: true, icon: 'i-lucide-arrow-right' } }],
['with trailingIcon', { props: { trailingIcon: 'i-lucide-arrow-right' } }],
['with loading', { props: { loading: true } }],
['with loading trailing', { props: { loading: true, trailing: true } }],
['with loadingIcon', { props: { loading: true, loadingIcon: 'i-lucide-loader' } }],
['with ariaLabel', { attrs: { 'aria-label': 'Aria label' } }],
['with as', { props: { as: 'section' } }],
['with class', { props: { class: '' } }],
['with ui', { props: { ui: {} } }],
...sizes.map((size: string) => [`with size ${size}`, { props: { size } }]),
...variants.map((variant: string) => [`with primary variant ${variant}`, { props: { variant } }]),
...variants.map((variant: string) => [`with neutral variant ${variant}`, { props: { variant, color: 'neutral' } }]),
// Slots
['with leading slot', { slots: { leading: () => 'Leading slot' } }],
['with default slot', { slots: { default: () => 'Default slot' } }],
['with trailing slot', { slots: { trailing: () => 'Trailing slot' } }],
['with item-text slot', { slots: { ['item-text']: () => 'Item Text slot' } }],
['with item-delete slot', { slots: { ['item-delete']: () => 'Item Delete slot' } }]
])('renders %s correctly', async (nameOrHtml: string, options: { props?: InputTagsProps, slots?: Partial<InputTagsSlots>, attrs?: Record<string, unknown> }) => {
const html = await ComponentRender(nameOrHtml, options, InputTags)
expect(html).toMatchSnapshot()
})
})

View File

@@ -1,57 +0,0 @@
import { describe, it, expect } from 'vitest'
import Timeline, { type TimelineProps, type TimelineSlots } from '../../src/runtime/components/Timeline.vue'
import ComponentRender from '../component-render'
import theme from '#build/ui/timeline'
describe('Timeline', () => {
const sizes = Object.keys(theme.variants.size) as any
const items = [{
date: 'Mar 15, 2025',
title: 'Project Kickoff',
description: 'Kicked off the project with team alignment. Set up project milestones and allocated resources.',
icon: 'i-lucide-rocket',
value: 'kickoff'
}, {
date: 'Mar 22, 2025',
title: 'Design Phase',
description: 'User research and design workshops. Created wireframes and prototypes for user testing',
icon: 'i-lucide-palette',
value: 'design'
}, {
date: 'Mar 29, 2025',
title: 'Development Sprint',
description: 'Frontend and backend development. Implemented core features and integrated with APIs.',
icon: 'i-lucide-code',
value: 'development'
}, {
date: 'Apr 5, 2025',
title: 'Testing & Deployment',
description: 'QA testing and performance optimization. Deployed the application to production.',
icon: 'i-lucide-check-circle',
value: 'deployment'
}]
const props = { items }
it.each([
// Props
['with items', { props }],
['with modelValue', { props: { ...props, modelValue: 'assigned' } }],
['with defaultValue', { props: { ...props, defaultValue: 'assigned' } }],
['with neutral color', { props: { ...props, color: 'neutral' } }],
...sizes.map((size: string) => [`with size ${size} horizontal`, { props: { ...props, size } }]),
...sizes.map((size: string) => [`with size ${size} vertical`, { props: { ...props, size, orientation: 'vertical' } }]),
['with as', { props: { ...props, as: 'section' } }],
['with class', { props: { ...props, class: 'gap-8' } }],
['with ui', { props: { ...props, ui: { title: 'font-bold' } } }],
// Slots
['with indicator slot', { props, slots: { indicator: () => 'Indicator slot' } }],
['with date slot', { props, slots: { date: () => 'Date slot' } }],
['with title slot', { props, slots: { title: () => 'Title slot' } }],
['with description slot', { props, slots: { description: () => 'Description slot' } }]
])('renders %s correctly', async (nameOrHtml: string, options: { props?: TimelineProps, slots?: Partial<TimelineSlots> }) => {
const html = await ComponentRender(nameOrHtml, options, Timeline)
expect(html).toMatchSnapshot()
})
})

View File

@@ -28,7 +28,7 @@ exports[`Alert > renders with as correctly 1`] = `
`;
exports[`Alert > renders with avatar correctly 1`] = `
"<div data-orientation="vertical" class="relative overflow-hidden w-full rounded-lg p-4 flex gap-2.5 items-start bg-primary text-inverted"><span class="inline-flex items-center justify-center select-none rounded-full align-middle bg-elevated size-11 text-[22px] shrink-0"><img role="img" src="https://github.com/benjamincanac.png" width="44" height="44" class="h-full w-full rounded-[inherit] object-cover"></span>
"<div data-orientation="vertical" class="relative overflow-hidden w-full rounded-lg p-4 flex gap-2.5 items-start bg-primary text-inverted"><span class="inline-flex items-center justify-center select-none overflow-hidden rounded-full align-middle bg-elevated size-11 text-[22px] shrink-0"><img role="img" src="https://github.com/benjamincanac.png" width="44" height="44" class="h-full w-full rounded-[inherit] object-cover"></span>
<div class="min-w-0 flex-1 flex flex-col">
<div class="text-sm font-medium">Alert</div>
<!--v-if-->

View File

@@ -28,7 +28,7 @@ exports[`Alert > renders with as correctly 1`] = `
`;
exports[`Alert > renders with avatar correctly 1`] = `
"<div data-orientation="vertical" class="relative overflow-hidden w-full rounded-lg p-4 flex gap-2.5 items-start bg-primary text-inverted"><span class="inline-flex items-center justify-center select-none rounded-full align-middle bg-elevated size-11 text-[22px] shrink-0"><img role="img" src="https://github.com/benjamincanac.png" width="44" height="44" class="h-full w-full rounded-[inherit] object-cover"></span>
"<div data-orientation="vertical" class="relative overflow-hidden w-full rounded-lg p-4 flex gap-2.5 items-start bg-primary text-inverted"><span class="inline-flex items-center justify-center select-none overflow-hidden rounded-full align-middle bg-elevated size-11 text-[22px] shrink-0"><img role="img" src="https://github.com/benjamincanac.png" width="44" height="44" class="h-full w-full rounded-[inherit] object-cover"></span>
<div class="min-w-0 flex-1 flex flex-col">
<div class="text-sm font-medium">Alert</div>
<!--v-if-->

View File

@@ -1,37 +1,35 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Avatar > renders with alt correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-8 text-base"><span class="font-medium leading-none text-muted truncate">BC</span></span>"`;
exports[`Avatar > renders with alt correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-elevated size-8 text-base"><span class="font-medium leading-none text-muted truncate">BC</span></span>"`;
exports[`Avatar > renders with as correctly 1`] = `"<section class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-8 text-base"><span class="font-medium leading-none text-muted truncate">&nbsp;</span></section>"`;
exports[`Avatar > renders with as correctly 1`] = `"<section class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-elevated size-8 text-base"><span class="font-medium leading-none text-muted truncate">&nbsp;</span></section>"`;
exports[`Avatar > renders with chip correctly 1`] = `"<span class="relative inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-8 text-base"><span class="font-medium leading-none text-muted truncate">&nbsp;</span><span class="rounded-full ring ring-bg flex items-center justify-center text-inverted font-medium whitespace-nowrap bg-primary h-[8px] min-w-[8px] text-[8px] top-0 right-0 absolute">1</span></span>"`;
exports[`Avatar > renders with class correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle size-8 text-base bg-default"><span class="font-medium leading-none text-muted truncate">&nbsp;</span></span>"`;
exports[`Avatar > renders with class correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle size-8 text-base bg-default"><span class="font-medium leading-none text-muted truncate">&nbsp;</span></span>"`;
exports[`Avatar > renders with default slot correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-elevated size-8 text-base">🇫🇷</span>"`;
exports[`Avatar > renders with default slot correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-8 text-base">🇫🇷</span>"`;
exports[`Avatar > renders with icon correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-elevated size-8 text-base"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" viewBox="0 0 16 16" class="text-muted shrink-0"></svg></span>"`;
exports[`Avatar > renders with icon correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-8 text-base"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" viewBox="0 0 16 16" class="text-muted shrink-0"></svg></span>"`;
exports[`Avatar > renders with size 2xl correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-elevated size-11 text-[22px]"><img role="img" src="https://github.com/benjamincanac.png" width="44" height="44" class="h-full w-full rounded-[inherit] object-cover"></span>"`;
exports[`Avatar > renders with size 2xl correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-11 text-[22px]"><img role="img" src="https://github.com/benjamincanac.png" width="44" height="44" class="h-full w-full rounded-[inherit] object-cover"></span>"`;
exports[`Avatar > renders with size 2xs correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-elevated size-5 text-[10px]"><img role="img" src="https://github.com/benjamincanac.png" width="20" height="20" class="h-full w-full rounded-[inherit] object-cover"></span>"`;
exports[`Avatar > renders with size 2xs correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-5 text-[10px]"><img role="img" src="https://github.com/benjamincanac.png" width="20" height="20" class="h-full w-full rounded-[inherit] object-cover"></span>"`;
exports[`Avatar > renders with size 3xl correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-elevated size-12 text-2xl"><img role="img" src="https://github.com/benjamincanac.png" width="48" height="48" class="h-full w-full rounded-[inherit] object-cover"></span>"`;
exports[`Avatar > renders with size 3xl correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-12 text-2xl"><img role="img" src="https://github.com/benjamincanac.png" width="48" height="48" class="h-full w-full rounded-[inherit] object-cover"></span>"`;
exports[`Avatar > renders with size 3xs correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-elevated size-4 text-[8px]"><img role="img" src="https://github.com/benjamincanac.png" width="16" height="16" class="h-full w-full rounded-[inherit] object-cover"></span>"`;
exports[`Avatar > renders with size 3xs correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-4 text-[8px]"><img role="img" src="https://github.com/benjamincanac.png" width="16" height="16" class="h-full w-full rounded-[inherit] object-cover"></span>"`;
exports[`Avatar > renders with size lg correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-elevated size-9 text-lg"><img role="img" src="https://github.com/benjamincanac.png" width="36" height="36" class="h-full w-full rounded-[inherit] object-cover"></span>"`;
exports[`Avatar > renders with size lg correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-9 text-lg"><img role="img" src="https://github.com/benjamincanac.png" width="36" height="36" class="h-full w-full rounded-[inherit] object-cover"></span>"`;
exports[`Avatar > renders with size md correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-elevated size-8 text-base"><img role="img" src="https://github.com/benjamincanac.png" width="32" height="32" class="h-full w-full rounded-[inherit] object-cover"></span>"`;
exports[`Avatar > renders with size md correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-8 text-base"><img role="img" src="https://github.com/benjamincanac.png" width="32" height="32" class="h-full w-full rounded-[inherit] object-cover"></span>"`;
exports[`Avatar > renders with size sm correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-elevated size-7 text-sm"><img role="img" src="https://github.com/benjamincanac.png" width="28" height="28" class="h-full w-full rounded-[inherit] object-cover"></span>"`;
exports[`Avatar > renders with size sm correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-7 text-sm"><img role="img" src="https://github.com/benjamincanac.png" width="28" height="28" class="h-full w-full rounded-[inherit] object-cover"></span>"`;
exports[`Avatar > renders with size xl correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-elevated size-10 text-xl"><img role="img" src="https://github.com/benjamincanac.png" width="40" height="40" class="h-full w-full rounded-[inherit] object-cover"></span>"`;
exports[`Avatar > renders with size xl correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-10 text-xl"><img role="img" src="https://github.com/benjamincanac.png" width="40" height="40" class="h-full w-full rounded-[inherit] object-cover"></span>"`;
exports[`Avatar > renders with size xs correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-elevated size-6 text-xs"><img role="img" src="https://github.com/benjamincanac.png" width="24" height="24" class="h-full w-full rounded-[inherit] object-cover"></span>"`;
exports[`Avatar > renders with size xs correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-6 text-xs"><img role="img" src="https://github.com/benjamincanac.png" width="24" height="24" class="h-full w-full rounded-[inherit] object-cover"></span>"`;
exports[`Avatar > renders with src correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-elevated size-8 text-base"><img role="img" src="https://github.com/benjamincanac.png" width="32" height="32" class="h-full w-full rounded-[inherit] object-cover"></span>"`;
exports[`Avatar > renders with src correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-8 text-base"><img role="img" src="https://github.com/benjamincanac.png" width="32" height="32" class="h-full w-full rounded-[inherit] object-cover"></span>"`;
exports[`Avatar > renders with text correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-elevated size-8 text-base"><span class="font-medium leading-none text-muted truncate">+1</span></span>"`;
exports[`Avatar > renders with text correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-8 text-base"><span class="font-medium leading-none text-muted truncate">+1</span></span>"`;
exports[`Avatar > renders with ui correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-8 text-base"><span class="leading-none text-muted truncate font-bold">&nbsp;</span></span>"`;
exports[`Avatar > renders with ui correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-elevated size-8 text-base"><span class="leading-none text-muted truncate font-bold">&nbsp;</span></span>"`;

View File

@@ -1,37 +1,35 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Avatar > renders with alt correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-8 text-base"><span class="font-medium leading-none text-muted truncate">BC</span></span>"`;
exports[`Avatar > renders with alt correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-elevated size-8 text-base"><span class="font-medium leading-none text-muted truncate">BC</span></span>"`;
exports[`Avatar > renders with as correctly 1`] = `"<section class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-8 text-base"><span class="font-medium leading-none text-muted truncate">&nbsp;</span></section>"`;
exports[`Avatar > renders with as correctly 1`] = `"<section class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-elevated size-8 text-base"><span class="font-medium leading-none text-muted truncate">&nbsp;</span></section>"`;
exports[`Avatar > renders with chip correctly 1`] = `"<span class="relative inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-8 text-base"><span class="font-medium leading-none text-muted truncate">&nbsp;</span><span class="rounded-full ring ring-bg flex items-center justify-center text-inverted font-medium whitespace-nowrap bg-primary h-[8px] min-w-[8px] text-[8px] top-0 right-0 absolute">1</span></span>"`;
exports[`Avatar > renders with class correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle size-8 text-base bg-default"><span class="font-medium leading-none text-muted truncate">&nbsp;</span></span>"`;
exports[`Avatar > renders with class correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle size-8 text-base bg-default"><span class="font-medium leading-none text-muted truncate">&nbsp;</span></span>"`;
exports[`Avatar > renders with default slot correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-elevated size-8 text-base">🇫🇷</span>"`;
exports[`Avatar > renders with default slot correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-8 text-base">🇫🇷</span>"`;
exports[`Avatar > renders with icon correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-elevated size-8 text-base"><span class="iconify i-lucide:image text-muted shrink-0" aria-hidden="true"></span></span>"`;
exports[`Avatar > renders with icon correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-8 text-base"><span class="iconify i-lucide:image text-muted shrink-0" aria-hidden="true"></span></span>"`;
exports[`Avatar > renders with size 2xl correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-elevated size-11 text-[22px]"><img role="img" src="https://github.com/benjamincanac.png" width="44" height="44" class="h-full w-full rounded-[inherit] object-cover"></span>"`;
exports[`Avatar > renders with size 2xl correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-11 text-[22px]"><img role="img" src="https://github.com/benjamincanac.png" width="44" height="44" class="h-full w-full rounded-[inherit] object-cover"></span>"`;
exports[`Avatar > renders with size 2xs correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-elevated size-5 text-[10px]"><img role="img" src="https://github.com/benjamincanac.png" width="20" height="20" class="h-full w-full rounded-[inherit] object-cover"></span>"`;
exports[`Avatar > renders with size 2xs correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-5 text-[10px]"><img role="img" src="https://github.com/benjamincanac.png" width="20" height="20" class="h-full w-full rounded-[inherit] object-cover"></span>"`;
exports[`Avatar > renders with size 3xl correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-elevated size-12 text-2xl"><img role="img" src="https://github.com/benjamincanac.png" width="48" height="48" class="h-full w-full rounded-[inherit] object-cover"></span>"`;
exports[`Avatar > renders with size 3xl correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-12 text-2xl"><img role="img" src="https://github.com/benjamincanac.png" width="48" height="48" class="h-full w-full rounded-[inherit] object-cover"></span>"`;
exports[`Avatar > renders with size 3xs correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-elevated size-4 text-[8px]"><img role="img" src="https://github.com/benjamincanac.png" width="16" height="16" class="h-full w-full rounded-[inherit] object-cover"></span>"`;
exports[`Avatar > renders with size 3xs correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-4 text-[8px]"><img role="img" src="https://github.com/benjamincanac.png" width="16" height="16" class="h-full w-full rounded-[inherit] object-cover"></span>"`;
exports[`Avatar > renders with size lg correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-elevated size-9 text-lg"><img role="img" src="https://github.com/benjamincanac.png" width="36" height="36" class="h-full w-full rounded-[inherit] object-cover"></span>"`;
exports[`Avatar > renders with size lg correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-9 text-lg"><img role="img" src="https://github.com/benjamincanac.png" width="36" height="36" class="h-full w-full rounded-[inherit] object-cover"></span>"`;
exports[`Avatar > renders with size md correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-elevated size-8 text-base"><img role="img" src="https://github.com/benjamincanac.png" width="32" height="32" class="h-full w-full rounded-[inherit] object-cover"></span>"`;
exports[`Avatar > renders with size md correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-8 text-base"><img role="img" src="https://github.com/benjamincanac.png" width="32" height="32" class="h-full w-full rounded-[inherit] object-cover"></span>"`;
exports[`Avatar > renders with size sm correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-elevated size-7 text-sm"><img role="img" src="https://github.com/benjamincanac.png" width="28" height="28" class="h-full w-full rounded-[inherit] object-cover"></span>"`;
exports[`Avatar > renders with size sm correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-7 text-sm"><img role="img" src="https://github.com/benjamincanac.png" width="28" height="28" class="h-full w-full rounded-[inherit] object-cover"></span>"`;
exports[`Avatar > renders with size xl correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-elevated size-10 text-xl"><img role="img" src="https://github.com/benjamincanac.png" width="40" height="40" class="h-full w-full rounded-[inherit] object-cover"></span>"`;
exports[`Avatar > renders with size xl correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-10 text-xl"><img role="img" src="https://github.com/benjamincanac.png" width="40" height="40" class="h-full w-full rounded-[inherit] object-cover"></span>"`;
exports[`Avatar > renders with size xs correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-elevated size-6 text-xs"><img role="img" src="https://github.com/benjamincanac.png" width="24" height="24" class="h-full w-full rounded-[inherit] object-cover"></span>"`;
exports[`Avatar > renders with size xs correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-6 text-xs"><img role="img" src="https://github.com/benjamincanac.png" width="24" height="24" class="h-full w-full rounded-[inherit] object-cover"></span>"`;
exports[`Avatar > renders with src correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-elevated size-8 text-base"><img role="img" src="https://github.com/benjamincanac.png" width="32" height="32" class="h-full w-full rounded-[inherit] object-cover"></span>"`;
exports[`Avatar > renders with src correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-8 text-base"><img role="img" src="https://github.com/benjamincanac.png" width="32" height="32" class="h-full w-full rounded-[inherit] object-cover"></span>"`;
exports[`Avatar > renders with text correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-elevated size-8 text-base"><span class="font-medium leading-none text-muted truncate">+1</span></span>"`;
exports[`Avatar > renders with text correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-8 text-base"><span class="font-medium leading-none text-muted truncate">+1</span></span>"`;
exports[`Avatar > renders with ui correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle bg-elevated size-8 text-base"><span class="leading-none text-muted truncate font-bold">&nbsp;</span></span>"`;
exports[`Avatar > renders with ui correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-elevated size-8 text-base"><span class="leading-none text-muted truncate font-bold">&nbsp;</span></span>"`;

Some files were not shown because too many files have changed in this diff Show More