Compare commits

...

48 Commits

Author SHA1 Message Date
Romain Hamel
af25f65e81 test: update vue snapshots 2025-06-14 18:13:06 +02:00
Romain Hamel
5829aebe7d feat(FormField): display multiple errors
Adds the `errors` and `maxErrors` properties to the FormField component which
allows to display multiple errors. The `maxErrors` prop is used to limit
the number of errors displayed and defaults to 1 for backward
compatibility.

This change is compatible with the `Form` component error's too.

Resolves #2389
2025-06-14 17:19:16 +02:00
Hugo Richard
59c26ec123 feat(CommandPalette): handle children in items (#4226)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-06-13 14:49:43 +02:00
Julien Augugliaro
67ef866a40 docs(input): fix typo in mask example (#4334) 2025-06-12 23:48:01 +02:00
J-Michalek
5170cfd7eb feat(Timeline): add reverse prop (#4316)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-06-12 17:06:51 +02:00
Benjamin Canac
9bcf1ad92f docs(input-tags): add illustration 2025-06-12 16:40:08 +02:00
Benjamin Canac
7a2bd4e617 feat(Select/SelectMenu/Tabs): expose trigger refs
Resolves #4292
2025-06-12 15:38:43 +02:00
Benjamin Canac
8781a07909 fix(InputTags): extend emits interface 2025-06-12 15:35:20 +02:00
Benjamin Canac
2492526d7c docs(showcase): improve file parse 2025-06-12 12:40:57 +02:00
zhong666
54bb2282c5 feat(InputTags): new component (#4261)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-06-12 12:10:48 +02:00
renovate[bot]
2a2495a652 chore(deps): update tailwindcss to ^4.1.10 (v3) (#4332)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-12 10:53:49 +02:00
renovate[bot]
f17b15ed1e chore(deps): update tailwindcss to ^4.1.9 (v3) (#4326)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-11 17:19:05 +02:00
Eugen Istoc
66355ba301 fix(useOverlay): set props to original props when defaultOpen is set (#4308)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-06-11 17:05:55 +02:00
Benjamin Canac
4873b3a043 chore(deps): update @nuxt/ui-pro 2025-06-11 17:02:44 +02:00
Benjamin Canac
0d4baf7851 docs(app): improve header dropdown items 2025-06-11 16:55:25 +02:00
syvixor
04333cd8cb docs(app): update v2 versions in header (#4325)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-06-11 15:27:19 +02:00
renovate[bot]
cb3522ed18 chore(deps): update dependency colortranslator to v5 (v3) (#4314)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-11 12:56:00 +02:00
Benjamin Canac
1a4de49c16 feat(Select/SelectMenu): handle dynamic autofocus
Resolves #4324
2025-06-11 12:53:57 +02:00
Benjamin Canac
3eb7812f2d chore(github): update stale workflow 2025-06-11 12:17:19 +02:00
Benjamin Canac
080aed7225 chore(github): handle workflow_dispatch 2025-06-11 12:11:55 +02:00
Benjamin Canac
d77fd6102a chore(github): update stale workflow 2025-06-11 12:04:30 +02:00
Benjamin Canac
0f558fc0d0 feat(extendLocale): new composable
Resolves #3729
2025-06-11 10:58:20 +02:00
J-Michalek
1841e13b32 docs(timeline): fix duplicate separator field on item.ui (#4313)
Co-authored-by: Jakub <jakub.michalek@freelo.io>
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-06-10 18:21:14 +02:00
Hugo Richard
4e7c1c9c30 fix(defineShortcuts): allow meta_- shortcut (#4321) 2025-06-10 17:54:15 +02:00
renovate[bot]
4b3dd48778 chore(deps): update nuxt-hub/action action to v2 (v3) (#4315)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-06-10 17:44:59 +02:00
renovate[bot]
a9e8ea9231 chore(deps): update all non-major dependencies (v3) (#4277)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-10 17:17:33 +02:00
Benjamin Canac
4dd9344ff9 chore(deps): use workspace:* syntax 2025-06-10 16:53:07 +02:00
renovate[bot]
180c150e0f chore(deps): update dependency reka-ui to v2.3.1 (v3) (#4310)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-10 16:16:01 +02:00
Estéban
145cae798c docs(installation): add tip to improve types in vue (#4318)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-06-10 15:58:09 +02:00
Benjamin Canac
9aea54267a chore(deps): add @internationalized/date in playground 2025-06-10 15:49:17 +02:00
Benjamin Canac
9400552491 chore(github): improve sha retrieval 2025-06-10 14:52:35 +02:00
Benjamin Canac
89753fc337 chore(github): update workflows permissions 2025-06-10 13:02:32 +02:00
Benjamin Canac
f8a6bd3bf6 chore(github): add stale workflow 2025-06-06 12:29:23 +02:00
Benjamin Canac
228d4c9835 docs(input): add mask example 2025-06-06 11:37:26 +02:00
Benjamin Canac
150b334b1d fix(Modal/Slideover): don't emit close:prevent on closeAutoFocus 2025-06-05 17:22:42 +02:00
Eugen Istoc
bf56e15a2e fix(useOverlay): use original props when not provided to open (#4269) 2025-06-05 15:48:47 +02:00
Benjamin Canac
09151df170 chore(renovate): add vaul-vue to group 2025-06-05 12:47:08 +02:00
Benjamin Canac
22b917a0f7 chore(deps): pin reka-ui & vaul-vue dependencies
Related to https://github.com/nuxt/ui/issues/4257
2025-06-05 12:47:08 +02:00
adjabaev
43cbb94ee2 feat(locale): add Luxembourgish language (#4264) 2025-06-05 12:34:38 +02:00
Benjamin Canac
3bf5acb683 fix(Toast): calc height on next tick
Resolves #4265
2025-06-05 12:13:35 +02:00
Eugen Istoc
d37315cc83 docs(use-overlay): add caveats section regarding provide/inject limit (#4287)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-06-04 16:01:02 +02:00
renovate[bot]
326bb9a31e chore(deps): update nuxt framework to ^3.17.5 (v3) (#4284)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-04 10:34:41 +02:00
Benjamin Canac
4157260a02 docs(timeline): improve responsive 2025-06-03 18:09:36 +02:00
Benjamin Canac
03b20fdb26 chore: prefer nuxt over nuxi 2025-06-02 12:51:40 +02:00
renovate[bot]
004c93bfa2 chore(deps): lock file maintenance (v3) (#4278)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-02 12:46:59 +02:00
renovate[bot]
18eb5e6b97 chore(deps): update tailwindcss to ^4.1.8 (v3) (#4275)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-02 10:55:39 +02:00
renovate[bot]
42f7f94521 chore(deps): update all non-major dependencies (v3) (#4273)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-02 10:22:38 +02:00
dan hale
ea0c459306 feat(Form): expose loading state to default slot (#4247)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-06-01 16:48:16 +02:00
138 changed files with 4455 additions and 1296 deletions

View File

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

View File

@@ -6,10 +6,6 @@ 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
@@ -40,14 +36,10 @@ jobs:
- name: Prepare build
run: pnpm run dev:prepare
- name: Build application
run: pnpm run docs:build
- name: Deploy to NuxtHub
uses: nuxt-hub/action@v2
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/dist
directory: docs

View File

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

View File

@@ -9,10 +9,6 @@ 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
@@ -40,14 +36,10 @@ jobs:
- name: Prepare build
run: pnpm run dev:prepare
- name: Build application
run: pnpm run dev:build
env:
NITRO_PRESET: cloudflare-pages
- name: Deploy to NuxtHub
uses: nuxt-hub/action@v1
id: deploy
uses: nuxt-hub/action@v2
env:
NODE_OPTIONS: '--max-old-space-size=8192'
with:
project-key: ui3-playground-pb9b
directory: playground/dist
directory: playground

27
.github/workflows/reproduction.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
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,6 +1,7 @@
name: stale
on:
workflow_dispatch:
schedule:
- cron: '30 1 * * *'
@@ -9,17 +10,28 @@ jobs:
runs-on: ubuntu-latest
permissions:
actions: write
issues: write
steps:
- uses: actions/stale@v9
- uses: actions/stale@4c023f01d613e60293d8004f251a18bfb9bbd71d
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
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

View File

@@ -22,9 +22,7 @@ onMounted(() => {
const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
const githubLink = computed(() => {
return `https://github.com/nuxt/${value.value}`
})
const githubLink = computed(() => `https://github.com/nuxt/${value.value}`)
const desktopLinks = computed(() => props.links.map(({ icon, ...link }) => link))
const mobileLinks = computed(() => [
@@ -36,6 +34,16 @@ 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>
@@ -53,7 +61,7 @@ const mobileLinks = computed(() => [
<UDropdownMenu
v-slot="{ open }"
:modal="false"
: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' }]"
:items="items"
:ui="{ content: 'w-(--reka-dropdown-menu-trigger-width) min-w-0' }"
size="xs"
>

View File

@@ -26,6 +26,7 @@ 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

@@ -83,7 +83,7 @@ const groups = [
</template>
<template #billing-label="{ item }">
{{ item.label }}
<span class="font-medium text-primary">{{ item.label }}</span>
<UBadge variant="subtle" size="sm">
50% off

View File

@@ -0,0 +1,119 @@
<script setup lang="ts">
const toast = useToast()
const groups = [{
id: 'actions',
label: 'Actions',
items: [{
label: 'Create new',
icon: 'i-lucide-plus',
children: [{
label: 'New file',
icon: 'i-lucide-file-plus',
suffix: 'Create a new file in the current directory',
onSelect(e: Event) {
e.preventDefault()
toast.add({ title: 'New file created!' })
},
kbds: ['meta', 'N']
}, {
label: 'New folder',
icon: 'i-lucide-folder-plus',
suffix: 'Create a new folder in the current directory',
onSelect(e: Event) {
e.preventDefault()
toast.add({ title: 'New folder created!' })
},
kbds: ['meta', 'F']
}, {
label: 'New project',
icon: 'i-lucide-folder-git',
suffix: 'Create a new project from a template',
onSelect(e: Event) {
e.preventDefault()
toast.add({ title: 'New project created!' })
},
kbds: ['meta', 'P']
}]
}, {
label: 'Share',
icon: 'i-lucide-share',
children: [{
label: 'Copy link',
icon: 'i-lucide-link',
suffix: 'Copy a link to the current item',
onSelect(e: Event) {
e.preventDefault()
toast.add({ title: 'Link copied to clipboard!' })
},
kbds: ['meta', 'L']
}, {
label: 'Share via email',
icon: 'i-lucide-mail',
suffix: 'Share the current item via email',
onSelect(e: Event) {
e.preventDefault()
toast.add({ title: 'Share via email dialog opened!' })
}
}, {
label: 'Share on social',
icon: 'i-lucide-share-2',
suffix: 'Share the current item on social media',
children: [{
label: 'Twitter',
icon: 'i-simple-icons-twitter',
onSelect(e: Event) {
e.preventDefault()
toast.add({ title: 'Shared on Twitter!' })
}
}, {
label: 'LinkedIn',
icon: 'i-simple-icons-linkedin',
onSelect(e: Event) {
e.preventDefault()
toast.add({ title: 'Shared on LinkedIn!' })
}
}, {
label: 'Facebook',
icon: 'i-simple-icons-facebook',
onSelect(e: Event) {
e.preventDefault()
toast.add({ title: 'Shared on Facebook!' })
}
}]
}]
}, {
label: 'Settings',
icon: 'i-lucide-settings',
children: [{
label: 'General',
icon: 'i-lucide-sliders',
suffix: 'Configure general settings',
onSelect(e: Event) {
e.preventDefault()
toast.add({ title: 'General settings opened!' })
}
}, {
label: 'Appearance',
icon: 'i-lucide-palette',
suffix: 'Customize the appearance',
onSelect(e: Event) {
e.preventDefault()
toast.add({ title: 'Appearance settings opened!' })
}
}, {
label: 'Security',
icon: 'i-lucide-shield',
suffix: 'Manage security settings',
onSelect(e: Event) {
e.preventDefault()
toast.add({ title: 'Security settings opened!' })
}
}]
}]
}]
</script>
<template>
<UCommandPalette :groups="groups" class="flex-1" />
</template>

View File

@@ -0,0 +1,9 @@
<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

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

View File

@@ -0,0 +1,14 @@
<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

@@ -29,6 +29,6 @@ const items: TimelineItem[] = [{
:items="items"
:ui="{ item: 'even:flex-row-reverse even:-translate-x-[calc(100%-2rem)] even:text-right' }"
:default-value="2"
class="w-full translate-x-[calc(50%-2rem)]"
class="translate-x-[calc(50%-1rem)]"
/>
</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 nuxi module add compodium
npx nuxt 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]
npx nuxi init -t ui <my-app>
npm create nuxt@latest -- -t ui
```
::note

View File

@@ -78,6 +78,22 @@ 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}
@@ -179,7 +195,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]
npx nuxi init -t github:nuxtlabs/nuxt-ui-vue-starter <my-app>
npm create nuxt@latest -- -t github:nuxtlabs/nuxt-ui-vue-starter
```
::note

View File

@@ -60,7 +60,7 @@ import { fr } from '@nuxt/ui-pro/locale'
### Custom locale
You also have the option to add your own locale using `defineLocale`:
You can create your own locale using the `defineLocale` composable:
::module-only
@@ -125,6 +125,65 @@ 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 also have the option to add your locale using `defineLocale`:
You can create your own locale using the `defineLocale` composable:
::module-only
@@ -127,6 +127,67 @@ 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,9 +9,11 @@ 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(MyModal)
const modal = overlay.create(LazyModalExample)
async function openModal() {
modal.open()
@@ -29,71 +31,73 @@ In order to return a value from the overlay, the `overlay.open().instance.result
### `create(component: T, options: OverlayOptions): OverlayInstance`
Creates an overlay, and returns a factory instance
Create an overlay, and return a factory instance.
- Parameters:
- `component`: The overlay component
- `options` The overlay options
- `defaultOpen?: boolean` Opens the overlay immediately after being created `default: false`
- `component`: The overlay component.
- `options`:
- `defaultOpen?: boolean` Open the overlay immediately after being created. Defaults to `false`.
- `props?: ComponentProps`: An optional object of props to pass to the rendered component.
- `destroyOnClose?: boolean` Removes the overlay from memory when closed `default: false`
- `destroyOnClose?: boolean` Removes the overlay from memory when closed. Defaults to `false`.
### `open(id: symbol, props?: ComponentProps<T>): OpenedOverlay<T>`
Opens the overlay using its `id`
Open an overlay by 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 using its `id`
Close an overlay by 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 using its `id`
Update an overlay by 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`
Removes the overlay from the DOM using its `id`
Remove an overlay from the DOM by its `id`.
- Parameters:
- `id`: The identifier of the overlay
- `id`: The identifier of the overlay.
### `isOpen(id: symbol): boolean`
Checks if an overlay its open using its `id`
Check if an overlay is open using its `id`.
- Parameters:
- `id`: The identifier of the overlay
- `id`: The identifier of the overlay.
### `overlays: Overlay[]`
In-memory list of overlays that were created
In-memory list of all overlays that were created.
## Overlay Instance API
## Instance API
### `open(props?: ComponentProps<T>): Promise<OpenedOverlay<T>>`
Opens the overlay
Open 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(MyModalContent)
const modal = overlay.create(LazyModalExample)
function openModal() {
modal.open({
@@ -105,23 +109,25 @@ 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>)`
Updates the props of the overlay.
Update 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(MyModal, {
const modal = overlay.create(LazyModalExample, {
title: 'Welcome'
})
@@ -141,6 +147,8 @@ 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
@@ -150,7 +158,7 @@ const modalB = overlay.create(ModalB)
const slideoverA = overlay.create(SlideoverA)
const openModalA = () => {
// Open Modal A, but override the title prop
// Open modalA, but override the title prop
modalA.open({ title: 'Hello' })
}
@@ -160,16 +168,37 @@ 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>
<div>
<button @click="openModal">Open Modal</button>
</div>
<button @click="openModalA">Open Modal</button>
</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

@@ -52,9 +52,11 @@ Each group contains an `items` array of objects that define the commands. Each i
- `loading?: boolean`{lang="ts-type"}
- `disabled?: boolean`{lang="ts-type"}
- [`slot?: string`{lang="ts-type"}](#with-custom-slot)
- `placeholder?: string`{lang="ts-type"} :badge{label="Soon"}
- `children?: CommandPaletteItem[]`{lang="ts-type"} :badge{label="Soon"}
- `onSelect?(e?: Event): void`{lang="ts-type"}
- `class?: any`{lang="ts-type"}
- `ui?: { item?: ClassNameValue, itemLeadingIcon?: ClassNameValue, itemLeadingAvatarSize?: ClassNameValue, itemLeadingAvatar?: ClassNameValue, itemLeadingChipSize?: ClassNameValue, itemLeadingChip?: ClassNameValue, itemLabel?: ClassNameValue, itemLabelPrefix?: ClassNameValue, itemLabelBase?: ClassNameValue, itemLabelSuffix?: ClassNameValue, itemTrailing?: ClassNameValue, itemTrailingKbds?: ClassNameValue, itemTrailingKbdsSize?: ClassNameValue, itemTrailingHighlightedIcon?: ClassNameValue, itemTrailingIcon?: ClassNameValue,}`{lang="ts-type"}
- `ui?: { item?: ClassNameValue, itemLeadingIcon?: ClassNameValue, itemLeadingAvatarSize?: ClassNameValue, itemLeadingAvatar?: ClassNameValue, itemLeadingChipSize?: ClassNameValue, itemLeadingChip?: ClassNameValue, itemLabel?: ClassNameValue, itemLabelPrefix?: ClassNameValue, itemLabelBase?: ClassNameValue, itemLabelSuffix?: ClassNameValue, itemTrailing?: ClassNameValue, itemTrailingKbds?: ClassNameValue, itemTrailingKbdsSize?: ClassNameValue, itemTrailingHighlightedIcon?: ClassNameValue, itemTrailingIcon?: ClassNameValue }`{lang="ts-type"}
You can pass any property from the [Link](/components/link#props) component such as `to`, `target`, etc.
@@ -110,6 +112,10 @@ props:
---
::
::tip{to="#with-children-in-items"}
Each item can take a `children` array of objects with the following properties to create submenus:
::
### Multiple
Use the `multiple` prop to allow multiple selections.
@@ -246,6 +252,128 @@ You can customize this icon globally in your `vite.config.ts` under `ui.icons.se
:::
::
### Selected Icon
Use the `selected-icon` prop to customize the selected item [Icon](/components/icon). Defaults to `i-lucide-check`.
::component-code
---
collapse: true
hide:
- autofocus
ignore:
- groups
- modelValue
- multiple
- class
external:
- groups
- modelValue
class: '!p-0'
props:
multiple: true
autofocus: false
modelValue:
- label: 'Benjamin Canac'
suffix: 'benjamincanac'
avatar:
src: 'https://github.com/benjamincanac.png'
selectedIcon: 'i-lucide-circle-check'
groups:
- id: 'users'
label: 'Users'
items:
- label: 'Benjamin Canac'
suffix: 'benjamincanac'
avatar:
src: 'https://github.com/benjamincanac.png'
- label: 'Sylvain Marroufin'
suffix: 'smarroufin'
avatar:
src: 'https://github.com/smarroufin.png'
- label: 'Sébastien Chopin'
suffix: 'atinux'
avatar:
src: 'https://github.com/atinux.png'
- label: 'Romain Hamel'
suffix: 'romhml'
avatar:
src: 'https://github.com/romhml.png'
- label: 'Haytham A. Salama'
suffix: 'Haythamasalama'
avatar:
src: 'https://github.com/Haythamasalama.png'
- label: 'Daniel Roe'
suffix: 'danielroe'
avatar:
src: 'https://github.com/danielroe.png'
- label: 'Neil Richter'
suffix: 'noook'
avatar:
src: 'https://github.com/noook.png'
class: 'flex-1'
---
::
::framework-only
#nuxt
:::tip{to="/getting-started/icons/nuxt#theme"}
You can customize this icon globally in your `app.config.ts` under `ui.icons.check` key.
:::
#vue
:::tip{to="/getting-started/icons/vue#theme"}
You can customize this icon globally in your `vite.config.ts` under `ui.icons.check` key.
:::
::
### Trailing Icon :badge{label="Soon" class="align-text-top"}
Use the `trailing-icon` prop to customize the trailing [Icon](/components/icon) when an item has children. Defaults to `i-lucide-chevron-right`.
::component-code
---
collapse: true
prettier: true
hide:
- autofocus
ignore:
- groups
- class
external:
- groups
class: '!p-0'
props:
autofocus: false
trailingIcon: 'i-lucide-arrow-right'
groups:
- id: 'actions'
items:
- label: 'Share'
icon: 'i-lucide-share'
children:
- label: 'Email'
icon: 'i-lucide-mail'
- label: 'Copy'
icon: 'i-lucide-copy'
- label: 'Link'
icon: 'i-lucide-link'
class: 'flex-1'
---
::
::framework-only
#nuxt
:::tip{to="/getting-started/icons/nuxt#theme"}
You can customize this icon globally in your `app.config.ts` under `ui.icons.chevronRight` key.
:::
#vue
:::tip{to="/getting-started/icons/vue#theme"}
You can customize this icon globally in your `vite.config.ts` under `ui.icons.chevronRight` key.
:::
::
### Loading
Use the `loading` prop to show a loading icon on the CommandPalette.
@@ -321,37 +449,6 @@ You can customize this icon globally in your `vite.config.ts` under `ui.icons.lo
:::
::
### Disabled
Use the `disabled` prop to disable the CommandPalette.
::component-code
---
collapse: true
hide:
- autofocus
ignore:
- groups
- class
external:
- groups
class: '!p-0'
props:
autofocus: false
disabled: true
groups:
- id: 'apps'
items:
- label: 'Calendar'
icon: 'i-lucide-calendar'
- label: 'Music'
icon: 'i-lucide-music'
- label: 'Maps'
icon: 'i-lucide-map'
class: 'flex-1'
---
::
### Close
Use the `close` prop to display a [Button](/components/button) to dismiss the CommandPalette.
@@ -468,6 +565,124 @@ You can customize this icon globally in your `vite.config.ts` under `ui.icons.cl
:::
::
### Back :badge{label="Soon" class="align-text-top"}
Use the `back` prop to customize or hide the back button (with `false` value) displayed when navigating into a submenu.
You can pass any property from the [Button](/components/button) component to customize it.
::component-code
---
collapse: true
prettier: true
hide:
- autofocus
ignore:
- back.color
- groups
- class
external:
- groups
class: '!p-0'
props:
autofocus: false
back:
color: primary
groups:
- id: 'actions'
items:
- label: 'Share'
icon: 'i-lucide-share'
children:
- label: 'Email'
icon: 'i-lucide-mail'
- label: 'Copy'
icon: 'i-lucide-copy'
- label: 'Link'
icon: 'i-lucide-link'
class: 'flex-1'
---
::
### Back Icon :badge{label="Soon" class="align-text-top"}
Use the `back-icon` prop to customize the back button [Icon](/components/icon). Defaults to `i-lucide-arrow-left`.
::component-code
---
collapse: true
hide:
- autofocus
ignore:
- class
- groups
- back
external:
- groups
class: '!p-0'
props:
autofocus: false
back: true
backIcon: 'i-lucide-house'
groups:
- id: 'actions'
items:
- label: 'Share'
icon: 'i-lucide-share'
children:
- label: 'Email'
icon: 'i-lucide-mail'
- label: 'Copy'
icon: 'i-lucide-copy'
- label: 'Link'
icon: 'i-lucide-link'
class: 'flex-1'
---
::
::framework-only
#nuxt
:::tip{to="/getting-started/icons/nuxt#theme"}
You can customize this icon globally in your `app.config.ts` under `ui.icons.arrowLeft` key.
:::
#vue
:::tip{to="/getting-started/icons/vue#theme"}
You can customize this icon globally in your `vite.config.ts` under `ui.icons.arrowLeft` key.
:::
::
### Disabled
Use the `disabled` prop to disable the CommandPalette.
::component-code
---
collapse: true
hide:
- autofocus
ignore:
- groups
- class
external:
- groups
class: '!p-0'
props:
autofocus: false
disabled: true
groups:
- id: 'apps'
items:
- label: 'Calendar'
icon: 'i-lucide-calendar'
- label: 'Music'
icon: 'i-lucide-music'
- label: 'Maps'
icon: 'i-lucide-map'
class: 'flex-1'
---
::
## Examples
### Control selected item(s)
@@ -502,6 +717,28 @@ props:
This example uses the `@update:model-value` event to reset the search term when an item is selected.
::
### With children in items :badge{label="Soon" class="align-text-top"}
You can create hierarchical menus by using the `children` property in items. When an item has children, it will automatically display a chevron icon and enable navigation into a submenu.
::component-example
---
collapse: true
prettier: true
name: 'command-palette-items-children-example'
class: '!p-0'
props:
autofocus: false
---
::
::note
When navigating into a submenu:
- The search term is reset
- A back button appears in the input
- You can go back to the previous group by pressing the :kbd{value="backspace"} key
::
### With fetched items
You can fetch items from an API and use them in the CommandPalette.
@@ -658,6 +895,7 @@ You will have access to the following slots:
::component-example
---
collapse: true
name: 'command-palette-custom-slot-example'
class: '!p-0'
props:

View File

@@ -135,7 +135,7 @@ props:
### Multiple
Use the `multiple` prop to allow multiple selections, the selected items will be displayed as badges.
Use the `multiple` prop to allow multiple selections, the selected items will be displayed as tags.
::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 badges. Defaults to `i-lucide-x`.
With `multiple`, use the `delete-icon` prop to customize the delete [Icon](/components/icon) in the tags. Defaults to `i-lucide-x`.
::component-code
---
@@ -782,6 +782,14 @@ name: 'input-menu-countries-example'
:component-emits
### Expose
When accessing the component via a template ref, you can use the following:
| Name | Type |
| ---- | ---- |
| `inputRef`{lang="ts-type"} | `Ref<InstanceType<typeof ComboboxTrigger> \| null>`{lang="ts-type"} |
## Theme
:component-theme

View File

@@ -1,6 +1,6 @@
---
title: InputNumber
description: Input numerical values with a customizable range.
description: An input for numerical values with a customizable range.
category: form
links:
- label: NumberField
@@ -287,8 +287,8 @@ name: 'input-number-slots-example'
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"} |
| -------------------------- | ----------------------------------------------- |
| `inputRef`{lang="ts-type"} | `Ref<InstanceType<typeof NumberFieldInput> \| null>`{lang="ts-type"} |
## Theme

View File

@@ -0,0 +1,285 @@
---
title: InputTags
description: An input element that displays interactive tags.
category: form
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<InstanceType<typeof TagsInputInput> \| null>`{lang="ts-type"} |
## Theme
:component-theme

View File

@@ -278,6 +278,16 @@ 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 libraries 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

@@ -180,6 +180,8 @@ props:
:component-emits
### Expose
When accessing the component via a template ref, you can use the following:
| Name | Type |

View File

@@ -815,6 +815,14 @@ name: 'select-menu-countries-example'
:component-emits
### Expose
When accessing the component via a template ref, you can use the following:
| Name | Type |
| ---- | ---- |
| `triggerRef`{lang="ts-type"} | `Ref<InstanceType<typeof ComboboxTrigger> \| null>`{lang="ts-type"} |
## Theme
:component-theme

View File

@@ -709,6 +709,14 @@ collapse: true
:component-emits
### Expose
When accessing the component via a template ref, you can use the following:
| Name | Type |
| ---- | ---- |
| `triggerRef`{lang="ts-type"} | `Ref<InstanceType<typeof SelectTrigger> \| null>`{lang="ts-type"} |
## Theme
:component-theme

View File

@@ -242,6 +242,14 @@ You will have access to the following slots:
:component-emits
### Expose
When accessing the component via a template ref, you can use the following:
| Name | Type |
| ---- | ---- |
| `triggersRef`{lang="ts-type"} | `Ref<ComponentPublicInstance[]>`{lang="ts-type"} |
## Theme
:component-theme

View File

@@ -23,7 +23,7 @@ Use the `items` prop as an array of objects with the following properties:
- `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, separator?: ClassNameValue, date?: ClassNameValue, title?: ClassNameValue, description?: ClassNameValue }`{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
---
@@ -169,6 +169,47 @@ props:
description: 'QA testing and performance optimization.'
icon: 'i-lucide-check-circle'
class: 'w-full'
class: 'overflow-x-auto'
---
::
### Reverse
Use the reverse prop to reverse the direction of the Timeline.
::component-code
---
ignore:
- items
- class
- defaultValue
external:
- items
externalTypes:
- TimelineItem[]
props:
reverse: true
modelValue: 2
orientation: 'vertical'
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'
---
::

View File

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

View File

@@ -2,41 +2,48 @@
"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.44",
"@iconify-json/simple-icons": "^1.2.35",
"@iconify-json/vscode-icons": "^1.2.21",
"@iconify-json/lucide": "^1.2.47",
"@iconify-json/simple-icons": "^1.2.38",
"@iconify-json/vscode-icons": "^1.2.22",
"@nuxt/content": "^3.5.1",
"@nuxt/image": "^1.10.0",
"@nuxt/ui": "latest",
"@nuxt/ui-pro": "https://pkg.pr.new/@nuxt/ui-pro@f06b49c",
"@nuxthub/core": "^0.8.27",
"@nuxt/ui": "workspace:*",
"@nuxt/ui-pro": "https://pkg.pr.new/@nuxt/ui-pro@beebbd4",
"@nuxthub/core": "^0.9.0",
"@nuxtjs/plausible": "^1.2.0",
"@octokit/rest": "^22.0.0",
"@rollup/plugin-yaml": "^4.1.2",
"@vueuse/integrations": "^13.2.0",
"@vueuse/nuxt": "^13.2.0",
"@vueuse/integrations": "^13.3.0",
"@vueuse/nuxt": "^13.3.0",
"ai": "^4.3.16",
"capture-website": "^4.2.0",
"joi": "^17.13.3",
"motion-v": "^1.1.1",
"nuxt": "^3.17.4",
"maska": "^3.1.1",
"motion-v": "^1.2.1",
"nuxt": "^3.17.5",
"nuxt-component-meta": "^0.11.0",
"nuxt-llms": "^0.1.2",
"nuxt-og-image": "^5.1.4",
"nuxt-llms": "^0.1.3",
"nuxt-og-image": "^5.1.6",
"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.5.2",
"workers-ai-provider": "^0.6.0",
"yup": "^1.6.1",
"zod": "^3.25.28"
"zod": "^3.25.57"
},
"devDependencies": {
"wrangler": "^4.16.1"
"wrangler": "^4.19.1"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

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

View File

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

View File

@@ -40,6 +40,7 @@ const components = [
'input',
'input-menu',
'input-number',
'input-tags',
'kbd',
'link',
'modal',

View File

@@ -40,6 +40,7 @@ const components = [
'input',
'input-menu',
'input-number',
'input-tags',
'kbd',
'link',
'modal',

View File

@@ -74,6 +74,51 @@ const groups = computed(() => [{
toast.add({ title: 'Label added!' })
},
kbds: ['meta', 'L']
}, {
label: 'More actions',
placeholder: 'Search actions...',
children: [{
label: 'Create new file',
suffix: 'Create a new file in the current directory or workspace.',
icon: 'i-lucide-file-plus',
onSelect(e: Event) {
e.preventDefault()
toast.add({ title: 'New file added!' })
}
}, {
label: 'Create new folder',
suffix: 'Create a new folder in the current directory or workspace.',
icon: 'i-lucide-folder-plus',
onSelect(e: Event) {
e.preventDefault()
toast.add({ title: 'New folder added!' })
}
}, {
label: 'Share',
placeholder: 'Search share options...',
icon: 'i-lucide-share',
children: [{
label: 'Share with everyone',
suffix: 'Share with everyone in the current directory or workspace.',
icon: 'i-lucide-share',
onSelect(e: Event) {
e.preventDefault()
toast.add({ title: 'Shared with everyone!' })
}
}, {
label: 'Share with team',
suffix: 'Share with the team in the current directory or workspace.',
icon: 'i-lucide-users',
onSelect(e: Event) {
e.preventDefault()
toast.add({ title: 'Shared with team!' })
}
}]
}]
}]
}])

View File

@@ -5,7 +5,9 @@ const sizes = Object.keys(theme.variants.size) as Array<keyof typeof theme.varia
const feedbacks = [
{ description: 'This is a description' },
{ error: true },
{ error: 'This is an error' },
{ errors: ['This is an error', 'This is another error', 'This one is not visible'], maxErrors: 2 },
{ hint: 'This is a hint' },
{ help: 'Help! I need somebody!' },
{ required: true }
@@ -14,7 +16,7 @@ const feedbacks = [
<template>
<div class="flex flex-col items-center gap-4">
<div class="flex flex-col gap-4 ms-[-38px]">
<div class="flex flex-col gap-4 ms-[-92px]">
<div v-for="(feedback, count) in feedbacks" :key="count" class="flex items-center">
<UFormField v-bind="feedback" label="Email" name="email">
<UInput placeholder="john@lennon.com" />

View File

@@ -145,5 +145,18 @@ 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

@@ -0,0 +1,87 @@
<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

@@ -9,6 +9,7 @@ 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 reverse = ref(false)
const items = [{
date: 'Mar 15, 2025',
@@ -46,6 +47,7 @@ const value = ref('kickoff')
<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" />
<USelect v-model="reverse" :items="[true, false]" placeholder="Reverse" />
</div>
<UTimeline
@@ -54,6 +56,7 @@ const value = ref('kickoff')
:orientation="orientation"
:size="size"
:items="items"
:reverse="reverse"
class="data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-96"
/>
</div>

View File

@@ -3,18 +3,19 @@
"name": "@nuxt/ui-playground",
"type": "module",
"scripts": {
"dev": "nuxi dev",
"build": "nuxi build",
"generate": "nuxi generate",
"dev": "nuxt dev",
"build": "nuxt build",
"generate": "nuxt generate",
"typecheck": "nuxt typecheck"
},
"dependencies": {
"@iconify-json/lucide": "^1.2.44",
"@iconify-json/simple-icons": "^1.2.35",
"@nuxt/ui": "latest",
"@nuxthub/core": "^0.8.27",
"nuxt": "^3.17.4",
"zod": "^3.25.28"
"@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"
},
"devDependencies": {
"typescript": "^5.8.3",

2236
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -26,13 +26,18 @@ export interface CommandPaletteItem extends Omit<LinkProps, 'type' | 'raw' | 'cu
loading?: boolean
disabled?: boolean
slot?: string
/**
* The placeholder to display when the item has children.
*/
placeholder?: string
children?: CommandPaletteItem[]
onSelect?(e?: Event): void
class?: any
ui?: Pick<CommandPalette['slots'], 'item' | 'itemLeadingIcon' | 'itemLeadingAvatarSize' | 'itemLeadingAvatar' | 'itemLeadingChipSize' | 'itemLeadingChip' | 'itemLabel' | 'itemLabelPrefix' | 'itemLabelBase' | 'itemLabelSuffix' | 'itemTrailing' | 'itemTrailingKbds' | 'itemTrailingKbdsSize' | 'itemTrailingHighlightedIcon' | 'itemTrailingIcon'>
[key: string]: any
}
export interface CommandPaletteGroup<T> {
export interface CommandPaletteGroup<T extends CommandPaletteItem = CommandPaletteItem> {
id: string
label?: string
slot?: string
@@ -52,7 +57,7 @@ export interface CommandPaletteGroup<T> {
highlightedIcon?: string
}
export interface CommandPaletteProps<G, T> extends Pick<ListboxRootProps, 'multiple' | 'disabled' | 'modelValue' | 'defaultValue' | 'highlightOnHover'>, Pick<UseComponentIconsProps, 'loading' | 'loadingIcon'> {
export interface CommandPaletteProps<G extends CommandPaletteGroup<T> = CommandPaletteGroup<any>, T extends CommandPaletteItem = CommandPaletteItem> extends Pick<ListboxRootProps, 'multiple' | 'disabled' | 'modelValue' | 'defaultValue' | 'highlightOnHover' | 'selectionBehavior'>, Pick<UseComponentIconsProps, 'loading' | 'loadingIcon'> {
/**
* The element or component this component should render as.
* @defaultValue 'div'
@@ -70,6 +75,12 @@ export interface CommandPaletteProps<G, T> extends Pick<ListboxRootProps, 'multi
* @IconifyIcon
*/
selectedIcon?: string
/**
* The icon displayed when an item has children.
* @defaultValue appConfig.ui.icons.chevronRight
* @IconifyIcon
*/
trailingIcon?: string
/**
* The placeholder text for the input.
* @defaultValue t('commandPalette.placeholder')
@@ -93,6 +104,18 @@ export interface CommandPaletteProps<G, T> extends Pick<ListboxRootProps, 'multi
* @IconifyIcon
*/
closeIcon?: string
/**
* Display a button to navigate back in history.
* `{ size: 'md', color: 'neutral', variant: 'link' }`{lang="ts-type"}
* @defaultValue true
*/
back?: boolean | ButtonProps
/**
* The icon displayed in the back button.
* @defaultValue appConfig.ui.icons.arrowLeft
* @IconifyIcon
*/
backIcon?: string
groups?: G[]
/**
* Options for [useFuse](https://vueuse.org/integrations/useFuse).
@@ -116,14 +139,15 @@ export interface CommandPaletteProps<G, T> extends Pick<ListboxRootProps, 'multi
ui?: CommandPalette['slots']
}
export type CommandPaletteEmits<T> = ListboxRootEmits<T> & {
export type CommandPaletteEmits<T extends CommandPaletteItem = CommandPaletteItem> = ListboxRootEmits<T> & {
'update:open': [value: boolean]
}
type SlotProps<T> = (props: { item: T, index: number }) => any
export type CommandPaletteSlots<G extends { slot?: string }, T extends { slot?: string }> = {
export type CommandPaletteSlots<G extends CommandPaletteGroup<T> = CommandPaletteGroup<any>, T extends CommandPaletteItem = CommandPaletteItem> = {
'empty'(props: { searchTerm?: string }): any
'back'(props: { ui: { [K in keyof Required<CommandPalette['slots']>]: (props?: Record<string, any>) => string } }): any
'close'(props: { ui: { [K in keyof Required<CommandPalette['slots']>]: (props?: Record<string, any>) => string } }): any
'item': SlotProps<T>
'item-leading': SlotProps<T>
@@ -134,7 +158,7 @@ export type CommandPaletteSlots<G extends { slot?: string }, T extends { slot?:
</script>
<script setup lang="ts" generic="G extends CommandPaletteGroup<T>, T extends CommandPaletteItem">
import { computed } from 'vue'
import { computed, ref, useTemplateRef } from 'vue'
import { ListboxRoot, ListboxFilter, ListboxContent, ListboxGroup, ListboxGroupLabel, ListboxItem, ListboxItemIndicator, useForwardProps, useForwardPropsEmits } from 'reka-ui'
import { defu } from 'defu'
import { reactivePick } from '@vueuse/core'
@@ -157,7 +181,8 @@ import UKbd from './Kbd.vue'
const props = withDefaults(defineProps<CommandPaletteProps<G, T>>(), {
modelValue: '',
labelKey: 'label',
autofocus: true
autofocus: true,
back: true
})
const emits = defineEmits<CommandPaletteEmits<T>>()
const slots = defineSlots<CommandPaletteSlots<G, T>>()
@@ -167,7 +192,7 @@ const searchTerm = defineModel<string>('searchTerm', { default: '' })
const { t } = useLocale()
const appConfig = useAppConfig() as CommandPalette['AppConfig']
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'disabled', 'multiple', 'modelValue', 'defaultValue', 'highlightOnHover'), emits)
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'disabled', 'multiple', 'modelValue', 'defaultValue', 'highlightOnHover', 'selectionBehavior'), emits)
const inputProps = useForwardProps(reactivePick(props, 'loading', 'loadingIcon'))
// eslint-disable-next-line vue/no-dupe-keys
@@ -183,18 +208,22 @@ const fuse = computed(() => defu({}, props.fuse, {
matchAllWhenSearchEmpty: true
}))
const items = computed(() => props.groups?.filter((group) => {
const history = ref<(CommandPaletteGroup & { placeholder?: string })[]>([])
const placeholder = computed(() => history.value[history.value.length - 1]?.placeholder || props.placeholder || t('commandPalette.placeholder'))
const groups = computed(() => history.value?.length ? [history.value[history.value.length - 1] as G] : props.groups)
const items = computed(() => groups.value?.filter((group) => {
if (!group.id) {
console.warn(`[@nuxt/ui] CommandPalette group is missing an \`id\` property`)
return false
}
if (group.ignoreFilter) {
return false
}
return true
}).flatMap(group => group.items?.map(item => ({ ...item, group: group.id })) || []) || [])
})?.flatMap(group => group.items?.map(item => ({ ...item, group: group.id })) || []) || [])
const { results: fuseResults } = useFuse<typeof items.value[number]>(searchTerm, items, fuse)
@@ -215,7 +244,7 @@ function getGroupWithItems(group: G, items: (T & { matches?: FuseResult<T>['matc
}
}
const groups = computed(() => {
const filteredGroups = computed(() => {
const groupsById = fuseResults.value.reduce((acc, result) => {
const { item, matches } = result
if (!item.group) {
@@ -229,7 +258,7 @@ const groups = computed(() => {
}, {} as Record<string, (T & { matches?: FuseResult<T>['matches'] })[]>)
const fuseGroups = Object.entries(groupsById).map(([id, items]) => {
const group = props.groups?.find(group => group.id === id)
const group = groups.value?.find(group => group.id === id)
if (!group) {
return
}
@@ -237,7 +266,7 @@ const groups = computed(() => {
return getGroupWithItems(group, items)
}).filter(group => !!group)
const nonFuseGroups = props.groups
const nonFuseGroups = groups.value
?.map((group, index) => ({ ...group, index }))
?.filter(group => group.ignoreFilter && group.items?.length)
?.map(group => ({ ...getGroupWithItems(group, group.items || []), index: group.index })) || []
@@ -247,20 +276,84 @@ const groups = computed(() => {
return acc
}, [...fuseGroups])
})
const listboxRootRef = useTemplateRef('listboxRootRef')
function navigate(item: T) {
if (!item.children?.length) {
return
}
history.value.push({
id: `history-${history.value.length}`,
label: item.label,
slot: item.slot,
placeholder: item.placeholder,
items: item.children
} as any)
searchTerm.value = ''
listboxRootRef.value?.highlightFirstItem()
}
function navigateBack() {
if (!history.value.length) {
return
}
history.value.pop()
searchTerm.value = ''
listboxRootRef.value?.highlightFirstItem()
}
function onBackspace() {
if (!searchTerm.value) {
navigateBack()
}
}
function onSelect(e: Event, item: T) {
if (item.children?.length) {
e.preventDefault()
navigate(item)
} else {
item.onSelect?.(e)
}
}
</script>
<!-- eslint-disable vue/no-v-html -->
<template>
<ListboxRoot v-bind="rootProps" :class="ui.root({ class: [props.ui?.root, props.class] })">
<ListboxRoot v-bind="rootProps" ref="listboxRootRef" :class="ui.root({ class: [props.ui?.root, props.class] })">
<ListboxFilter v-model="searchTerm" as-child>
<UInput
:placeholder="placeholder || t('commandPalette.placeholder')"
:placeholder="placeholder"
variant="none"
:autofocus="autofocus"
v-bind="inputProps"
:icon="icon || appConfig.ui.icons.search"
:class="ui.input({ class: props.ui?.input })"
@keydown.backspace="onBackspace"
>
<template v-if="history?.length && (back || !!slots.back)" #leading>
<slot name="back" :ui="ui">
<UButton
:icon="backIcon || appConfig.ui.icons.arrowLeft"
size="md"
color="neutral"
variant="link"
:aria-label="t('commandPalette.back')"
v-bind="(typeof back === 'object' ? back as Partial<ButtonProps> : {})"
:class="ui.back({ class: props.ui?.back })"
@click="navigateBack"
/>
</slot>
</template>
<template v-if="close || !!slots.close" #trailing>
<slot name="close" :ui="ui">
<UButton
@@ -280,8 +373,8 @@ const groups = computed(() => {
</ListboxFilter>
<ListboxContent :class="ui.content({ class: props.ui?.content })">
<div v-if="groups?.length" role="presentation" :class="ui.viewport({ class: props.ui?.viewport })">
<ListboxGroup v-for="group in groups" :key="`group-${group.id}`" :class="ui.group({ class: props.ui?.group })">
<div v-if="filteredGroups?.length" role="presentation" :class="ui.viewport({ class: props.ui?.viewport })">
<ListboxGroup v-for="group in filteredGroups" :key="`group-${group.id}`" :class="ui.group({ class: props.ui?.group })">
<ListboxGroupLabel v-if="get(group, props.labelKey as string)" :class="ui.label({ class: props.ui?.label })">
{{ get(group, props.labelKey as string) }}
</ListboxGroupLabel>
@@ -289,10 +382,10 @@ const groups = computed(() => {
<ListboxItem
v-for="(item, index) in group.items"
:key="`group-${group.id}-${index}`"
:value="omit(item, ['matches' as any, 'group' as any, 'onSelect', 'labelHtml', 'suffixHtml'])"
:value="omit(item, ['matches' as any, 'group' as any, 'onSelect', 'labelHtml', 'suffixHtml', 'children'])"
:disabled="item.disabled"
as-child
@select="item.onSelect"
@select="onSelect($event, item)"
>
<ULink v-slot="{ active, ...slotProps }" v-bind="pickLinkProps(item)" custom>
<ULinkBase v-bind="slotProps" :class="ui.item({ class: [props.ui?.item, item.ui?.item, item.class], active: active || item.active })">
@@ -323,13 +416,20 @@ const groups = computed(() => {
<span :class="ui.itemTrailing({ class: [props.ui?.itemTrailing, item.ui?.itemTrailing] })">
<slot :name="((item.slot ? `${item.slot}-trailing` : group.slot ? `${group.slot}-trailing` : `item-trailing`) as keyof CommandPaletteSlots<G, T>)" :item="(item as any)" :index="index">
<span v-if="item.kbds?.length" :class="ui.itemTrailingKbds({ class: [props.ui?.itemTrailingKbds, item.ui?.itemTrailingKbds] })">
<UIcon
v-if="item.children && item.children.length > 0"
:name="trailingIcon || appConfig.ui.icons.chevronRight"
:class="ui.itemTrailingIcon({ class: [props.ui?.itemTrailingIcon, item.ui?.itemTrailingIcon] })"
/>
<span v-else-if="item.kbds?.length" :class="ui.itemTrailingKbds({ class: [props.ui?.itemTrailingKbds, item.ui?.itemTrailingKbds] })">
<UKbd v-for="(kbd, kbdIndex) in item.kbds" :key="kbdIndex" :size="((item.ui?.itemTrailingKbdsSize || props.ui?.itemTrailingKbdsSize || ui.itemTrailingKbdsSize()) as KbdProps['size'])" v-bind="typeof kbd === 'string' ? { value: kbd } : kbd" />
</span>
<UIcon v-else-if="group.highlightedIcon" :name="group.highlightedIcon" :class="ui.itemTrailingHighlightedIcon({ class: [props.ui?.itemTrailingHighlightedIcon, item.ui?.itemTrailingHighlightedIcon] })" />
</slot>
<ListboxItemIndicator as-child>
<ListboxItemIndicator v-if="!item.children?.length" as-child>
<UIcon :name="selectedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: [props.ui?.itemTrailingIcon, item.ui?.itemTrailingIcon] })" />
</ListboxItemIndicator>
</span>

View File

@@ -59,7 +59,7 @@ export interface FormEmits<S extends FormSchema, T extends boolean = true> {
}
export interface FormSlots {
default(props?: { errors: FormError[] }): any
default(props?: { errors: FormError[], loading: boolean }): any
}
</script>
@@ -315,6 +315,6 @@ defineExpose<Form<S>>({
:class="ui({ class: props.class })"
@submit.prevent="onSubmitWrapper"
>
<slot :errors="errors" />
<slot :errors="errors" :loading="loading" />
</component>
</template>

View File

@@ -19,6 +19,20 @@ export interface FormFieldProps {
description?: string
help?: string
error?: string | boolean
/**
* An array of errors for this field.
* Note that only one error is displayed by default. You can use `maxErrors` to control the number of displayed errors.
* @defaultValue `1`
*/
errors?: string[]
/**
* The maximum number of errors to display. If `false` or negative, display all available errors.
* @defaultValue `1`
*/
maxErrors?: number | false
hint?: string
/**
* @defaultValue 'md'
@@ -42,7 +56,7 @@ export interface FormFieldSlots {
description(props: { description?: string }): any
help(props: { help?: string }): any
error(props: { error?: string | boolean }): any
default(props: { error?: string | boolean }): any
default(props: { error?: string | boolean, errors?: string[] }): any
}
</script>
@@ -54,7 +68,7 @@ import { formFieldInjectionKey, inputIdInjectionKey } from '../composables/useFo
import { tv } from '../utils/tv'
import type { FormError, FormFieldInjectedOptions } from '../types/form'
const props = defineProps<FormFieldProps>()
const props = withDefaults(defineProps<FormFieldProps>(), { maxErrors: 1 })
const slots = defineSlots<FormFieldSlots>()
const appConfig = useAppConfig() as FormField['AppConfig']
@@ -66,7 +80,24 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.formField ||
const formErrors = inject<Ref<FormError[]> | null>('form-errors', null)
const error = computed(() => props.error || formErrors?.value?.find(error => error.name && (error.name === props.name || (props.errorPattern && error.name.match(props.errorPattern))))?.message)
const errors = computed(() =>
(props.error && typeof props.error === 'string' ? [props.error] : props.errors)
|| formErrors?.value?.flatMap((error) => {
if (!error.name) return []
if (error.name === props.name || (props.errorPattern && error.name.match(props.errorPattern))) {
return [error.message]
}
return []
}))
const error = computed(() => errors.value?.[0] ?? props.error)
const displayedErrors = computed(() =>
props.maxErrors === false
|| (!!props.maxErrors && props.maxErrors < 0)
? errors.value
: errors.value?.slice(0, props.maxErrors)
)
const id = ref(useId())
// Copies id's initial value to bind aria-attributes such as aria-describedby.
@@ -113,9 +144,16 @@ provide(formFieldInjectionKey, computed(() => ({
</div>
<div :class="[(label || !!slots.label || description || !!slots.description) && ui.container({ class: props.ui?.container })]">
<slot :error="error" />
<slot :error="error" :errors="errors" />
<div v-if="(typeof error === 'string' && error) || !!slots.error" :id="`${ariaId}-error`" :class="ui.error({ class: props.ui?.error })">
<template v-if="(typeof error === 'string' && error)">
<div v-for="err in displayedErrors" :id="`${ariaId}-error`" :key="err" :class="ui.error({ class: props.ui?.error })">
<slot name="error" :error="err">
{{ err }}
</slot>
</div>
</template>
<div v-else-if="!!slots.error" :id="`${ariaId}-error`" :class="ui.error({ class: props.ui?.error })">
<slot name="error" :error="error">
{{ error }}
</slot>

View File

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

View File

@@ -0,0 +1,203 @@
<script lang="ts">
import type { AppConfig } from '@nuxt/schema'
import type { TagsInputRootProps, TagsInputRootEmits, 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 interface InputTagsEmits<T extends InputTagItem> extends TagsInputRootEmits<T> {
change: [event: Event]
blur: [event: FocusEvent]
focus: [event: 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'
import UAvatar from './Avatar.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

@@ -104,15 +104,15 @@ const contentEvents = computed(() => {
}
if (!props.dismissible) {
const events = ['pointerDownOutside', 'interactOutside', 'escapeKeyDown', 'closeAutoFocus'] as const
type EventType = typeof events[number]
const events = ['pointerDownOutside', 'interactOutside', 'escapeKeyDown']
return events.reduce((acc, curr) => {
acc[curr] = (e: Event) => {
e.preventDefault()
emits('close:prevent')
}
return acc
}, {} as Record<EventType, (e: Event) => void>)
}, defaultEvents as Record<typeof events[number] | keyof typeof defaultEvents, (e: Event) => void>)
}
return defaultEvents

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'] as const
type EventType = typeof events[number]
const events = ['pointerDownOutside', 'interactOutside', 'escapeKeyDown']
return events.reduce((acc, curr) => {
acc[curr] = (e: Event) => {
e.preventDefault()
emits('close:prevent')
}
return acc
}, {} as Record<EventType, (e: Event) => void>)
}, {} as Record<typeof events[number], (e: Event) => void>)
}
return {}

View File

@@ -92,6 +92,8 @@ 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']
}
@@ -134,7 +136,7 @@ export interface SelectSlots<
</script>
<script setup lang="ts" generic="T extends ArrayOrNested<SelectItem>, VK extends GetItemKeys<T> = 'value', M extends boolean = false">
import { computed, toRef } from 'vue'
import { ref, computed, onMounted, 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'
@@ -154,7 +156,8 @@ defineOptions({ inheritAttrs: false })
const props = withDefaults(defineProps<SelectProps<T, VK, M>>(), {
valueKey: 'value' as never,
labelKey: 'label' as never,
portal: true
portal: true,
autofocusDelay: 0
})
const emits = defineEmits<SelectEmits<T, VK, M>>()
const slots = defineSlots<SelectSlots<T, VK, M>>()
@@ -203,6 +206,22 @@ function displayValue(value: GetItemValue<T, VK> | GetItemValue<T, VK>[]): strin
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 } })
@@ -226,6 +245,10 @@ function onUpdateOpen(value: boolean) {
function isSelectItem(item: SelectItem): item is SelectItemBase {
return typeof item === 'object' && item !== null
}
defineExpose({
triggerRef
})
</script>
<!-- eslint-disable vue/no-template-shadow -->
@@ -241,7 +264,7 @@ function isSelectItem(item: SelectItem): item is SelectItemBase {
@update:model-value="onUpdate"
@update:open="onUpdateOpen"
>
<SelectTrigger :id="id" :class="ui.base({ class: [props.ui?.base, props.class] })" v-bind="{ ...$attrs, ...ariaAttrs }">
<SelectTrigger :id="id" ref="triggerRef" :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 })" />

View File

@@ -115,6 +115,8 @@ export interface SelectMenuProps<T extends ArrayOrNested<SelectMenuItem> = Array
* @defaultValue false
*/
ignoreFilter?: boolean
autofocus?: boolean
autofocusDelay?: number
class?: any
ui?: SelectMenu['slots']
}
@@ -165,7 +167,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 { computed, toRef, toRaw } from 'vue'
import { ref, computed, onMounted, 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'
@@ -189,7 +191,8 @@ const props = withDefaults(defineProps<SelectMenuProps<T, VK, M>>(), {
searchInput: true,
labelKey: 'label' as never,
resetSearchTermOnBlur: true,
resetSearchTermOnSelect: true
resetSearchTermOnSelect: true,
autofocusDelay: 0
})
const emits = defineEmits<SelectMenuEmits<T, VK, M>>()
const slots = defineSlots<SelectMenuSlots<T, VK, M>>()
@@ -287,6 +290,22 @@ 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
@@ -344,6 +363,10 @@ function onSelect(e: Event, item: SelectMenuItem) {
function isSelectItem(item: SelectMenuItem): item is _SelectMenuItem {
return typeof item === 'object' && item !== null
}
defineExpose({
triggerRef
})
</script>
<!-- eslint-disable vue/no-template-shadow -->
@@ -376,7 +399,7 @@ function isSelectItem(item: SelectMenuItem): item is _SelectMenuItem {
@update:open="onUpdateOpen"
>
<ComboboxAnchor as-child>
<ComboboxTrigger :class="ui.base({ class: [props.ui?.base, props.class] })" tabindex="0">
<ComboboxTrigger ref="triggerRef" :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 })" />

View File

@@ -103,16 +103,17 @@ const contentEvents = computed(() => {
const defaultEvents = {
closeAutoFocus: (e: Event) => e.preventDefault()
}
if (!props.dismissible) {
const events = ['pointerDownOutside', 'interactOutside', 'escapeKeyDown', 'closeAutoFocus'] as const
type EventType = typeof events[number]
const events = ['pointerDownOutside', 'interactOutside', 'escapeKeyDown']
return events.reduce((acc, curr) => {
acc[curr] = (e: Event) => {
e.preventDefault()
emits('close:prevent')
}
return acc
}, {} as Record<EventType, (e: Event) => void>)
}, defaultEvents as Record<typeof events[number] | keyof typeof defaultEvents, (e: Event) => void>)
}
return defaultEvents

View File

@@ -61,13 +61,13 @@ export type TableRow<T> = Row<T>
export type TableData = RowData
export type TableColumn<T extends TableData, D = unknown> = ColumnDef<T, D>
export interface TableOptions<T extends TableData> extends Omit<CoreOptions<T>, 'data' | 'columns' | 'getCoreRowModel' | 'state' | 'onStateChange' | 'renderFallbackValue'> {
export interface TableOptions<T extends TableData = TableData> extends Omit<CoreOptions<T>, 'data' | 'columns' | 'getCoreRowModel' | 'state' | 'onStateChange' | 'renderFallbackValue'> {
state?: CoreOptions<T>['state']
onStateChange?: CoreOptions<T>['onStateChange']
renderFallbackValue?: CoreOptions<T>['renderFallbackValue']
}
export interface TableProps<T extends TableData> extends TableOptions<T> {
export interface TableProps<T extends TableData = TableData> extends TableOptions<T> {
/**
* The element or component this component should render as.
* @defaultValue 'div'
@@ -172,7 +172,7 @@ export interface TableProps<T extends TableData> extends TableOptions<T> {
type DynamicHeaderSlots<T, K = keyof T> = Record<string, (props: HeaderContext<T, unknown>) => any> & Record<`${K extends string ? K : never}-header`, (props: HeaderContext<T, unknown>) => any>
type DynamicCellSlots<T, K = keyof T> = Record<string, (props: CellContext<T, unknown>) => any> & Record<`${K extends string ? K : never}-cell`, (props: CellContext<T, unknown>) => any>
export type TableSlots<T> = {
export type TableSlots<T extends TableData = TableData> = {
expanded: (props: { row: Row<T> }) => any
empty: (props?: {}) => any
loading: (props?: {}) => any
@@ -226,7 +226,7 @@ const groupingState = defineModel<GroupingState>('grouping', { default: [] })
const expandedState = defineModel<ExpandedState>('expanded', { default: {} })
const paginationState = defineModel<PaginationState>('pagination', { default: {} })
const tableRef = ref<HTMLTableElement>()
const tableRef = ref<HTMLTableElement | null>(null)
const tableApi = useVueTable({
...reactiveOmit(props, 'as', 'data', 'columns', 'caption', 'sticky', 'loading', 'loadingColor', 'loadingAnimation', 'class', 'ui'),

View File

@@ -79,7 +79,8 @@ export type TabsSlots<T extends TabsItem = TabsItem> = {
</script>
<script setup lang="ts" generic="T extends TabsItem">
import { computed } from 'vue'
import type { ComponentPublicInstance } from 'vue'
import { ref, computed } from 'vue'
import { TabsRoot, TabsList, TabsIndicator, TabsTrigger, TabsContent, useForwardPropsEmits } from 'reka-ui'
import { reactivePick } from '@vueuse/core'
import { useAppConfig } from '#imports'
@@ -108,6 +109,12 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.tabs || {})
size: props.size,
orientation: props.orientation
}))
const triggersRef = ref<ComponentPublicInstance[]>([])
defineExpose({
triggersRef
})
</script>
<template>
@@ -117,7 +124,14 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.tabs || {})
<slot name="list-leading" />
<TabsTrigger v-for="(item, index) of items" :key="index" :value="item.value || String(index)" :disabled="item.disabled" :class="ui.trigger({ class: [props.ui?.trigger, item.ui?.trigger] })">
<TabsTrigger
v-for="(item, index) of items"
:key="index"
:ref="el => (triggersRef[index] = el as ComponentPublicInstance)"
:value="item.value || String(index)"
:disabled="item.disabled"
:class="ui.trigger({ class: [props.ui?.trigger, item.ui?.trigger] })"
>
<slot name="leading" :item="item" :index="index">
<UIcon v-if="item.icon" :name="item.icon" :class="ui.leadingIcon({ class: [props.ui?.leadingIcon, item.ui?.leadingIcon] })" />
<UAvatar v-else-if="item.avatar" :size="((props.ui?.leadingAvatarSize || ui.leadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.leadingAvatar({ class: [props.ui?.leadingAvatar, item.ui?.leadingAvatar] })" />

View File

@@ -41,6 +41,7 @@ export interface TimelineProps<T extends TimelineItem = TimelineItem> {
*/
orientation?: Timeline['variants']['orientation']
defaultValue?: string | number
reverse?: boolean
class?: any
ui?: Timeline['slots']
}
@@ -75,16 +76,34 @@ 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
color: props.color,
reverse: props.reverse
}))
const currentStepIndex = computed(() => {
const value = modelValue.value ?? props.defaultValue
return ((typeof value === 'string')
? props.items.findIndex(item => item.value === value)
: value) ?? -1
if (typeof value === 'string') {
return props.items.findIndex(item => item.value === value) ?? -1
}
if (props.reverse) {
return value != null ? props.items.length - 1 - value : -1
} else {
return value ?? -1
}
})
function getItemState(index: number): 'active' | 'completed' | undefined {
if (currentStepIndex.value === -1) return undefined
if (index === currentStepIndex.value) return 'active'
if (props.reverse) {
return index > currentStepIndex.value ? 'completed' : undefined
} else {
return index < currentStepIndex.value ? 'completed' : undefined
}
}
</script>
<template>
@@ -93,7 +112,7 @@ const currentStepIndex = computed(() => {
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"
:data-state="getItemState(index)"
>
<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' }">

View File

@@ -69,7 +69,7 @@ export interface ToastSlots {
</script>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ref, computed, onMounted, nextTick } 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
}
setTimeout(() => {
height.value = el.value.$el.getBoundingClientRect()?.height
}, 0)
nextTick(() => {
height.value = el.value?.$el?.getBoundingClientRect()?.height
})
})
defineExpose({

View File

@@ -1,5 +1,6 @@
import { defu } from 'defu'
import type { Locale, Direction } from '../types/locale'
import type { DeepPartial } from '../types/utils'
interface DefineLocaleOptions<M> {
name: string
@@ -12,3 +13,8 @@ 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.match(chainedShortcutRegex)?.length) {
if (key.includes('-') && key !== '-' && !key.includes('_') && !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 !== '-'
const chained = key.includes('-') && key !== '-' && !key.includes('_')
if (chained) {
shortcut = {
key: key.toLowerCase(),

View File

@@ -17,6 +17,7 @@ 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>
@@ -26,7 +27,6 @@ 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: props, defaultOpen, destroyOnClose } = _options || {}
const { props, defaultOpen, destroyOnClose } = _options || {}
const options = reactive<Overlay>({
id: Symbol(import.meta.dev ? 'useOverlay' : ''),
@@ -45,7 +45,8 @@ function _useOverlay() {
component: markRaw(component!),
isMounted: !!defaultOpen,
destroyOnClose: !!destroyOnClose,
props: props || {}
originalProps: props || {},
props: { ...(props || {}) }
})
overlays.push(options)
@@ -64,6 +65,8 @@ 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
@@ -107,9 +110,7 @@ function _useOverlay() {
const patch = <T extends Component>(id: symbol, props: Partial<ComponentProps<T>>): void => {
const overlay = getOverlay(id)
Object.entries(props!).forEach(([key, value]) => {
(overlay.props as any)[key] = value
})
overlay.props = { ...props }
}
const getOverlay = (id: symbol): Overlay => {

View File

@@ -25,7 +25,8 @@ export default defineLocale<Messages>({
placeholder: 'اكتب أمرًا أو ابحث...',
noMatch: 'لا توجد نتائج مطابقة',
noData: 'لا توجد بيانات',
close: 'إغلاق'
close: 'إغلاق',
back: 'رجوع'
},
selectMenu: {
noMatch: 'لا توجد نتائج مطابقة',

View File

@@ -24,7 +24,8 @@ export default defineLocale<Messages>({
placeholder: 'Əmr daxil edin və ya axtarın...',
noMatch: 'Uyğun məlumat tapılmadı',
noData: 'Məlumat yoxdur',
close: 'Bağla'
close: 'Bağla',
back: 'Geri'
},
selectMenu: {
noMatch: 'Uyğun məlumat tapılmadı',

View File

@@ -24,7 +24,8 @@ export default defineLocale<Messages>({
placeholder: 'Въведете команда или потърсете...',
noMatch: 'Няма съвпадение на данни',
noData: 'Няма данни',
close: 'Затворете'
close: 'Затворете',
back: 'Назад'
},
selectMenu: {
noMatch: 'Няма съвпадение на данни',

View File

@@ -24,7 +24,8 @@ export default defineLocale<Messages>({
placeholder: 'কমান্ড টাইপ করুন বা অনুসন্ধান করুন...',
noMatch: 'কোন মিল পাওয়া যায়নি',
noData: 'কোন তথ্য নেই',
close: 'বন্ধ করুন'
close: 'বন্ধ করুন',
back: 'পেছনে'
},
selectMenu: {
noMatch: 'কোন মিল পাওয়া যায়নি',

View File

@@ -24,7 +24,8 @@ export default defineLocale<Messages>({
placeholder: 'Escriu una ordre o cerca...',
noMatch: 'No hi ha dades coincidents',
noData: 'Sense dades',
close: 'Tancar'
close: 'Tancar',
back: 'Enrere'
},
selectMenu: {
noMatch: 'No hi ha dades coincidents',

View File

@@ -25,7 +25,8 @@ export default defineLocale<Messages>({
placeholder: 'فەرمانێک بنووسە یان بگەڕێ...',
noMatch: 'هیچ ئەنجامێک نەدۆزرایەوە',
noData: 'هیچ داتایەک نییە',
close: 'داخستن'
close: 'داخستن',
back: 'گەڕانەوە'
},
selectMenu: {
noMatch: 'هیچ ئەنجامێک نەدۆزرایەوە',

View File

@@ -24,7 +24,8 @@ export default defineLocale<Messages>({
placeholder: 'Zadejte příkaz nebo hledejte...',
noMatch: 'Žádná shoda',
noData: 'Žádná data',
close: 'Zavřít'
close: 'Zavřít',
back: 'Zpět'
},
selectMenu: {
noMatch: 'Žádná shoda',

View File

@@ -24,7 +24,8 @@ export default defineLocale<Messages>({
placeholder: 'Skriv en kommando eller søg...',
noMatch: 'Ingen matchende data',
noData: 'Ingen data',
close: 'Luk'
close: 'Luk',
back: 'Tilbage'
},
selectMenu: {
noMatch: 'Ingen matchende data',

View File

@@ -24,7 +24,8 @@ export default defineLocale<Messages>({
placeholder: 'Geben Sie einen Befehl ein oder suchen Sie...',
noMatch: 'Nichts gefunden',
noData: 'Keine Daten',
close: 'Schließen'
close: 'Schließen',
back: 'Zurück'
},
selectMenu: {
noMatch: 'Nichts gefunden',

View File

@@ -24,7 +24,8 @@ export default defineLocale<Messages>({
placeholder: 'Πληκτρολογήστε μια εντολή ή αναζητήστε...',
noMatch: 'Δεν βρέθηκαν δεδομένα',
noData: 'Δεν υπάρχουν δεδομένα',
close: 'Κλείσιμο'
close: 'Κλείσιμο',
back: 'Πίσω'
},
selectMenu: {
noMatch: 'Δεν βρέθηκαν δεδομένα',

View File

@@ -24,7 +24,8 @@ export default defineLocale<Messages>({
placeholder: 'Type a command or search...',
noMatch: 'No matching data',
noData: 'No data',
close: 'Close'
close: 'Close',
back: 'Back'
},
selectMenu: {
noMatch: 'No matching data',

View File

@@ -24,7 +24,8 @@ export default defineLocale<Messages>({
placeholder: 'Escribe un comando o busca...',
noMatch: 'No hay datos coincidentes',
noData: 'Sin datos',
close: 'Cerrar'
close: 'Cerrar',
back: 'Atrás'
},
selectMenu: {
noMatch: 'No hay datos coincidentes',

View File

@@ -24,7 +24,8 @@ export default defineLocale<Messages>({
placeholder: 'Sisesta käsk või otsi...',
noMatch: 'Pole vastavaid andmeid',
noData: 'Pole andmeid',
close: 'Sulge'
close: 'Sulge',
back: 'Tagasi'
},
selectMenu: {
noMatch: 'Pole vastavaid andmeid',

View File

@@ -25,7 +25,8 @@ export default defineLocale<Messages>({
placeholder: 'یک دستور وارد کنید یا جستجو کنید...',
noMatch: 'داده‌ای یافت نشد',
noData: 'داده‌ای موجود نیست',
close: 'بستن'
close: 'بستن',
back: 'بازگشت'
},
selectMenu: {
noMatch: 'داده‌ای یافت نشد',

View File

@@ -24,7 +24,8 @@ export default defineLocale<Messages>({
placeholder: 'Kirjoita komento tai hae...',
noMatch: 'Ei vastaavia tietoja',
noData: 'Ei tietoja',
close: 'Sulje'
close: 'Sulje',
back: 'Takaisin'
},
selectMenu: {
noMatch: 'Ei vastaavia tietoja',

View File

@@ -24,7 +24,8 @@ export default defineLocale<Messages>({
placeholder: 'Tapez une commande ou recherchez...',
noMatch: 'Aucune donnée correspondante',
noData: 'Aucune donnée',
close: 'Fermer'
close: 'Fermer',
back: 'Retour'
},
selectMenu: {
noMatch: 'Aucune donnée correspondante',

View File

@@ -25,7 +25,8 @@ export default defineLocale<Messages>({
placeholder: 'הקלד פקודה...',
noMatch: 'לא נמצאה התאמה',
noData: 'אין נתונים זמינים',
close: 'סגור'
close: 'סגור',
back: 'חזור'
},
selectMenu: {
noMatch: 'לא נמצאה התאמה',

View File

@@ -24,7 +24,8 @@ export default defineLocale<Messages>({
placeholder: 'एक आदेश या खोज टाइप करें...',
noMatch: 'कोई मेल खाता डेटा नहीं',
noData: 'कोई डेटा नहीं',
close: 'बंद करें'
close: 'बंद करें',
back: 'वापस'
},
selectMenu: {
noMatch: 'कोई मेल खाता डेटा नहीं',

View File

@@ -24,7 +24,8 @@ export default defineLocale<Messages>({
placeholder: 'Írjon be egy parancsot vagy keressen...',
noMatch: 'Nincs találat',
noData: 'Nincs adat',
close: 'Bezárás'
close: 'Bezárás',
back: 'Vissza'
},
selectMenu: {
noMatch: 'Nincs találat',

View File

@@ -24,7 +24,8 @@ export default defineLocale<Messages>({
placeholder: 'Մուտքագրեք հրաման կամ որոնեք...',
noMatch: 'Համընկնումներ չեն գտնվել',
noData: 'Տվյալներ չկան',
close: 'Փակել'
close: 'Փակել',
back: 'Հետ'
},
selectMenu: {
noMatch: 'Համընկնումներ չեն գտնվել',

View File

@@ -24,7 +24,8 @@ export default defineLocale<Messages>({
placeholder: 'Ketik perintah atau cari...',
noMatch: 'Tidak ada data yang cocok',
noData: 'Tidak ada data',
close: 'Tutup'
close: 'Tutup',
back: 'Kembali'
},
selectMenu: {
noMatch: 'Tidak ada data yang cocok',

View File

@@ -25,6 +25,7 @@ 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

@@ -24,7 +24,8 @@ export default defineLocale<Messages>({
placeholder: 'Digita un comando o cerca...',
noMatch: 'Nessun dato corrispondente',
noData: 'Nessun dato',
close: 'Chiudi'
close: 'Chiudi',
back: 'Indietro'
},
selectMenu: {
noMatch: 'Nessun dato corrispondente',

View File

@@ -24,7 +24,8 @@ export default defineLocale<Messages>({
placeholder: 'コマンドを入力するか検索...',
noMatch: '一致するデータがありません',
noData: 'データがありません',
close: '閉じる'
close: '閉じる',
back: '戻る'
},
selectMenu: {
noMatch: '一致するデータがありません',

View File

@@ -24,7 +24,8 @@ export default defineLocale<Messages>({
placeholder: 'Команда енгізіңіз немесе іздеңіз...',
noMatch: 'Сәйкес келетін деректер жоқ',
noData: 'Деректер жоқ',
close: 'Жабу'
close: 'Жабу',
back: 'Артқа'
},
selectMenu: {
noMatch: 'Сәйкес келетін деректер жоқ',

View File

@@ -24,7 +24,8 @@ export default defineLocale<Messages>({
placeholder: 'វាយពាក្យបញ្ជា ឬស្វែងរក...',
noMatch: 'មិនមានទិន្នន័យដែលត្រូវគ្នាទេ',
noData: 'មិនមានទិន្នន័យ',
close: 'បិទ'
close: 'បិទ',
back: 'ត្រឡប់'
},
selectMenu: {
noMatch: 'មិនមានទិន្នន័យដែលត្រូវគ្នាទេ',

View File

@@ -24,7 +24,8 @@ export default defineLocale<Messages>({
placeholder: '명령을 입력하거나 검색...',
noMatch: '일치하는 데이터가 없습니다.',
noData: '데이터가 없습니다.',
close: '닫기'
close: '닫기',
back: '뒤로'
},
selectMenu: {
noMatch: '일치하는 데이터가 없습니다.',

View File

@@ -24,7 +24,8 @@ export default defineLocale<Messages>({
placeholder: 'Буйрук киргизиңиз же издөө…',
noMatch: 'Эч нерсе табылган жок',
noData: 'Маалымат жок',
close: 'Жабуу'
close: 'Жабуу',
back: 'Артка'
},
selectMenu: {
noMatch: 'Сүйлөшкөн маалыматтар жок',

57
src/runtime/locale/lb.ts Normal file
View File

@@ -0,0 +1,57 @@
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',
back: 'Zréck'
},
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

@@ -24,7 +24,8 @@ export default defineLocale<Messages>({
placeholder: 'Įveskite komandą arba ieškokite...',
noMatch: 'Nėra atitinkančių duomenų',
noData: 'Nėra duomenų',
close: 'Uždaryti'
close: 'Uždaryti',
back: 'Atgal'
},
selectMenu: {
noMatch: 'Nėra atitinkančių duomenų',

View File

@@ -24,7 +24,8 @@ export default defineLocale<Messages>({
placeholder: 'Комманд бичих эсвэл хайлт хийх...',
noMatch: 'Тохирох мэдээлэл олдсонгүй',
noData: 'Мэдээлэл байхгүй',
close: 'Хаах'
close: 'Хаах',
back: 'Буцах'
},
selectMenu: {
noMatch: 'Тохирох мэдээлэл олдсонгүй',

View File

@@ -24,7 +24,8 @@ export default defineLocale<Messages>({
placeholder: 'Taip arahan atau carian...',
noMatch: 'Tiada data yang sepadan',
noData: 'Tiada data',
close: 'Tutup'
close: 'Tutup',
back: 'Kembali'
},
selectMenu: {
noMatch: 'Tiada data yang sepadan',

View File

@@ -24,7 +24,8 @@ export default defineLocale<Messages>({
placeholder: 'Skriv inn en kommando eller søk...',
noMatch: 'Ingen samsvarende data',
noData: 'Ingen data',
close: 'Lukk'
close: 'Lukk',
back: 'Tilbake'
},
selectMenu: {
noMatch: 'Ingen samsvarende data',

View File

@@ -24,7 +24,8 @@ export default defineLocale<Messages>({
placeholder: 'Typ een commando of zoek...',
noMatch: 'Geen overeenkomende gegevens',
noData: 'Geen gegevens',
close: 'Sluiten'
close: 'Sluiten',
back: 'Terug'
},
selectMenu: {
noMatch: 'Geen overeenkomende gegevens',

View File

@@ -24,7 +24,8 @@ export default defineLocale<Messages>({
placeholder: 'Wpisz polecenie lub wyszukaj...',
noMatch: 'Brak pasujących danych',
noData: 'Brak danych',
close: 'Zamknij'
close: 'Zamknij',
back: 'Wstecz'
},
selectMenu: {
noMatch: 'Brak pasujących danych',

View File

@@ -24,7 +24,8 @@ export default defineLocale<Messages>({
placeholder: 'Digite um comando ou pesquise...',
noMatch: 'Nenhum dado correspondente',
noData: 'Sem dados',
close: 'Fechar'
close: 'Fechar',
back: 'Voltar'
},
selectMenu: {
noMatch: 'Nenhum dado correspondente',

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