mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-15 20:48:12 +01:00
Compare commits
1 Commits
feat/updat
...
fix/form-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a56de3be0 |
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,20 +29,11 @@ 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.x
|
||||
placeholder: v3.0.0-alpha.5
|
||||
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.0.0-alpha.x
|
||||
- v3-alpha
|
||||
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.0.0-alpha.x
|
||||
- v3-alpha
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
||||
64
README.md
64
README.md
@@ -1,6 +1,6 @@
|
||||
[](https://ui.nuxt.com)
|
||||
|
||||
# Nuxt UI
|
||||
# Nuxt UI v3
|
||||
|
||||
[![npm version][npm-version-src]][npm-version-href]
|
||||
[![npm downloads][npm-downloads-src]][npm-downloads-href]
|
||||
@@ -9,15 +9,10 @@
|
||||
|
||||
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.
|
||||
|
||||
> [!NOTE]
|
||||
> You are on the `v3` development branch, check out the [dev branch](https://github.com/nuxt/ui) for Nuxt UI v2.
|
||||
|
||||
## Documentation
|
||||
|
||||
Visit https://ui3.nuxt.dev to explore the documentation.
|
||||
|
||||
## Installation
|
||||
|
||||
1. Install the Nuxt UI v3 alpha package:
|
||||
|
||||
```bash [pnpm]
|
||||
pnpm add @nuxt/ui@next
|
||||
```
|
||||
@@ -34,9 +29,10 @@ npm install @nuxt/ui@next
|
||||
bun add @nuxt/ui@next
|
||||
```
|
||||
|
||||
### Nuxt
|
||||
> [!WARNING]
|
||||
> Make sure you have `typescript` installed in your dev dependencies.
|
||||
|
||||
1. Add the Nuxt UI module in your `nuxt.config.ts`:
|
||||
2. Register the Nuxt UI module in your `nuxt.config.ts`:
|
||||
|
||||
```ts [nuxt.config.ts]
|
||||
export default defineNuxtConfig({
|
||||
@@ -44,54 +40,18 @@ export default defineNuxtConfig({
|
||||
})
|
||||
```
|
||||
|
||||
2. Import Tailwind CSS and Nuxt UI in your CSS:
|
||||
3. Import Tailwind CSS and Nuxt UI in your `app.vue` or [CSS](https://nuxt.com/docs/getting-started/styling#the-css-property):
|
||||
|
||||
```css [assets/css/main.css]
|
||||
```vue [app.vue]
|
||||
<style>
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui";
|
||||
</style>
|
||||
```
|
||||
|
||||
Learn more in the [installation guide](https://ui3.nuxt.dev/getting-started/installation/nuxt).
|
||||
## 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).
|
||||
Visit https://ui3.nuxt.dev to explore the documentation.
|
||||
|
||||
## 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: 'component',
|
||||
description: 'Make a new component.'
|
||||
name: 'init',
|
||||
description: 'Init a new component.'
|
||||
},
|
||||
args: {
|
||||
name: {
|
||||
@@ -1,14 +0,0 @@
|
||||
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
|
||||
}
|
||||
})
|
||||
@@ -1,53 +0,0 @@
|
||||
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 make from './commands/make/index.mjs'
|
||||
import init from './commands/init.mjs'
|
||||
|
||||
const main = defineCommand({
|
||||
meta: {
|
||||
@@ -8,7 +8,7 @@ const main = defineCommand({
|
||||
description: 'Nuxt UI CLI'
|
||||
},
|
||||
subCommands: {
|
||||
make
|
||||
init
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -15,17 +15,3 @@ 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-[var(--ui-radius)] border-[var(--ui-border)]">
|
||||
<div class="border rounded 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-[calc(var(--ui-radius)*1.5)] 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-lg 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-[var(--ui-radius)]">$1</code>')
|
||||
return props.meta.description?.replace(/`([^`]+)`/g, '<code class="font-medium bg-[var(--ui-bg-elevated)] px-1 py-0.5 rounded">$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-[var(--ui-radius)] 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 bg-[var(--ui-bg-elevated)]">
|
||||
{{ meta?.name }}
|
||||
</p>
|
||||
</template>
|
||||
|
||||
@@ -8,9 +8,7 @@ const appConfig = useAppConfig()
|
||||
const colorMode = useColorMode()
|
||||
|
||||
const { data: navigation } = await useAsyncData('navigation', () => queryCollectionNavigation('content'))
|
||||
const { data: files } = useLazyAsyncData('search', () => queryCollectionSearchSections('content'), {
|
||||
server: false
|
||||
})
|
||||
const { data: files } = await useAsyncData('files', () => queryCollectionSearchSections('content', { ignoredTags: ['style'] }))
|
||||
|
||||
const searchTerm = ref('')
|
||||
|
||||
@@ -81,11 +79,6 @@ 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: []
|
||||
})
|
||||
})) || []
|
||||
})))
|
||||
@@ -124,8 +117,6 @@ provide('navigation', updatedNavigation)
|
||||
@source "../content/**/*.md";
|
||||
|
||||
@theme {
|
||||
--container-8xl: 90rem;
|
||||
|
||||
--font-family-sans: 'Public Sans', sans-serif;
|
||||
|
||||
--color-green-50: #EFFDF5;
|
||||
@@ -142,6 +133,6 @@ provide('navigation', updatedNavigation)
|
||||
}
|
||||
|
||||
:root {
|
||||
--ui-container: var(--container-8xl);
|
||||
--ui-container-width: 90rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
<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" class="w-48" />
|
||||
<USelect v-model="state.select" :items="items" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField name="selectMenu" label="Select Menu">
|
||||
<USelectMenu v-model="state.selectMenu" :items="items" class="w-48" />
|
||||
<USelectMenu v-model="state.selectMenu" :items="items" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField name="selectMenuMultiple" label="Select Menu (Multiple)">
|
||||
<USelectMenu v-model="state.selectMenuMultiple" multiple :items="items" class="w-48" />
|
||||
<USelectMenu v-model="state.selectMenuMultiple" multiple :items="items" />
|
||||
</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 type="submit">
|
||||
<UButton color="neutral" type="submit">
|
||||
Submit
|
||||
</UButton>
|
||||
|
||||
<UButton variant="outline" @click="form?.clear()">
|
||||
<UButton color="neutral" variant="outline" @click="form?.clear()">
|
||||
Clear
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
<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="['label', 'email']"
|
||||
:filter="['name', 'email']"
|
||||
icon="i-lucide-user"
|
||||
placeholder="Select user"
|
||||
class="w-80"
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
<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="['label', 'email']"
|
||||
:filter="['name', '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-[var(--ui-radius)]"
|
||||
class="inline-flex ring ring-[var(--ui-border-accented)] rounded"
|
||||
:ui="{
|
||||
wrapper: 'bg-[var(--ui-bg-elevated)]/50 rounded-l-[var(--ui-radius)] flex border-r border-[var(--ui-border-accented)]',
|
||||
wrapper: 'bg-[var(--ui-bg-elevated)]/50 rounded-l flex border-r border-[var(--ui-border-accented)]',
|
||||
label: 'text-[var(--ui-text-muted)] px-2 py-1.5',
|
||||
container: 'mt-0'
|
||||
}"
|
||||
@@ -18,7 +18,8 @@ const appConfig = useAppConfig()
|
||||
v-model="appConfig.toaster.duration"
|
||||
color="neutral"
|
||||
variant="soft"
|
||||
:ui="{ base: 'rounded-[var(--ui-radius)] rounded-l-none min-w-12' }"
|
||||
class="rounded rounded-l-none min-w-12"
|
||||
:search-input="false"
|
||||
/>
|
||||
</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-[var(--ui-radius)]"
|
||||
class="inline-flex ring ring-[var(--ui-border-accented)] rounded"
|
||||
:ui="{
|
||||
wrapper: 'bg-[var(--ui-bg-elevated)]/50 rounded-l-[var(--ui-radius)] flex border-r border-[var(--ui-border-accented)]',
|
||||
wrapper: 'bg-[var(--ui-bg-elevated)]/50 rounded-l 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-[var(--ui-radius)] rounded-l-none min-w-12"
|
||||
class="rounded 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-[var(--ui-radius)]"
|
||||
class="inline-flex ring ring-[var(--ui-border-accented)] rounded"
|
||||
:ui="{
|
||||
wrapper: 'bg-[var(--ui-bg-elevated)]/50 rounded-l-[var(--ui-radius)] flex border-r border-[var(--ui-border-accented)]',
|
||||
wrapper: 'bg-[var(--ui-bg-elevated)]/50 rounded-l 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-[var(--ui-radius)] rounded-l-none min-w-12"
|
||||
class="rounded rounded-l-none min-w-12"
|
||||
:search-input="false"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
@@ -14,9 +14,7 @@ select:
|
||||
|
||||
## Setup
|
||||
|
||||
::steps{level="4"}
|
||||
|
||||
#### Install the Nuxt UI v3 alpha package
|
||||
1. Install the Nuxt UI v3 alpha package:
|
||||
|
||||
::code-group{sync="pm"}
|
||||
|
||||
@@ -39,10 +37,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` 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` directly in your project's root directory.
|
||||
::
|
||||
|
||||
#### Add the Nuxt UI module in your `nuxt.config.ts`{lang="ts-type"}
|
||||
2. Register the Nuxt UI module in your `nuxt.config.ts`{lang="ts-type"}:
|
||||
|
||||
```ts [nuxt.config.ts]
|
||||
export default defineNuxtConfig({
|
||||
@@ -50,24 +48,15 @@ export default defineNuxtConfig({
|
||||
})
|
||||
```
|
||||
|
||||
#### Import Tailwind CSS and Nuxt UI in your CSS
|
||||
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):
|
||||
|
||||
```css [assets/css/main.css]
|
||||
```vue [app.vue]
|
||||
<style>
|
||||
@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
|
||||
@@ -81,6 +70,8 @@ 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,9 +14,7 @@ select:
|
||||
|
||||
## Setup
|
||||
|
||||
::steps{level="4"}
|
||||
|
||||
#### Install the Nuxt UI v3 alpha package
|
||||
1. Install the Nuxt UI v3 alpha package:
|
||||
|
||||
::code-group{sync="pm"}
|
||||
|
||||
@@ -39,12 +37,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` 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` directly in your project's root directory.
|
||||
::
|
||||
|
||||
#### Add the Nuxt UI Vite plugin in your `vite.config.ts`{lang="ts-type"}
|
||||
2. Add the Nuxt UI Vite plugin in your `vite.config.ts`{lang="ts-type"}:
|
||||
|
||||
```ts [vite.config.ts]{3,8}
|
||||
```ts [vite.config.ts]
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import ui from '@nuxt/ui/vite'
|
||||
@@ -53,7 +51,7 @@ export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
ui()
|
||||
]
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
@@ -73,45 +71,28 @@ components.d.ts
|
||||
```
|
||||
::
|
||||
|
||||
#### Use the Nuxt UI Vue plugin in your `main.ts`
|
||||
3. Register the Nuxt UI Vue plugin in your app:
|
||||
|
||||
```ts [main.ts]{2,7}
|
||||
```ts [main.ts]
|
||||
import { createApp } from 'vue'
|
||||
import ui from '@nuxt/ui/vue-plugin'
|
||||
import nuxtUI from '@nuxt/ui/vue-plugin'
|
||||
import App from './App.vue'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(ui)
|
||||
|
||||
// ...
|
||||
app.use(nuxtUI)
|
||||
app.mount('#app')
|
||||
```
|
||||
|
||||
#### Import Tailwind CSS and Nuxt UI in your CSS
|
||||
4. Import Tailwind CSS and Nuxt UI in your `App.vue`{lang="ts-type"} or CSS:
|
||||
|
||||
```css [assets/main.css]
|
||||
```vue [App.vue]
|
||||
<style>
|
||||
@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
|
||||
@@ -125,6 +106,8 @@ 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,7 +11,8 @@ 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:
|
||||
|
||||
```css [main.css]
|
||||
```vue [app.vue]
|
||||
<style>
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui";
|
||||
|
||||
@@ -32,6 +33,7 @@ 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.
|
||||
@@ -46,11 +48,13 @@ 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):
|
||||
|
||||
```css [main.css]
|
||||
```vue [app.vue]
|
||||
<style>
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui";
|
||||
|
||||
@source "../content/**/*.md";
|
||||
</style>
|
||||
```
|
||||
|
||||
::note{to="https://github.com/tailwindlabs/tailwindcss/pull/14078"}
|
||||
@@ -61,11 +65,13 @@ You can learn more about the `@source` directive in this pull request.
|
||||
|
||||
You can use the `@plugin` directive to import Tailwind CSS plugins.
|
||||
|
||||
```css [main.css]
|
||||
```vue [app.vue]
|
||||
<style>
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui";
|
||||
|
||||
@plugin "@tailwindcss/typography";
|
||||
</style>
|
||||
```
|
||||
|
||||
::note{to="https://github.com/tailwindlabs/tailwindcss/pull/14264"}
|
||||
@@ -148,7 +154,8 @@ 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:
|
||||
|
||||
```css [main.css]
|
||||
```vue [app.vue]
|
||||
<style>
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui";
|
||||
|
||||
@@ -165,6 +172,7 @@ 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>
|
||||
```
|
||||
::
|
||||
|
||||
@@ -209,7 +217,8 @@ 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:
|
||||
|
||||
```css [main.css]
|
||||
```vue [app.vue]
|
||||
<style>
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui";
|
||||
|
||||
@@ -220,6 +229,7 @@ You can change which shade is used for each color on light and dark mode:
|
||||
.dark {
|
||||
--ui-primary: var(--ui-color-primary-200);
|
||||
}
|
||||
</style>
|
||||
```
|
||||
::
|
||||
|
||||
@@ -314,7 +324,8 @@ body {
|
||||
::tip
|
||||
You can customize these CSS variables to tailor the appearance of your application:
|
||||
|
||||
```css [main.css]
|
||||
```vue [app.vue]
|
||||
<style>
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui";
|
||||
|
||||
@@ -327,6 +338,7 @@ 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>
|
||||
```
|
||||
::
|
||||
|
||||
@@ -347,40 +359,15 @@ 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:
|
||||
|
||||
```css [main.css]
|
||||
```vue [app.vue]
|
||||
<style>
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui";
|
||||
|
||||
:root {
|
||||
--ui-radius: var(--radius-sm);
|
||||
}
|
||||
```
|
||||
::
|
||||
|
||||
#### 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);
|
||||
}
|
||||
</style>
|
||||
```
|
||||
::
|
||||
|
||||
@@ -397,7 +384,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-sm',
|
||||
root: 'bg-[var(--ui-bg)] ring ring-[var(--ui-border)] divide-y divide-[var(--ui-border)] rounded-[calc(var(--ui-radius)*2)] shadow',
|
||||
header: 'p-4 sm:px-6',
|
||||
body: 'p-4 sm:p-6',
|
||||
footer: 'p-4 sm:px-6'
|
||||
@@ -431,7 +418,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-[var(--ui-container)] mx-auto px-4 sm:px-6 lg:px-8'
|
||||
base: 'max-w-7xl mx-auto px-4 sm:px-6 lg:px-8'
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -12,13 +12,15 @@ 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:
|
||||
|
||||
```css [main.css]
|
||||
```vue [app.vue]
|
||||
<style>
|
||||
@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.
|
||||
|
||||
@@ -1,189 +0,0 @@
|
||||
---
|
||||
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>
|
||||
```
|
||||
|
||||
::
|
||||
@@ -1,198 +0,0 @@
|
||||
---
|
||||
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,14 +27,6 @@ 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` 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"}.
|
||||
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"}.
|
||||
|
||||
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. Defaults to `[labelKey]`.
|
||||
Use the `filter` prop with an array of fields to filter on.
|
||||
|
||||
::component-example
|
||||
---
|
||||
@@ -703,17 +703,6 @@ 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. You can control the open state of each item using the `open` and `defaultOpen` properties.
|
||||
When orientation is `vertical`, a [Collapsible](/components/collapsible) component is used to display children.
|
||||
|
||||
::component-code
|
||||
---
|
||||
@@ -152,7 +152,6 @@ props:
|
||||
items:
|
||||
- - label: Guide
|
||||
icon: i-lucide-book-open
|
||||
defaultOpen: true
|
||||
children:
|
||||
- label: Introduction
|
||||
description: Fully styled and customizable components for Nuxt.
|
||||
|
||||
@@ -200,9 +200,7 @@ props:
|
||||
|
||||
### Search Input
|
||||
|
||||
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.
|
||||
Use the `search-input` prop to customize the search input. Defaults to `{ placeholder: 'Search...' }`{lang="ts-type"}.
|
||||
|
||||
::component-code
|
||||
---
|
||||
@@ -221,7 +219,6 @@ props:
|
||||
icon: 'i-lucide-circle-help'
|
||||
searchInput:
|
||||
placeholder: 'Filter...'
|
||||
icon: 'i-lucide-search'
|
||||
items:
|
||||
- label: Backlog
|
||||
icon: 'i-lucide-circle-help'
|
||||
@@ -235,6 +232,10 @@ 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.
|
||||
@@ -731,7 +732,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. Defaults to `[labelKey]`.
|
||||
Use the `filter` prop with an array of fields to filter on.
|
||||
|
||||
::component-example
|
||||
---
|
||||
@@ -740,17 +741,6 @@ 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,4 +1,5 @@
|
||||
import { createResolver } from '@nuxt/kit'
|
||||
import module from '../src/module'
|
||||
import pkg from '../package.json'
|
||||
|
||||
const { resolve } = createResolver(import.meta.url)
|
||||
@@ -9,7 +10,7 @@ export default defineNuxtConfig({
|
||||
// ],
|
||||
|
||||
modules: [
|
||||
'../src/module',
|
||||
module,
|
||||
'@nuxt/ui-pro',
|
||||
'@nuxt/content',
|
||||
'@nuxt/image',
|
||||
@@ -56,7 +57,6 @@ 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,8 +70,7 @@ export default defineNuxtConfig({
|
||||
nitro: {
|
||||
prerender: {
|
||||
routes: [
|
||||
'/getting-started',
|
||||
'/api/countries.json'
|
||||
'/getting-started'
|
||||
// '/api/releases.json',
|
||||
// '/api/pulls.json'
|
||||
],
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
"name": "@nuxt/ui-docs",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@iconify-json/lucide": "^1.2.13",
|
||||
"@iconify-json/lucide": "^1.2.12",
|
||||
"@iconify-json/simple-icons": "^1.2.11",
|
||||
"@iconify-json/vscode-icons": "^1.2.2",
|
||||
"@nuxt/content": "3.0.0-alpha.6",
|
||||
"@nuxt/content": "3.0.0-alpha.5",
|
||||
"@nuxt/image": "^1.8.1",
|
||||
"@nuxt/ui": "latest",
|
||||
"@nuxt/ui-pro": "https://pkg.pr.new/@nuxt/ui-pro@7c62edd",
|
||||
"@nuxt/ui-pro": "https://pkg.pr.new/@nuxt/ui-pro@62862c8",
|
||||
"@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.86.1"
|
||||
"wrangler": "^3.85.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,202 +0,0 @@
|
||||
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,11 +30,7 @@
|
||||
"./vue-plugin": {
|
||||
"types": "./vue-plugin.d.ts"
|
||||
},
|
||||
"./runtime/*": "./dist/runtime/*",
|
||||
"./locale": {
|
||||
"types": "./dist/runtime/locale/index.d.ts",
|
||||
"import": "./dist/runtime/locale/index.js"
|
||||
}
|
||||
"./runtime/*": "./dist/runtime/*"
|
||||
},
|
||||
"imports": {
|
||||
"#build/ui/*": "./.nuxt/ui/*.ts"
|
||||
@@ -74,12 +70,12 @@
|
||||
"@iconify/vue": "^4.1.2",
|
||||
"@nuxt/devtools-kit": "^1.6.0",
|
||||
"@nuxt/fonts": "^0.10.2",
|
||||
"@nuxt/icon": "^1.7.2",
|
||||
"@nuxt/icon": "^1.6.1",
|
||||
"@nuxt/kit": "^3.14.159",
|
||||
"@nuxt/schema": "^3.14.159",
|
||||
"@nuxtjs/color-mode": "^3.5.2",
|
||||
"@tailwindcss/postcss": "4.0.0-alpha.33",
|
||||
"@tailwindcss/vite": "4.0.0-alpha.33",
|
||||
"@tailwindcss/postcss": "4.0.0-alpha.30",
|
||||
"@tailwindcss/vite": "4.0.0-alpha.30",
|
||||
"@tanstack/vue-table": "^8.20.5",
|
||||
"@unhead/vue": "^1.11.11",
|
||||
"@vueuse/core": "^11.2.0",
|
||||
@@ -100,11 +96,11 @@
|
||||
"mlly": "^1.7.2",
|
||||
"ohash": "^1.1.4",
|
||||
"pathe": "^1.1.2",
|
||||
"radix-vue": "^1.9.9",
|
||||
"radix-vue": "^1.9.8",
|
||||
"scule": "^1.3.0",
|
||||
"sirv": "^3.0.0",
|
||||
"tailwind-variants": "^0.2.1",
|
||||
"tailwindcss": "4.0.0-alpha.33",
|
||||
"tailwindcss": "4.0.0-alpha.30",
|
||||
"tinyglobby": "^0.2.10",
|
||||
"unplugin": "^1.15.0",
|
||||
"unplugin-auto-import": "^0.18.3",
|
||||
@@ -116,7 +112,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.3",
|
||||
"@standard-schema/spec": "1.0.0-beta.1",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"embla-carousel": "^8.3.1",
|
||||
"eslint": "^9.14.0",
|
||||
@@ -139,7 +135,6 @@
|
||||
},
|
||||
"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.5",
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
"typescript": "^5.6.3",
|
||||
"unplugin-auto-import": "^0.18.3",
|
||||
"unplugin-vue-components": "^0.27.4",
|
||||
"vite": "^5.4.11",
|
||||
"vite": "^5.4.10",
|
||||
"vue-tsc": "^2.1.10"
|
||||
}
|
||||
}
|
||||
|
||||
108
playground/app/components/FormElementsExample.vue
Normal file
108
playground/app/components/FormElementsExample.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<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>
|
||||
65
playground/app/components/FormNestedExample.vue
Normal file
65
playground/app/components/FormNestedExample.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<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>
|
||||
83
playground/app/components/FormNestedListExample.vue
Normal file
83
playground/app/components/FormNestedListExample.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<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,9 +1,6 @@
|
||||
<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(),
|
||||
@@ -11,20 +8,18 @@ const schema = z.object({
|
||||
tos: z.literal(true)
|
||||
})
|
||||
|
||||
type Schema = z.input<typeof schema>
|
||||
type Schema = z.output<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-8">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex gap-4">
|
||||
<UForm
|
||||
:state="state"
|
||||
@@ -45,24 +40,75 @@ const disabled = ref(false)
|
||||
</UFormField>
|
||||
|
||||
<div>
|
||||
<UButton type="submit">
|
||||
<UButton color="neutral" type="submit">
|
||||
Submit
|
||||
</UButton>
|
||||
</div>
|
||||
</UForm>
|
||||
<FormExampleNested />
|
||||
<FormExampleNestedList />
|
||||
|
||||
<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 />
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<USeparator class="my-8" />
|
||||
|
||||
<FormExampleElements :validate-on="validateOn" :disabled="disabled" class="border-t border-[var(--ui-border)] p-4" />
|
||||
<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>
|
||||
</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('vertical' as const)
|
||||
const orientation = ref('horizontal' as const)
|
||||
const highlight = ref(true)
|
||||
|
||||
const items = [
|
||||
@@ -16,7 +16,6 @@ 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.13",
|
||||
"@iconify-json/lucide": "^1.2.12",
|
||||
"@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
36
scripts/bump-edge.ts
Normal file
36
scripts/bump-edge.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
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)
|
||||
})
|
||||
19
scripts/release-edge.sh
Executable file
19
scripts/release-edge.sh
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/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
|
||||
16
scripts/release.sh
Executable file
16
scripts/release.sh
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/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,18 +176,16 @@ export default defineNuxtModule<ModuleOptions>({
|
||||
|
||||
nuxt.options.routeRules = defu(nuxt.options.routeRules, { '/__nuxt_ui__/**': { ssr: false } })
|
||||
extendPages((pages) => {
|
||||
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
|
||||
}
|
||||
})
|
||||
}
|
||||
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,7 +3,6 @@ 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'
|
||||
|
||||
@@ -75,7 +74,6 @@ const emits = defineEmits<AlertEmits>()
|
||||
const slots = defineSlots<AlertSlots>()
|
||||
|
||||
const appConfig = useAppConfig()
|
||||
const { t } = useLocale()
|
||||
|
||||
const multiline = computed(() => !!props.title && !!props.description)
|
||||
|
||||
@@ -125,7 +123,7 @@ const ui = computed(() => alert({
|
||||
size="md"
|
||||
color="neutral"
|
||||
variant="link"
|
||||
:aria-label="t('ui.alert.close')"
|
||||
aria-label="Close"
|
||||
v-bind="typeof close === 'object' ? close : undefined"
|
||||
:class="ui.close({ class: props.ui?.close })"
|
||||
@click="emits('update:open', false)"
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
<script lang="ts">
|
||||
import type { ConfigProviderProps, TooltipProviderProps } from 'radix-vue'
|
||||
import { localeContextInjectionKey } from '../composables/useLocale'
|
||||
import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
|
||||
import type { ToasterProps, Locale } from '../types'
|
||||
import type { ToasterProps } from '../types'
|
||||
|
||||
export interface AppProps extends Omit<ConfigProviderProps, 'useId'> {
|
||||
tooltip?: TooltipProviderProps
|
||||
toaster?: ToasterProps | null
|
||||
locale?: Locale
|
||||
}
|
||||
|
||||
export interface AppSlots {
|
||||
@@ -22,7 +20,7 @@ extendDevtoolsMeta({ ignore: true })
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { toRef, useId, provide, computed } from 'vue'
|
||||
import { toRef, useId } from 'vue'
|
||||
import { ConfigProvider, TooltipProvider, useForwardProps } from 'radix-vue'
|
||||
import { reactivePick } from '@vueuse/core'
|
||||
import UToaster from './Toaster.vue'
|
||||
@@ -35,8 +33,6 @@ 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,7 +10,6 @@ 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'
|
||||
@@ -135,7 +134,6 @@ 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({
|
||||
@@ -281,7 +279,7 @@ defineExpose({
|
||||
size="md"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
:aria-label="t('ui.carousel.prev')"
|
||||
aria-label="Prev"
|
||||
v-bind="typeof prev === 'object' ? prev : undefined"
|
||||
:class="ui.prev({ class: props.ui?.prev })"
|
||||
@click="scrollPrev"
|
||||
@@ -292,7 +290,7 @@ defineExpose({
|
||||
size="md"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
:aria-label="t('ui.carousel.next')"
|
||||
aria-label="Next"
|
||||
v-bind="typeof next === 'object' ? next : undefined"
|
||||
:class="ui.next({ class: props.ui?.next })"
|
||||
@click="scrollNext"
|
||||
@@ -302,7 +300,7 @@ defineExpose({
|
||||
<div v-if="dots" :class="ui.dots({ class: props.ui?.dots })">
|
||||
<template v-for="(_, index) in scrollSnaps" :key="index">
|
||||
<button
|
||||
:aria-label="t('ui.carousel.goto', { slide: index + 1 })"
|
||||
:aria-label="`Go to slide ${index + 1}`"
|
||||
:class="ui.dot({ class: props.ui?.dot, active: selectedIndex === index })"
|
||||
@click="scrollTo(index)"
|
||||
/>
|
||||
|
||||
@@ -7,7 +7,6 @@ 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'
|
||||
@@ -145,7 +144,6 @@ 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'))
|
||||
|
||||
@@ -247,7 +245,7 @@ const groups = computed(() => {
|
||||
size="md"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
:aria-label="t('ui.commandPalette.close')"
|
||||
aria-label="Close"
|
||||
v-bind="typeof close === 'object' ? close : undefined"
|
||||
:class="ui.close({ class: props.ui?.close })"
|
||||
@click="emits('update:open', false)"
|
||||
@@ -261,7 +259,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 ? t('ui.commandPalette.noMatch', { searchTerm }) : t('ui.commandPalette.noData') }}
|
||||
{{ searchTerm ? `No results for ${searchTerm}` : 'No results' }}
|
||||
</slot>
|
||||
</ComboboxEmpty>
|
||||
|
||||
|
||||
@@ -34,8 +34,6 @@ 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,8 +40,6 @@ 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, { id?: string, pattern?: RegExp }>>({})
|
||||
const inputs = ref<Record<string, string>>({})
|
||||
provide(formInputsInjectionKey, inputs)
|
||||
|
||||
function resolveErrorIds(errs: FormError[]): FormErrorWithId[] {
|
||||
return errs.map(err => ({
|
||||
...err,
|
||||
id: inputs.value[err.name]?.id
|
||||
id: inputs.value[err.name]
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -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 as string[]
|
||||
const names = opts.name && !Array.isArray(opts.name) ? [opts.name] : opts.name
|
||||
|
||||
const nestedValidatePromises = !names && opts.nested
|
||||
? Array.from(nestedForms.value.values()).map(
|
||||
@@ -143,16 +143,9 @@ async function _validate(opts: { name?: string | string[], silent?: boolean, nes
|
||||
: []
|
||||
|
||||
if (names) {
|
||||
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))
|
||||
}))
|
||||
|
||||
const otherErrors = errors.value.filter(error => !names!.includes(error.name))
|
||||
const pathErrors = (await getErrors()).filter(error => names!.includes(error.name)
|
||||
)
|
||||
errors.value = otherErrors.concat(pathErrors)
|
||||
} else {
|
||||
errors.value = await getErrors()
|
||||
@@ -203,7 +196,7 @@ provide(formOptionsInjectionKey, computed(() => ({
|
||||
validateOnInputDelay: props.validateOnInputDelay
|
||||
})))
|
||||
|
||||
defineExpose<Form<T>>({
|
||||
defineExpose<{ $el: HTMLFormElement | HTMLDivElement } & Form<T>>({
|
||||
validate: _validate,
|
||||
errors,
|
||||
|
||||
@@ -237,7 +230,7 @@ defineExpose<Form<T>>({
|
||||
},
|
||||
|
||||
disabled
|
||||
})
|
||||
} as { $el: HTMLFormElement | HTMLDivElement } & Form<T>)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -12,10 +12,7 @@ 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
|
||||
@@ -57,7 +54,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 || (props.errorPattern && error.name.match(props.errorPattern)))?.message)
|
||||
const error = computed(() => props.error || formErrors?.value?.find(error => error.name === props.name)?.message)
|
||||
|
||||
const id = ref(useId())
|
||||
|
||||
@@ -68,8 +65,7 @@ provide(formFieldInjectionKey, computed(() => ({
|
||||
name: props.name,
|
||||
size: props.size,
|
||||
eagerValidation: props.eagerValidation,
|
||||
validateOnInputDelay: props.validateOnInputDelay,
|
||||
errorPattern: props.errorPattern
|
||||
validateOnInputDelay: props.validateOnInputDelay
|
||||
}) as FormFieldInjectedOptions<FormFieldProps>))
|
||||
</script>
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ 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'
|
||||
@@ -79,10 +78,9 @@ 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. 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
|
||||
* 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']
|
||||
*/
|
||||
filter?: boolean | string[]
|
||||
/**
|
||||
@@ -150,7 +148,7 @@ const props = withDefaults(defineProps<InputMenuProps<T, I, V, M>>(), {
|
||||
type: 'text',
|
||||
autofocusDelay: 0,
|
||||
portal: true,
|
||||
filter: true,
|
||||
filter: () => ['label'],
|
||||
labelKey: 'label' as never
|
||||
})
|
||||
const emits = defineEmits<InputMenuEmits<T, V, M>>()
|
||||
@@ -159,7 +157,6 @@ 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)
|
||||
@@ -350,7 +347,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 ? t('ui.inputMenu.noMatch', { searchTerm }) : t('ui.inputMenu.noData') }}
|
||||
{{ searchTerm ? `No results for ${searchTerm}` : 'No results' }}
|
||||
</slot>
|
||||
</ComboboxEmpty>
|
||||
|
||||
|
||||
@@ -98,7 +98,6 @@ 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,7 +4,6 @@ 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'
|
||||
|
||||
@@ -96,8 +95,7 @@ const contentEvents = computed(() => {
|
||||
if (props.preventClose) {
|
||||
return {
|
||||
pointerDownOutside: (e: Event) => e.preventDefault(),
|
||||
interactOutside: (e: Event) => e.preventDefault(),
|
||||
escapeKeyDown: (e: Event) => e.preventDefault()
|
||||
interactOutside: (e: Event) => e.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +103,6 @@ const contentEvents = computed(() => {
|
||||
})
|
||||
|
||||
const appConfig = useAppConfig()
|
||||
const { t } = useLocale()
|
||||
|
||||
const ui = computed(() => modal({
|
||||
transition: props.transition,
|
||||
@@ -146,7 +143,7 @@ const ui = computed(() => modal({
|
||||
size="md"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
:aria-label="t('ui.modal.close')"
|
||||
aria-label="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, CollapsibleRootProps } from 'radix-vue'
|
||||
import type { NavigationMenuRootProps, NavigationMenuRootEmits, NavigationMenuContentProps } 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'>, Pick<CollapsibleRootProps, 'defaultOpen' | 'open'> {
|
||||
export interface NavigationMenuItem extends Omit<LinkProps, 'raw' | 'custom'> {
|
||||
label?: string
|
||||
icon?: string
|
||||
avatar?: AvatarProps
|
||||
@@ -208,8 +208,6 @@ 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,11 +31,6 @@ 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>
|
||||
}
|
||||
@@ -69,17 +64,6 @@ 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
|
||||
@@ -97,7 +81,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] })" v-on="contentEvents">
|
||||
<Component.Content v-bind="contentProps" :class="ui.content({ class: [props.class, props.ui?.content] })">
|
||||
<slot name="content" />
|
||||
|
||||
<Component.Arrow v-if="!!arrow" v-bind="arrowProps" :class="ui.arrow({ class: props.ui?.arrow })" />
|
||||
|
||||
@@ -5,7 +5,6 @@ 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'
|
||||
@@ -37,10 +36,9 @@ 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.
|
||||
* `{ placeholder: 'Search...', variant: 'none' }`{lang="ts-type"}
|
||||
* @defaultValue true
|
||||
* @defaultValue { placeholder: 'Search...' }
|
||||
*/
|
||||
searchInput?: boolean | InputProps
|
||||
searchInput?: boolean | { placeholder?: string }
|
||||
color?: SelectMenuVariants['color']
|
||||
variant?: SelectMenuVariants['variant']
|
||||
size?: SelectMenuVariants['size']
|
||||
@@ -71,10 +69,9 @@ 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. Defaults to `[labelKey]`.
|
||||
* 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 (useAsyncData, useFetch, etc.).
|
||||
* `['label']`{lang="ts-type"}
|
||||
* @defaultValue true
|
||||
* @defaultValue ['label']
|
||||
*/
|
||||
filter?: boolean | string[]
|
||||
/**
|
||||
@@ -134,14 +131,13 @@ 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: true,
|
||||
filter: true,
|
||||
searchInput: () => ({ placeholder: 'Search...' }),
|
||||
filter: () => ['label'],
|
||||
labelKey: 'label' as never
|
||||
})
|
||||
|
||||
@@ -151,12 +147,9 @@ 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)
|
||||
|
||||
@@ -281,13 +274,17 @@ function onUpdateOpen(value: boolean) {
|
||||
|
||||
<ComboboxPortal :disabled="!portal">
|
||||
<ComboboxContent :class="ui.content({ class: props.ui?.content })" v-bind="contentProps">
|
||||
<ComboboxInput v-if="!!searchInput" as-child>
|
||||
<UInput autofocus autocomplete="off" v-bind="searchInputProps" :class="ui.input({ class: props.ui?.input })" />
|
||||
</ComboboxInput>
|
||||
<ComboboxInput
|
||||
v-if="!!searchInput"
|
||||
autofocus
|
||||
autocomplete="off"
|
||||
v-bind="typeof searchInput === 'object' ? searchInput : {}"
|
||||
:class="ui.input({ class: props.ui?.input })"
|
||||
/>
|
||||
|
||||
<ComboboxEmpty :class="ui.empty({ class: props.ui?.empty })">
|
||||
<slot name="empty" :search-term="searchTerm">
|
||||
{{ searchTerm ? t('ui.selectMenu.noMatch', { searchTerm }) : t('ui.selectMenu.noData') }}
|
||||
{{ searchTerm ? `No results for ${searchTerm}` : 'No results' }}
|
||||
</slot>
|
||||
</ComboboxEmpty>
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ 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'
|
||||
|
||||
@@ -95,8 +94,7 @@ const contentEvents = computed(() => {
|
||||
if (props.preventClose) {
|
||||
return {
|
||||
pointerDownOutside: (e: Event) => e.preventDefault(),
|
||||
interactOutside: (e: Event) => e.preventDefault(),
|
||||
escapeKeyDown: (e: Event) => e.preventDefault()
|
||||
interactOutside: (e: Event) => e.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +102,6 @@ const contentEvents = computed(() => {
|
||||
})
|
||||
|
||||
const appConfig = useAppConfig()
|
||||
const { t } = useLocale()
|
||||
|
||||
const ui = computed(() => slideover({
|
||||
transition: props.transition,
|
||||
@@ -145,7 +142,7 @@ const ui = computed(() => slideover({
|
||||
size="md"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
:aria-label="t('ui.slideover.close')"
|
||||
aria-label="Close"
|
||||
v-bind="typeof close === 'object' ? close : undefined"
|
||||
:class="ui.close({ class: props.ui?.close })"
|
||||
/>
|
||||
|
||||
@@ -25,7 +25,6 @@ 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> } }
|
||||
|
||||
@@ -42,7 +41,6 @@ export interface TableData {
|
||||
export interface TableProps<T> {
|
||||
data?: T[]
|
||||
columns?: TableColumn<T>[]
|
||||
caption?: string
|
||||
/**
|
||||
* Whether the table should have a sticky header.
|
||||
* @defaultValue false
|
||||
@@ -97,7 +95,6 @@ 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>
|
||||
@@ -117,7 +114,6 @@ 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) })))
|
||||
|
||||
@@ -194,12 +190,6 @@ 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
|
||||
@@ -241,7 +231,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">
|
||||
{{ t('ui.table.noData') }}
|
||||
No results
|
||||
</slot>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -4,7 +4,6 @@ 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'
|
||||
|
||||
@@ -75,7 +74,6 @@ 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)
|
||||
@@ -153,7 +151,7 @@ defineExpose({
|
||||
size="md"
|
||||
color="neutral"
|
||||
variant="link"
|
||||
:aria-label="t('ui.toast.close')"
|
||||
aria-label="Close"
|
||||
v-bind="typeof close === 'object' ? close : undefined"
|
||||
:class="ui.close({ class: props.ui?.close })"
|
||||
@click.stop
|
||||
|
||||
@@ -19,11 +19,6 @@ 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>
|
||||
}
|
||||
@@ -49,7 +44,6 @@ import UToast from './Toast.vue'
|
||||
|
||||
const props = withDefaults(defineProps<ToasterProps>(), {
|
||||
expand: true,
|
||||
portal: true,
|
||||
duration: 5000
|
||||
})
|
||||
defineSlots<ToasterSlots>()
|
||||
@@ -126,20 +120,18 @@ function getOffset(index: number) {
|
||||
@click="toast.click && toast.click(toast)"
|
||||
/>
|
||||
|
||||
<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>
|
||||
<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"
|
||||
/>
|
||||
</ToastProvider>
|
||||
</template>
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
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, { id?: string, pattern?: RegExp }>>> = Symbol('nuxt-ui.form-inputs')
|
||||
export const formInputsInjectionKey: InjectionKey<Ref<Record<string, string>>> = 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] = { id: inputId.value, pattern: formField.value.errorPattern }
|
||||
formInputs.value[formField.value.name] = inputId.value
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
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,8 +31,7 @@
|
||||
--ui-border-accented: var(--ui-color-neutral-300);
|
||||
--ui-border-inverted: var(--ui-color-neutral-900);
|
||||
|
||||
--ui-radius: var(--radius-sm);
|
||||
--ui-container: var(--container-7xl);
|
||||
--ui-radius: var(--radius);
|
||||
}
|
||||
|
||||
.dark {
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
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: 'لا توجد بيانات'
|
||||
}
|
||||
})
|
||||
@@ -1,37 +0,0 @@
|
||||
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'
|
||||
}
|
||||
})
|
||||
@@ -1,37 +0,0 @@
|
||||
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'
|
||||
}
|
||||
})
|
||||
@@ -1,37 +0,0 @@
|
||||
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'
|
||||
}
|
||||
})
|
||||
@@ -1,37 +0,0 @@
|
||||
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'
|
||||
}
|
||||
})
|
||||
@@ -1,9 +0,0 @@
|
||||
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'
|
||||
@@ -1,37 +0,0 @@
|
||||
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'
|
||||
}
|
||||
})
|
||||
@@ -1,37 +0,0 @@
|
||||
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: 'Нет данных'
|
||||
}
|
||||
})
|
||||
@@ -1,37 +0,0 @@
|
||||
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: '没有数据'
|
||||
}
|
||||
})
|
||||
@@ -1,37 +0,0 @@
|
||||
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 { v1 } from '@standard-schema/spec'
|
||||
import type { StandardSchema } 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>
|
||||
| v1.StandardSchema
|
||||
| StandardSchema
|
||||
|
||||
export type FormInputEvents = 'input' | 'blur' | 'change'
|
||||
|
||||
@@ -82,7 +82,6 @@ export interface FormFieldInjectedOptions<T> {
|
||||
error?: string | boolean
|
||||
eagerValidation?: boolean
|
||||
validateOnInputDelay?: number
|
||||
errorPattern?: RegExp
|
||||
}
|
||||
|
||||
export class FormValidationException extends Error {
|
||||
|
||||
@@ -42,4 +42,3 @@ export * from '../components/Toast.vue'
|
||||
export * from '../components/Toaster.vue'
|
||||
export * from '../components/Tooltip.vue'
|
||||
export * from './form'
|
||||
export * from './locale'
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
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 { v1 } from '@standard-schema/spec'
|
||||
import type { StandardSchema } 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 v1.StandardSchema {
|
||||
export function isStandardSchema(schema: any): schema is StandardSchema {
|
||||
return '~standard' in schema
|
||||
}
|
||||
|
||||
export async function getStandardErrors(
|
||||
state: any,
|
||||
schema: v1.StandardSchema
|
||||
schema: StandardSchema
|
||||
): Promise<FormError[]> {
|
||||
const result = await schema['~standard'].validate(state)
|
||||
const result = await schema['~validate']({ value: state })
|
||||
return result.issues?.map(issue => ({
|
||||
name: issue.path?.map(item => typeof item === 'object' ? item.key : item).join('.') || '',
|
||||
message: issue.message
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
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-sm',
|
||||
root: 'bg-[var(--ui-bg)] ring ring-[var(--ui-border)] divide-y divide-[var(--ui-border)] rounded-[calc(var(--ui-radius)*2)] shadow',
|
||||
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-[var(--ui-container)] mx-auto px-4 sm:px-6 lg:px-8'
|
||||
base: 'max-w-7xl 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',
|
||||
base: 'px-2 py-1 text-xs gap-1',
|
||||
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',
|
||||
base: 'px-2.5 py-1.5 text-xs gap-1.5',
|
||||
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',
|
||||
base: 'px-2.5 py-1.5 text-sm gap-1.5',
|
||||
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',
|
||||
base: 'px-3 py-2 text-sm gap-2',
|
||||
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',
|
||||
base: 'px-3 py-2 text-base gap-2',
|
||||
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)] 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'
|
||||
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)]'
|
||||
},
|
||||
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-[calc(var(--ui-radius)]',
|
||||
linkTrailingBadge: 'shrink-0 rounded',
|
||||
linkTrailingBadgeSize: 'sm',
|
||||
linkTrailingIcon: 'size-5 transform shrink-0 group-data-[state=open]:rotate-180 transition-transform duration-200',
|
||||
linkLabel: 'truncate',
|
||||
|
||||
@@ -7,7 +7,26 @@ export default (options: Required<ModuleOptions>) => {
|
||||
slots: {
|
||||
value: 'truncate',
|
||||
placeholder: 'truncate text-[var(--ui-text-dimmed)]',
|
||||
input: 'border-b border-[var(--ui-border)]'
|
||||
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'
|
||||
}
|
||||
}
|
||||
}
|
||||
}, select(options))
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ 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-xs'
|
||||
indicator: 'rounded-[calc(var(--ui-radius)*1.5)] shadow-sm'
|
||||
},
|
||||
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-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]',
|
||||
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]',
|
||||
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 data', {}],
|
||||
['without results', {}],
|
||||
['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,34 +363,4 @@ 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,7 +49,6 @@ 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,8 +145,7 @@ describe('Table', () => {
|
||||
it.each([
|
||||
// Props
|
||||
['with data', { props }],
|
||||
['without data', {}],
|
||||
['with caption', { props: { ...props, caption: 'Table caption' } }],
|
||||
['without results', {}],
|
||||
['with columns', { props: { ...props, columns } }],
|
||||
['with sticky', { props: { ...props, sticky: true } }],
|
||||
['with loading', { props: { ...props, loading: true } }],
|
||||
@@ -158,8 +157,7 @@ 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 caption slot', { props, slots: { caption: () => 'Caption slot' } }]
|
||||
['with empty slot', { props, slots: { empty: () => 'Empty 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 :portal="false">
|
||||
template: `<UToaster>
|
||||
<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 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 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">
|
||||
<!--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 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 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">
|
||||
<!--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 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 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">
|
||||
<!--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 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 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">
|
||||
<!--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 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 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">
|
||||
<!--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 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 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">
|
||||
<!--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 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 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">
|
||||
<!--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