mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-15 12:39:35 +01:00
Compare commits
44 Commits
fix/form-e
...
feat/updat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49498d53a2 | ||
|
|
17170bb998 | ||
|
|
fa5a3752c9 | ||
|
|
8b975de35e | ||
|
|
fc9711223b | ||
|
|
8a8b1ee2e1 | ||
|
|
30218f1b5b | ||
|
|
3584a3328b | ||
|
|
6d3dbdbee5 | ||
|
|
c614a0aafc | ||
|
|
df7a61a97a | ||
|
|
143612ec73 | ||
|
|
18931acdb3 | ||
|
|
bbc6bf2455 | ||
|
|
ff1e0798d3 | ||
|
|
b0be26d67f | ||
|
|
36ea3e4045 | ||
|
|
4889d30b44 | ||
|
|
944a7e0f07 | ||
|
|
d6943e39c0 | ||
|
|
ddb46905e7 | ||
|
|
0e74dbebce | ||
|
|
9e2cc5b125 | ||
|
|
ea97759c2c | ||
|
|
95a0bbc581 | ||
|
|
ecd63ad8d6 | ||
|
|
47f58f52ef | ||
|
|
446f9c1085 | ||
|
|
7e8a1dd496 | ||
|
|
89ee31b7ae | ||
|
|
95be76940c | ||
|
|
761afaf40d | ||
|
|
d167c9b807 | ||
|
|
824ba56291 | ||
|
|
4fbbb25f68 | ||
|
|
602a667343 | ||
|
|
febda5c2b6 | ||
|
|
20379f51cc | ||
|
|
1ec56f3326 | ||
|
|
1f44d58b64 | ||
|
|
5392f988b8 | ||
|
|
26362408b1 | ||
|
|
1e7638bd03 | ||
|
|
afe40033b0 |
15
.github/ISSUE_TEMPLATE/bug-v3.yml
vendored
15
.github/ISSUE_TEMPLATE/bug-v3.yml
vendored
@@ -5,8 +5,8 @@ body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
> [!IMPORTANT]
|
||||
> As Nuxt UI v3 is currently in alpha, we recommend thorough testing before using it in production environments. We're actively working on stabilization and welcome feedback from early adopters to improve the library.
|
||||
> [!IMPORTANT]
|
||||
> As Nuxt UI v3 is currently in alpha, we recommend thorough testing before using it in production environments. We're actively working on stabilization and welcome feedback from early adopters to improve the library.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
@@ -29,11 +29,20 @@ body:
|
||||
- Build Modules: `-`
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: package
|
||||
attributes:
|
||||
label: Is this bug related to Nuxt or Vue?
|
||||
options:
|
||||
- Nuxt
|
||||
- Vue
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
placeholder: v3.0.0-alpha.5
|
||||
placeholder: v3.0.0-alpha.x
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
2
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
@@ -12,7 +12,7 @@ body:
|
||||
label: For what version of Nuxt UI are you suggesting this?
|
||||
options:
|
||||
- v2.x
|
||||
- v3-alpha
|
||||
- v3.0.0-alpha.x
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/question.yml
vendored
2
.github/ISSUE_TEMPLATE/question.yml
vendored
@@ -12,7 +12,7 @@ body:
|
||||
label: For what version of Nuxt UI are you asking this question?
|
||||
options:
|
||||
- v2.x
|
||||
- v3-alpha
|
||||
- v3.0.0-alpha.x
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
||||
64
README.md
64
README.md
@@ -1,6 +1,6 @@
|
||||
[](https://ui.nuxt.com)
|
||||
|
||||
# Nuxt UI v3
|
||||
# Nuxt UI
|
||||
|
||||
[![npm version][npm-version-src]][npm-version-href]
|
||||
[![npm downloads][npm-downloads-src]][npm-downloads-href]
|
||||
@@ -9,9 +9,14 @@
|
||||
|
||||
We're thrilled to introduce Nuxt UI v3, a significant upgrade to our UI library that delivers extensive improvements and robust new capabilities. This major update harnesses the combined strengths of [Radix Vue](https://www.radix-vue.com/), [Tailwind CSS v4](https://tailwindcss.com/blog/tailwindcss-v4-alpha), and [Tailwind Variants](https://www.tailwind-variants.org/) to offer developers an unparalleled set of tools for creating sophisticated, accessible, and highly performant user interfaces.
|
||||
|
||||
## Installation
|
||||
> [!NOTE]
|
||||
> You are on the `v3` development branch, check out the [dev branch](https://github.com/nuxt/ui) for Nuxt UI v2.
|
||||
|
||||
1. Install the Nuxt UI v3 alpha package:
|
||||
## Documentation
|
||||
|
||||
Visit https://ui3.nuxt.dev to explore the documentation.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash [pnpm]
|
||||
pnpm add @nuxt/ui@next
|
||||
@@ -29,10 +34,9 @@ npm install @nuxt/ui@next
|
||||
bun add @nuxt/ui@next
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> Make sure you have `typescript` installed in your dev dependencies.
|
||||
### Nuxt
|
||||
|
||||
2. Register the Nuxt UI module in your `nuxt.config.ts`:
|
||||
1. Add the Nuxt UI module in your `nuxt.config.ts`:
|
||||
|
||||
```ts [nuxt.config.ts]
|
||||
export default defineNuxtConfig({
|
||||
@@ -40,18 +44,54 @@ export default defineNuxtConfig({
|
||||
})
|
||||
```
|
||||
|
||||
3. Import Tailwind CSS and Nuxt UI in your `app.vue` or [CSS](https://nuxt.com/docs/getting-started/styling#the-css-property):
|
||||
2. Import Tailwind CSS and Nuxt UI in your CSS:
|
||||
|
||||
```vue [app.vue]
|
||||
<style>
|
||||
```css [assets/css/main.css]
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui";
|
||||
</style>
|
||||
```
|
||||
|
||||
## Documentation
|
||||
Learn more in the [installation guide](https://ui3.nuxt.dev/getting-started/installation/nuxt).
|
||||
|
||||
Visit https://ui3.nuxt.dev to explore the documentation.
|
||||
### Vue
|
||||
|
||||
1. Add the Nuxt UI Vite plugin in your `vite.config.ts`:
|
||||
|
||||
```ts [vite.config.ts]
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import ui from '@nuxt/ui/vite'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
ui()
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
2. Use the Nuxt UI Vue plugin in your `main.ts`:
|
||||
|
||||
```ts [main.ts]
|
||||
import { createApp } from 'vue'
|
||||
import ui from '@nuxt/ui/vue-plugin'
|
||||
import App from './App.vue'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(ui)
|
||||
|
||||
app.mount('#app')
|
||||
```
|
||||
|
||||
3. Import Tailwind CSS and Nuxt UI in your CSS:
|
||||
|
||||
```css [assets/main.css]
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui";
|
||||
```
|
||||
|
||||
Learn more in the [installation guide](https://ui3.nuxt.dev/getting-started/installation/vue).
|
||||
|
||||
## Credits
|
||||
|
||||
|
||||
@@ -3,13 +3,13 @@ import { resolve } from 'pathe'
|
||||
import { defineCommand } from 'citty'
|
||||
import { consola } from 'consola'
|
||||
import { splitByCase, upperFirst, camelCase, kebabCase } from 'scule'
|
||||
import { appendFile, sortFile } from '../utils.mjs'
|
||||
import templates from '../templates.mjs'
|
||||
import { appendFile, sortFile } from '../../utils.mjs'
|
||||
import templates from '../../templates.mjs'
|
||||
|
||||
export default defineCommand({
|
||||
meta: {
|
||||
name: 'init',
|
||||
description: 'Init a new component.'
|
||||
name: 'component',
|
||||
description: 'Make a new component.'
|
||||
},
|
||||
args: {
|
||||
name: {
|
||||
14
cli/commands/make/index.mjs
Normal file
14
cli/commands/make/index.mjs
Normal file
@@ -0,0 +1,14 @@
|
||||
import { defineCommand } from 'citty'
|
||||
import component from './component.mjs'
|
||||
import locale from './locale.mjs'
|
||||
|
||||
export default defineCommand({
|
||||
meta: {
|
||||
name: 'make',
|
||||
description: 'Commands to create new Nuxt UI entities.'
|
||||
},
|
||||
subCommands: {
|
||||
component,
|
||||
locale
|
||||
}
|
||||
})
|
||||
53
cli/commands/make/locale.mjs
Normal file
53
cli/commands/make/locale.mjs
Normal file
@@ -0,0 +1,53 @@
|
||||
import { existsSync, promises as fsp } from 'node:fs'
|
||||
import { resolve } from 'pathe'
|
||||
import { consola } from 'consola'
|
||||
import { appendFile, sortFile, normalizeLocale } from '../../utils.mjs'
|
||||
import { defineCommand } from 'citty'
|
||||
|
||||
export default defineCommand({
|
||||
meta: {
|
||||
name: 'locale',
|
||||
description: 'Make a new locale.'
|
||||
},
|
||||
args: {
|
||||
code: {
|
||||
description: 'Locale code to create. For example: en.',
|
||||
required: true
|
||||
},
|
||||
name: {
|
||||
description: 'Locale name to create. For example: English.',
|
||||
required: true
|
||||
}
|
||||
},
|
||||
async setup({ args }) {
|
||||
const path = resolve('.')
|
||||
const localePath = resolve(path, `src/runtime/locale`)
|
||||
|
||||
const originLocaleFilePath = resolve(localePath, 'en.ts')
|
||||
const newLocaleFilePath = resolve(localePath, `${args.code}.ts`)
|
||||
|
||||
// Validate locale code
|
||||
if (existsSync(newLocaleFilePath)) {
|
||||
consola.error(`🚨 ${args.code} already exists!`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (!args.code.match(/^[a-z]{2}(?:_[a-z]{2,4})?$/)) {
|
||||
consola.error(`🚨 ${args.code} is not a valid locale code!\nExample: en or en_us`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Create new locale export
|
||||
const localeExportFile = resolve(localePath, `index.ts`)
|
||||
await appendFile(localeExportFile, `export { default as ${args.code} } from './${args.code}'`)
|
||||
await sortFile(localeExportFile)
|
||||
|
||||
// Create new locale file
|
||||
await fsp.copyFile(originLocaleFilePath, newLocaleFilePath)
|
||||
const localeFile = await fsp.readFile(newLocaleFilePath, 'utf-8')
|
||||
const rewrittenLocaleFile = localeFile.replace(/defineLocale\('(.*)'/, `defineLocale('${args.name}', '${normalizeLocale(args.code)}'`)
|
||||
await fsp.writeFile(newLocaleFilePath, rewrittenLocaleFile)
|
||||
|
||||
consola.success(`🪄 Generated ${newLocaleFilePath}`)
|
||||
}
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
import { defineCommand, runMain } from 'citty'
|
||||
import init from './commands/init.mjs'
|
||||
import make from './commands/make/index.mjs'
|
||||
|
||||
const main = defineCommand({
|
||||
meta: {
|
||||
@@ -8,7 +8,7 @@ const main = defineCommand({
|
||||
description: 'Nuxt UI CLI'
|
||||
},
|
||||
subCommands: {
|
||||
init
|
||||
make
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -15,3 +15,17 @@ export async function appendFile(path, contents) {
|
||||
await fsp.writeFile(path, file.trim() + '\n' + contents + '\n')
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeLocale(locale) {
|
||||
if (!locale) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (locale.includes('_')) {
|
||||
return locale.split('_')
|
||||
.map((part, index) => index === 0 ? part.toLowerCase() : part.toUpperCase())
|
||||
.join('-')
|
||||
}
|
||||
|
||||
return locale.toLowerCase()
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="border rounded border-[var(--ui-border)]">
|
||||
<div class="border rounded-[var(--ui-radius)] border-[var(--ui-border)]">
|
||||
<div
|
||||
ref="wrapper"
|
||||
:class="['overflow-hidden', collapsed && overflow ? 'max-h-48' : 'max-h-none']"
|
||||
|
||||
@@ -126,7 +126,7 @@ const previewUrl = computed(() => {
|
||||
</div>
|
||||
<div v-if="highlightedCode && formattedCode" v-show="rendererReady" class="relative w-full p-3">
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<pre class="p-4 min-h-40 max-h-72 text-sm overflow-y-auto rounded-lg border border-[var(--ui-border)] bg-neutral-50 dark:bg-neutral-800" v-html="highlightedCode" />
|
||||
<pre class="p-4 min-h-40 max-h-72 text-sm overflow-y-auto rounded-[calc(var(--ui-radius)*1.5)] border border-[var(--ui-border)] bg-neutral-50 dark:bg-neutral-800" v-html="highlightedCode" />
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="link"
|
||||
|
||||
@@ -17,14 +17,14 @@ watchEffect(() => {
|
||||
})
|
||||
|
||||
const description = computed(() => {
|
||||
return props.meta.description?.replace(/`([^`]+)`/g, '<code class="font-medium bg-[var(--ui-bg-elevated)] px-1 py-0.5 rounded">$1</code>')
|
||||
return props.meta.description?.replace(/`([^`]+)`/g, '<code class="font-medium bg-[var(--ui-bg-elevated)] px-1 py-0.5 rounded-[var(--ui-radius)]">$1</code>')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UFormField :name="meta?.name" class="" :ui="{ wrapper: 'mb-2' }" :class="{ 'opacity-70 cursor-not-allowed': !matchedInput || ignore }">
|
||||
<template #label>
|
||||
<p v-if="meta?.name" class="font-mono font-bold px-1.5 py-0.5 border border-[var(--ui-border-accented)] border-dashed rounded bg-[var(--ui-bg-elevated)]">
|
||||
<p v-if="meta?.name" class="font-mono font-bold px-1.5 py-0.5 border border-[var(--ui-border-accented)] border-dashed rounded-[var(--ui-radius)] bg-[var(--ui-bg-elevated)]">
|
||||
{{ meta?.name }}
|
||||
</p>
|
||||
</template>
|
||||
|
||||
@@ -8,7 +8,9 @@ const appConfig = useAppConfig()
|
||||
const colorMode = useColorMode()
|
||||
|
||||
const { data: navigation } = await useAsyncData('navigation', () => queryCollectionNavigation('content'))
|
||||
const { data: files } = await useAsyncData('files', () => queryCollectionSearchSections('content', { ignoredTags: ['style'] }))
|
||||
const { data: files } = useLazyAsyncData('search', () => queryCollectionSearchSections('content'), {
|
||||
server: false
|
||||
})
|
||||
|
||||
const searchTerm = ref('')
|
||||
|
||||
@@ -79,6 +81,11 @@ const updatedNavigation = computed(() => navigation.value?.map(item => ({
|
||||
title: 'Installation',
|
||||
active: route.path.startsWith('/getting-started/installation'),
|
||||
children: []
|
||||
}),
|
||||
...(child.path === '/getting-started/i18n' && {
|
||||
title: 'I18n',
|
||||
active: route.path.startsWith('/getting-started/i18n'),
|
||||
children: []
|
||||
})
|
||||
})) || []
|
||||
})))
|
||||
@@ -117,6 +124,8 @@ provide('navigation', updatedNavigation)
|
||||
@source "../content/**/*.md";
|
||||
|
||||
@theme {
|
||||
--container-8xl: 90rem;
|
||||
|
||||
--font-family-sans: 'Public Sans', sans-serif;
|
||||
|
||||
--color-green-50: #EFFDF5;
|
||||
@@ -133,6 +142,6 @@ provide('navigation', updatedNavigation)
|
||||
}
|
||||
|
||||
:root {
|
||||
--ui-container-width: 90rem;
|
||||
--ui-container: var(--container-8xl);
|
||||
}
|
||||
</style>
|
||||
|
||||
34
docs/app/components/content/SupportedLanguages.vue
Normal file
34
docs/app/components/content/SupportedLanguages.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import * as locales from '@nuxt/ui/locale'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
default?: string
|
||||
}>(), {
|
||||
default: 'en'
|
||||
})
|
||||
|
||||
const getLocaleKeys = () => Object.keys(locales) as Array<keyof typeof locales>
|
||||
const localesList = getLocaleKeys().map(locale => [locale, locales[locale].name])
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable vue/singleline-html-element-content-newline -->
|
||||
<template>
|
||||
<div>
|
||||
<ProseUl>
|
||||
<ProseLi v-for="[key, label] in localesList" :key="key">
|
||||
<ProseCode>{{ key }}</ProseCode> - {{ label }}
|
||||
<template v-if="key === props.default">
|
||||
(default)
|
||||
</template>
|
||||
</ProseLi>
|
||||
</ProseUl>
|
||||
<Note to="https://github.com/nuxt/ui/tree/v3/src/runtime/locale" target="_blank">
|
||||
If you need additional languages, you can contribute by creating a PR to add a new locale in <ProseCode>src/runtime/locale/</ProseCode>.
|
||||
</Note>
|
||||
<Tip>
|
||||
You can use the <ProseCode>nuxt-ui</ProseCode> CLI to create a new locale:
|
||||
|
||||
<ProsePre language="bash">nuxt-ui make locale --code "en" --name "English"</ProsePre>
|
||||
</Tip>
|
||||
</div>
|
||||
</template>
|
||||
@@ -73,15 +73,15 @@ async function onSubmit(event: FormSubmitEvent<any>) {
|
||||
</UFormField>
|
||||
|
||||
<UFormField name="select" label="Select">
|
||||
<USelect v-model="state.select" :items="items" />
|
||||
<USelect v-model="state.select" :items="items" class="w-48" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField name="selectMenu" label="Select Menu">
|
||||
<USelectMenu v-model="state.selectMenu" :items="items" />
|
||||
<USelectMenu v-model="state.selectMenu" :items="items" class="w-48" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField name="selectMenuMultiple" label="Select Menu (Multiple)">
|
||||
<USelectMenu v-model="state.selectMenuMultiple" multiple :items="items" />
|
||||
<USelectMenu v-model="state.selectMenuMultiple" multiple :items="items" class="w-48" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField name="inputMenu" label="Input Menu">
|
||||
@@ -104,11 +104,11 @@ async function onSubmit(event: FormSubmitEvent<any>) {
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 mt-8">
|
||||
<UButton color="neutral" type="submit">
|
||||
<UButton type="submit">
|
||||
Submit
|
||||
</UButton>
|
||||
|
||||
<UButton color="neutral" variant="outline" @click="form?.clear()">
|
||||
<UButton variant="outline" @click="form?.clear()">
|
||||
Clear
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
const { data: countries, status, execute } = await useLazyFetch<{
|
||||
name: string
|
||||
code: string
|
||||
emoji: string
|
||||
}[]>('/api/countries.json', {
|
||||
immediate: false
|
||||
})
|
||||
|
||||
function onOpen() {
|
||||
if (!countries.value?.length) {
|
||||
execute()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UInputMenu
|
||||
:items="countries || []"
|
||||
:loading="status === 'pending'"
|
||||
label-key="name"
|
||||
:search-input="{ icon: 'i-lucide-search' }"
|
||||
placeholder="Select country"
|
||||
class="w-48"
|
||||
@update:open="onOpen"
|
||||
>
|
||||
<template #leading="{ modelValue, ui }">
|
||||
<span v-if="modelValue" class="size-5 text-center">
|
||||
{{ modelValue?.emoji }}
|
||||
</span>
|
||||
<UIcon v-else name="i-lucide-earth" :class="ui.leadingIcon()" />
|
||||
</template>
|
||||
<template #item-leading="{ item }">
|
||||
<span class="size-5 text-center">
|
||||
{{ item.emoji }}
|
||||
</span>
|
||||
</template>
|
||||
</UInputMenu>
|
||||
</template>
|
||||
@@ -16,7 +16,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
||||
<UInputMenu
|
||||
:items="users || []"
|
||||
:loading="status === 'pending'"
|
||||
:filter="['name', 'email']"
|
||||
:filter="['label', 'email']"
|
||||
icon="i-lucide-user"
|
||||
placeholder="Select user"
|
||||
class="w-80"
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
const { data: countries, status, execute } = await useLazyFetch<{
|
||||
name: string
|
||||
code: string
|
||||
emoji: string
|
||||
}[]>('/api/countries.json', {
|
||||
immediate: false,
|
||||
default: () => []
|
||||
})
|
||||
|
||||
function onOpen() {
|
||||
if (!countries.value?.length) {
|
||||
execute()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<USelectMenu
|
||||
:items="countries"
|
||||
:loading="status === 'pending'"
|
||||
label-key="name"
|
||||
:search-input="{ icon: 'i-lucide-search' }"
|
||||
placeholder="Select country"
|
||||
class="w-48"
|
||||
@update:open="onOpen"
|
||||
>
|
||||
<template #leading="{ modelValue, ui }">
|
||||
<span v-if="modelValue" class="size-5 text-center">
|
||||
{{ modelValue?.emoji }}
|
||||
</span>
|
||||
<UIcon v-else name="i-lucide-earth" :class="ui.leadingIcon()" />
|
||||
</template>
|
||||
<template #item-leading="{ item }">
|
||||
<span class="size-5 text-center">
|
||||
{{ item.emoji }}
|
||||
</span>
|
||||
</template>
|
||||
</USelectMenu>
|
||||
</template>
|
||||
@@ -16,7 +16,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
||||
<USelectMenu
|
||||
:items="users || []"
|
||||
:loading="status === 'pending'"
|
||||
:filter="['name', 'email']"
|
||||
:filter="['label', 'email']"
|
||||
icon="i-lucide-user"
|
||||
placeholder="Select user"
|
||||
class="w-80"
|
||||
|
||||
@@ -7,9 +7,9 @@ const appConfig = useAppConfig()
|
||||
<UFormField
|
||||
label="toaster.duration"
|
||||
size="sm"
|
||||
class="inline-flex ring ring-[var(--ui-border-accented)] rounded"
|
||||
class="inline-flex ring ring-[var(--ui-border-accented)] rounded-[var(--ui-radius)]"
|
||||
:ui="{
|
||||
wrapper: 'bg-[var(--ui-bg-elevated)]/50 rounded-l flex border-r border-[var(--ui-border-accented)]',
|
||||
wrapper: 'bg-[var(--ui-bg-elevated)]/50 rounded-l-[var(--ui-radius)] flex border-r border-[var(--ui-border-accented)]',
|
||||
label: 'text-[var(--ui-text-muted)] px-2 py-1.5',
|
||||
container: 'mt-0'
|
||||
}"
|
||||
@@ -18,8 +18,7 @@ const appConfig = useAppConfig()
|
||||
v-model="appConfig.toaster.duration"
|
||||
color="neutral"
|
||||
variant="soft"
|
||||
class="rounded rounded-l-none min-w-12"
|
||||
:search-input="false"
|
||||
:ui="{ base: 'rounded-[var(--ui-radius)] rounded-l-none min-w-12' }"
|
||||
/>
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
@@ -7,9 +7,9 @@ const appConfig = useAppConfig()
|
||||
<UFormField
|
||||
label="toaster.expand"
|
||||
size="sm"
|
||||
class="inline-flex ring ring-[var(--ui-border-accented)] rounded"
|
||||
class="inline-flex ring ring-[var(--ui-border-accented)] rounded-[var(--ui-radius)]"
|
||||
:ui="{
|
||||
wrapper: 'bg-[var(--ui-bg-elevated)]/50 rounded-l flex border-r border-[var(--ui-border-accented)]',
|
||||
wrapper: 'bg-[var(--ui-bg-elevated)]/50 rounded-l-[var(--ui-radius)] flex border-r border-[var(--ui-border-accented)]',
|
||||
label: 'text-[var(--ui-text-muted)] px-2 py-1.5',
|
||||
container: 'mt-0'
|
||||
}"
|
||||
@@ -19,7 +19,7 @@ const appConfig = useAppConfig()
|
||||
:items="[true, false]"
|
||||
color="neutral"
|
||||
variant="soft"
|
||||
class="rounded rounded-l-none min-w-12"
|
||||
class="rounded-[var(--ui-radius)] rounded-l-none min-w-12"
|
||||
:search-input="false"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
@@ -10,9 +10,9 @@ const appConfig = useAppConfig()
|
||||
<UFormField
|
||||
label="toaster.position"
|
||||
size="sm"
|
||||
class="inline-flex ring ring-[var(--ui-border-accented)] rounded"
|
||||
class="inline-flex ring ring-[var(--ui-border-accented)] rounded-[var(--ui-radius)]"
|
||||
:ui="{
|
||||
wrapper: 'bg-[var(--ui-bg-elevated)]/50 rounded-l flex border-r border-[var(--ui-border-accented)]',
|
||||
wrapper: 'bg-[var(--ui-bg-elevated)]/50 rounded-l-[var(--ui-radius)] flex border-r border-[var(--ui-border-accented)]',
|
||||
label: 'text-[var(--ui-text-muted)] px-2 py-1.5',
|
||||
container: 'mt-0'
|
||||
}"
|
||||
@@ -22,7 +22,7 @@ const appConfig = useAppConfig()
|
||||
:items="positions"
|
||||
color="neutral"
|
||||
variant="soft"
|
||||
class="rounded rounded-l-none min-w-12"
|
||||
class="rounded-[var(--ui-radius)] rounded-l-none min-w-12"
|
||||
:search-input="false"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
@@ -14,7 +14,9 @@ select:
|
||||
|
||||
## Setup
|
||||
|
||||
1. Install the Nuxt UI v3 alpha package:
|
||||
::steps{level="4"}
|
||||
|
||||
#### Install the Nuxt UI v3 alpha package
|
||||
|
||||
::code-group{sync="pm"}
|
||||
|
||||
@@ -37,10 +39,10 @@ bun add @nuxt/ui@next
|
||||
::
|
||||
|
||||
::warning
|
||||
If you're using **pnpm**, ensure that you either set [`shamefully-hoist=true`](https://pnpm.io/npmrc#shamefully-hoist) in your `.npmrc` file or install `tailwindcss@next` directly in your project's root directory.
|
||||
If you're using **pnpm**, ensure that you either set [`shamefully-hoist=true`](https://pnpm.io/npmrc#shamefully-hoist) in your `.npmrc` file or install `tailwindcss@next` in your project's root directory.
|
||||
::
|
||||
|
||||
2. Register the Nuxt UI module in your `nuxt.config.ts`{lang="ts-type"}:
|
||||
#### Add the Nuxt UI module in your `nuxt.config.ts`{lang="ts-type"}
|
||||
|
||||
```ts [nuxt.config.ts]
|
||||
export default defineNuxtConfig({
|
||||
@@ -48,15 +50,24 @@ export default defineNuxtConfig({
|
||||
})
|
||||
```
|
||||
|
||||
3. Import Tailwind CSS and Nuxt UI in your `app.vue`{lang="ts-type"} or [CSS](https://nuxt.com/docs/getting-started/styling#the-css-property):
|
||||
#### Import Tailwind CSS and Nuxt UI in your CSS
|
||||
|
||||
```vue [app.vue]
|
||||
<style>
|
||||
```css [assets/css/main.css]
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui";
|
||||
</style>
|
||||
```
|
||||
|
||||
::note
|
||||
Use the `css` property in your `nuxt.config.ts` to import your CSS file.
|
||||
|
||||
```ts [nuxt.config.ts]
|
||||
export default defineNuxtConfig({
|
||||
modules: ['@nuxt/ui'],
|
||||
css: ['~/assets/css/main.css']
|
||||
})
|
||||
```
|
||||
::
|
||||
|
||||
::tip
|
||||
It's recommended to install the [Tailwind CSS IntelliSense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) extension for VSCode and add the following settings:
|
||||
```json
|
||||
@@ -70,8 +81,6 @@ It's recommended to install the [Tailwind CSS IntelliSense](https://marketplace.
|
||||
|
||||
::
|
||||
|
||||
::warning
|
||||
IntelliSense works better when importing `tailwindcss` in a proper `.css` file which will be automatically detected.
|
||||
::
|
||||
|
||||
## Options
|
||||
|
||||
@@ -14,7 +14,9 @@ select:
|
||||
|
||||
## Setup
|
||||
|
||||
1. Install the Nuxt UI v3 alpha package:
|
||||
::steps{level="4"}
|
||||
|
||||
#### Install the Nuxt UI v3 alpha package
|
||||
|
||||
::code-group{sync="pm"}
|
||||
|
||||
@@ -37,12 +39,12 @@ bun add @nuxt/ui@next
|
||||
::
|
||||
|
||||
::warning
|
||||
If you're using **pnpm**, ensure that you either set [`shamefully-hoist=true`](https://pnpm.io/npmrc#shamefully-hoist) in your `.npmrc` file or install `tailwindcss@next`, `vue-router` and `@unhead/vue` directly in your project's root directory.
|
||||
If you're using **pnpm**, ensure that you either set [`shamefully-hoist=true`](https://pnpm.io/npmrc#shamefully-hoist) in your `.npmrc` file or install `tailwindcss@next`, `vue-router` and `@unhead/vue` in your project's root directory.
|
||||
::
|
||||
|
||||
2. Add the Nuxt UI Vite plugin in your `vite.config.ts`{lang="ts-type"}:
|
||||
#### Add the Nuxt UI Vite plugin in your `vite.config.ts`{lang="ts-type"}
|
||||
|
||||
```ts [vite.config.ts]
|
||||
```ts [vite.config.ts]{3,8}
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import ui from '@nuxt/ui/vite'
|
||||
@@ -51,7 +53,7 @@ export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
ui()
|
||||
],
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
@@ -71,28 +73,45 @@ components.d.ts
|
||||
```
|
||||
::
|
||||
|
||||
3. Register the Nuxt UI Vue plugin in your app:
|
||||
#### Use the Nuxt UI Vue plugin in your `main.ts`
|
||||
|
||||
```ts [main.ts]
|
||||
```ts [main.ts]{2,7}
|
||||
import { createApp } from 'vue'
|
||||
import nuxtUI from '@nuxt/ui/vue-plugin'
|
||||
import ui from '@nuxt/ui/vue-plugin'
|
||||
import App from './App.vue'
|
||||
|
||||
const app = createApp(App)
|
||||
// ...
|
||||
app.use(nuxtUI)
|
||||
|
||||
app.use(ui)
|
||||
|
||||
app.mount('#app')
|
||||
```
|
||||
|
||||
4. Import Tailwind CSS and Nuxt UI in your `App.vue`{lang="ts-type"} or CSS:
|
||||
#### Import Tailwind CSS and Nuxt UI in your CSS
|
||||
|
||||
```vue [App.vue]
|
||||
<style>
|
||||
```css [assets/main.css]
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui";
|
||||
</style>
|
||||
```
|
||||
|
||||
::note
|
||||
Import the CSS file in your `main.ts`.
|
||||
|
||||
```ts [main.ts]{1}
|
||||
import './assets/main.css'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import ui from '@nuxt/ui/vue-plugin'
|
||||
import App from './App.vue'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(ui)
|
||||
|
||||
app.mount('#app')
|
||||
```
|
||||
::
|
||||
|
||||
::tip
|
||||
It's recommended to install the [Tailwind CSS IntelliSense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) extension for VSCode and add the following settings:
|
||||
```json
|
||||
@@ -106,8 +125,6 @@ It's recommended to install the [Tailwind CSS IntelliSense](https://marketplace.
|
||||
|
||||
::
|
||||
|
||||
::warning
|
||||
IntelliSense works better when importing `tailwindcss` in a proper `.css` file which will be automatically detected.
|
||||
::
|
||||
|
||||
## Options
|
||||
|
||||
@@ -11,8 +11,7 @@ Nuxt UI v3 uses Tailwind CSS v4 alpha which doesn't have a documentation yet, le
|
||||
|
||||
Tailwind CSS v4 takes a CSS-first configuration approach, you now customize your theme with CSS variables inside a `@theme` directive:
|
||||
|
||||
```vue [app.vue]
|
||||
<style>
|
||||
```css [main.css]
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui";
|
||||
|
||||
@@ -33,7 +32,6 @@ Tailwind CSS v4 takes a CSS-first configuration approach, you now customize your
|
||||
--color-green-900: #0A5331;
|
||||
--color-green-950: #052E16;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
The `@theme` directive tells Tailwind to make new utilities and variants available based on these variables. It's the equivalent of the `theme.extend` key in Tailwind CSS v3 `tailwind.config.ts` file.
|
||||
@@ -48,13 +46,11 @@ You can use the `@source` directive to add explicit content glob patterns if you
|
||||
|
||||
This can be useful when writing Tailwind classes in markdown files with [`@nuxt/content`](https://github.com/nuxt/content):
|
||||
|
||||
```vue [app.vue]
|
||||
<style>
|
||||
```css [main.css]
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui";
|
||||
|
||||
@source "../content/**/*.md";
|
||||
</style>
|
||||
```
|
||||
|
||||
::note{to="https://github.com/tailwindlabs/tailwindcss/pull/14078"}
|
||||
@@ -65,13 +61,11 @@ You can learn more about the `@source` directive in this pull request.
|
||||
|
||||
You can use the `@plugin` directive to import Tailwind CSS plugins.
|
||||
|
||||
```vue [app.vue]
|
||||
<style>
|
||||
```css [main.css]
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui";
|
||||
|
||||
@plugin "@tailwindcss/typography";
|
||||
</style>
|
||||
```
|
||||
|
||||
::note{to="https://github.com/tailwindlabs/tailwindcss/pull/14264"}
|
||||
@@ -154,8 +148,7 @@ These color aliases are not automatically defined as Tailwind CSS colors, so cla
|
||||
|
||||
However, you can generate these classes using Tailwind's `@theme` directive, allowing you to use custom color utility classes while maintaining dynamic color aliases:
|
||||
|
||||
```vue [app.vue]
|
||||
<style>
|
||||
```css [main.css]
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui";
|
||||
|
||||
@@ -172,7 +165,6 @@ However, you can generate these classes using Tailwind's `@theme` directive, all
|
||||
--color-primary-900: var(--ui-color-primary-900);
|
||||
--color-primary-950: var(--ui-color-primary-950);
|
||||
}
|
||||
</style>
|
||||
```
|
||||
::
|
||||
|
||||
@@ -217,8 +209,7 @@ You can use these variables in classes like `text-[var(--ui-primary)]`, it will
|
||||
::tip
|
||||
You can change which shade is used for each color on light and dark mode:
|
||||
|
||||
```vue [app.vue]
|
||||
<style>
|
||||
```css [main.css]
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui";
|
||||
|
||||
@@ -229,7 +220,6 @@ You can change which shade is used for each color on light and dark mode:
|
||||
.dark {
|
||||
--ui-primary: var(--ui-color-primary-200);
|
||||
}
|
||||
</style>
|
||||
```
|
||||
::
|
||||
|
||||
@@ -324,8 +314,7 @@ body {
|
||||
::tip
|
||||
You can customize these CSS variables to tailor the appearance of your application:
|
||||
|
||||
```vue [app.vue]
|
||||
<style>
|
||||
```css [main.css]
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui";
|
||||
|
||||
@@ -338,7 +327,6 @@ You can customize these CSS variables to tailor the appearance of your applicati
|
||||
--ui-bg: var(--ui-color-neutral-950);
|
||||
--ui-border: var(--ui-color-neutral-900);
|
||||
}
|
||||
</style>
|
||||
```
|
||||
::
|
||||
|
||||
@@ -359,15 +347,40 @@ Try the :prose-icon{name="i-lucide-swatch-book" class="text-[var(--ui-primary)]"
|
||||
::tip
|
||||
You can customize the default radius value using the default Tailwind CSS variables or a value of your choice:
|
||||
|
||||
```vue [app.vue]
|
||||
<style>
|
||||
```css [main.css]
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui";
|
||||
|
||||
:root {
|
||||
--ui-radius: var(--radius-sm);
|
||||
}
|
||||
</style>
|
||||
```
|
||||
::
|
||||
|
||||
#### Container
|
||||
|
||||
Nuxt UI uses a global `--ui-container` CSS variable to define the width of the container:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--ui-container: var(--container-7xl);
|
||||
}
|
||||
```
|
||||
|
||||
::tip
|
||||
You can customize the default container width using the default Tailwind CSS variables or a value of your choice:
|
||||
|
||||
```css [main.css]
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui";
|
||||
|
||||
@theme {
|
||||
--container-8xl: 90rem;
|
||||
}
|
||||
|
||||
:root {
|
||||
--ui-container: var(--container-8xl);
|
||||
}
|
||||
```
|
||||
::
|
||||
|
||||
@@ -384,7 +397,7 @@ Components in Nuxt UI can have multiple `slots`, each representing a distinct HT
|
||||
```ts [src/theme/card.ts]
|
||||
export default {
|
||||
slots: {
|
||||
root: 'bg-[var(--ui-bg)] ring ring-[var(--ui-border)] divide-y divide-[var(--ui-border)] rounded-[calc(var(--ui-radius)*2)] shadow',
|
||||
root: 'bg-[var(--ui-bg)] ring ring-[var(--ui-border)] divide-y divide-[var(--ui-border)] rounded-[calc(var(--ui-radius)*2)] shadow-sm',
|
||||
header: 'p-4 sm:px-6',
|
||||
body: 'p-4 sm:p-6',
|
||||
footer: 'p-4 sm:px-6'
|
||||
@@ -418,7 +431,7 @@ Some components don't have slots, they are just composed of a single root elemen
|
||||
|
||||
```ts [src/theme/container.ts]
|
||||
export default {
|
||||
base: 'max-w-7xl mx-auto px-4 sm:px-6 lg:px-8'
|
||||
base: 'max-w-[var(--ui-container)] mx-auto px-4 sm:px-6 lg:px-8'
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -12,15 +12,13 @@ links:
|
||||
|
||||
Nuxt UI automatically registers the [`@nuxt/fonts`](https://github.com/nuxt/fonts) module for you, so there's no additional setup required. To use a font in your Nuxt UI application, you can simply declare it in your CSS:
|
||||
|
||||
```vue [app.vue]
|
||||
<style>
|
||||
```css [main.css]
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui";
|
||||
|
||||
@theme {
|
||||
--font-family-sans: 'Public Sans', sans-serif;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
That's it! Nuxt Fonts will detect this and you should immediately see the web font loaded in your browser.
|
||||
|
||||
189
docs/content/1.getting-started/7.i18n/1.nuxt.md
Normal file
189
docs/content/1.getting-started/7.i18n/1.nuxt.md
Normal file
@@ -0,0 +1,189 @@
|
||||
---
|
||||
navigation.title: Nuxt
|
||||
title: Internationalization (i18n) in a Nuxt app
|
||||
description: 'Learn how to internationalize your Nuxt app and support multi-directional support (LTR/RTL).'
|
||||
select:
|
||||
items:
|
||||
- label: Nuxt
|
||||
icon: i-logos-nuxt-icon
|
||||
to: /getting-started/i18n/nuxt
|
||||
- label: Vue
|
||||
icon: i-logos-vue
|
||||
to: /getting-started/i18n/vue
|
||||
---
|
||||
|
||||
::note{to="/components/app"}
|
||||
Nuxt UI provides an [App](/components/app) component that wraps your app to provide global configurations.
|
||||
::
|
||||
|
||||
## Locale
|
||||
|
||||
Use the `locale` prop with the locale you want to use from `@nuxt/ui/locale`:
|
||||
|
||||
```vue [app.vue]
|
||||
<script setup lang="ts">
|
||||
import { fr } from '@nuxt/ui/locale'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UApp :locale="fr">
|
||||
<NuxtPage />
|
||||
</UApp>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Custom locale
|
||||
|
||||
You also have the option to add your own locale using `defineLocale`:
|
||||
|
||||
```vue [app.vue]
|
||||
<script setup lang="ts">
|
||||
const locale = defineLocale('My custom locale', 'en', {
|
||||
// implement pairs
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UApp :locale="locale">
|
||||
<NuxtPage />
|
||||
</UApp>
|
||||
</template>
|
||||
```
|
||||
|
||||
::tip
|
||||
Look at the second parameter, there you need to pass the iso code of the language. Example:
|
||||
* `hi` Hindi (language)
|
||||
* `de-AT`: German (language) as used in Austria (region)
|
||||
::
|
||||
|
||||
### Dynamic locale
|
||||
|
||||
To dynamically switch between languages, you can use the [Nuxt I18n](https://i18n.nuxtjs.org/) module.
|
||||
|
||||
::steps{level="4"}
|
||||
|
||||
#### Install the Nuxt I18n package
|
||||
|
||||
::code-group{sync="pm"}
|
||||
|
||||
```bash [pnpm]
|
||||
pnpm add @nuxtjs/i18n@next
|
||||
```
|
||||
|
||||
```bash [yarn]
|
||||
yarn add @nuxtjs/i18n@next
|
||||
```
|
||||
|
||||
```bash [npm]
|
||||
npm install @nuxtjs/i18n@next
|
||||
```
|
||||
|
||||
```bash [bun]
|
||||
bun add @nuxtjs/i18n@next
|
||||
```
|
||||
|
||||
::
|
||||
|
||||
#### Add the Nuxt I18n module in your `nuxt.config.ts`{lang="ts-type"}
|
||||
|
||||
```ts [nuxt.config.ts]
|
||||
export default defineNuxtConfig({
|
||||
modules: [
|
||||
'@nuxt/ui',
|
||||
'@nuxtjs/i18n'
|
||||
],
|
||||
i18n: {
|
||||
locales: [{
|
||||
code: 'de',
|
||||
name: 'Deutsch'
|
||||
}, {
|
||||
code: 'en',
|
||||
name: 'English'
|
||||
}, {
|
||||
code: 'fr',
|
||||
name: 'Français'
|
||||
}]
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
#### Set the `locale` prop using `useI18n`
|
||||
|
||||
```vue [app.vue]
|
||||
<script setup lang="ts">
|
||||
import * as locales from '@nuxt/ui/locale'
|
||||
|
||||
const { locale } = useI18n()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UApp :locale="locales[locale]">
|
||||
<NuxtPage />
|
||||
</UApp>
|
||||
</template>
|
||||
```
|
||||
|
||||
::
|
||||
|
||||
### Supported languages
|
||||
|
||||
:supported-languages
|
||||
|
||||
## Direction
|
||||
|
||||
Use the `dir` prop with `ltr` or `rtl` to set the global reading direction of your app:
|
||||
|
||||
```vue [app.vue]
|
||||
<template>
|
||||
<UApp dir="rtl">
|
||||
<NuxtPage />
|
||||
</UApp>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Dynamic direction
|
||||
|
||||
To dynamically change the global reading direction of your app, you can use VueUse's [useTextDirection](https://vueuse.org/core/useTextDirection/) composable to detect and switch between LTR and RTL text directions.
|
||||
|
||||
::steps{level="4"}
|
||||
|
||||
#### Install the `@vueuse/core` package
|
||||
|
||||
::code-group{sync="pm"}
|
||||
|
||||
```bash [pnpm]
|
||||
pnpm add @vueuse/core
|
||||
```
|
||||
|
||||
```bash [yarn]
|
||||
yarn add @vueuse/core
|
||||
```
|
||||
|
||||
```bash [npm]
|
||||
npm install @vueuse/core
|
||||
```
|
||||
|
||||
```bash [bun]
|
||||
bun add @vueuse/core
|
||||
```
|
||||
|
||||
::
|
||||
|
||||
#### Set the `dir` prop using `useTextDirection`
|
||||
|
||||
```vue [app.vue]
|
||||
<script setup lang="ts">
|
||||
import { useTextDirection } from '@vueuse/core'
|
||||
|
||||
const textDirection = useTextDirection({ initialValue: 'ltr' })
|
||||
const dir = computed(() => textDirection.value === 'rtl' ? 'rtl' : 'ltr')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UApp :dir="dir">
|
||||
<NuxtPage />
|
||||
</UApp>
|
||||
</template>
|
||||
```
|
||||
|
||||
::
|
||||
198
docs/content/1.getting-started/7.i18n/2.vue.md
Normal file
198
docs/content/1.getting-started/7.i18n/2.vue.md
Normal file
@@ -0,0 +1,198 @@
|
||||
---
|
||||
navigation.title: Vue
|
||||
title: Internationalization (i18n) in a Vue app
|
||||
description: 'Learn how to internationalize your Vue app and support multi-directional support (LTR/RTL).'
|
||||
select:
|
||||
items:
|
||||
- label: Nuxt
|
||||
icon: i-logos-nuxt-icon
|
||||
to: /getting-started/i18n/nuxt
|
||||
- label: Vue
|
||||
icon: i-logos-vue
|
||||
to: /getting-started/i18n/vue
|
||||
---
|
||||
|
||||
::note{to="/components/app"}
|
||||
Nuxt UI provides an [App](/components/app) component that wraps your app to provide global configurations.
|
||||
::
|
||||
|
||||
## Locale
|
||||
|
||||
Use the `locale` prop with the locale you want to use from `@nuxt/ui/locale`:
|
||||
|
||||
```vue [App.vue]
|
||||
<script setup lang="ts">
|
||||
import { fr } from '@nuxt/ui/locale'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UApp :locale="fr">
|
||||
<RouterView />
|
||||
</UApp>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Custom locale
|
||||
|
||||
You also have the option to add your locale using `defineLocale`:
|
||||
|
||||
```vue [App.vue]
|
||||
<script setup lang="ts">
|
||||
import { defineLocale } from '@nuxt/ui/runtime/composables/defineLocale'
|
||||
|
||||
const locale = defineLocale('My custom locale', 'en', {
|
||||
// implement pairs
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UApp :locale="locale">
|
||||
<RouterView />
|
||||
</UApp>
|
||||
</template>
|
||||
```
|
||||
|
||||
::tip
|
||||
Look at the second parameter, there you need to pass the iso code of the language. Example:
|
||||
* `hi` Hindi (language)
|
||||
* `de-AT`: German (language) as used in Austria (region)
|
||||
::
|
||||
|
||||
### Dynamic locale
|
||||
|
||||
To dynamically switch between languages, you can use the [Vue I18n](https://vue-i18n.intlify.dev/) plugin.
|
||||
|
||||
::steps{level="4"}
|
||||
|
||||
#### Install the Vue I18n package
|
||||
|
||||
::code-group{sync="pm"}
|
||||
|
||||
```bash [pnpm]
|
||||
pnpm add vue-i18n@10
|
||||
```
|
||||
|
||||
```bash [yarn]
|
||||
yarn add vue-i18n@10
|
||||
```
|
||||
|
||||
```bash [npm]
|
||||
npm install vue-i18n@10
|
||||
```
|
||||
|
||||
```bash [bun]
|
||||
bun add vue-i18n@10
|
||||
```
|
||||
|
||||
::
|
||||
|
||||
#### Use the Vue I18n plugin in your `main.ts`
|
||||
|
||||
```ts [main.ts]{2,6-18,22}
|
||||
import { createApp } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import ui from '@nuxt/ui/vue-plugin'
|
||||
import App from './App.vue'
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
availableLocales: ['en', 'de'],
|
||||
messages: {
|
||||
en: {
|
||||
// ...
|
||||
},
|
||||
de: {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(i18n)
|
||||
app.use(ui)
|
||||
|
||||
app.mount('#app')
|
||||
```
|
||||
|
||||
#### Set the `locale` prop using `useI18n`
|
||||
|
||||
```vue [App.vue]
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import * as locales from '@nuxt/ui/locale'
|
||||
|
||||
const { locale } = useI18n()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UApp :locale="locales[locale]">
|
||||
<RouterView />
|
||||
</UApp>
|
||||
</template>
|
||||
```
|
||||
|
||||
::
|
||||
|
||||
## Supported languages
|
||||
|
||||
:supported-languages
|
||||
|
||||
## Direction
|
||||
|
||||
Use the `dir` prop with `ltr` or `rtl` to set the global reading direction of your app:
|
||||
|
||||
```vue [App.vue]
|
||||
<template>
|
||||
<UApp dir="rtl">
|
||||
<NuxtPage />
|
||||
</UApp>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Dynamic direction
|
||||
|
||||
To dynamically change the global reading direction of your app, you can use VueUse's [useTextDirection](https://vueuse.org/core/useTextDirection/) composable to detect and switch between LTR and RTL text directions.
|
||||
|
||||
::steps{level="4"}
|
||||
|
||||
#### Install the `@vueuse/core` package
|
||||
|
||||
::code-group{sync="pm"}
|
||||
|
||||
```bash [pnpm]
|
||||
pnpm add @vueuse/core
|
||||
```
|
||||
|
||||
```bash [yarn]
|
||||
yarn add @vueuse/core
|
||||
```
|
||||
|
||||
```bash [npm]
|
||||
npm install @vueuse/core
|
||||
```
|
||||
|
||||
```bash [bun]
|
||||
bun add @vueuse/core
|
||||
```
|
||||
|
||||
::
|
||||
|
||||
#### Set the `dir` prop using `useTextDirection`
|
||||
|
||||
```vue [App.vue]
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useTextDirection } from '@vueuse/core'
|
||||
|
||||
const textDirection = useTextDirection()
|
||||
const dir = computed(() => textDirection.value === 'rtl' ? 'rtl' : 'ltr')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UApp :dir="dir">
|
||||
<RouterView />
|
||||
</UApp>
|
||||
</template>
|
||||
```
|
||||
@@ -27,6 +27,14 @@ Use it as at the root of your app:
|
||||
</template>
|
||||
```
|
||||
|
||||
::tip{to="/getting-started/i18n/nuxt#locale"}
|
||||
Learn how to use the `locale` prop to change the locale of your app.
|
||||
::
|
||||
|
||||
::tip{to="/getting-started/i18n/nuxt#direction"}
|
||||
Learn how to use the `dir` prop to change the global reading direction of your app.
|
||||
::
|
||||
|
||||
## API
|
||||
|
||||
### Props
|
||||
|
||||
@@ -64,7 +64,7 @@ It requires two props:
|
||||
::
|
||||
::
|
||||
|
||||
Errors are reported directly to the [FormField](/components/form-field) component based on the `name` prop. This means the validation rules defined for the `email` attribute in your schema will be applied to `<FormField name="email">`{lang="vue"}.
|
||||
Errors are reported directly to the [FormField](/components/form-field) component based on the `name` or `error-pattern` prop. This means the validation rules defined for the `email` attribute in your schema will be applied to `<FormField name="email">`{lang="vue"}.
|
||||
|
||||
Nested validation rules are handled using dot notation. For example, a rule like `{ user: z.object({ email: z.string() }) }`{lang="ts"} will be applied to `<FormField name="user.email">`{lang="vue"}.
|
||||
|
||||
|
||||
@@ -694,7 +694,7 @@ This example uses [refDebounced](https://vueuse.org/shared/refDebounced/#refdebo
|
||||
|
||||
### With custom search
|
||||
|
||||
Use the `filter` prop with an array of fields to filter on.
|
||||
Use the `filter` prop with an array of fields to filter on. Defaults to `[labelKey]`.
|
||||
|
||||
::component-example
|
||||
---
|
||||
@@ -703,6 +703,17 @@ name: 'input-menu-filter-fields-example'
|
||||
---
|
||||
::
|
||||
|
||||
### As a country picker
|
||||
|
||||
This example demonstrates using the InputMenu as a country picker with lazy loading - countries are only fetched when the menu is opened.
|
||||
|
||||
::component-example
|
||||
---
|
||||
collapse: true
|
||||
name: 'input-menu-countries-example'
|
||||
---
|
||||
::
|
||||
|
||||
## API
|
||||
|
||||
### Props
|
||||
|
||||
@@ -137,7 +137,7 @@ Each item can take a `children` array of objects with the following properties t
|
||||
|
||||
Use the `orientation` prop to change the orientation of the NavigationMenu.
|
||||
|
||||
When orientation is `vertical`, a [Collapsible](/components/collapsible) component is used to display children.
|
||||
When orientation is `vertical`, a [Collapsible](/components/collapsible) component is used to display children. You can control the open state of each item using the `open` and `defaultOpen` properties.
|
||||
|
||||
::component-code
|
||||
---
|
||||
@@ -152,6 +152,7 @@ props:
|
||||
items:
|
||||
- - label: Guide
|
||||
icon: i-lucide-book-open
|
||||
defaultOpen: true
|
||||
children:
|
||||
- label: Introduction
|
||||
description: Fully styled and customizable components for Nuxt.
|
||||
|
||||
@@ -200,7 +200,9 @@ props:
|
||||
|
||||
### Search Input
|
||||
|
||||
Use the `search-input` prop to customize the search input. Defaults to `{ placeholder: 'Search...' }`{lang="ts-type"}.
|
||||
Use the `search-input` prop to customize or hide the search input (with `false` value).
|
||||
|
||||
You can pass all the props of the [Input](/components/input) component to customize it.
|
||||
|
||||
::component-code
|
||||
---
|
||||
@@ -219,6 +221,7 @@ props:
|
||||
icon: 'i-lucide-circle-help'
|
||||
searchInput:
|
||||
placeholder: 'Filter...'
|
||||
icon: 'i-lucide-search'
|
||||
items:
|
||||
- label: Backlog
|
||||
icon: 'i-lucide-circle-help'
|
||||
@@ -232,10 +235,6 @@ props:
|
||||
---
|
||||
::
|
||||
|
||||
::tip
|
||||
You can set the `search-input` prop to `false` to hide the search input.
|
||||
::
|
||||
|
||||
### Content
|
||||
|
||||
Use the `content` prop to control how the SelectMenu content is rendered, like its `align` or `side` for example.
|
||||
@@ -732,7 +731,7 @@ This example uses [refDebounced](https://vueuse.org/shared/refDebounced/#refdebo
|
||||
|
||||
### With custom search
|
||||
|
||||
Use the `filter` prop with an array of fields to filter on.
|
||||
Use the `filter` prop with an array of fields to filter on. Defaults to `[labelKey]`.
|
||||
|
||||
::component-example
|
||||
---
|
||||
@@ -741,6 +740,17 @@ name: 'select-menu-filter-fields-example'
|
||||
---
|
||||
::
|
||||
|
||||
### As a country picker
|
||||
|
||||
This example demonstrates using the SelectMenu as a country picker with lazy loading - countries are only fetched when the menu is opened.
|
||||
|
||||
::component-example
|
||||
---
|
||||
collapse: true
|
||||
name: 'select-menu-countries-example'
|
||||
---
|
||||
::
|
||||
|
||||
## API
|
||||
|
||||
### Props
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { createResolver } from '@nuxt/kit'
|
||||
import module from '../src/module'
|
||||
import pkg from '../package.json'
|
||||
|
||||
const { resolve } = createResolver(import.meta.url)
|
||||
@@ -10,7 +9,7 @@ export default defineNuxtConfig({
|
||||
// ],
|
||||
|
||||
modules: [
|
||||
module,
|
||||
'../src/module',
|
||||
'@nuxt/ui-pro',
|
||||
'@nuxt/content',
|
||||
'@nuxt/image',
|
||||
@@ -57,6 +56,7 @@ export default defineNuxtConfig({
|
||||
routeRules: {
|
||||
'/': { redirect: '/getting-started', prerender: false },
|
||||
'/getting-started/installation': { redirect: '/getting-started/installation/nuxt', prerender: false },
|
||||
'/getting-started/i18n': { redirect: '/getting-started/i18n/nuxt', prerender: false },
|
||||
'/composables': { redirect: '/composables/define-shortcuts', prerender: false },
|
||||
'/components': { redirect: '/components/app', prerender: false }
|
||||
},
|
||||
@@ -70,7 +70,8 @@ export default defineNuxtConfig({
|
||||
nitro: {
|
||||
prerender: {
|
||||
routes: [
|
||||
'/getting-started'
|
||||
'/getting-started',
|
||||
'/api/countries.json'
|
||||
// '/api/releases.json',
|
||||
// '/api/pulls.json'
|
||||
],
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
"name": "@nuxt/ui-docs",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@iconify-json/lucide": "^1.2.12",
|
||||
"@iconify-json/lucide": "^1.2.13",
|
||||
"@iconify-json/simple-icons": "^1.2.11",
|
||||
"@iconify-json/vscode-icons": "^1.2.2",
|
||||
"@nuxt/content": "3.0.0-alpha.5",
|
||||
"@nuxt/content": "3.0.0-alpha.6",
|
||||
"@nuxt/image": "^1.8.1",
|
||||
"@nuxt/ui": "latest",
|
||||
"@nuxt/ui-pro": "https://pkg.pr.new/@nuxt/ui-pro@62862c8",
|
||||
"@nuxt/ui-pro": "https://pkg.pr.new/@nuxt/ui-pro@7c62edd",
|
||||
"@nuxthub/core": "^0.8.6",
|
||||
"@nuxtjs/plausible": "^1.0.3",
|
||||
"@octokit/rest": "^21.0.2",
|
||||
@@ -27,6 +27,6 @@
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"wrangler": "^3.85.0"
|
||||
"wrangler": "^3.86.1"
|
||||
}
|
||||
}
|
||||
|
||||
202
docs/server/api/countries.json.get.ts
Normal file
202
docs/server/api/countries.json.get.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
type Country = {
|
||||
name: string
|
||||
code: string
|
||||
emoji: string
|
||||
}
|
||||
|
||||
const countries: Country[] = [
|
||||
{ name: 'Afghanistan', code: 'AF', emoji: '🇦🇫' },
|
||||
{ name: 'Albania', code: 'AL', emoji: '🇦🇱' },
|
||||
{ name: 'Algeria', code: 'DZ', emoji: '🇩🇿' },
|
||||
{ name: 'Andorra', code: 'AD', emoji: '🇦🇩' },
|
||||
{ name: 'Angola', code: 'AO', emoji: '🇦🇴' },
|
||||
{ name: 'Antigua and Barbuda', code: 'AG', emoji: '🇦🇬' },
|
||||
{ name: 'Argentina', code: 'AR', emoji: '🇦🇷' },
|
||||
{ name: 'Armenia', code: 'AM', emoji: '🇦🇲' },
|
||||
{ name: 'Australia', code: 'AU', emoji: '🇦🇺' },
|
||||
{ name: 'Austria', code: 'AT', emoji: '🇦🇹' },
|
||||
{ name: 'Azerbaijan', code: 'AZ', emoji: '🇦🇿' },
|
||||
{ name: 'Bahamas', code: 'BS', emoji: '🇧🇸' },
|
||||
{ name: 'Bahrain', code: 'BH', emoji: '🇧🇭' },
|
||||
{ name: 'Bangladesh', code: 'BD', emoji: '🇧🇩' },
|
||||
{ name: 'Barbados', code: 'BB', emoji: '🇧🇧' },
|
||||
{ name: 'Belarus', code: 'BY', emoji: '🇧🇾' },
|
||||
{ name: 'Belgium', code: 'BE', emoji: '🇧🇪' },
|
||||
{ name: 'Belize', code: 'BZ', emoji: '🇧🇿' },
|
||||
{ name: 'Benin', code: 'BJ', emoji: '🇧🇯' },
|
||||
{ name: 'Bhutan', code: 'BT', emoji: '🇧🇹' },
|
||||
{ name: 'Bolivia', code: 'BO', emoji: '🇧🇴' },
|
||||
{ name: 'Bosnia and Herzegovina', code: 'BA', emoji: '🇧🇦' },
|
||||
{ name: 'Botswana', code: 'BW', emoji: '🇧🇼' },
|
||||
{ name: 'Brazil', code: 'BR', emoji: '🇧🇷' },
|
||||
{ name: 'Brunei', code: 'BN', emoji: '🇧🇳' },
|
||||
{ name: 'Bulgaria', code: 'BG', emoji: '🇧🇬' },
|
||||
{ name: 'Burkina Faso', code: 'BF', emoji: '🇧🇫' },
|
||||
{ name: 'Burundi', code: 'BI', emoji: '🇧🇮' },
|
||||
{ name: 'Cambodia', code: 'KH', emoji: '🇰🇭' },
|
||||
{ name: 'Cameroon', code: 'CM', emoji: '🇨🇲' },
|
||||
{ name: 'Canada', code: 'CA', emoji: '🇨🇦' },
|
||||
{ name: 'Cape Verde', code: 'CV', emoji: '🇨🇻' },
|
||||
{ name: 'Central African Republic', code: 'CF', emoji: '🇨🇫' },
|
||||
{ name: 'Chad', code: 'TD', emoji: '🇹🇩' },
|
||||
{ name: 'Chile', code: 'CL', emoji: '🇨🇱' },
|
||||
{ name: 'China', code: 'CN', emoji: '🇨🇳' },
|
||||
{ name: 'Colombia', code: 'CO', emoji: '🇨🇴' },
|
||||
{ name: 'Comoros', code: 'KM', emoji: '🇰🇲' },
|
||||
{ name: 'Congo', code: 'CG', emoji: '🇨🇬' },
|
||||
{ name: 'Costa Rica', code: 'CR', emoji: '🇨🇷' },
|
||||
{ name: 'Croatia', code: 'HR', emoji: '🇭🇷' },
|
||||
{ name: 'Cuba', code: 'CU', emoji: '🇨🇺' },
|
||||
{ name: 'Cyprus', code: 'CY', emoji: '🇨🇾' },
|
||||
{ name: 'Czech Republic', code: 'CZ', emoji: '🇨🇿' },
|
||||
{ name: 'Denmark', code: 'DK', emoji: '🇩🇰' },
|
||||
{ name: 'Djibouti', code: 'DJ', emoji: '🇩🇯' },
|
||||
{ name: 'Dominica', code: 'DM', emoji: '🇩🇲' },
|
||||
{ name: 'Dominican Republic', code: 'DO', emoji: '🇩🇴' },
|
||||
{ name: 'East Timor', code: 'TL', emoji: '🇹🇱' },
|
||||
{ name: 'Ecuador', code: 'EC', emoji: '🇪🇨' },
|
||||
{ name: 'Egypt', code: 'EG', emoji: '🇪🇬' },
|
||||
{ name: 'El Salvador', code: 'SV', emoji: '🇸🇻' },
|
||||
{ name: 'Equatorial Guinea', code: 'GQ', emoji: '🇬🇶' },
|
||||
{ name: 'Eritrea', code: 'ER', emoji: '🇪🇷' },
|
||||
{ name: 'Estonia', code: 'EE', emoji: '🇪🇪' },
|
||||
{ name: 'Ethiopia', code: 'ET', emoji: '🇪🇹' },
|
||||
{ name: 'Fiji', code: 'FJ', emoji: '🇫🇯' },
|
||||
{ name: 'Finland', code: 'FI', emoji: '🇫🇮' },
|
||||
{ name: 'France', code: 'FR', emoji: '🇫🇷' },
|
||||
{ name: 'Gabon', code: 'GA', emoji: '🇬🇦' },
|
||||
{ name: 'Gambia', code: 'GM', emoji: '🇬🇲' },
|
||||
{ name: 'Georgia', code: 'GE', emoji: '🇬🇪' },
|
||||
{ name: 'Germany', code: 'DE', emoji: '🇩🇪' },
|
||||
{ name: 'Ghana', code: 'GH', emoji: '🇬🇭' },
|
||||
{ name: 'Greece', code: 'GR', emoji: '🇬🇷' },
|
||||
{ name: 'Grenada', code: 'GD', emoji: '🇬🇩' },
|
||||
{ name: 'Guatemala', code: 'GT', emoji: '🇬🇹' },
|
||||
{ name: 'Guinea', code: 'GN', emoji: '🇬🇳' },
|
||||
{ name: 'Guinea-Bissau', code: 'GW', emoji: '🇬🇼' },
|
||||
{ name: 'Guyana', code: 'GY', emoji: '🇬🇾' },
|
||||
{ name: 'Haiti', code: 'HT', emoji: '🇭🇹' },
|
||||
{ name: 'Honduras', code: 'HN', emoji: '🇭🇳' },
|
||||
{ name: 'Hungary', code: 'HU', emoji: '🇭🇺' },
|
||||
{ name: 'Iceland', code: 'IS', emoji: '🇮🇸' },
|
||||
{ name: 'India', code: 'IN', emoji: '🇮🇳' },
|
||||
{ name: 'Indonesia', code: 'ID', emoji: '🇮🇩' },
|
||||
{ name: 'Iran', code: 'IR', emoji: '🇮🇷' },
|
||||
{ name: 'Iraq', code: 'IQ', emoji: '🇮🇶' },
|
||||
{ name: 'Ireland', code: 'IE', emoji: '🇮🇪' },
|
||||
{ name: 'Israel', code: 'IL', emoji: '🇮🇱' },
|
||||
{ name: 'Italy', code: 'IT', emoji: '🇮🇹' },
|
||||
{ name: 'Jamaica', code: 'JM', emoji: '🇯🇲' },
|
||||
{ name: 'Japan', code: 'JP', emoji: '🇯🇵' },
|
||||
{ name: 'Jordan', code: 'JO', emoji: '🇯🇴' },
|
||||
{ name: 'Kazakhstan', code: 'KZ', emoji: '🇰🇿' },
|
||||
{ name: 'Kenya', code: 'KE', emoji: '🇰🇪' },
|
||||
{ name: 'Kiribati', code: 'KI', emoji: '🇰🇷' },
|
||||
{ name: 'Kuwait', code: 'KW', emoji: '🇰🇼' },
|
||||
{ name: 'Kyrgyzstan', code: 'KG', emoji: '🇰🇬' },
|
||||
{ name: 'Laos', code: 'LA', emoji: '🇱🇦' },
|
||||
{ name: 'Latvia', code: 'LV', emoji: '🇱🇻' },
|
||||
{ name: 'Lebanon', code: 'LB', emoji: '🇱🇧' },
|
||||
{ name: 'Lesotho', code: 'LS', emoji: '🇱🇸' },
|
||||
{ name: 'Liberia', code: 'LR', emoji: '🇱🇷' },
|
||||
{ name: 'Libya', code: 'LY', emoji: '🇱🇾' },
|
||||
{ name: 'Liechtenstein', code: 'LI', emoji: '🇱🇮' },
|
||||
{ name: 'Lithuania', code: 'LT', emoji: '🇱🇹' },
|
||||
{ name: 'Luxembourg', code: 'LU', emoji: '🇱🇺' },
|
||||
{ name: 'Madagascar', code: 'MG', emoji: '🇲🇬' },
|
||||
{ name: 'Malawi', code: 'MW', emoji: '🇲🇼' },
|
||||
{ name: 'Malaysia', code: 'MY', emoji: '🇲🇾' },
|
||||
{ name: 'Maldives', code: 'MV', emoji: '🇲🇻' },
|
||||
{ name: 'Mali', code: 'ML', emoji: '🇲🇱' },
|
||||
{ name: 'Malta', code: 'MT', emoji: '🇲🇹' },
|
||||
{ name: 'Marshall Islands', code: 'MH', emoji: '🇲🇭' },
|
||||
{ name: 'Mauritania', code: 'MR', emoji: '🇲🇦' },
|
||||
{ name: 'Mauritius', code: 'MU', emoji: '🇲🇺' },
|
||||
{ name: 'Mexico', code: 'MX', emoji: '🇲🇽' },
|
||||
{ name: 'Micronesia', code: 'FM', emoji: '🇫🇲' },
|
||||
{ name: 'Moldova', code: 'MD', emoji: '🇲🇩' },
|
||||
{ name: 'Monaco', code: 'MC', emoji: '🇲🇨' },
|
||||
{ name: 'Mongolia', code: 'MN', emoji: '🇲🇳' },
|
||||
{ name: 'Montenegro', code: 'ME', emoji: '🇲🇪' },
|
||||
{ name: 'Morocco', code: 'MA', emoji: '🇲🇦' },
|
||||
{ name: 'Mozambique', code: 'MZ', emoji: '🇲🇿' },
|
||||
{ name: 'Myanmar', code: 'MM', emoji: '🇲🇲' },
|
||||
{ name: 'Namibia', code: 'NA', emoji: '🇳🇦' },
|
||||
{ name: 'Nauru', code: 'NR', emoji: '🇳🇷' },
|
||||
{ name: 'Nepal', code: 'NP', emoji: '🇳🇵' },
|
||||
{ name: 'Netherlands', code: 'NL', emoji: '🇳🇱' },
|
||||
{ name: 'New Zealand', code: 'NZ', emoji: '🇳🇿' },
|
||||
{ name: 'Nicaragua', code: 'NI', emoji: '🇳🇮' },
|
||||
{ name: 'Niger', code: 'NE', emoji: '🇳🇪' },
|
||||
{ name: 'Nigeria', code: 'NG', emoji: '🇳🇬' },
|
||||
{ name: 'North Macedonia', code: 'MK', emoji: '🇲🇰' },
|
||||
{ name: 'Norway', code: 'NO', emoji: '🇳🇴' },
|
||||
{ name: 'Oman', code: 'OM', emoji: '🇴🇲' },
|
||||
{ name: 'Pakistan', code: 'PK', emoji: '🇵🇰' },
|
||||
{ name: 'Palau', code: 'PW', emoji: '🇵🇼' },
|
||||
{ name: 'Palestine', code: 'PS', emoji: '🇵🇸' },
|
||||
{ name: 'Panama', code: 'PA', emoji: '🇵🇦' },
|
||||
{ name: 'Papua New Guinea', code: 'PG', emoji: '🇵🇬' },
|
||||
{ name: 'Paraguay', code: 'PY', emoji: '🇵🇾' },
|
||||
{ name: 'Peru', code: 'PE', emoji: '🇵🇪' },
|
||||
{ name: 'Philippines', code: 'PH', emoji: '🇵🇭' },
|
||||
{ name: 'Poland', code: 'PL', emoji: '🇵🇱' },
|
||||
{ name: 'Portugal', code: 'PT', emoji: '🇵🇹' },
|
||||
{ name: 'Qatar', code: 'QA', emoji: '🇶🇦' },
|
||||
{ name: 'Romania', code: 'RO', emoji: '🇷🇴' },
|
||||
{ name: 'Russia', code: 'RU', emoji: '🇷🇺' },
|
||||
{ name: 'Rwanda', code: 'RW', emoji: '🇷🇼' },
|
||||
{ name: 'Saint Kitts and Nevis', code: 'KN', emoji: '🇰🇳' },
|
||||
{ name: 'Saint Lucia', code: 'LC', emoji: '🇱🇨' },
|
||||
{ name: 'Saint Vincent and the Grenadines', code: 'VC', emoji: '🇻🇨' },
|
||||
{ name: 'Samoa', code: 'WS', emoji: '🇼🇸' },
|
||||
{ name: 'San Marino', code: 'SM', emoji: '🇸🇲' },
|
||||
{ name: 'Sao Tome and Principe', code: 'ST', emoji: '🇸🇹' },
|
||||
{ name: 'Saudi Arabia', code: 'SA', emoji: '🇸🇦' },
|
||||
{ name: 'Senegal', code: 'SN', emoji: '🇸🇳' },
|
||||
{ name: 'Serbia', code: 'RS', emoji: '🇷🇸' },
|
||||
{ name: 'Seychelles', code: 'SC', emoji: '🇸🇨' },
|
||||
{ name: 'Sierra Leone', code: 'SL', emoji: '🇸🇱' },
|
||||
{ name: 'Singapore', code: 'SG', emoji: '🇸🇬' },
|
||||
{ name: 'Slovakia', code: 'SK', emoji: '🇸🇰' },
|
||||
{ name: 'Slovenia', code: 'SI', emoji: '🇸🇮' },
|
||||
{ name: 'Solomon Islands', code: 'SB', emoji: '🇸🇧' },
|
||||
{ name: 'Somalia', code: 'SO', emoji: '🇸🇴' },
|
||||
{ name: 'South Africa', code: 'ZA', emoji: '🇿🇦' },
|
||||
{ name: 'South Korea', code: 'KR', emoji: '🇰🇷' },
|
||||
{ name: 'South Sudan', code: 'SS', emoji: '🇸🇸' },
|
||||
{ name: 'Spain', code: 'ES', emoji: '🇪🇸' },
|
||||
{ name: 'Sri Lanka', code: 'LK', emoji: '🇱🇰' },
|
||||
{ name: 'Sudan', code: 'SD', emoji: '🇸🇩' },
|
||||
{ name: 'Suriname', code: 'SR', emoji: '🇸🇷' },
|
||||
{ name: 'Sweden', code: 'SE', emoji: '🇸🇪' },
|
||||
{ name: 'Switzerland', code: 'CH', emoji: '🇨🇭' },
|
||||
{ name: 'Syria', code: 'SY', emoji: '🇸🇾' },
|
||||
{ name: 'Taiwan', code: 'TW', emoji: '🇹🇼' },
|
||||
{ name: 'Tajikistan', code: 'TJ', emoji: '🇹🇯' },
|
||||
{ name: 'Tanzania', code: 'TZ', emoji: '🇹🇿' },
|
||||
{ name: 'Thailand', code: 'TH', emoji: '🇹🇭' },
|
||||
{ name: 'Togo', code: 'TG', emoji: '🇹🇬' },
|
||||
{ name: 'Tonga', code: 'TO', emoji: '🇹🇴' },
|
||||
{ name: 'Trinidad and Tobago', code: 'TT', emoji: '🇹🇹' },
|
||||
{ name: 'Tunisia', code: 'TN', emoji: '🇹🇳' },
|
||||
{ name: 'Turkey', code: 'TR', emoji: '🇹🇷' },
|
||||
{ name: 'Turkmenistan', code: 'TM', emoji: '🇹🇲' },
|
||||
{ name: 'Tuvalu', code: 'TV', emoji: '🇹🇻' },
|
||||
{ name: 'Uganda', code: 'UG', emoji: '🇺🇬' },
|
||||
{ name: 'Ukraine', code: 'UA', emoji: '🇺🇦' },
|
||||
{ name: 'United Arab Emirates', code: 'AE', emoji: '🇦🇪' },
|
||||
{ name: 'United Kingdom', code: 'GB', emoji: '🇬🇧' },
|
||||
{ name: 'United States', code: 'US', emoji: '🇺🇸' },
|
||||
{ name: 'Uruguay', code: 'UY', emoji: '🇺🇾' },
|
||||
{ name: 'Uzbekistan', code: 'UZ', emoji: '🇺🇿' },
|
||||
{ name: 'Vanuatu', code: 'VU', emoji: '🇻🇺' },
|
||||
{ name: 'Vatican City', code: 'VA', emoji: '🇻🇦' },
|
||||
{ name: 'Venezuela', code: 'VE', emoji: '🇻🇪' },
|
||||
{ name: 'Vietnam', code: 'VN', emoji: '🇻🇳' },
|
||||
{ name: 'Yemen', code: 'YE', emoji: '🇾🇪' },
|
||||
{ name: 'Zambia', code: 'ZM', emoji: '🇿🇲' },
|
||||
{ name: 'Zimbabwe', code: 'ZW', emoji: '🇿🇼' }
|
||||
]
|
||||
|
||||
export default eventHandler(async () => countries)
|
||||
19
package.json
19
package.json
@@ -30,7 +30,11 @@
|
||||
"./vue-plugin": {
|
||||
"types": "./vue-plugin.d.ts"
|
||||
},
|
||||
"./runtime/*": "./dist/runtime/*"
|
||||
"./runtime/*": "./dist/runtime/*",
|
||||
"./locale": {
|
||||
"types": "./dist/runtime/locale/index.d.ts",
|
||||
"import": "./dist/runtime/locale/index.js"
|
||||
}
|
||||
},
|
||||
"imports": {
|
||||
"#build/ui/*": "./.nuxt/ui/*.ts"
|
||||
@@ -70,12 +74,12 @@
|
||||
"@iconify/vue": "^4.1.2",
|
||||
"@nuxt/devtools-kit": "^1.6.0",
|
||||
"@nuxt/fonts": "^0.10.2",
|
||||
"@nuxt/icon": "^1.6.1",
|
||||
"@nuxt/icon": "^1.7.2",
|
||||
"@nuxt/kit": "^3.14.159",
|
||||
"@nuxt/schema": "^3.14.159",
|
||||
"@nuxtjs/color-mode": "^3.5.2",
|
||||
"@tailwindcss/postcss": "4.0.0-alpha.30",
|
||||
"@tailwindcss/vite": "4.0.0-alpha.30",
|
||||
"@tailwindcss/postcss": "4.0.0-alpha.33",
|
||||
"@tailwindcss/vite": "4.0.0-alpha.33",
|
||||
"@tanstack/vue-table": "^8.20.5",
|
||||
"@unhead/vue": "^1.11.11",
|
||||
"@vueuse/core": "^11.2.0",
|
||||
@@ -96,11 +100,11 @@
|
||||
"mlly": "^1.7.2",
|
||||
"ohash": "^1.1.4",
|
||||
"pathe": "^1.1.2",
|
||||
"radix-vue": "^1.9.8",
|
||||
"radix-vue": "^1.9.9",
|
||||
"scule": "^1.3.0",
|
||||
"sirv": "^3.0.0",
|
||||
"tailwind-variants": "^0.2.1",
|
||||
"tailwindcss": "4.0.0-alpha.30",
|
||||
"tailwindcss": "4.0.0-alpha.33",
|
||||
"tinyglobby": "^0.2.10",
|
||||
"unplugin": "^1.15.0",
|
||||
"unplugin-auto-import": "^0.18.3",
|
||||
@@ -112,7 +116,7 @@
|
||||
"@nuxt/module-builder": "^0.8.4",
|
||||
"@nuxt/test-utils": "^3.14.4",
|
||||
"@release-it/conventional-changelog": "^9.0.2",
|
||||
"@standard-schema/spec": "1.0.0-beta.1",
|
||||
"@standard-schema/spec": "1.0.0-beta.3",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"embla-carousel": "^8.3.1",
|
||||
"eslint": "^9.14.0",
|
||||
@@ -135,6 +139,7 @@
|
||||
},
|
||||
"resolutions": {
|
||||
"@nuxt/ui": "workspace:*",
|
||||
"@nuxt/content": "3.0.0-alpha.5",
|
||||
"happy-dom": "14.12.3",
|
||||
"rollup": "^4.24.0"
|
||||
}
|
||||
|
||||
@@ -14,11 +14,11 @@
|
||||
"vue-router": "^4.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
"@vitejs/plugin-vue": "^5.1.5",
|
||||
"typescript": "^5.6.3",
|
||||
"unplugin-auto-import": "^0.18.3",
|
||||
"unplugin-vue-components": "^0.27.4",
|
||||
"vite": "^5.4.10",
|
||||
"vite": "^5.4.11",
|
||||
"vue-tsc": "^2.1.10"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { z } from 'zod'
|
||||
import type { FormSubmitEvent } from '@nuxt/ui'
|
||||
|
||||
const schema = z.object({
|
||||
input: z.string().min(10),
|
||||
inputMenu: z.any().refine(option => option?.value === 'option-2', {
|
||||
message: 'Select Option 2'
|
||||
}),
|
||||
inputMenuMultiple: z.any().refine(values => !!values?.find((option: any) => option.value === 'option-2'), {
|
||||
message: 'Include Option 2'
|
||||
}),
|
||||
textarea: z.string().min(10),
|
||||
select: z.string().refine(value => value === 'option-2', {
|
||||
message: 'Select Option 2'
|
||||
}),
|
||||
selectMenu: z.any().refine(option => option?.value === 'option-2', {
|
||||
message: 'Select Option 2'
|
||||
}),
|
||||
selectMenuMultiple: z.any().refine(values => !!values?.find((option: any) => option.value === 'option-2'), {
|
||||
message: 'Include Option 2'
|
||||
}),
|
||||
switch: z.boolean().refine(value => value === true, {
|
||||
message: 'Toggle me'
|
||||
}),
|
||||
checkbox: z.boolean().refine(value => value === true, {
|
||||
message: 'Check me'
|
||||
}),
|
||||
radioGroup: z.string().refine(value => value === 'option-2', {
|
||||
message: 'Select Option 2'
|
||||
}),
|
||||
slider: z.number().max(20, { message: 'Must be less than 20' })
|
||||
})
|
||||
|
||||
type Schema = z.output<typeof schema>
|
||||
|
||||
const state = reactive<Partial<Schema>>({})
|
||||
|
||||
const form = useTemplateRef('form')
|
||||
|
||||
const items = [
|
||||
{ label: 'Option 1', value: 'option-1' },
|
||||
{ label: 'Option 2', value: 'option-2' },
|
||||
{ label: 'Option 3', value: 'option-3' }
|
||||
]
|
||||
|
||||
function onSubmit(event: FormSubmitEvent<Schema>) {
|
||||
console.log(event.data)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UForm ref="form" :state="state" :schema="schema" class="gap-4 flex flex-col w-60" @submit="onSubmit">
|
||||
<UFormField label="Input" name="input">
|
||||
<UInput v-model="state.input" placeholder="john@lennon.com" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField label="Textarea" name="textarea">
|
||||
<UTextarea v-model="state.textarea" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField name="select" label="Select">
|
||||
<USelect v-model="state.select" class="w-44" :items="items" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField name="selectMenu" label="Select Menu">
|
||||
<USelectMenu v-model="state.selectMenu" class="w-44" :items="items" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField name="selectMenuMultiple" label="Select Menu (Multiple)">
|
||||
<USelectMenu v-model="state.selectMenuMultiple" class="w-44" multiple :items="items" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField name="inputMenu" label="Input Menu">
|
||||
<UInputMenu v-model="state.inputMenu" :items="items" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField name="inputMenuMultiple" label="Input Menu (Multiple)">
|
||||
<UInputMenu v-model="state.inputMenuMultiple" multiple :items="items" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField name="checkbox">
|
||||
<UCheckbox v-model="state.checkbox" label="Check me" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField name="radioGroup">
|
||||
<URadioGroup v-model="state.radioGroup" legend="Radio group" :items="items" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField name="switch">
|
||||
<USwitch v-model="state.switch" label="Switch me" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField name="slider" label="Slider">
|
||||
<USlider v-model="state.slider" />
|
||||
</UFormField>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<UButton color="neutral" type="submit">
|
||||
Submit
|
||||
</UButton>
|
||||
|
||||
<UButton color="neutral" variant="outline" @click="form?.clear()">
|
||||
Clear
|
||||
</UButton>
|
||||
</div>
|
||||
</UForm>
|
||||
</template>
|
||||
@@ -1,65 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { z } from 'zod'
|
||||
import type { FormSubmitEvent } from '@nuxt/ui'
|
||||
|
||||
const schema = z.object({
|
||||
email: z.string().min(2),
|
||||
password: z.string().min(8)
|
||||
})
|
||||
|
||||
type Schema = z.output<typeof schema>
|
||||
|
||||
const nestedSchema = z.object({
|
||||
phone: z.string().length(10)
|
||||
})
|
||||
|
||||
type NestedSchema = z.output<typeof nestedSchema>
|
||||
|
||||
const state = reactive<Partial<Schema & { nested: Partial<NestedSchema> }>>({
|
||||
nested: {}
|
||||
})
|
||||
|
||||
const checked = ref(false)
|
||||
|
||||
function onSubmit(event: FormSubmitEvent<Schema>) {
|
||||
console.log('Success', event.data)
|
||||
}
|
||||
|
||||
function onError(event: any) {
|
||||
console.log('Error', event)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UForm
|
||||
:state="state"
|
||||
:schema="schema"
|
||||
class="gap-4 flex flex-col w-60"
|
||||
@submit="onSubmit"
|
||||
@error="onError"
|
||||
>
|
||||
<UFormField label="Email" name="email">
|
||||
<UInput v-model="state.email" placeholder="john@lennon.com" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField label="Password" name="password">
|
||||
<UInput v-model="state.password" type="password" />
|
||||
</UFormField>
|
||||
|
||||
<div>
|
||||
<UCheckbox v-model="checked" name="check" label="Check me" @change="state.nested = {}" />
|
||||
</div>
|
||||
|
||||
<UForm v-if="checked && state.nested" :state="state.nested" :schema="nestedSchema">
|
||||
<UFormField label="Phone" name="phone">
|
||||
<UInput v-model="state.nested.phone" />
|
||||
</UFormField>
|
||||
</UForm>
|
||||
|
||||
<div>
|
||||
<UButton color="neutral" type="submit">
|
||||
Submit
|
||||
</UButton>
|
||||
</div>
|
||||
</UForm>
|
||||
</template>
|
||||
@@ -1,83 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { z } from 'zod'
|
||||
import type { FormSubmitEvent } from '@nuxt/ui'
|
||||
|
||||
const schema = z.object({
|
||||
email: z.string().min(2),
|
||||
password: z.string().min(8)
|
||||
})
|
||||
|
||||
type Schema = z.output<typeof schema>
|
||||
|
||||
const itemSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
price: z.string().min(1)
|
||||
})
|
||||
|
||||
type ItemSchema = z.output<typeof itemSchema>
|
||||
|
||||
const state = reactive<Partial<Schema & { items: Partial<ItemSchema>[] }>>({})
|
||||
|
||||
function addItem() {
|
||||
if (!state.items) {
|
||||
state.items = []
|
||||
}
|
||||
state.items.push({})
|
||||
}
|
||||
|
||||
function removeItem() {
|
||||
if (state.items) {
|
||||
state.items.pop()
|
||||
}
|
||||
}
|
||||
|
||||
function onSubmit(event: FormSubmitEvent<Schema>) {
|
||||
console.log('Success', event.data)
|
||||
}
|
||||
|
||||
function onError(event: any) {
|
||||
console.log('Error', event)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UForm
|
||||
:state="state"
|
||||
:schema="schema"
|
||||
class="gap-4 flex flex-col w-60"
|
||||
@submit="onSubmit"
|
||||
@error="onError"
|
||||
>
|
||||
<UFormField label="Email" name="email">
|
||||
<UInput v-model="state.email" placeholder="john@lennon.com" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField label="Password" name="password">
|
||||
<UInput v-model="state.password" type="password" />
|
||||
</UFormField>
|
||||
|
||||
<UForm v-for="item, count in state.items" :key="count" :state="item" :schema="itemSchema" class="flex gap-2">
|
||||
<UFormField label="Name" name="name">
|
||||
<UInput v-model="item.name" />
|
||||
</UFormField>
|
||||
<UFormField label="Price" name="price">
|
||||
<UInput v-model="item.price" />
|
||||
</UFormField>
|
||||
</UForm>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<UButton color="neutral" variant="subtle" size="sm" @click="addItem()">
|
||||
Add Item
|
||||
</UButton>
|
||||
|
||||
<UButton color="neutral" variant="ghost" size="sm" @click="removeItem()">
|
||||
Remove Item
|
||||
</UButton>
|
||||
</div>
|
||||
<div>
|
||||
<UButton color="neutral" type="submit">
|
||||
Submit
|
||||
</UButton>
|
||||
</div>
|
||||
</UForm>
|
||||
</template>
|
||||
@@ -1,6 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { z } from 'zod'
|
||||
import type { FormSubmitEvent } from '@nuxt/ui'
|
||||
import FormExampleElements from '../../../../docs/app/components/content/examples/form/FormExampleElements.vue'
|
||||
import FormExampleNestedList from '../../../../docs/app/components/content/examples/form/FormExampleNestedList.vue'
|
||||
import FormExampleNested from '../../../../docs/app/components/content/examples/form/FormExampleNested.vue'
|
||||
|
||||
const schema = z.object({
|
||||
email: z.string().email(),
|
||||
@@ -8,18 +11,20 @@ const schema = z.object({
|
||||
tos: z.literal(true)
|
||||
})
|
||||
|
||||
type Schema = z.output<typeof schema>
|
||||
type Schema = z.input<typeof schema>
|
||||
|
||||
const state = reactive<Partial<Schema>>({})
|
||||
const state2 = reactive<Partial<Schema>>({})
|
||||
|
||||
function onSubmit(event: FormSubmitEvent<Schema>) {
|
||||
console.log(event.data)
|
||||
}
|
||||
|
||||
const validateOn = ref(['input', 'change', 'blur'])
|
||||
const disabled = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-8">
|
||||
<div class="flex gap-4">
|
||||
<UForm
|
||||
:state="state"
|
||||
@@ -40,75 +45,24 @@ function onSubmit(event: FormSubmitEvent<Schema>) {
|
||||
</UFormField>
|
||||
|
||||
<div>
|
||||
<UButton color="neutral" type="submit">
|
||||
<UButton type="submit">
|
||||
Submit
|
||||
</UButton>
|
||||
</div>
|
||||
</UForm>
|
||||
|
||||
<UForm
|
||||
:state="state2"
|
||||
:schema="schema"
|
||||
class="gap-4 flex flex-col w-60"
|
||||
:validate-on-input-delay="2000"
|
||||
@submit="onSubmit"
|
||||
>
|
||||
<UFormField label="Email" name="email">
|
||||
<UInput v-model="state2.email" placeholder="john@lennon.com" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField
|
||||
label="Password"
|
||||
name="password"
|
||||
:validate-on-input-delay="50"
|
||||
eager-validation
|
||||
>
|
||||
<UInput v-model="state2.password" type="password" />
|
||||
</UFormField>
|
||||
|
||||
<div>
|
||||
<UButton color="neutral" type="submit">
|
||||
Submit
|
||||
</UButton>
|
||||
</div>
|
||||
</UForm>
|
||||
<FormNestedExample />
|
||||
<FormNestedListExample />
|
||||
<FormExampleNested />
|
||||
<FormExampleNestedList />
|
||||
</div>
|
||||
|
||||
<USeparator class="my-8" />
|
||||
<div class="border border-[var(--ui-border)] rounded-lg">
|
||||
<div class="py-2 px-4 flex gap-4 items-center">
|
||||
<UFormField label="Validate on" class="flex items-center gap-2">
|
||||
<USelectMenu v-model="validateOn" :items="['input', 'change', 'blur']" multiple class="w-48" />
|
||||
</UFormField>
|
||||
<UCheckbox v-model="disabled" label="Disabled" />
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 flex-wrap">
|
||||
<div>
|
||||
<p class="text-lg font-bold underline mb-4">
|
||||
Validate on input
|
||||
</p>
|
||||
<FormElementsExample :validate-on="['input']" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-lg font-bold underline mb-4">
|
||||
Validate on change
|
||||
</p>
|
||||
<FormElementsExample :validate-on="['change']" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-lg font-bold underline mb-4">
|
||||
Validate on blur
|
||||
</p>
|
||||
<FormElementsExample :validate-on="['blur']" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-lg font-bold underline mb-4">
|
||||
Default
|
||||
</p>
|
||||
<FormElementsExample />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-lg font-bold underline mb-4">
|
||||
Disabled
|
||||
</p>
|
||||
<FormElementsExample disabled />
|
||||
</div>
|
||||
<FormExampleElements :validate-on="validateOn" :disabled="disabled" class="border-t border-[var(--ui-border)] p-4" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -8,7 +8,7 @@ const orientations = Object.keys(theme.variants.orientation)
|
||||
const color = ref(theme.defaultVariants.color)
|
||||
const highlightColor = ref()
|
||||
const variant = ref(theme.defaultVariants.variant)
|
||||
const orientation = ref('horizontal' as const)
|
||||
const orientation = ref('vertical' as const)
|
||||
const highlight = ref(true)
|
||||
|
||||
const items = [
|
||||
@@ -16,6 +16,7 @@ const items = [
|
||||
label: 'Documentation',
|
||||
icon: 'i-lucide-book-open',
|
||||
badge: 10,
|
||||
defaultOpen: true,
|
||||
children: [{
|
||||
label: 'Introduction',
|
||||
description: 'Fully styled and customizable components for Nuxt.',
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"generate": "nuxi generate"
|
||||
},
|
||||
"dependencies": {
|
||||
"@iconify-json/lucide": "^1.2.12",
|
||||
"@iconify-json/lucide": "^1.2.13",
|
||||
"@nuxt/ui": "latest",
|
||||
"nuxt": "^3.14.159"
|
||||
}
|
||||
|
||||
1661
pnpm-lock.yaml
generated
1661
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,36 +0,0 @@
|
||||
import { promises as fsp } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { execSync } from 'node:child_process'
|
||||
|
||||
async function loadPackage(dir: string) {
|
||||
const pkgPath = resolve(dir, 'package.json')
|
||||
|
||||
const data = JSON.parse(await fsp.readFile(pkgPath, 'utf-8').catch(() => '{}'))
|
||||
|
||||
const save = () => fsp.writeFile(pkgPath, JSON.stringify(data, null, 2) + '\n')
|
||||
|
||||
return {
|
||||
dir,
|
||||
data,
|
||||
save
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const pkg = await loadPackage(process.cwd())
|
||||
|
||||
const commit = execSync('git rev-parse --short HEAD').toString('utf-8').trim()
|
||||
|
||||
const date = Math.round(Date.now() / (1000 * 60))
|
||||
|
||||
pkg.data.name = `${pkg.data.name}-edge`
|
||||
|
||||
pkg.data.version = `${pkg.data.version}-${date}.${commit}`
|
||||
|
||||
pkg.save()
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -1,19 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Restore all git changes
|
||||
git restore -s@ -SW -- .
|
||||
|
||||
# Bump versions to edge
|
||||
pnpm jiti ./scripts/bump-edge
|
||||
|
||||
# Update token
|
||||
if [[ ! -z ${NODE_AUTH_TOKEN} ]] ; then
|
||||
echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" >> ~/.npmrc
|
||||
echo "registry=https://registry.npmjs.org/" >> ~/.npmrc
|
||||
echo "always-auth=true" >> ~/.npmrc
|
||||
npm whoami
|
||||
fi
|
||||
|
||||
# Release package
|
||||
echo "Publishing @nuxt/ui"
|
||||
npm publish -q --access public
|
||||
@@ -1,16 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Restore all git changes
|
||||
git restore -s@ -SW -- .
|
||||
|
||||
# Update token
|
||||
if [[ ! -z ${NODE_AUTH_TOKEN} ]] ; then
|
||||
echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" >> ~/.npmrc
|
||||
echo "registry=https://registry.npmjs.org/" >> ~/.npmrc
|
||||
echo "always-auth=true" >> ~/.npmrc
|
||||
npm whoami
|
||||
fi
|
||||
|
||||
# Release package
|
||||
echo "Publishing @nuxt/ui"
|
||||
npm publish -q --access public
|
||||
@@ -176,16 +176,18 @@ export default defineNuxtModule<ModuleOptions>({
|
||||
|
||||
nuxt.options.routeRules = defu(nuxt.options.routeRules, { '/__nuxt_ui__/**': { ssr: false } })
|
||||
extendPages((pages) => {
|
||||
pages.unshift({
|
||||
name: 'ui-devtools',
|
||||
path: '/__nuxt_ui__/components/:slug',
|
||||
file: resolve('./devtools/runtime/DevtoolsRenderer.vue'),
|
||||
meta: {
|
||||
// https://github.com/nuxt/nuxt/pull/29366
|
||||
// isolate: true
|
||||
layout: false
|
||||
}
|
||||
})
|
||||
if (pages.length) {
|
||||
pages.unshift({
|
||||
name: 'ui-devtools',
|
||||
path: '/__nuxt_ui__/components/:slug',
|
||||
file: resolve('./devtools/runtime/DevtoolsRenderer.vue'),
|
||||
meta: {
|
||||
// https://github.com/nuxt/nuxt/pull/29366
|
||||
// isolate: true
|
||||
layout: false
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
addCustomTab({
|
||||
|
||||
@@ -3,6 +3,7 @@ import { tv, type VariantProps } from 'tailwind-variants'
|
||||
import type { AppConfig } from '@nuxt/schema'
|
||||
import _appConfig from '#build/app.config'
|
||||
import theme from '#build/ui/alert'
|
||||
import { useLocale } from '../composables/useLocale'
|
||||
import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
|
||||
import type { AvatarProps, ButtonProps } from '../types'
|
||||
|
||||
@@ -74,6 +75,7 @@ const emits = defineEmits<AlertEmits>()
|
||||
const slots = defineSlots<AlertSlots>()
|
||||
|
||||
const appConfig = useAppConfig()
|
||||
const { t } = useLocale()
|
||||
|
||||
const multiline = computed(() => !!props.title && !!props.description)
|
||||
|
||||
@@ -123,7 +125,7 @@ const ui = computed(() => alert({
|
||||
size="md"
|
||||
color="neutral"
|
||||
variant="link"
|
||||
aria-label="Close"
|
||||
:aria-label="t('ui.alert.close')"
|
||||
v-bind="typeof close === 'object' ? close : undefined"
|
||||
:class="ui.close({ class: props.ui?.close })"
|
||||
@click="emits('update:open', false)"
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<script lang="ts">
|
||||
import type { ConfigProviderProps, TooltipProviderProps } from 'radix-vue'
|
||||
import { localeContextInjectionKey } from '../composables/useLocale'
|
||||
import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
|
||||
import type { ToasterProps } from '../types'
|
||||
import type { ToasterProps, Locale } from '../types'
|
||||
|
||||
export interface AppProps extends Omit<ConfigProviderProps, 'useId'> {
|
||||
tooltip?: TooltipProviderProps
|
||||
toaster?: ToasterProps | null
|
||||
locale?: Locale
|
||||
}
|
||||
|
||||
export interface AppSlots {
|
||||
@@ -20,7 +22,7 @@ extendDevtoolsMeta({ ignore: true })
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { toRef, useId } from 'vue'
|
||||
import { toRef, useId, provide, computed } from 'vue'
|
||||
import { ConfigProvider, TooltipProvider, useForwardProps } from 'radix-vue'
|
||||
import { reactivePick } from '@vueuse/core'
|
||||
import UToaster from './Toaster.vue'
|
||||
@@ -33,6 +35,8 @@ defineSlots<AppSlots>()
|
||||
const configProviderProps = useForwardProps(reactivePick(props, 'dir', 'scrollBody'))
|
||||
const tooltipProps = toRef(() => props.tooltip)
|
||||
const toasterProps = toRef(() => props.toaster)
|
||||
|
||||
provide(localeContextInjectionKey, computed(() => props.locale))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { FadeOptionsType } from 'embla-carousel-fade'
|
||||
import type { WheelGesturesPluginOptions } from 'embla-carousel-wheel-gestures'
|
||||
import _appConfig from '#build/app.config'
|
||||
import theme from '#build/ui/carousel'
|
||||
import { useLocale } from '../composables/useLocale'
|
||||
import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
|
||||
import type { ButtonProps } from '../types'
|
||||
import type { AcceptableValue, PartialString } from '../types/utils'
|
||||
@@ -134,6 +135,7 @@ const props = withDefaults(defineProps<CarouselProps<T>>(), {
|
||||
defineSlots<CarouselSlots<T>>()
|
||||
|
||||
const appConfig = useAppConfig()
|
||||
const { t } = useLocale()
|
||||
const rootProps = useForwardProps(reactivePick(props, 'active', 'align', 'breakpoints', 'containScroll', 'dragFree', 'dragThreshold', 'duration', 'inViewThreshold', 'loop', 'skipSnaps', 'slidesToScroll', 'startIndex', 'watchDrag', 'watchResize', 'watchSlides', 'watchFocus'))
|
||||
|
||||
const ui = computed(() => carousel({
|
||||
@@ -279,7 +281,7 @@ defineExpose({
|
||||
size="md"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
aria-label="Prev"
|
||||
:aria-label="t('ui.carousel.prev')"
|
||||
v-bind="typeof prev === 'object' ? prev : undefined"
|
||||
:class="ui.prev({ class: props.ui?.prev })"
|
||||
@click="scrollPrev"
|
||||
@@ -290,7 +292,7 @@ defineExpose({
|
||||
size="md"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
aria-label="Next"
|
||||
:aria-label="t('ui.carousel.next')"
|
||||
v-bind="typeof next === 'object' ? next : undefined"
|
||||
:class="ui.next({ class: props.ui?.next })"
|
||||
@click="scrollNext"
|
||||
@@ -300,7 +302,7 @@ defineExpose({
|
||||
<div v-if="dots" :class="ui.dots({ class: props.ui?.dots })">
|
||||
<template v-for="(_, index) in scrollSnaps" :key="index">
|
||||
<button
|
||||
:aria-label="`Go to slide ${index + 1}`"
|
||||
:aria-label="t('ui.carousel.goto', { slide: index + 1 })"
|
||||
:class="ui.dot({ class: props.ui?.dot, active: selectedIndex === index })"
|
||||
@click="scrollTo(index)"
|
||||
/>
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { UseFuseOptions } from '@vueuse/integrations/useFuse'
|
||||
import _appConfig from '#build/app.config'
|
||||
import theme from '#build/ui/command-palette'
|
||||
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
|
||||
import { useLocale } from '../composables/useLocale'
|
||||
import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
|
||||
import type { AvatarProps, ButtonProps, ChipProps, KbdProps, InputProps } from '../types'
|
||||
import type { DynamicSlots, PartialString } from '../types/utils'
|
||||
@@ -144,6 +145,7 @@ const slots = defineSlots<CommandPaletteSlots<G, T>>()
|
||||
const searchTerm = defineModel<string>('searchTerm', { default: '' })
|
||||
|
||||
const appConfig = useAppConfig()
|
||||
const { t } = useLocale()
|
||||
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'disabled', 'multiple', 'modelValue', 'defaultValue', 'selectedValue', 'resetSearchTermOnBlur'), emits)
|
||||
const inputProps = useForwardProps(reactivePick(props, 'loading', 'loadingIcon', 'placeholder'))
|
||||
|
||||
@@ -245,7 +247,7 @@ const groups = computed(() => {
|
||||
size="md"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
aria-label="Close"
|
||||
:aria-label="t('ui.commandPalette.close')"
|
||||
v-bind="typeof close === 'object' ? close : undefined"
|
||||
:class="ui.close({ class: props.ui?.close })"
|
||||
@click="emits('update:open', false)"
|
||||
@@ -259,7 +261,7 @@ const groups = computed(() => {
|
||||
<ComboboxContent :class="ui.content({ class: props.ui?.content })" :dismissable="false">
|
||||
<ComboboxEmpty :class="ui.empty({ class: props.ui?.empty })">
|
||||
<slot name="empty" :search-term="searchTerm">
|
||||
{{ searchTerm ? `No results for ${searchTerm}` : 'No results' }}
|
||||
{{ searchTerm ? t('ui.commandPalette.noMatch', { searchTerm }) : t('ui.commandPalette.noData') }}
|
||||
</slot>
|
||||
</ComboboxEmpty>
|
||||
|
||||
|
||||
@@ -34,6 +34,8 @@ import ULink from './Link.vue'
|
||||
import UAvatar from './Avatar.vue'
|
||||
import UIcon from './Icon.vue'
|
||||
import UKbd from './Kbd.vue'
|
||||
// eslint-disable-next-line import/no-self-import
|
||||
import UContextMenuContent from './ContextMenuContent.vue'
|
||||
|
||||
const props = defineProps<ContextMenuContentProps<T>>()
|
||||
const emits = defineEmits<ContextMenuContentEmits>()
|
||||
|
||||
@@ -40,6 +40,8 @@ import ULink from './Link.vue'
|
||||
import UAvatar from './Avatar.vue'
|
||||
import UIcon from './Icon.vue'
|
||||
import UKbd from './Kbd.vue'
|
||||
// eslint-disable-next-line import/no-self-import
|
||||
import UDropdownMenuContent from './DropdownMenuContent.vue'
|
||||
|
||||
const props = defineProps<DropdownMenuContentProps<T>>()
|
||||
const emits = defineEmits<DropdownMenuContentEmits>()
|
||||
|
||||
@@ -94,13 +94,13 @@ onUnmounted(() => {
|
||||
const errors = ref<FormErrorWithId[]>([])
|
||||
provide('form-errors', errors)
|
||||
|
||||
const inputs = ref<Record<string, string>>({})
|
||||
const inputs = ref<Record<string, { id?: string, pattern?: RegExp }>>({})
|
||||
provide(formInputsInjectionKey, inputs)
|
||||
|
||||
function resolveErrorIds(errs: FormError[]): FormErrorWithId[] {
|
||||
return errs.map(err => ({
|
||||
...err,
|
||||
id: inputs.value[err.name]
|
||||
id: inputs.value[err.name]?.id
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ async function getErrors(): Promise<FormErrorWithId[]> {
|
||||
}
|
||||
|
||||
async function _validate(opts: { name?: string | string[], silent?: boolean, nested?: boolean } = { silent: false, nested: true }): Promise<T | false> {
|
||||
const names = opts.name && !Array.isArray(opts.name) ? [opts.name] : opts.name
|
||||
const names = opts.name && !Array.isArray(opts.name) ? [opts.name] : opts.name as string[]
|
||||
|
||||
const nestedValidatePromises = !names && opts.nested
|
||||
? Array.from(nestedForms.value.values()).map(
|
||||
@@ -143,9 +143,16 @@ async function _validate(opts: { name?: string | string[], silent?: boolean, nes
|
||||
: []
|
||||
|
||||
if (names) {
|
||||
const otherErrors = errors.value.filter(error => !names!.includes(error.name))
|
||||
const pathErrors = (await getErrors()).filter(error => names!.includes(error.name)
|
||||
)
|
||||
const otherErrors = errors.value.filter(error => !names.some((name) => {
|
||||
const pattern = inputs.value?.[name]?.pattern
|
||||
return name === error.name || (pattern && error.name.match(pattern))
|
||||
}))
|
||||
|
||||
const pathErrors = (await getErrors()).filter(error => names.some((name) => {
|
||||
const pattern = inputs.value?.[name]?.pattern
|
||||
return name === error.name || (pattern && error.name.match(pattern))
|
||||
}))
|
||||
|
||||
errors.value = otherErrors.concat(pathErrors)
|
||||
} else {
|
||||
errors.value = await getErrors()
|
||||
@@ -196,7 +203,7 @@ provide(formOptionsInjectionKey, computed(() => ({
|
||||
validateOnInputDelay: props.validateOnInputDelay
|
||||
})))
|
||||
|
||||
defineExpose<{ $el: HTMLFormElement | HTMLDivElement } & Form<T>>({
|
||||
defineExpose<Form<T>>({
|
||||
validate: _validate,
|
||||
errors,
|
||||
|
||||
@@ -230,7 +237,7 @@ defineExpose<{ $el: HTMLFormElement | HTMLDivElement } & Form<T>>({
|
||||
},
|
||||
|
||||
disabled
|
||||
} as { $el: HTMLFormElement | HTMLDivElement } & Form<T>)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -12,7 +12,10 @@ const formField = tv({ extend: tv(theme), ...(appConfig.ui?.formField || {}) })
|
||||
type FormFieldVariants = VariantProps<typeof formField>
|
||||
|
||||
export interface FormFieldProps {
|
||||
/** The name of the FormField. Also used to match form errors. */
|
||||
name?: string
|
||||
/** A regular expression to match form error names. */
|
||||
errorPattern?: RegExp
|
||||
label?: string
|
||||
description?: string
|
||||
help?: string
|
||||
@@ -54,7 +57,7 @@ const ui = computed(() => formField({
|
||||
|
||||
const formErrors = inject<Ref<FormError[]> | null>('form-errors', null)
|
||||
|
||||
const error = computed(() => props.error || formErrors?.value?.find(error => error.name === props.name)?.message)
|
||||
const error = computed(() => props.error || formErrors?.value?.find(error => error.name === props.name || (props.errorPattern && error.name.match(props.errorPattern)))?.message)
|
||||
|
||||
const id = ref(useId())
|
||||
|
||||
@@ -65,7 +68,8 @@ provide(formFieldInjectionKey, computed(() => ({
|
||||
name: props.name,
|
||||
size: props.size,
|
||||
eagerValidation: props.eagerValidation,
|
||||
validateOnInputDelay: props.validateOnInputDelay
|
||||
validateOnInputDelay: props.validateOnInputDelay,
|
||||
errorPattern: props.errorPattern
|
||||
}) as FormFieldInjectedOptions<FormFieldProps>))
|
||||
</script>
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { AppConfig } from '@nuxt/schema'
|
||||
import _appConfig from '#build/app.config'
|
||||
import theme from '#build/ui/input-menu'
|
||||
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
|
||||
import { useLocale } from '../composables/useLocale'
|
||||
import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
|
||||
import type { AvatarProps, ChipProps, InputProps } from '../types'
|
||||
import type { AcceptableValue, ArrayOrWrapped, PartialString, MaybeArrayOfArray, MaybeArrayOfArrayItem, SelectModelValue, SelectModelValueEmits, SelectItemKey } from '../types/utils'
|
||||
@@ -78,9 +79,10 @@ export interface InputMenuProps<T extends MaybeArrayOfArrayItem<I>, I extends Ma
|
||||
*/
|
||||
portal?: boolean
|
||||
/**
|
||||
* Whether to filter items or not, can be an array of fields to filter.
|
||||
* When `false`, items will not be filtered which is useful for custom filtering.
|
||||
* @defaultValue ['label']
|
||||
* Whether to filter items or not, can be an array of fields to filter. Defaults to `[labelKey]`.
|
||||
* When `false`, items will not be filtered which is useful for custom filtering (useAsyncData, useFetch, etc.).
|
||||
* `['label']`{lang="ts-type"}
|
||||
* @defaultValue true
|
||||
*/
|
||||
filter?: boolean | string[]
|
||||
/**
|
||||
@@ -148,7 +150,7 @@ const props = withDefaults(defineProps<InputMenuProps<T, I, V, M>>(), {
|
||||
type: 'text',
|
||||
autofocusDelay: 0,
|
||||
portal: true,
|
||||
filter: () => ['label'],
|
||||
filter: true,
|
||||
labelKey: 'label' as never
|
||||
})
|
||||
const emits = defineEmits<InputMenuEmits<T, V, M>>()
|
||||
@@ -157,6 +159,7 @@ const slots = defineSlots<InputMenuSlots<T>>()
|
||||
const searchTerm = defineModel<string>('searchTerm', { default: '' })
|
||||
|
||||
const appConfig = useAppConfig()
|
||||
const { t } = useLocale()
|
||||
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'selectedValue', 'open', 'defaultOpen', 'resetSearchTermOnBlur'), emits)
|
||||
const contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffset: 8, position: 'popper' }) as ComboboxContentProps)
|
||||
const arrowProps = toRef(() => props.arrow as ComboboxArrowProps)
|
||||
@@ -347,7 +350,7 @@ defineExpose({
|
||||
<ComboboxContent :class="ui.content({ class: props.ui?.content })" v-bind="contentProps">
|
||||
<ComboboxEmpty :class="ui.empty({ class: props.ui?.empty })">
|
||||
<slot name="empty" :search-term="searchTerm">
|
||||
{{ searchTerm ? `No results for ${searchTerm}` : 'No results' }}
|
||||
{{ searchTerm ? t('ui.inputMenu.noMatch', { searchTerm }) : t('ui.inputMenu.noData') }}
|
||||
</slot>
|
||||
</ComboboxEmpty>
|
||||
|
||||
|
||||
@@ -98,6 +98,7 @@ import { isEqual } from 'ohash'
|
||||
import { useForwardProps } from 'radix-vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { useRoute } from '#imports'
|
||||
import ULinkBase from './LinkBase.vue'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { DialogRootProps, DialogRootEmits, DialogContentProps } from 'radix
|
||||
import type { AppConfig } from '@nuxt/schema'
|
||||
import _appConfig from '#build/app.config'
|
||||
import theme from '#build/ui/modal'
|
||||
import { useLocale } from '../composables/useLocale'
|
||||
import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
|
||||
import type { ButtonProps } from '../types'
|
||||
|
||||
@@ -95,7 +96,8 @@ const contentEvents = computed(() => {
|
||||
if (props.preventClose) {
|
||||
return {
|
||||
pointerDownOutside: (e: Event) => e.preventDefault(),
|
||||
interactOutside: (e: Event) => e.preventDefault()
|
||||
interactOutside: (e: Event) => e.preventDefault(),
|
||||
escapeKeyDown: (e: Event) => e.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +105,7 @@ const contentEvents = computed(() => {
|
||||
})
|
||||
|
||||
const appConfig = useAppConfig()
|
||||
const { t } = useLocale()
|
||||
|
||||
const ui = computed(() => modal({
|
||||
transition: props.transition,
|
||||
@@ -143,7 +146,7 @@ const ui = computed(() => modal({
|
||||
size="md"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
aria-label="Close"
|
||||
:aria-label="t('ui.modal.close')"
|
||||
v-bind="typeof close === 'object' ? close : undefined"
|
||||
:class="ui.close({ class: props.ui?.close })"
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { tv, type VariantProps } from 'tailwind-variants'
|
||||
import type { NavigationMenuRootProps, NavigationMenuRootEmits, NavigationMenuContentProps } from 'radix-vue'
|
||||
import type { NavigationMenuRootProps, NavigationMenuRootEmits, NavigationMenuContentProps, CollapsibleRootProps } from 'radix-vue'
|
||||
import type { AppConfig } from '@nuxt/schema'
|
||||
import _appConfig from '#build/app.config'
|
||||
import theme from '#build/ui/navigation-menu'
|
||||
@@ -17,7 +17,7 @@ export interface NavigationMenuChildItem extends Omit<NavigationMenuItem, 'child
|
||||
description?: string
|
||||
}
|
||||
|
||||
export interface NavigationMenuItem extends Omit<LinkProps, 'raw' | 'custom'> {
|
||||
export interface NavigationMenuItem extends Omit<LinkProps, 'raw' | 'custom'>, Pick<CollapsibleRootProps, 'defaultOpen' | 'open'> {
|
||||
label?: string
|
||||
icon?: string
|
||||
avatar?: AvatarProps
|
||||
@@ -208,6 +208,8 @@ const lists = computed(() => props.items?.length ? (Array.isArray(props.items[0]
|
||||
:key="`list-${listIndex}-${index}`"
|
||||
as="li"
|
||||
:value="item.value || String(index)"
|
||||
:default-open="item.defaultOpen"
|
||||
:open="item.open"
|
||||
:class="ui.item({ class: props.ui?.item })"
|
||||
>
|
||||
<ULink v-slot="{ active, ...slotProps }" v-bind="pickLinkProps(item)" custom>
|
||||
|
||||
@@ -31,6 +31,11 @@ export interface PopoverProps extends PopoverRootProps, Pick<HoverCardRootProps,
|
||||
* @defaultValue true
|
||||
*/
|
||||
portal?: boolean
|
||||
/**
|
||||
* When `true`, the popover will not close when clicking outside.
|
||||
* @defaultValue false
|
||||
*/
|
||||
preventClose?: boolean
|
||||
class?: any
|
||||
ui?: Partial<typeof popover.slots>
|
||||
}
|
||||
@@ -64,6 +69,17 @@ const slots = defineSlots<PopoverSlots>()
|
||||
const pick = props.mode === 'hover' ? reactivePick(props, 'defaultOpen', 'open', 'openDelay', 'closeDelay') : reactivePick(props, 'defaultOpen', 'open', 'modal')
|
||||
const rootProps = useForwardPropsEmits(pick, emits)
|
||||
const contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffset: 8 }) as PopoverContentProps)
|
||||
const contentEvents = computed(() => {
|
||||
if (props.preventClose) {
|
||||
return {
|
||||
pointerDownOutside: (e: Event) => e.preventDefault(),
|
||||
interactOutside: (e: Event) => e.preventDefault(),
|
||||
escapeKeyDown: (e: Event) => e.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
return {}
|
||||
})
|
||||
const arrowProps = toRef(() => props.arrow as PopoverArrowProps)
|
||||
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
@@ -81,7 +97,7 @@ const Component = computed(() => props.mode === 'hover' ? HoverCard : Popover)
|
||||
</Component.Trigger>
|
||||
|
||||
<Component.Portal :disabled="!portal">
|
||||
<Component.Content v-bind="contentProps" :class="ui.content({ class: [props.class, props.ui?.content] })">
|
||||
<Component.Content v-bind="contentProps" :class="ui.content({ class: [props.class, props.ui?.content] })" v-on="contentEvents">
|
||||
<slot name="content" />
|
||||
|
||||
<Component.Arrow v-if="!!arrow" v-bind="arrowProps" :class="ui.arrow({ class: props.ui?.arrow })" />
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { AppConfig } from '@nuxt/schema'
|
||||
import _appConfig from '#build/app.config'
|
||||
import theme from '#build/ui/select-menu'
|
||||
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
|
||||
import { useLocale } from '../composables/useLocale'
|
||||
import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
|
||||
import type { AvatarProps, ChipProps, InputProps } from '../types'
|
||||
import type { AcceptableValue, ArrayOrWrapped, PartialString, MaybeArrayOfArray, MaybeArrayOfArrayItem, SelectModelValue, SelectModelValueEmits, SelectItemKey } from '../types/utils'
|
||||
@@ -36,9 +37,10 @@ export interface SelectMenuProps<T extends MaybeArrayOfArrayItem<I>, I extends M
|
||||
/**
|
||||
* Wether to display the search input or not.
|
||||
* Can be an object to pass additional props to the input.
|
||||
* @defaultValue { placeholder: 'Search...' }
|
||||
* `{ placeholder: 'Search...', variant: 'none' }`{lang="ts-type"}
|
||||
* @defaultValue true
|
||||
*/
|
||||
searchInput?: boolean | { placeholder?: string }
|
||||
searchInput?: boolean | InputProps
|
||||
color?: SelectMenuVariants['color']
|
||||
variant?: SelectMenuVariants['variant']
|
||||
size?: SelectMenuVariants['size']
|
||||
@@ -69,9 +71,10 @@ export interface SelectMenuProps<T extends MaybeArrayOfArrayItem<I>, I extends M
|
||||
*/
|
||||
portal?: boolean
|
||||
/**
|
||||
* Whether to filter items or not, can be an array of fields to filter.
|
||||
* Whether to filter items or not, can be an array of fields to filter. Defaults to `[labelKey]`.
|
||||
* When `false`, items will not be filtered which is useful for custom filtering (useAsyncData, useFetch, etc.).
|
||||
* @defaultValue ['label']
|
||||
* `['label']`{lang="ts-type"}
|
||||
* @defaultValue true
|
||||
*/
|
||||
filter?: boolean | string[]
|
||||
/**
|
||||
@@ -131,13 +134,14 @@ import { get, escapeRegExp } from '../utils'
|
||||
import UIcon from './Icon.vue'
|
||||
import UAvatar from './Avatar.vue'
|
||||
import UChip from './Chip.vue'
|
||||
import UInput from './Input.vue'
|
||||
|
||||
const props = withDefaults(defineProps<SelectMenuProps<T, I, V, M>>(), {
|
||||
search: true,
|
||||
portal: true,
|
||||
autofocusDelay: 0,
|
||||
searchInput: () => ({ placeholder: 'Search...' }),
|
||||
filter: () => ['label'],
|
||||
searchInput: true,
|
||||
filter: true,
|
||||
labelKey: 'label' as never
|
||||
})
|
||||
|
||||
@@ -147,9 +151,12 @@ const slots = defineSlots<SelectMenuSlots<T>>()
|
||||
const searchTerm = defineModel<string>('searchTerm', { default: '' })
|
||||
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const { t } = useLocale()
|
||||
const rootProps = useForwardPropsEmits(reactivePick(props, 'modelValue', 'defaultValue', 'selectedValue', 'open', 'defaultOpen', 'resetSearchTermOnBlur'), emits)
|
||||
const contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffset: 8, position: 'popper' }) as ComboboxContentProps)
|
||||
const arrowProps = toRef(() => props.arrow as ComboboxArrowProps)
|
||||
const searchInputProps = toRef(() => defu(props.searchInput, { placeholder: 'Search...', variant: 'none' }) as InputProps)
|
||||
// This is a hack due to generic boolean casting (see https://github.com/nuxt/ui/issues/2541)
|
||||
const multiple = toRef(() => typeof props.multiple === 'string' ? true : props.multiple)
|
||||
|
||||
@@ -274,17 +281,13 @@ function onUpdateOpen(value: boolean) {
|
||||
|
||||
<ComboboxPortal :disabled="!portal">
|
||||
<ComboboxContent :class="ui.content({ class: props.ui?.content })" v-bind="contentProps">
|
||||
<ComboboxInput
|
||||
v-if="!!searchInput"
|
||||
autofocus
|
||||
autocomplete="off"
|
||||
v-bind="typeof searchInput === 'object' ? searchInput : {}"
|
||||
:class="ui.input({ class: props.ui?.input })"
|
||||
/>
|
||||
<ComboboxInput v-if="!!searchInput" as-child>
|
||||
<UInput autofocus autocomplete="off" v-bind="searchInputProps" :class="ui.input({ class: props.ui?.input })" />
|
||||
</ComboboxInput>
|
||||
|
||||
<ComboboxEmpty :class="ui.empty({ class: props.ui?.empty })">
|
||||
<slot name="empty" :search-term="searchTerm">
|
||||
{{ searchTerm ? `No results for ${searchTerm}` : 'No results' }}
|
||||
{{ searchTerm ? t('ui.selectMenu.noMatch', { searchTerm }) : t('ui.selectMenu.noData') }}
|
||||
</slot>
|
||||
</ComboboxEmpty>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { DialogRootProps, DialogRootEmits, DialogContentProps } from 'radix
|
||||
import type { AppConfig } from '@nuxt/schema'
|
||||
import _appConfig from '#build/app.config'
|
||||
import theme from '#build/ui/slideover'
|
||||
import { useLocale } from '../composables/useLocale'
|
||||
import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
|
||||
import type { ButtonProps } from '../types'
|
||||
|
||||
@@ -94,7 +95,8 @@ const contentEvents = computed(() => {
|
||||
if (props.preventClose) {
|
||||
return {
|
||||
pointerDownOutside: (e: Event) => e.preventDefault(),
|
||||
interactOutside: (e: Event) => e.preventDefault()
|
||||
interactOutside: (e: Event) => e.preventDefault(),
|
||||
escapeKeyDown: (e: Event) => e.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +104,7 @@ const contentEvents = computed(() => {
|
||||
})
|
||||
|
||||
const appConfig = useAppConfig()
|
||||
const { t } = useLocale()
|
||||
|
||||
const ui = computed(() => slideover({
|
||||
transition: props.transition,
|
||||
@@ -142,7 +145,7 @@ const ui = computed(() => slideover({
|
||||
size="md"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
aria-label="Close"
|
||||
:aria-label="t('ui.slideover.close')"
|
||||
v-bind="typeof close === 'object' ? close : undefined"
|
||||
:class="ui.close({ class: props.ui?.close })"
|
||||
/>
|
||||
|
||||
@@ -25,6 +25,7 @@ import type {
|
||||
} from '@tanstack/vue-table'
|
||||
import _appConfig from '#build/app.config'
|
||||
import theme from '#build/ui/table'
|
||||
import { useLocale } from '../composables/useLocale'
|
||||
|
||||
const appConfig = _appConfig as AppConfig & { ui: { table: Partial<typeof theme> } }
|
||||
|
||||
@@ -41,6 +42,7 @@ export interface TableData {
|
||||
export interface TableProps<T> {
|
||||
data?: T[]
|
||||
columns?: TableColumn<T>[]
|
||||
caption?: string
|
||||
/**
|
||||
* Whether the table should have a sticky header.
|
||||
* @defaultValue false
|
||||
@@ -95,6 +97,7 @@ type DynamicCellSlots<T, K = keyof T> = Record<string, (props: CellContext<T, un
|
||||
export type TableSlots<T> = {
|
||||
expanded: (props: { row: Row<T> }) => any
|
||||
empty: (props?: {}) => any
|
||||
caption: (props?: {}) => any
|
||||
} & DynamicHeaderSlots<T> & DynamicCellSlots<T>
|
||||
|
||||
</script>
|
||||
@@ -114,6 +117,7 @@ import { upperFirst } from 'scule'
|
||||
const props = defineProps<TableProps<T>>()
|
||||
defineSlots<TableSlots<T>>()
|
||||
|
||||
const { t } = useLocale()
|
||||
const data = computed(() => props.data ?? [])
|
||||
const columns = computed<TableColumn<T>[]>(() => props.columns ?? Object.keys(data.value[0] ?? {}).map((accessorKey: string) => ({ accessorKey, header: upperFirst(accessorKey) })))
|
||||
|
||||
@@ -190,6 +194,12 @@ defineExpose({
|
||||
<template>
|
||||
<div :class="ui.root({ class: [props.class, props.ui?.root] })">
|
||||
<table :class="ui.base({ class: [props.ui?.base] })">
|
||||
<caption v-if="caption" :class="ui.caption({ class: [props.ui?.caption] })">
|
||||
<slot name="caption">
|
||||
{{ caption }}
|
||||
</slot>
|
||||
</caption>
|
||||
|
||||
<thead :class="ui.thead({ class: [props.ui?.thead] })">
|
||||
<tr v-for="headerGroup in tableApi.getHeaderGroups()" :key="headerGroup.id" :class="ui.tr({ class: [props.ui?.tr] })">
|
||||
<th
|
||||
@@ -231,7 +241,7 @@ defineExpose({
|
||||
<tr v-else :class="ui.tr({ class: [props.ui?.tr] })">
|
||||
<td :colspan="columns?.length" :class="ui.empty({ class: props.ui?.empty })">
|
||||
<slot name="empty">
|
||||
No results
|
||||
{{ t('ui.table.noData') }}
|
||||
</slot>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { ToastRootProps, ToastRootEmits } from 'radix-vue'
|
||||
import type { AppConfig } from '@nuxt/schema'
|
||||
import _appConfig from '#build/app.config'
|
||||
import theme from '#build/ui/toast'
|
||||
import { useLocale } from '../composables/useLocale'
|
||||
import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
|
||||
import type { AvatarProps, ButtonProps } from '../types'
|
||||
|
||||
@@ -74,6 +75,7 @@ const emits = defineEmits<ToastEmits>()
|
||||
const slots = defineSlots<ToastSlots>()
|
||||
|
||||
const appConfig = useAppConfig()
|
||||
const { t } = useLocale()
|
||||
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'defaultOpen', 'open', 'duration', 'type'), emits)
|
||||
|
||||
const multiline = computed(() => !!props.title && !!props.description)
|
||||
@@ -151,7 +153,7 @@ defineExpose({
|
||||
size="md"
|
||||
color="neutral"
|
||||
variant="link"
|
||||
aria-label="Close"
|
||||
:aria-label="t('ui.toast.close')"
|
||||
v-bind="typeof close === 'object' ? close : undefined"
|
||||
:class="ui.close({ class: props.ui?.close })"
|
||||
@click.stop
|
||||
|
||||
@@ -19,6 +19,11 @@ export interface ToasterProps extends Omit<ToastProviderProps, 'swipeDirection'>
|
||||
* @defaultValue true
|
||||
*/
|
||||
expand?: boolean
|
||||
/**
|
||||
* Render the toaster in a portal.
|
||||
* @defaultValue true
|
||||
*/
|
||||
portal?: boolean
|
||||
class?: any
|
||||
ui?: Partial<typeof toaster.slots>
|
||||
}
|
||||
@@ -44,6 +49,7 @@ import UToast from './Toast.vue'
|
||||
|
||||
const props = withDefaults(defineProps<ToasterProps>(), {
|
||||
expand: true,
|
||||
portal: true,
|
||||
duration: 5000
|
||||
})
|
||||
defineSlots<ToasterSlots>()
|
||||
@@ -120,18 +126,20 @@ function getOffset(index: number) {
|
||||
@click="toast.click && toast.click(toast)"
|
||||
/>
|
||||
|
||||
<ToastViewport
|
||||
:data-expanded="expanded"
|
||||
:class="ui.viewport({ class: [props.class, props.ui?.viewport] })"
|
||||
:style="{
|
||||
'--scale-factor': '0.05',
|
||||
'--translate-factor': position?.startsWith('top') ? '1px' : '-1px',
|
||||
'--gap': position?.startsWith('top') ? '16px' : '-16px',
|
||||
'--front-height': `${frontHeight}px`,
|
||||
'--height': `${height}px`
|
||||
}"
|
||||
@mouseenter="hovered = true"
|
||||
@mouseleave="hovered = false"
|
||||
/>
|
||||
<Teleport to="body" :disabled="!portal">
|
||||
<ToastViewport
|
||||
:data-expanded="expanded"
|
||||
:class="ui.viewport({ class: [props.class, props.ui?.viewport] })"
|
||||
:style="{
|
||||
'--scale-factor': '0.05',
|
||||
'--translate-factor': position?.startsWith('top') ? '1px' : '-1px',
|
||||
'--gap': position?.startsWith('top') ? '16px' : '-16px',
|
||||
'--front-height': `${frontHeight}px`,
|
||||
'--height': `${height}px`
|
||||
}"
|
||||
@mouseenter="hovered = true"
|
||||
@mouseleave="hovered = false"
|
||||
/>
|
||||
</Teleport>
|
||||
</ToastProvider>
|
||||
</template>
|
||||
|
||||
9
src/runtime/composables/defineLocale.ts
Normal file
9
src/runtime/composables/defineLocale.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { Locale, LocalePair } from '../types/locale'
|
||||
|
||||
export function defineLocale(name: string, code: string, pair: LocalePair): Locale {
|
||||
return {
|
||||
name,
|
||||
code,
|
||||
ui: pair
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ export const formOptionsInjectionKey: InjectionKey<ComputedRef<FormInjectedOptio
|
||||
export const formBusInjectionKey: InjectionKey<UseEventBusReturn<FormEvent, string>> = Symbol('nuxt-ui.form-events')
|
||||
export const formFieldInjectionKey: InjectionKey<ComputedRef<FormFieldInjectedOptions<FormFieldProps>>> = Symbol('nuxt-ui.form-field')
|
||||
export const inputIdInjectionKey: InjectionKey<Ref<string | undefined>> = Symbol('nuxt-ui.input-id')
|
||||
export const formInputsInjectionKey: InjectionKey<Ref<Record<string, string>>> = Symbol('nuxt-ui.form-inputs')
|
||||
export const formInputsInjectionKey: InjectionKey<Ref<Record<string, { id?: string, pattern?: RegExp }>>> = Symbol('nuxt-ui.form-inputs')
|
||||
export const formLoadingInjectionKey: InjectionKey<Readonly<Ref<boolean>>> = Symbol('nuxt-ui.form-loading')
|
||||
|
||||
export function useFormField<T>(props?: Props<T>, opts?: { bind?: boolean }) {
|
||||
@@ -38,7 +38,7 @@ export function useFormField<T>(props?: Props<T>, opts?: { bind?: boolean }) {
|
||||
inputId.value = props?.id
|
||||
}
|
||||
if (formInputs && formField.value.name && inputId.value) {
|
||||
formInputs.value[formField.value.name] = inputId.value
|
||||
formInputs.value[formField.value.name] = { id: inputId.value, pattern: formField.value.errorPattern }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
13
src/runtime/composables/useLocale.ts
Normal file
13
src/runtime/composables/useLocale.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { computed, inject, ref } from 'vue'
|
||||
import type { InjectionKey, Ref } from 'vue'
|
||||
import type { Locale } from '../types/locale'
|
||||
import { buildLocaleContext } from '../utils/locale'
|
||||
import { en } from '../locale'
|
||||
|
||||
export const localeContextInjectionKey: InjectionKey<Ref<Locale | undefined>> = Symbol('nuxt-ui.locale-context')
|
||||
|
||||
export const useLocale = (localeOverrides?: Ref<Locale | undefined>) => {
|
||||
const locale = localeOverrides || inject(localeContextInjectionKey, ref())!
|
||||
|
||||
return buildLocaleContext(computed(() => locale.value || en))
|
||||
}
|
||||
@@ -31,7 +31,8 @@
|
||||
--ui-border-accented: var(--ui-color-neutral-300);
|
||||
--ui-border-inverted: var(--ui-color-neutral-900);
|
||||
|
||||
--ui-radius: var(--radius);
|
||||
--ui-radius: var(--radius-sm);
|
||||
--ui-container: var(--container-7xl);
|
||||
}
|
||||
|
||||
.dark {
|
||||
|
||||
37
src/runtime/locale/ar.ts
Normal file
37
src/runtime/locale/ar.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { defineLocale } from '../composables/defineLocale'
|
||||
|
||||
export default defineLocale('العربية', 'ar', {
|
||||
inputMenu: {
|
||||
noMatch: 'لا توجد نتائج مطابقة',
|
||||
noData: 'لا توجد بيانات'
|
||||
},
|
||||
commandPalette: {
|
||||
noMatch: 'لا توجد نتائج مطابقة',
|
||||
noData: 'لا توجد بيانات',
|
||||
close: 'إغلاق'
|
||||
},
|
||||
selectMenu: {
|
||||
noMatch: 'لا توجد نتائج مطابقة',
|
||||
noData: 'لا توجد بيانات'
|
||||
},
|
||||
toast: {
|
||||
close: 'إغلاق'
|
||||
},
|
||||
carousel: {
|
||||
prev: 'السابق',
|
||||
next: 'التالي',
|
||||
goto: 'الذهاب إلي شريحة {slide}'
|
||||
},
|
||||
modal: {
|
||||
close: 'إغلاق'
|
||||
},
|
||||
slideover: {
|
||||
close: 'إغلاق'
|
||||
},
|
||||
alert: {
|
||||
close: 'إغلاق'
|
||||
},
|
||||
table: {
|
||||
noData: 'لا توجد بيانات'
|
||||
}
|
||||
})
|
||||
37
src/runtime/locale/cs.ts
Normal file
37
src/runtime/locale/cs.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { defineLocale } from '../composables/defineLocale'
|
||||
|
||||
export default defineLocale('Čeština', 'cs', {
|
||||
inputMenu: {
|
||||
noMatch: 'Žádná shoda',
|
||||
noData: 'Žádná data'
|
||||
},
|
||||
commandPalette: {
|
||||
noMatch: 'Žádná shoda',
|
||||
noData: 'Žádná data',
|
||||
close: 'Zavřít'
|
||||
},
|
||||
selectMenu: {
|
||||
noMatch: 'Žádná shoda',
|
||||
noData: 'Žádná data'
|
||||
},
|
||||
toast: {
|
||||
close: 'Zavřít'
|
||||
},
|
||||
carousel: {
|
||||
prev: 'Předchozí',
|
||||
next: 'Další',
|
||||
goto: 'Přejít na {slide}'
|
||||
},
|
||||
modal: {
|
||||
close: 'Zavřít'
|
||||
},
|
||||
slideover: {
|
||||
close: 'Zavřít'
|
||||
},
|
||||
alert: {
|
||||
close: 'Zavřít'
|
||||
},
|
||||
table: {
|
||||
noData: 'Žádná data'
|
||||
}
|
||||
})
|
||||
37
src/runtime/locale/de.ts
Normal file
37
src/runtime/locale/de.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { defineLocale } from '../composables/defineLocale'
|
||||
|
||||
export default defineLocale('Deutsch', 'de', {
|
||||
inputMenu: {
|
||||
noMatch: 'Nichts gefunden',
|
||||
noData: 'Keine Daten'
|
||||
},
|
||||
commandPalette: {
|
||||
noMatch: 'Nichts gefunden',
|
||||
noData: 'Keine Daten',
|
||||
close: 'Schließen'
|
||||
},
|
||||
selectMenu: {
|
||||
noMatch: 'Nichts gefunden',
|
||||
noData: 'Keine Daten'
|
||||
},
|
||||
toast: {
|
||||
close: 'Schließen'
|
||||
},
|
||||
carousel: {
|
||||
prev: 'Weiter',
|
||||
next: 'Zurück',
|
||||
goto: 'Gehe zu {slide}'
|
||||
},
|
||||
modal: {
|
||||
close: 'Schließen'
|
||||
},
|
||||
slideover: {
|
||||
close: 'Schließen'
|
||||
},
|
||||
alert: {
|
||||
close: 'Schließen'
|
||||
},
|
||||
table: {
|
||||
noData: 'Keine Daten'
|
||||
}
|
||||
})
|
||||
37
src/runtime/locale/en.ts
Normal file
37
src/runtime/locale/en.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { defineLocale } from '../composables/defineLocale'
|
||||
|
||||
export default defineLocale('English', 'en', {
|
||||
inputMenu: {
|
||||
noMatch: 'No matching data',
|
||||
noData: 'No data'
|
||||
},
|
||||
commandPalette: {
|
||||
noMatch: 'No matching data',
|
||||
noData: 'No data',
|
||||
close: 'Close'
|
||||
},
|
||||
selectMenu: {
|
||||
noMatch: 'No matching data',
|
||||
noData: 'No data'
|
||||
},
|
||||
toast: {
|
||||
close: 'Close'
|
||||
},
|
||||
carousel: {
|
||||
prev: 'Prev',
|
||||
next: 'Next',
|
||||
goto: 'Go to slide {slide}'
|
||||
},
|
||||
modal: {
|
||||
close: 'Close'
|
||||
},
|
||||
slideover: {
|
||||
close: 'Close'
|
||||
},
|
||||
alert: {
|
||||
close: 'Close'
|
||||
},
|
||||
table: {
|
||||
noData: 'No data'
|
||||
}
|
||||
})
|
||||
37
src/runtime/locale/fr.ts
Normal file
37
src/runtime/locale/fr.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { defineLocale } from '../composables/defineLocale'
|
||||
|
||||
export default defineLocale('Français', 'fr', {
|
||||
inputMenu: {
|
||||
noMatch: 'Aucune donnée correspondante',
|
||||
noData: 'Aucune donnée'
|
||||
},
|
||||
commandPalette: {
|
||||
noMatch: 'Aucune donnée correspondante',
|
||||
noData: 'Aucune donnée',
|
||||
close: 'Fermer'
|
||||
},
|
||||
selectMenu: {
|
||||
noMatch: 'Aucune donnée correspondante',
|
||||
noData: 'Aucune donnée'
|
||||
},
|
||||
toast: {
|
||||
close: 'Fermer'
|
||||
},
|
||||
carousel: {
|
||||
prev: 'Précédent',
|
||||
next: 'Suivant',
|
||||
goto: 'Aller à {slide}'
|
||||
},
|
||||
modal: {
|
||||
close: 'Fermer'
|
||||
},
|
||||
slideover: {
|
||||
close: 'Fermer'
|
||||
},
|
||||
alert: {
|
||||
close: 'Fermer'
|
||||
},
|
||||
table: {
|
||||
noData: 'Aucune donnée'
|
||||
}
|
||||
})
|
||||
9
src/runtime/locale/index.ts
Normal file
9
src/runtime/locale/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export { default as ar } from './ar'
|
||||
export { default as cs } from './cs'
|
||||
export { default as de } from './de'
|
||||
export { default as en } from './en'
|
||||
export { default as fr } from './fr'
|
||||
export { default as it } from './it'
|
||||
export { default as ru } from './ru'
|
||||
export { default as zh_hans } from './zh_hans'
|
||||
export { default as zh_hant } from './zh_hant'
|
||||
37
src/runtime/locale/it.ts
Normal file
37
src/runtime/locale/it.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { defineLocale } from '../composables/defineLocale'
|
||||
|
||||
export default defineLocale('Italiano', 'it', {
|
||||
inputMenu: {
|
||||
noMatch: 'Nessun dato corrispondente',
|
||||
noData: 'Nessun dato'
|
||||
},
|
||||
commandPalette: {
|
||||
noMatch: 'Nessun dato corrispondente',
|
||||
noData: 'Nessun dato',
|
||||
close: 'Chiudi'
|
||||
},
|
||||
selectMenu: {
|
||||
noMatch: 'Nessun dato corrispondente',
|
||||
noData: 'Nessun dato'
|
||||
},
|
||||
toast: {
|
||||
close: 'Chiudi'
|
||||
},
|
||||
carousel: {
|
||||
prev: 'Precedente',
|
||||
next: 'Successiva',
|
||||
goto: 'Vai alla slide {slide}'
|
||||
},
|
||||
modal: {
|
||||
close: 'Chiudi'
|
||||
},
|
||||
slideover: {
|
||||
close: 'Chiudi'
|
||||
},
|
||||
alert: {
|
||||
close: 'Chiudi'
|
||||
},
|
||||
table: {
|
||||
noData: 'Nessuno dato'
|
||||
}
|
||||
})
|
||||
37
src/runtime/locale/ru.ts
Normal file
37
src/runtime/locale/ru.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { defineLocale } from '../composables/defineLocale'
|
||||
|
||||
export default defineLocale('Русский', 'ru', {
|
||||
inputMenu: {
|
||||
noMatch: 'Совпадений не найдено',
|
||||
noData: 'Нет данных'
|
||||
},
|
||||
commandPalette: {
|
||||
noMatch: 'Совпадений не найдено',
|
||||
noData: 'Нет данных',
|
||||
close: 'Закрыть'
|
||||
},
|
||||
selectMenu: {
|
||||
noMatch: 'Совпадений не найдено',
|
||||
noData: 'Нет данных'
|
||||
},
|
||||
toast: {
|
||||
close: 'Закрыть'
|
||||
},
|
||||
carousel: {
|
||||
prev: 'Назад',
|
||||
next: 'Далее',
|
||||
goto: 'Перейти к {slide}'
|
||||
},
|
||||
modal: {
|
||||
close: 'Закрыть'
|
||||
},
|
||||
slideover: {
|
||||
close: 'Закрыть'
|
||||
},
|
||||
alert: {
|
||||
close: 'Закрыть'
|
||||
},
|
||||
table: {
|
||||
noData: 'Нет данных'
|
||||
}
|
||||
})
|
||||
37
src/runtime/locale/zh_hans.ts
Normal file
37
src/runtime/locale/zh_hans.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { defineLocale } from '../composables/defineLocale'
|
||||
|
||||
export default defineLocale('简体中文', 'zh-Hans', {
|
||||
inputMenu: {
|
||||
noMatch: '没有匹配的数据',
|
||||
noData: '没有数据'
|
||||
},
|
||||
commandPalette: {
|
||||
noMatch: '没有匹配的数据',
|
||||
noData: '没有数据',
|
||||
close: '关闭'
|
||||
},
|
||||
selectMenu: {
|
||||
noMatch: '没有匹配的数据',
|
||||
noData: '没有数据'
|
||||
},
|
||||
toast: {
|
||||
close: '关闭'
|
||||
},
|
||||
carousel: {
|
||||
prev: '上一页',
|
||||
next: '下一页',
|
||||
goto: '跳转到第 {slide} 页'
|
||||
},
|
||||
modal: {
|
||||
close: '关闭'
|
||||
},
|
||||
slideover: {
|
||||
close: '关闭'
|
||||
},
|
||||
alert: {
|
||||
close: '关闭'
|
||||
},
|
||||
table: {
|
||||
noData: '没有数据'
|
||||
}
|
||||
})
|
||||
37
src/runtime/locale/zh_hant.ts
Normal file
37
src/runtime/locale/zh_hant.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { defineLocale } from '../composables/defineLocale'
|
||||
|
||||
export default defineLocale('繁体中文', 'zh-Hant', {
|
||||
inputMenu: {
|
||||
noMatch: '沒有匹配的資料',
|
||||
noData: '沒有資料'
|
||||
},
|
||||
commandPalette: {
|
||||
noMatch: '沒有匹配的資料',
|
||||
noData: '沒有資料',
|
||||
close: '關閉'
|
||||
},
|
||||
selectMenu: {
|
||||
noMatch: '沒有匹配的資料',
|
||||
noData: '沒有資料'
|
||||
},
|
||||
toast: {
|
||||
close: '關閉'
|
||||
},
|
||||
carousel: {
|
||||
prev: '上一頁',
|
||||
next: '下一頁',
|
||||
goto: '跳轉到第 {slide} 頁'
|
||||
},
|
||||
modal: {
|
||||
close: '關閉'
|
||||
},
|
||||
slideover: {
|
||||
close: '關閉'
|
||||
},
|
||||
alert: {
|
||||
close: '關閉'
|
||||
},
|
||||
table: {
|
||||
noData: '沒有資料'
|
||||
}
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { StandardSchema } from '@standard-schema/spec'
|
||||
import type { v1 } from '@standard-schema/spec'
|
||||
import type { ComputedRef, Ref } from 'vue'
|
||||
import type { ZodSchema } from 'zod'
|
||||
import type { Schema as JoiSchema } from 'joi'
|
||||
@@ -26,7 +26,7 @@ export type FormSchema<T extends Record<string, any>> =
|
||||
| ValibotSafeParserAsync<any, any>
|
||||
| JoiSchema<T>
|
||||
| SuperstructSchema<any, any>
|
||||
| StandardSchema
|
||||
| v1.StandardSchema
|
||||
|
||||
export type FormInputEvents = 'input' | 'blur' | 'change'
|
||||
|
||||
@@ -82,6 +82,7 @@ export interface FormFieldInjectedOptions<T> {
|
||||
error?: string | boolean
|
||||
eagerValidation?: boolean
|
||||
validateOnInputDelay?: number
|
||||
errorPattern?: RegExp
|
||||
}
|
||||
|
||||
export class FormValidationException extends Error {
|
||||
|
||||
@@ -42,3 +42,4 @@ export * from '../components/Toast.vue'
|
||||
export * from '../components/Toaster.vue'
|
||||
export * from '../components/Tooltip.vue'
|
||||
export * from './form'
|
||||
export * from './locale'
|
||||
|
||||
41
src/runtime/types/locale.ts
Normal file
41
src/runtime/types/locale.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
export type LocalePair = {
|
||||
inputMenu: {
|
||||
noMatch: string
|
||||
noData: string
|
||||
}
|
||||
commandPalette: {
|
||||
noMatch: string
|
||||
noData: string
|
||||
close: string
|
||||
}
|
||||
selectMenu: {
|
||||
noMatch: string
|
||||
noData: string
|
||||
}
|
||||
toast: {
|
||||
close: string
|
||||
}
|
||||
carousel: {
|
||||
prev: string
|
||||
next: string
|
||||
goto: string
|
||||
}
|
||||
modal: {
|
||||
close: string
|
||||
}
|
||||
slideover: {
|
||||
close: string
|
||||
}
|
||||
alert: {
|
||||
close: string
|
||||
}
|
||||
table: {
|
||||
noData: string
|
||||
}
|
||||
}
|
||||
|
||||
export type Locale = {
|
||||
name: string
|
||||
code: string
|
||||
ui: LocalePair
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { StandardSchema } from '@standard-schema/spec'
|
||||
import type { v1 } from '@standard-schema/spec'
|
||||
import type { ZodSchema } from 'zod'
|
||||
import type { ValidationError as JoiError, Schema as JoiSchema } from 'joi'
|
||||
import type { ObjectSchema as YupObjectSchema, ValidationError as YupError } from 'yup'
|
||||
@@ -94,15 +94,15 @@ export async function getValibotErrors(
|
||||
})) || []
|
||||
}
|
||||
|
||||
export function isStandardSchema(schema: any): schema is StandardSchema {
|
||||
export function isStandardSchema(schema: any): schema is v1.StandardSchema {
|
||||
return '~standard' in schema
|
||||
}
|
||||
|
||||
export async function getStandardErrors(
|
||||
state: any,
|
||||
schema: StandardSchema
|
||||
schema: v1.StandardSchema
|
||||
): Promise<FormError[]> {
|
||||
const result = await schema['~validate']({ value: state })
|
||||
const result = await schema['~standard'].validate(state)
|
||||
return result.issues?.map(issue => ({
|
||||
name: issue.path?.map(item => typeof item === 'object' ? item.key : item).join('.') || '',
|
||||
message: issue.message
|
||||
|
||||
40
src/runtime/utils/locale.ts
Normal file
40
src/runtime/utils/locale.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { Ref } from 'vue'
|
||||
import type { Locale } from '../types/locale'
|
||||
import type { MaybeRef } from '@vueuse/core'
|
||||
import { computed, isRef, ref, unref } from 'vue'
|
||||
import { get } from './index'
|
||||
|
||||
export type TranslatorOption = Record<string, string | number>
|
||||
export type Translator = (path: string, option?: TranslatorOption) => string
|
||||
export type LocaleContext = {
|
||||
locale: Ref<Locale>
|
||||
lang: Ref<string>
|
||||
code: Ref<string>
|
||||
t: Translator
|
||||
}
|
||||
|
||||
export function buildTranslator(locale: MaybeRef<Locale>): Translator {
|
||||
return (path, option) => translate(path, option, unref(locale))
|
||||
}
|
||||
|
||||
export function translate(path: string, option: undefined | TranslatorOption, locale: Locale): string {
|
||||
const prop: string = get(locale, path, path)
|
||||
|
||||
return prop.replace(
|
||||
/\{(\w+)\}/g,
|
||||
(_, key) => `${option?.[key] ?? `{${key}}`}`
|
||||
)
|
||||
}
|
||||
|
||||
export function buildLocaleContext(locale: MaybeRef<Locale>): LocaleContext {
|
||||
const lang = computed(() => unref(locale).name)
|
||||
const code = computed(() => unref(locale).code)
|
||||
const localeRef = isRef(locale) ? locale : ref(locale)
|
||||
|
||||
return {
|
||||
lang,
|
||||
code,
|
||||
locale: localeRef,
|
||||
t: buildTranslator(locale)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
export default {
|
||||
slots: {
|
||||
root: 'bg-[var(--ui-bg)] ring ring-[var(--ui-border)] divide-y divide-[var(--ui-border)] rounded-[calc(var(--ui-radius)*2)] shadow',
|
||||
root: 'bg-[var(--ui-bg)] ring ring-[var(--ui-border)] divide-y divide-[var(--ui-border)] rounded-[calc(var(--ui-radius)*2)] shadow-sm',
|
||||
header: 'p-4 sm:px-6',
|
||||
body: 'p-4 sm:p-6',
|
||||
footer: 'p-4 sm:px-6'
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export default {
|
||||
base: 'max-w-7xl mx-auto px-4 sm:px-6 lg:px-8'
|
||||
base: 'max-w-[var(--ui-container)] mx-auto px-4 sm:px-6 lg:px-8'
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export default (options: Required<ModuleOptions>) => ({
|
||||
...buttonGroupVariantWithRoot,
|
||||
size: {
|
||||
xs: {
|
||||
base: 'px-2 py-1 text-xs gap-1',
|
||||
base: 'px-2 py-1 text-xs',
|
||||
leading: 'ps-2',
|
||||
trailing: 'pe-2',
|
||||
leadingIcon: 'size-4',
|
||||
@@ -24,7 +24,7 @@ export default (options: Required<ModuleOptions>) => ({
|
||||
trailingIcon: 'size-4'
|
||||
},
|
||||
sm: {
|
||||
base: 'px-2.5 py-1.5 text-xs gap-1.5',
|
||||
base: 'px-2.5 py-1.5 text-xs',
|
||||
leading: 'ps-2.5',
|
||||
trailing: 'pe-2.5',
|
||||
leadingIcon: 'size-4',
|
||||
@@ -32,7 +32,7 @@ export default (options: Required<ModuleOptions>) => ({
|
||||
trailingIcon: 'size-4'
|
||||
},
|
||||
md: {
|
||||
base: 'px-2.5 py-1.5 text-sm gap-1.5',
|
||||
base: 'px-2.5 py-1.5 text-sm',
|
||||
leading: 'ps-2.5',
|
||||
trailing: 'pe-2.5',
|
||||
leadingIcon: 'size-5',
|
||||
@@ -40,7 +40,7 @@ export default (options: Required<ModuleOptions>) => ({
|
||||
trailingIcon: 'size-5'
|
||||
},
|
||||
lg: {
|
||||
base: 'px-3 py-2 text-sm gap-2',
|
||||
base: 'px-3 py-2 text-sm',
|
||||
leading: 'ps-3',
|
||||
trailing: 'pe-3',
|
||||
leadingIcon: 'size-5',
|
||||
@@ -48,7 +48,7 @@ export default (options: Required<ModuleOptions>) => ({
|
||||
trailingIcon: 'size-5'
|
||||
},
|
||||
xl: {
|
||||
base: 'px-3 py-2 text-base gap-2',
|
||||
base: 'px-3 py-2 text-base',
|
||||
leading: 'ps-3',
|
||||
trailing: 'pe-3',
|
||||
leadingIcon: 'size-6',
|
||||
@@ -60,8 +60,8 @@ export default (options: Required<ModuleOptions>) => ({
|
||||
outline: 'text-[var(--ui-text-highlighted)] bg-[var(--ui-bg)] ring ring-inset ring-[var(--ui-border-accented)]',
|
||||
soft: 'text-[var(--ui-text-highlighted)] bg-[var(--ui-bg-elevated)]/50 hover:bg-[var(--ui-bg-elevated)] focus:bg-[var(--ui-bg-elevated)] disabled:bg-[var(--ui-bg-elevated)]/50',
|
||||
subtle: 'text-[var(--ui-text-highlighted)] bg-[var(--ui-bg-elevated)] ring ring-inset ring-[var(--ui-border-accented)]',
|
||||
ghost: 'text-[var(--ui-text-highlighted)] hover:bg-[var(--ui-bg-elevated)] focus:bg-[var(--ui-bg-elevated)] disabled:bg-transparent dark:disabled:bg-transparent',
|
||||
none: 'text-[var(--ui-text-highlighted)]'
|
||||
ghost: 'text-[var(--ui-text-highlighted)] bg-transparent hover:bg-[var(--ui-bg-elevated)] focus:bg-[var(--ui-bg-elevated)] disabled:bg-transparent dark:disabled:bg-transparent',
|
||||
none: 'text-[var(--ui-text-highlighted)] bg-transparent'
|
||||
},
|
||||
color: {
|
||||
...Object.fromEntries((options.theme.colors || []).map((color: string) => [color, ''])),
|
||||
|
||||
@@ -10,7 +10,7 @@ export default (options: Required<ModuleOptions>) => ({
|
||||
linkLeadingAvatar: 'shrink-0',
|
||||
linkLeadingAvatarSize: '2xs',
|
||||
linkTrailing: 'ms-auto inline-flex',
|
||||
linkTrailingBadge: 'shrink-0 rounded',
|
||||
linkTrailingBadge: 'shrink-0 rounded-[calc(var(--ui-radius)]',
|
||||
linkTrailingBadgeSize: 'sm',
|
||||
linkTrailingIcon: 'size-5 transform shrink-0 group-data-[state=open]:rotate-180 transition-transform duration-200',
|
||||
linkLabel: 'truncate',
|
||||
|
||||
@@ -7,26 +7,7 @@ export default (options: Required<ModuleOptions>) => {
|
||||
slots: {
|
||||
value: 'truncate',
|
||||
placeholder: 'truncate text-[var(--ui-text-dimmed)]',
|
||||
input: 'placeholder:text-[var(--ui-text-dimmed)] border-0 border-b border-[var(--ui-border)] focus:outline-none'
|
||||
},
|
||||
variants: {
|
||||
size: {
|
||||
xs: {
|
||||
input: 'text-xs px-2 py-1'
|
||||
},
|
||||
sm: {
|
||||
input: 'text-xs px-2.5 py-1.5'
|
||||
},
|
||||
md: {
|
||||
input: 'text-sm px-2.5 py-1.5'
|
||||
},
|
||||
lg: {
|
||||
input: 'text-sm px-3 py-2'
|
||||
},
|
||||
xl: {
|
||||
input: 'text-base px-3 py-2'
|
||||
}
|
||||
}
|
||||
input: 'border-b border-[var(--ui-border)]'
|
||||
}
|
||||
}, select(options))
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ export default (options: Required<ModuleOptions>) => ({
|
||||
slots: {
|
||||
root: 'relative overflow-auto',
|
||||
base: 'min-w-full overflow-clip',
|
||||
caption: 'sr-only',
|
||||
thead: 'relative [&>tr]:after:absolute [&>tr]:after:inset-x-0 [&>tr]:after:bottom-0 [&>tr]:after:h-px [&>tr]:after:bg-[var(--ui-border-accented)]',
|
||||
tbody: 'divide-y divide-[var(--ui-border)]',
|
||||
tr: 'data-[selected=true]:bg-[var(--ui-bg-elevated)]/50',
|
||||
|
||||
@@ -21,7 +21,7 @@ export default (options: Required<ModuleOptions>) => ({
|
||||
pill: {
|
||||
list: 'bg-[var(--ui-bg-elevated)] rounded-[calc(var(--ui-radius)*2)]',
|
||||
trigger: 'flex-1 w-full',
|
||||
indicator: 'rounded-[calc(var(--ui-radius)*1.5)] shadow-sm'
|
||||
indicator: 'rounded-[calc(var(--ui-radius)*1.5)] shadow-xs'
|
||||
},
|
||||
link: {
|
||||
list: 'border-[var(--ui-border)]',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export default {
|
||||
slots: {
|
||||
content: 'flex items-center gap-1 bg-[var(--ui-bg)] text-[var(--ui-text-highlighted)] shadow rounded-[var(--ui-radius)] ring ring-[var(--ui-border)] h-6 px-2 py-1 text-xs select-none data-[state=delayed-open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in]',
|
||||
content: 'flex items-center gap-1 bg-[var(--ui-bg)] text-[var(--ui-text-highlighted)] shadow-sm rounded-[var(--ui-radius)] ring ring-[var(--ui-border)] h-6 px-2 py-1 text-xs select-none data-[state=delayed-open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in]',
|
||||
arrow: 'fill-[var(--ui-border)]',
|
||||
text: 'truncate',
|
||||
kbds: `hidden lg:inline-flex items-center shrink-0 gap-0.5 before:content-['·'] before:me-0.5`,
|
||||
|
||||
@@ -62,7 +62,7 @@ describe('CommandPalette', () => {
|
||||
it.each([
|
||||
// Props
|
||||
['with groups', { props }],
|
||||
['without results', {}],
|
||||
['without data', {}],
|
||||
['with modelValue', { props: { ...props, modelValue: groups[2].items[0] } }],
|
||||
['with defaultValue', { props: { ...props, defaultValue: groups[2].items[0] } }],
|
||||
['with labelKey', { props: { ...props, labelKey: 'icon' } }],
|
||||
|
||||
@@ -363,4 +363,34 @@ describe('Form', () => {
|
||||
expect(wrapper.setupState.onError).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('form field errorPattern works', async () => {
|
||||
const wrapper = await mountSuspended({
|
||||
components: {
|
||||
UFormField,
|
||||
UForm,
|
||||
UInput
|
||||
},
|
||||
setup() {
|
||||
const form = ref()
|
||||
const state = reactive({})
|
||||
function validate() {
|
||||
return [{ name: 'email.1', message: 'Error message' }]
|
||||
}
|
||||
return { state, validate, form }
|
||||
},
|
||||
template: `
|
||||
<UForm ref="form" :state="state" :validate="validate">
|
||||
<UFormField id="emailField" :error-pattern="/(email)\\..*/">
|
||||
<UInput id="emailInput" v-model="state.email" />
|
||||
</UFormField>
|
||||
</UForm>
|
||||
`
|
||||
})
|
||||
|
||||
const form = wrapper.setupState.form
|
||||
form.value.submit()
|
||||
await flushPromises()
|
||||
expect(wrapper.html()).toContain('Error message')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -49,6 +49,7 @@ describe('SelectMenu', () => {
|
||||
['with placeholder', { props: { ...props, placeholder: 'Search...' } }],
|
||||
['without searchInput', { props: { ...props, searchInput: false } }],
|
||||
['with searchInput placeholder', { props: { ...props, searchInput: { placeholder: 'Filter items...' } } }],
|
||||
['with searchInput icon', { props: { ...props, searchInput: { icon: 'i-lucide-search' } } }],
|
||||
['with disabled', { props: { ...props, disabled: true } }],
|
||||
['with required', { props: { ...props, required: true } }],
|
||||
['with icon', { props: { icon: 'i-lucide-search' } }],
|
||||
|
||||
@@ -145,7 +145,8 @@ describe('Table', () => {
|
||||
it.each([
|
||||
// Props
|
||||
['with data', { props }],
|
||||
['without results', {}],
|
||||
['without data', {}],
|
||||
['with caption', { props: { ...props, caption: 'Table caption' } }],
|
||||
['with columns', { props: { ...props, columns } }],
|
||||
['with sticky', { props: { ...props, sticky: true } }],
|
||||
['with loading', { props: { ...props, loading: true } }],
|
||||
@@ -157,7 +158,8 @@ describe('Table', () => {
|
||||
['with header slot', { props, slots: { 'id-header': () => 'ID Header slot' } }],
|
||||
['with cell slot', { props, slots: { 'id-cell': () => 'ID Cell slot' } }],
|
||||
['with expanded slot', { props, slots: { expanded: () => 'Expanded slot' } }],
|
||||
['with empty slot', { props, slots: { empty: () => 'Empty slot' } }]
|
||||
['with empty slot', { props, slots: { empty: () => 'Empty slot' } }],
|
||||
['with caption slot', { props, slots: { caption: () => 'Caption slot' } }]
|
||||
])('renders %s correctly', async (nameOrHtml: string, options: { props?: TableProps<typeof data[number]>, slots?: Partial<TableSlots<typeof data[number]>> }) => {
|
||||
const html = await ComponentRender(nameOrHtml, options, Table)
|
||||
expect(html).toMatchSnapshot()
|
||||
|
||||
@@ -12,7 +12,7 @@ const ToastWrapper = defineComponent({
|
||||
ClientOnly
|
||||
},
|
||||
inheritAttrs: false,
|
||||
template: `<UToaster>
|
||||
template: `<UToaster :portal="false">
|
||||
<ClientOnly>
|
||||
<UToast v-bind="$attrs">
|
||||
<template v-for="(_, name) in $slots" #[name]="slotData">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
exports[`ButtonGroup > renders orientation vertical with default slot correctly 1`] = `
|
||||
"<div class="relative flex flex-col -space-y-px">
|
||||
<div class="relative inline-flex items-center group"><input type="text" class="w-full rounded-[calc(var(--ui-radius)*1.5)] border-0 placeholder:text-[var(--ui-text-dimmed)] focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 transition-colors group-not-only:group-first:rounded-b-none group-not-only:group-last:rounded-t-none group-not-last:group-not-first:rounded-none px-2.5 py-1.5 text-sm gap-1.5 text-[var(--ui-text-highlighted)] bg-[var(--ui-bg)] ring ring-inset ring-[var(--ui-border-accented)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--ui-primary)]" autocomplete="off">
|
||||
<div class="relative inline-flex items-center group"><input type="text" class="w-full rounded-[calc(var(--ui-radius)*1.5)] border-0 placeholder:text-[var(--ui-text-dimmed)] focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 transition-colors group-not-only:group-first:rounded-b-none group-not-only:group-last:rounded-t-none group-not-last:group-not-first:rounded-none px-2.5 py-1.5 text-sm text-[var(--ui-text-highlighted)] bg-[var(--ui-bg)] ring ring-inset ring-[var(--ui-border-accented)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--ui-primary)]" autocomplete="off">
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
</div> <button type="button" class="rounded-[calc(var(--ui-radius)*1.5)] font-medium inline-flex items-center focus:outline-none disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75 transition-colors not-only:first:rounded-b-none not-only:last:rounded-t-none not-last:not-first:rounded-none px-2.5 py-1.5 text-sm gap-1.5 text-[var(--ui-bg)] bg-[var(--ui-primary)] hover:bg-[var(--ui-primary)]/75 disabled:bg-[var(--ui-primary)] aria-disabled:bg-[var(--ui-primary)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ui-primary)]">
|
||||
@@ -18,7 +18,7 @@ exports[`ButtonGroup > renders with class correctly 1`] = `"<div class="inline-f
|
||||
|
||||
exports[`ButtonGroup > renders with default slot correctly 1`] = `
|
||||
"<div class="relative inline-flex -space-x-px">
|
||||
<div class="relative inline-flex items-center group"><input type="text" class="w-full rounded-[calc(var(--ui-radius)*1.5)] border-0 placeholder:text-[var(--ui-text-dimmed)] focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 transition-colors group-not-only:group-first:rounded-e-none group-not-only:group-last:rounded-s-none group-not-last:group-not-first:rounded-none px-2.5 py-1.5 text-sm gap-1.5 text-[var(--ui-text-highlighted)] bg-[var(--ui-bg)] ring ring-inset ring-[var(--ui-border-accented)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--ui-primary)]" autocomplete="off">
|
||||
<div class="relative inline-flex items-center group"><input type="text" class="w-full rounded-[calc(var(--ui-radius)*1.5)] border-0 placeholder:text-[var(--ui-text-dimmed)] focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 transition-colors group-not-only:group-first:rounded-e-none group-not-only:group-last:rounded-s-none group-not-last:group-not-first:rounded-none px-2.5 py-1.5 text-sm text-[var(--ui-text-highlighted)] bg-[var(--ui-bg)] ring ring-inset ring-[var(--ui-border-accented)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--ui-primary)]" autocomplete="off">
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
</div> <button type="button" class="rounded-[calc(var(--ui-radius)*1.5)] font-medium inline-flex items-center focus:outline-none disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75 transition-colors not-only:first:rounded-e-none not-only:last:rounded-s-none not-last:not-first:rounded-none px-2.5 py-1.5 text-sm gap-1.5 text-[var(--ui-bg)] bg-[var(--ui-primary)] hover:bg-[var(--ui-primary)]/75 disabled:bg-[var(--ui-primary)] aria-disabled:bg-[var(--ui-primary)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ui-primary)]">
|
||||
@@ -30,7 +30,7 @@ exports[`ButtonGroup > renders with default slot correctly 1`] = `
|
||||
|
||||
exports[`ButtonGroup > renders with size lg correctly 1`] = `
|
||||
"<div class="relative inline-flex -space-x-px">
|
||||
<div class="relative inline-flex items-center group"><input type="text" class="w-full rounded-[calc(var(--ui-radius)*1.5)] border-0 placeholder:text-[var(--ui-text-dimmed)] focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 transition-colors group-not-only:group-first:rounded-e-none group-not-only:group-last:rounded-s-none group-not-last:group-not-first:rounded-none px-3 py-2 text-sm gap-2 text-[var(--ui-text-highlighted)] bg-[var(--ui-bg)] ring ring-inset ring-[var(--ui-border-accented)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--ui-primary)]" autocomplete="off">
|
||||
<div class="relative inline-flex items-center group"><input type="text" class="w-full rounded-[calc(var(--ui-radius)*1.5)] border-0 placeholder:text-[var(--ui-text-dimmed)] focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 transition-colors group-not-only:group-first:rounded-e-none group-not-only:group-last:rounded-s-none group-not-last:group-not-first:rounded-none px-3 py-2 text-sm text-[var(--ui-text-highlighted)] bg-[var(--ui-bg)] ring ring-inset ring-[var(--ui-border-accented)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--ui-primary)]" autocomplete="off">
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
</div> <button type="button" class="rounded-[calc(var(--ui-radius)*1.5)] font-medium inline-flex items-center focus:outline-none disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75 transition-colors not-only:first:rounded-e-none not-only:last:rounded-s-none not-last:not-first:rounded-none px-3 py-2 text-sm gap-2 text-[var(--ui-bg)] bg-[var(--ui-primary)] hover:bg-[var(--ui-primary)]/75 disabled:bg-[var(--ui-primary)] aria-disabled:bg-[var(--ui-primary)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ui-primary)]">
|
||||
@@ -42,7 +42,7 @@ exports[`ButtonGroup > renders with size lg correctly 1`] = `
|
||||
|
||||
exports[`ButtonGroup > renders with size md correctly 1`] = `
|
||||
"<div class="relative inline-flex -space-x-px">
|
||||
<div class="relative inline-flex items-center group"><input type="text" class="w-full rounded-[calc(var(--ui-radius)*1.5)] border-0 placeholder:text-[var(--ui-text-dimmed)] focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 transition-colors group-not-only:group-first:rounded-e-none group-not-only:group-last:rounded-s-none group-not-last:group-not-first:rounded-none px-2.5 py-1.5 text-sm gap-1.5 text-[var(--ui-text-highlighted)] bg-[var(--ui-bg)] ring ring-inset ring-[var(--ui-border-accented)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--ui-primary)]" autocomplete="off">
|
||||
<div class="relative inline-flex items-center group"><input type="text" class="w-full rounded-[calc(var(--ui-radius)*1.5)] border-0 placeholder:text-[var(--ui-text-dimmed)] focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 transition-colors group-not-only:group-first:rounded-e-none group-not-only:group-last:rounded-s-none group-not-last:group-not-first:rounded-none px-2.5 py-1.5 text-sm text-[var(--ui-text-highlighted)] bg-[var(--ui-bg)] ring ring-inset ring-[var(--ui-border-accented)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--ui-primary)]" autocomplete="off">
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
</div> <button type="button" class="rounded-[calc(var(--ui-radius)*1.5)] font-medium inline-flex items-center focus:outline-none disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75 transition-colors not-only:first:rounded-e-none not-only:last:rounded-s-none not-last:not-first:rounded-none px-2.5 py-1.5 text-sm gap-1.5 text-[var(--ui-bg)] bg-[var(--ui-primary)] hover:bg-[var(--ui-primary)]/75 disabled:bg-[var(--ui-primary)] aria-disabled:bg-[var(--ui-primary)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ui-primary)]">
|
||||
@@ -54,7 +54,7 @@ exports[`ButtonGroup > renders with size md correctly 1`] = `
|
||||
|
||||
exports[`ButtonGroup > renders with size sm correctly 1`] = `
|
||||
"<div class="relative inline-flex -space-x-px">
|
||||
<div class="relative inline-flex items-center group"><input type="text" class="w-full rounded-[calc(var(--ui-radius)*1.5)] border-0 placeholder:text-[var(--ui-text-dimmed)] focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 transition-colors group-not-only:group-first:rounded-e-none group-not-only:group-last:rounded-s-none group-not-last:group-not-first:rounded-none px-2.5 py-1.5 text-xs gap-1.5 text-[var(--ui-text-highlighted)] bg-[var(--ui-bg)] ring ring-inset ring-[var(--ui-border-accented)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--ui-primary)]" autocomplete="off">
|
||||
<div class="relative inline-flex items-center group"><input type="text" class="w-full rounded-[calc(var(--ui-radius)*1.5)] border-0 placeholder:text-[var(--ui-text-dimmed)] focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 transition-colors group-not-only:group-first:rounded-e-none group-not-only:group-last:rounded-s-none group-not-last:group-not-first:rounded-none px-2.5 py-1.5 text-xs text-[var(--ui-text-highlighted)] bg-[var(--ui-bg)] ring ring-inset ring-[var(--ui-border-accented)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--ui-primary)]" autocomplete="off">
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
</div> <button type="button" class="rounded-[calc(var(--ui-radius)*1.5)] font-medium inline-flex items-center focus:outline-none disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75 transition-colors not-only:first:rounded-e-none not-only:last:rounded-s-none not-last:not-first:rounded-none px-2.5 py-1.5 text-xs gap-1.5 text-[var(--ui-bg)] bg-[var(--ui-primary)] hover:bg-[var(--ui-primary)]/75 disabled:bg-[var(--ui-primary)] aria-disabled:bg-[var(--ui-primary)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ui-primary)]">
|
||||
@@ -66,7 +66,7 @@ exports[`ButtonGroup > renders with size sm correctly 1`] = `
|
||||
|
||||
exports[`ButtonGroup > renders with size xl correctly 1`] = `
|
||||
"<div class="relative inline-flex -space-x-px">
|
||||
<div class="relative inline-flex items-center group"><input type="text" class="w-full rounded-[calc(var(--ui-radius)*1.5)] border-0 placeholder:text-[var(--ui-text-dimmed)] focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 transition-colors group-not-only:group-first:rounded-e-none group-not-only:group-last:rounded-s-none group-not-last:group-not-first:rounded-none px-3 py-2 text-base gap-2 text-[var(--ui-text-highlighted)] bg-[var(--ui-bg)] ring ring-inset ring-[var(--ui-border-accented)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--ui-primary)]" autocomplete="off">
|
||||
<div class="relative inline-flex items-center group"><input type="text" class="w-full rounded-[calc(var(--ui-radius)*1.5)] border-0 placeholder:text-[var(--ui-text-dimmed)] focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 transition-colors group-not-only:group-first:rounded-e-none group-not-only:group-last:rounded-s-none group-not-last:group-not-first:rounded-none px-3 py-2 text-base text-[var(--ui-text-highlighted)] bg-[var(--ui-bg)] ring ring-inset ring-[var(--ui-border-accented)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--ui-primary)]" autocomplete="off">
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
</div> <button type="button" class="rounded-[calc(var(--ui-radius)*1.5)] font-medium inline-flex items-center focus:outline-none disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75 transition-colors not-only:first:rounded-e-none not-only:last:rounded-s-none not-last:not-first:rounded-none px-3 py-2 text-base gap-2 text-[var(--ui-bg)] bg-[var(--ui-primary)] hover:bg-[var(--ui-primary)]/75 disabled:bg-[var(--ui-primary)] aria-disabled:bg-[var(--ui-primary)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ui-primary)]">
|
||||
@@ -78,7 +78,7 @@ exports[`ButtonGroup > renders with size xl correctly 1`] = `
|
||||
|
||||
exports[`ButtonGroup > renders with size xs correctly 1`] = `
|
||||
"<div class="relative inline-flex -space-x-px">
|
||||
<div class="relative inline-flex items-center group"><input type="text" class="w-full rounded-[calc(var(--ui-radius)*1.5)] border-0 placeholder:text-[var(--ui-text-dimmed)] focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 transition-colors group-not-only:group-first:rounded-e-none group-not-only:group-last:rounded-s-none group-not-last:group-not-first:rounded-none px-2 py-1 text-xs gap-1 text-[var(--ui-text-highlighted)] bg-[var(--ui-bg)] ring ring-inset ring-[var(--ui-border-accented)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--ui-primary)]" autocomplete="off">
|
||||
<div class="relative inline-flex items-center group"><input type="text" class="w-full rounded-[calc(var(--ui-radius)*1.5)] border-0 placeholder:text-[var(--ui-text-dimmed)] focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 transition-colors group-not-only:group-first:rounded-e-none group-not-only:group-last:rounded-s-none group-not-last:group-not-first:rounded-none px-2 py-1 text-xs text-[var(--ui-text-highlighted)] bg-[var(--ui-bg)] ring ring-inset ring-[var(--ui-border-accented)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--ui-primary)]" autocomplete="off">
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
</div> <button type="button" class="rounded-[calc(var(--ui-radius)*1.5)] font-medium inline-flex items-center focus:outline-none disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75 transition-colors not-only:first:rounded-e-none not-only:last:rounded-s-none not-last:not-first:rounded-none px-2 py-1 text-xs gap-1 text-[var(--ui-bg)] bg-[var(--ui-primary)] hover:bg-[var(--ui-primary)]/75 disabled:bg-[var(--ui-primary)] aria-disabled:bg-[var(--ui-primary)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ui-primary)]">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user