Compare commits

..

44 Commits

Author SHA1 Message Date
Benjamin Canac
49498d53a2 Merge branch 'v3' into feat/update-playground-form 2024-11-12 14:18:35 +01:00
Romain Hamel
17170bb998 playground(form): update examples (#2613) 2024-11-12 13:57:04 +01:00
renovate[bot]
fa5a3752c9 chore(deps): update tailwindcss to v4.0.0-alpha.33 (v3) (#2493)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-11-12 13:56:31 +01:00
Romain Hamel
8b975de35e feat(playground): update form examples 2024-11-12 13:44:09 +01:00
Benjamin Canac
fc9711223b chore(github): update issue templates 2024-11-12 13:11:42 +01:00
Alex
8a8b1ee2e1 feat(locale): provide code (#2611) 2024-11-12 12:57:40 +01:00
Benjamin Canac
30218f1b5b feat(NavigationMenu): control items open & defaultOpen on vertical
Resolves #2608
2024-11-12 11:12:19 +01:00
Romain Hamel
3584a3328b fix(Form): match error-pattern on input validation (#2606) 2024-11-11 22:50:22 +01:00
renovate[bot]
6d3dbdbee5 chore(deps): update all non-major dependencies (v3) (#2598)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-11 21:07:54 +01:00
renovate[bot]
c614a0aafc chore(deps): lock file maintenance (v3) (#2596)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-11 19:26:57 +01:00
Sandro Circi
df7a61a97a fix(useLocale): missing import in various components (#2603) 2024-11-11 19:24:33 +01:00
Romain Hamel
143612ec73 feat(FormField): add error-pattern prop (#2601) 2024-11-11 18:35:27 +01:00
Benjamin Canac
18931acdb3 fix(InputMenu/SelectMenu): init filter with labelKey 2024-11-11 00:28:43 +01:00
Benjamin Canac
bbc6bf2455 docs(input-menu/select-menu): add countries picker examples 2024-11-11 00:08:16 +01:00
Benjamin Canac
ff1e0798d3 feat(SelectMenu): use UInput in search to handle props like icon
Resolves #2021
2024-11-10 23:22:44 +01:00
Benjamin Canac
b0be26d67f fix(Toaster): teleport to body
Resolves #2404
2024-11-10 19:21:50 +01:00
Benjamin Canac
36ea3e4045 chore(scripts): remove 2024-11-10 18:36:52 +01:00
Adam Kasper
4889d30b44 feat(locale): add support for Czech translation (#2593) 2024-11-10 18:17:32 +01:00
Benjamin Canac
944a7e0f07 Revert "fix(module): resolve #build/app.config import for vue and nuxt"
This reverts commit d6943e39c0.
2024-11-10 17:25:33 +01:00
Benjamin Canac
d6943e39c0 fix(module): resolve #build/app.config import for vue and nuxt
Resolves #2560
2024-11-10 16:45:46 +01:00
Benjamin Canac
ddb46905e7 fix(App): missing vue imports 2024-11-10 16:44:09 +01:00
renovate[bot]
0e74dbebce chore(deps): update all non-major dependencies (v3) (#2579)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-10 14:45:10 +01:00
Benjamin Canac
9e2cc5b125 fix(Modal/Slideover): prevent esc with prevent-close prop
Resolves #2501
2024-11-10 10:19:47 +01:00
Benjamin Canac
ea97759c2c feat(Popover): add prevent-close prop
Resolves #2245
2024-11-10 10:18:08 +01:00
Dewdew
95a0bbc581 fix(Link): missing relative import (#2588)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-11-10 10:05:17 +01:00
Benjamin Canac
ecd63ad8d6 test: update vue snapshots 2024-11-10 09:42:11 +01:00
Benjamin Canac
47f58f52ef fix(ContextMenu/DropdownMenu): relative imports with prefix 2024-11-10 09:39:37 +01:00
Benjamin Canac
446f9c1085 feat(Table): add caption prop 2024-11-09 23:55:26 +01:00
Benjamin Canac
7e8a1dd496 chore(readme): update 2024-11-09 22:06:55 +01:00
Benjamin Canac
89ee31b7ae chore(readme): update 2024-11-09 22:03:56 +01:00
Benjamin Canac
95be76940c docs(getting-started): use ::steps and mention css files 2024-11-09 22:03:49 +01:00
Benjamin Canac
761afaf40d docs(deps): update @nuxt/ui-pro 2024-11-09 21:50:20 +01:00
Sandro Circi
d167c9b807 fix(locale): Italian translation (#2584) 2024-11-09 16:15:43 +01:00
Alex
824ba56291 feat(cli): add locale command (#2586) 2024-11-09 16:14:29 +01:00
Sandro Circi
4fbbb25f68 feat(locale): add support for Italian (#2583) 2024-11-09 13:50:03 +01:00
Muhammad Mahmoud
602a667343 feat(locale): add support for Arabic (#2582) 2024-11-09 13:49:50 +01:00
BlackWhite
febda5c2b6 feat(locale): translate chinese (#2580) 2024-11-09 10:40:42 +01:00
Malik-Jouda
20379f51cc docs(nuxt.config): cannot use import.meta outside a module (#2578) 2024-11-09 10:06:56 +01:00
renovate[bot]
1ec56f3326 chore(deps): update all non-major dependencies (v3) (#2572)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-08 18:03:14 +01:00
Alex
1f44d58b64 docs(i18n): auto generated lang support (#2574)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-11-08 17:48:42 +01:00
Benjamin Canac
5392f988b8 docs(app): fetch files lazy on client 2024-11-08 17:35:42 +01:00
Alex
26362408b1 feat(module): support i18n in components (#2553)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-11-08 17:22:57 +01:00
renovate[bot]
1e7638bd03 chore(deps): update all non-major dependencies (v3) (#2563)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2024-11-08 16:55:39 +01:00
Romain Hamel
afe40033b0 fix(module): skip devtools renderer page injection if router integration is disabled (#2571) 2024-11-08 16:27:25 +01:00
127 changed files with 4048 additions and 2157 deletions

View File

@@ -5,8 +5,8 @@ body:
- type: markdown
attributes:
value: |
> [!IMPORTANT]
> As Nuxt UI v3 is currently in alpha, we recommend thorough testing before using it in production environments. We're actively working on stabilization and welcome feedback from early adopters to improve the library.
> [!IMPORTANT]
> As Nuxt UI v3 is currently in alpha, we recommend thorough testing before using it in production environments. We're actively working on stabilization and welcome feedback from early adopters to improve the library.
- type: markdown
attributes:
value: |
@@ -29,11 +29,20 @@ body:
- Build Modules: `-`
validations:
required: true
- type: dropdown
id: package
attributes:
label: Is this bug related to Nuxt or Vue?
options:
- Nuxt
- Vue
validations:
required: true
- type: input
id: version
attributes:
label: Version
placeholder: v3.0.0-alpha.5
placeholder: v3.0.0-alpha.x
validations:
required: true
- type: textarea

View File

@@ -12,7 +12,7 @@ body:
label: For what version of Nuxt UI are you suggesting this?
options:
- v2.x
- v3-alpha
- v3.0.0-alpha.x
validations:
required: true
- type: textarea

View File

@@ -12,7 +12,7 @@ body:
label: For what version of Nuxt UI are you asking this question?
options:
- v2.x
- v3-alpha
- v3.0.0-alpha.x
validations:
required: true
- type: textarea

View File

@@ -1,6 +1,6 @@
[![nuxt-ui.png](https://repository-images.githubusercontent.com/428329515/43fec891-9030-4601-8233-5d45ba5c6013)](https://ui.nuxt.com)
# Nuxt UI v3
# Nuxt UI
[![npm version][npm-version-src]][npm-version-href]
[![npm downloads][npm-downloads-src]][npm-downloads-href]
@@ -9,9 +9,14 @@
We're thrilled to introduce Nuxt UI v3, a significant upgrade to our UI library that delivers extensive improvements and robust new capabilities. This major update harnesses the combined strengths of [Radix Vue](https://www.radix-vue.com/), [Tailwind CSS v4](https://tailwindcss.com/blog/tailwindcss-v4-alpha), and [Tailwind Variants](https://www.tailwind-variants.org/) to offer developers an unparalleled set of tools for creating sophisticated, accessible, and highly performant user interfaces.
## Installation
> [!NOTE]
> You are on the `v3` development branch, check out the [dev branch](https://github.com/nuxt/ui) for Nuxt UI v2.
1. Install the Nuxt UI v3 alpha package:
## Documentation
Visit https://ui3.nuxt.dev to explore the documentation.
## Installation
```bash [pnpm]
pnpm add @nuxt/ui@next
@@ -29,10 +34,9 @@ npm install @nuxt/ui@next
bun add @nuxt/ui@next
```
> [!WARNING]
> Make sure you have `typescript` installed in your dev dependencies.
### Nuxt
2. Register the Nuxt UI module in your `nuxt.config.ts`:
1. Add the Nuxt UI module in your `nuxt.config.ts`:
```ts [nuxt.config.ts]
export default defineNuxtConfig({
@@ -40,18 +44,54 @@ export default defineNuxtConfig({
})
```
3. Import Tailwind CSS and Nuxt UI in your `app.vue` or [CSS](https://nuxt.com/docs/getting-started/styling#the-css-property):
2. Import Tailwind CSS and Nuxt UI in your CSS:
```vue [app.vue]
<style>
```css [assets/css/main.css]
@import "tailwindcss";
@import "@nuxt/ui";
</style>
```
## Documentation
Learn more in the [installation guide](https://ui3.nuxt.dev/getting-started/installation/nuxt).
Visit https://ui3.nuxt.dev to explore the documentation.
### Vue
1. Add the Nuxt UI Vite plugin in your `vite.config.ts`:
```ts [vite.config.ts]
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'
export default defineConfig({
plugins: [
vue(),
ui()
]
})
```
2. Use the Nuxt UI Vue plugin in your `main.ts`:
```ts [main.ts]
import { createApp } from 'vue'
import ui from '@nuxt/ui/vue-plugin'
import App from './App.vue'
const app = createApp(App)
app.use(ui)
app.mount('#app')
```
3. Import Tailwind CSS and Nuxt UI in your CSS:
```css [assets/main.css]
@import "tailwindcss";
@import "@nuxt/ui";
```
Learn more in the [installation guide](https://ui3.nuxt.dev/getting-started/installation/vue).
## Credits

View File

@@ -3,13 +3,13 @@ import { resolve } from 'pathe'
import { defineCommand } from 'citty'
import { consola } from 'consola'
import { splitByCase, upperFirst, camelCase, kebabCase } from 'scule'
import { appendFile, sortFile } from '../utils.mjs'
import templates from '../templates.mjs'
import { appendFile, sortFile } from '../../utils.mjs'
import templates from '../../templates.mjs'
export default defineCommand({
meta: {
name: 'init',
description: 'Init a new component.'
name: 'component',
description: 'Make a new component.'
},
args: {
name: {

View File

@@ -0,0 +1,14 @@
import { defineCommand } from 'citty'
import component from './component.mjs'
import locale from './locale.mjs'
export default defineCommand({
meta: {
name: 'make',
description: 'Commands to create new Nuxt UI entities.'
},
subCommands: {
component,
locale
}
})

View File

@@ -0,0 +1,53 @@
import { existsSync, promises as fsp } from 'node:fs'
import { resolve } from 'pathe'
import { consola } from 'consola'
import { appendFile, sortFile, normalizeLocale } from '../../utils.mjs'
import { defineCommand } from 'citty'
export default defineCommand({
meta: {
name: 'locale',
description: 'Make a new locale.'
},
args: {
code: {
description: 'Locale code to create. For example: en.',
required: true
},
name: {
description: 'Locale name to create. For example: English.',
required: true
}
},
async setup({ args }) {
const path = resolve('.')
const localePath = resolve(path, `src/runtime/locale`)
const originLocaleFilePath = resolve(localePath, 'en.ts')
const newLocaleFilePath = resolve(localePath, `${args.code}.ts`)
// Validate locale code
if (existsSync(newLocaleFilePath)) {
consola.error(`🚨 ${args.code} already exists!`)
process.exit(1)
}
if (!args.code.match(/^[a-z]{2}(?:_[a-z]{2,4})?$/)) {
consola.error(`🚨 ${args.code} is not a valid locale code!\nExample: en or en_us`)
process.exit(1)
}
// Create new locale export
const localeExportFile = resolve(localePath, `index.ts`)
await appendFile(localeExportFile, `export { default as ${args.code} } from './${args.code}'`)
await sortFile(localeExportFile)
// Create new locale file
await fsp.copyFile(originLocaleFilePath, newLocaleFilePath)
const localeFile = await fsp.readFile(newLocaleFilePath, 'utf-8')
const rewrittenLocaleFile = localeFile.replace(/defineLocale\('(.*)'/, `defineLocale('${args.name}', '${normalizeLocale(args.code)}'`)
await fsp.writeFile(newLocaleFilePath, rewrittenLocaleFile)
consola.success(`🪄 Generated ${newLocaleFilePath}`)
}
})

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env node
import { defineCommand, runMain } from 'citty'
import init from './commands/init.mjs'
import make from './commands/make/index.mjs'
const main = defineCommand({
meta: {
@@ -8,7 +8,7 @@ const main = defineCommand({
description: 'Nuxt UI CLI'
},
subCommands: {
init
make
}
})

View File

@@ -15,3 +15,17 @@ export async function appendFile(path, contents) {
await fsp.writeFile(path, file.trim() + '\n' + contents + '\n')
}
}
export function normalizeLocale(locale) {
if (!locale) {
return ''
}
if (locale.includes('_')) {
return locale.split('_')
.map((part, index) => index === 0 ? part.toLowerCase() : part.toUpperCase())
.join('-')
}
return locale.toLowerCase()
}

View File

@@ -18,7 +18,7 @@ onMounted(() => {
</script>
<template>
<div class="border rounded border-[var(--ui-border)]">
<div class="border rounded-[var(--ui-radius)] border-[var(--ui-border)]">
<div
ref="wrapper"
:class="['overflow-hidden', collapsed && overflow ? 'max-h-48' : 'max-h-none']"

View File

@@ -126,7 +126,7 @@ const previewUrl = computed(() => {
</div>
<div v-if="highlightedCode && formattedCode" v-show="rendererReady" class="relative w-full p-3">
<!-- eslint-disable vue/no-v-html -->
<pre class="p-4 min-h-40 max-h-72 text-sm overflow-y-auto rounded-lg border border-[var(--ui-border)] bg-neutral-50 dark:bg-neutral-800" v-html="highlightedCode" />
<pre class="p-4 min-h-40 max-h-72 text-sm overflow-y-auto rounded-[calc(var(--ui-radius)*1.5)] border border-[var(--ui-border)] bg-neutral-50 dark:bg-neutral-800" v-html="highlightedCode" />
<UButton
color="neutral"
variant="link"

View File

@@ -17,14 +17,14 @@ watchEffect(() => {
})
const description = computed(() => {
return props.meta.description?.replace(/`([^`]+)`/g, '<code class="font-medium bg-[var(--ui-bg-elevated)] px-1 py-0.5 rounded">$1</code>')
return props.meta.description?.replace(/`([^`]+)`/g, '<code class="font-medium bg-[var(--ui-bg-elevated)] px-1 py-0.5 rounded-[var(--ui-radius)]">$1</code>')
})
</script>
<template>
<UFormField :name="meta?.name" class="" :ui="{ wrapper: 'mb-2' }" :class="{ 'opacity-70 cursor-not-allowed': !matchedInput || ignore }">
<template #label>
<p v-if="meta?.name" class="font-mono font-bold px-1.5 py-0.5 border border-[var(--ui-border-accented)] border-dashed rounded bg-[var(--ui-bg-elevated)]">
<p v-if="meta?.name" class="font-mono font-bold px-1.5 py-0.5 border border-[var(--ui-border-accented)] border-dashed rounded-[var(--ui-radius)] bg-[var(--ui-bg-elevated)]">
{{ meta?.name }}
</p>
</template>

View File

@@ -8,7 +8,9 @@ const appConfig = useAppConfig()
const colorMode = useColorMode()
const { data: navigation } = await useAsyncData('navigation', () => queryCollectionNavigation('content'))
const { data: files } = await useAsyncData('files', () => queryCollectionSearchSections('content', { ignoredTags: ['style'] }))
const { data: files } = useLazyAsyncData('search', () => queryCollectionSearchSections('content'), {
server: false
})
const searchTerm = ref('')
@@ -79,6 +81,11 @@ const updatedNavigation = computed(() => navigation.value?.map(item => ({
title: 'Installation',
active: route.path.startsWith('/getting-started/installation'),
children: []
}),
...(child.path === '/getting-started/i18n' && {
title: 'I18n',
active: route.path.startsWith('/getting-started/i18n'),
children: []
})
})) || []
})))
@@ -117,6 +124,8 @@ provide('navigation', updatedNavigation)
@source "../content/**/*.md";
@theme {
--container-8xl: 90rem;
--font-family-sans: 'Public Sans', sans-serif;
--color-green-50: #EFFDF5;
@@ -133,6 +142,6 @@ provide('navigation', updatedNavigation)
}
:root {
--ui-container-width: 90rem;
--ui-container: var(--container-8xl);
}
</style>

View File

@@ -0,0 +1,34 @@
<script setup lang="ts">
import * as locales from '@nuxt/ui/locale'
const props = withDefaults(defineProps<{
default?: string
}>(), {
default: 'en'
})
const getLocaleKeys = () => Object.keys(locales) as Array<keyof typeof locales>
const localesList = getLocaleKeys().map(locale => [locale, locales[locale].name])
</script>
<!-- eslint-disable vue/singleline-html-element-content-newline -->
<template>
<div>
<ProseUl>
<ProseLi v-for="[key, label] in localesList" :key="key">
<ProseCode>{{ key }}</ProseCode> - {{ label }}
<template v-if="key === props.default">
(default)
</template>
</ProseLi>
</ProseUl>
<Note to="https://github.com/nuxt/ui/tree/v3/src/runtime/locale" target="_blank">
If you need additional languages, you can contribute by creating a PR to add a new locale in <ProseCode>src/runtime/locale/</ProseCode>.
</Note>
<Tip>
You can use the <ProseCode>nuxt-ui</ProseCode> CLI to create a new locale:
<ProsePre language="bash">nuxt-ui make locale --code "en" --name "English"</ProsePre>
</Tip>
</div>
</template>

View File

@@ -73,15 +73,15 @@ async function onSubmit(event: FormSubmitEvent<any>) {
</UFormField>
<UFormField name="select" label="Select">
<USelect v-model="state.select" :items="items" />
<USelect v-model="state.select" :items="items" class="w-48" />
</UFormField>
<UFormField name="selectMenu" label="Select Menu">
<USelectMenu v-model="state.selectMenu" :items="items" />
<USelectMenu v-model="state.selectMenu" :items="items" class="w-48" />
</UFormField>
<UFormField name="selectMenuMultiple" label="Select Menu (Multiple)">
<USelectMenu v-model="state.selectMenuMultiple" multiple :items="items" />
<USelectMenu v-model="state.selectMenuMultiple" multiple :items="items" class="w-48" />
</UFormField>
<UFormField name="inputMenu" label="Input Menu">
@@ -104,11 +104,11 @@ async function onSubmit(event: FormSubmitEvent<any>) {
</div>
<div class="flex gap-2 mt-8">
<UButton color="neutral" type="submit">
<UButton type="submit">
Submit
</UButton>
<UButton color="neutral" variant="outline" @click="form?.clear()">
<UButton variant="outline" @click="form?.clear()">
Clear
</UButton>
</div>

View File

@@ -0,0 +1,39 @@
<script setup lang="ts">
const { data: countries, status, execute } = await useLazyFetch<{
name: string
code: string
emoji: string
}[]>('/api/countries.json', {
immediate: false
})
function onOpen() {
if (!countries.value?.length) {
execute()
}
}
</script>
<template>
<UInputMenu
:items="countries || []"
:loading="status === 'pending'"
label-key="name"
:search-input="{ icon: 'i-lucide-search' }"
placeholder="Select country"
class="w-48"
@update:open="onOpen"
>
<template #leading="{ modelValue, ui }">
<span v-if="modelValue" class="size-5 text-center">
{{ modelValue?.emoji }}
</span>
<UIcon v-else name="i-lucide-earth" :class="ui.leadingIcon()" />
</template>
<template #item-leading="{ item }">
<span class="size-5 text-center">
{{ item.emoji }}
</span>
</template>
</UInputMenu>
</template>

View File

@@ -16,7 +16,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
<UInputMenu
:items="users || []"
:loading="status === 'pending'"
:filter="['name', 'email']"
:filter="['label', 'email']"
icon="i-lucide-user"
placeholder="Select user"
class="w-80"

View File

@@ -0,0 +1,40 @@
<script setup lang="ts">
const { data: countries, status, execute } = await useLazyFetch<{
name: string
code: string
emoji: string
}[]>('/api/countries.json', {
immediate: false,
default: () => []
})
function onOpen() {
if (!countries.value?.length) {
execute()
}
}
</script>
<template>
<USelectMenu
:items="countries"
:loading="status === 'pending'"
label-key="name"
:search-input="{ icon: 'i-lucide-search' }"
placeholder="Select country"
class="w-48"
@update:open="onOpen"
>
<template #leading="{ modelValue, ui }">
<span v-if="modelValue" class="size-5 text-center">
{{ modelValue?.emoji }}
</span>
<UIcon v-else name="i-lucide-earth" :class="ui.leadingIcon()" />
</template>
<template #item-leading="{ item }">
<span class="size-5 text-center">
{{ item.emoji }}
</span>
</template>
</USelectMenu>
</template>

View File

@@ -16,7 +16,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
<USelectMenu
:items="users || []"
:loading="status === 'pending'"
:filter="['name', 'email']"
:filter="['label', 'email']"
icon="i-lucide-user"
placeholder="Select user"
class="w-80"

View File

@@ -7,9 +7,9 @@ const appConfig = useAppConfig()
<UFormField
label="toaster.duration"
size="sm"
class="inline-flex ring ring-[var(--ui-border-accented)] rounded"
class="inline-flex ring ring-[var(--ui-border-accented)] rounded-[var(--ui-radius)]"
:ui="{
wrapper: 'bg-[var(--ui-bg-elevated)]/50 rounded-l flex border-r border-[var(--ui-border-accented)]',
wrapper: 'bg-[var(--ui-bg-elevated)]/50 rounded-l-[var(--ui-radius)] flex border-r border-[var(--ui-border-accented)]',
label: 'text-[var(--ui-text-muted)] px-2 py-1.5',
container: 'mt-0'
}"
@@ -18,8 +18,7 @@ const appConfig = useAppConfig()
v-model="appConfig.toaster.duration"
color="neutral"
variant="soft"
class="rounded rounded-l-none min-w-12"
:search-input="false"
:ui="{ base: 'rounded-[var(--ui-radius)] rounded-l-none min-w-12' }"
/>
</UFormField>
</div>

View File

@@ -7,9 +7,9 @@ const appConfig = useAppConfig()
<UFormField
label="toaster.expand"
size="sm"
class="inline-flex ring ring-[var(--ui-border-accented)] rounded"
class="inline-flex ring ring-[var(--ui-border-accented)] rounded-[var(--ui-radius)]"
:ui="{
wrapper: 'bg-[var(--ui-bg-elevated)]/50 rounded-l flex border-r border-[var(--ui-border-accented)]',
wrapper: 'bg-[var(--ui-bg-elevated)]/50 rounded-l-[var(--ui-radius)] flex border-r border-[var(--ui-border-accented)]',
label: 'text-[var(--ui-text-muted)] px-2 py-1.5',
container: 'mt-0'
}"
@@ -19,7 +19,7 @@ const appConfig = useAppConfig()
:items="[true, false]"
color="neutral"
variant="soft"
class="rounded rounded-l-none min-w-12"
class="rounded-[var(--ui-radius)] rounded-l-none min-w-12"
:search-input="false"
/>
</UFormField>

View File

@@ -10,9 +10,9 @@ const appConfig = useAppConfig()
<UFormField
label="toaster.position"
size="sm"
class="inline-flex ring ring-[var(--ui-border-accented)] rounded"
class="inline-flex ring ring-[var(--ui-border-accented)] rounded-[var(--ui-radius)]"
:ui="{
wrapper: 'bg-[var(--ui-bg-elevated)]/50 rounded-l flex border-r border-[var(--ui-border-accented)]',
wrapper: 'bg-[var(--ui-bg-elevated)]/50 rounded-l-[var(--ui-radius)] flex border-r border-[var(--ui-border-accented)]',
label: 'text-[var(--ui-text-muted)] px-2 py-1.5',
container: 'mt-0'
}"
@@ -22,7 +22,7 @@ const appConfig = useAppConfig()
:items="positions"
color="neutral"
variant="soft"
class="rounded rounded-l-none min-w-12"
class="rounded-[var(--ui-radius)] rounded-l-none min-w-12"
:search-input="false"
/>
</UFormField>

View File

@@ -14,7 +14,9 @@ select:
## Setup
1. Install the Nuxt UI v3 alpha package:
::steps{level="4"}
#### Install the Nuxt UI v3 alpha package
::code-group{sync="pm"}
@@ -37,10 +39,10 @@ bun add @nuxt/ui@next
::
::warning
If you're using **pnpm**, ensure that you either set [`shamefully-hoist=true`](https://pnpm.io/npmrc#shamefully-hoist) in your `.npmrc` file or install `tailwindcss@next` directly in your project's root directory.
If you're using **pnpm**, ensure that you either set [`shamefully-hoist=true`](https://pnpm.io/npmrc#shamefully-hoist) in your `.npmrc` file or install `tailwindcss@next` in your project's root directory.
::
2. Register the Nuxt UI module in your `nuxt.config.ts`{lang="ts-type"}:
#### Add the Nuxt UI module in your `nuxt.config.ts`{lang="ts-type"}
```ts [nuxt.config.ts]
export default defineNuxtConfig({
@@ -48,15 +50,24 @@ export default defineNuxtConfig({
})
```
3. Import Tailwind CSS and Nuxt UI in your `app.vue`{lang="ts-type"} or [CSS](https://nuxt.com/docs/getting-started/styling#the-css-property):
#### Import Tailwind CSS and Nuxt UI in your CSS
```vue [app.vue]
<style>
```css [assets/css/main.css]
@import "tailwindcss";
@import "@nuxt/ui";
</style>
```
::note
Use the `css` property in your `nuxt.config.ts` to import your CSS file.
```ts [nuxt.config.ts]
export default defineNuxtConfig({
modules: ['@nuxt/ui'],
css: ['~/assets/css/main.css']
})
```
::
::tip
It's recommended to install the [Tailwind CSS IntelliSense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) extension for VSCode and add the following settings:
```json
@@ -70,8 +81,6 @@ It's recommended to install the [Tailwind CSS IntelliSense](https://marketplace.
::
::warning
IntelliSense works better when importing `tailwindcss` in a proper `.css` file which will be automatically detected.
::
## Options

View File

@@ -14,7 +14,9 @@ select:
## Setup
1. Install the Nuxt UI v3 alpha package:
::steps{level="4"}
#### Install the Nuxt UI v3 alpha package
::code-group{sync="pm"}
@@ -37,12 +39,12 @@ bun add @nuxt/ui@next
::
::warning
If you're using **pnpm**, ensure that you either set [`shamefully-hoist=true`](https://pnpm.io/npmrc#shamefully-hoist) in your `.npmrc` file or install `tailwindcss@next`, `vue-router` and `@unhead/vue` directly in your project's root directory.
If you're using **pnpm**, ensure that you either set [`shamefully-hoist=true`](https://pnpm.io/npmrc#shamefully-hoist) in your `.npmrc` file or install `tailwindcss@next`, `vue-router` and `@unhead/vue` in your project's root directory.
::
2. Add the Nuxt UI Vite plugin in your `vite.config.ts`{lang="ts-type"}:
#### Add the Nuxt UI Vite plugin in your `vite.config.ts`{lang="ts-type"}
```ts [vite.config.ts]
```ts [vite.config.ts]{3,8}
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'
@@ -51,7 +53,7 @@ export default defineConfig({
plugins: [
vue(),
ui()
],
]
})
```
@@ -71,28 +73,45 @@ components.d.ts
```
::
3. Register the Nuxt UI Vue plugin in your app:
#### Use the Nuxt UI Vue plugin in your `main.ts`
```ts [main.ts]
```ts [main.ts]{2,7}
import { createApp } from 'vue'
import nuxtUI from '@nuxt/ui/vue-plugin'
import ui from '@nuxt/ui/vue-plugin'
import App from './App.vue'
const app = createApp(App)
// ...
app.use(nuxtUI)
app.use(ui)
app.mount('#app')
```
4. Import Tailwind CSS and Nuxt UI in your `App.vue`{lang="ts-type"} or CSS:
#### Import Tailwind CSS and Nuxt UI in your CSS
```vue [App.vue]
<style>
```css [assets/main.css]
@import "tailwindcss";
@import "@nuxt/ui";
</style>
```
::note
Import the CSS file in your `main.ts`.
```ts [main.ts]{1}
import './assets/main.css'
import { createApp } from 'vue'
import ui from '@nuxt/ui/vue-plugin'
import App from './App.vue'
const app = createApp(App)
app.use(ui)
app.mount('#app')
```
::
::tip
It's recommended to install the [Tailwind CSS IntelliSense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) extension for VSCode and add the following settings:
```json
@@ -106,8 +125,6 @@ It's recommended to install the [Tailwind CSS IntelliSense](https://marketplace.
::
::warning
IntelliSense works better when importing `tailwindcss` in a proper `.css` file which will be automatically detected.
::
## Options

View File

@@ -11,8 +11,7 @@ Nuxt UI v3 uses Tailwind CSS v4 alpha which doesn't have a documentation yet, le
Tailwind CSS v4 takes a CSS-first configuration approach, you now customize your theme with CSS variables inside a `@theme` directive:
```vue [app.vue]
<style>
```css [main.css]
@import "tailwindcss";
@import "@nuxt/ui";
@@ -33,7 +32,6 @@ Tailwind CSS v4 takes a CSS-first configuration approach, you now customize your
--color-green-900: #0A5331;
--color-green-950: #052E16;
}
</style>
```
The `@theme` directive tells Tailwind to make new utilities and variants available based on these variables. It's the equivalent of the `theme.extend` key in Tailwind CSS v3 `tailwind.config.ts` file.
@@ -48,13 +46,11 @@ You can use the `@source` directive to add explicit content glob patterns if you
This can be useful when writing Tailwind classes in markdown files with [`@nuxt/content`](https://github.com/nuxt/content):
```vue [app.vue]
<style>
```css [main.css]
@import "tailwindcss";
@import "@nuxt/ui";
@source "../content/**/*.md";
</style>
```
::note{to="https://github.com/tailwindlabs/tailwindcss/pull/14078"}
@@ -65,13 +61,11 @@ You can learn more about the `@source` directive in this pull request.
You can use the `@plugin` directive to import Tailwind CSS plugins.
```vue [app.vue]
<style>
```css [main.css]
@import "tailwindcss";
@import "@nuxt/ui";
@plugin "@tailwindcss/typography";
</style>
```
::note{to="https://github.com/tailwindlabs/tailwindcss/pull/14264"}
@@ -154,8 +148,7 @@ These color aliases are not automatically defined as Tailwind CSS colors, so cla
However, you can generate these classes using Tailwind's `@theme` directive, allowing you to use custom color utility classes while maintaining dynamic color aliases:
```vue [app.vue]
<style>
```css [main.css]
@import "tailwindcss";
@import "@nuxt/ui";
@@ -172,7 +165,6 @@ However, you can generate these classes using Tailwind's `@theme` directive, all
--color-primary-900: var(--ui-color-primary-900);
--color-primary-950: var(--ui-color-primary-950);
}
</style>
```
::
@@ -217,8 +209,7 @@ You can use these variables in classes like `text-[var(--ui-primary)]`, it will
::tip
You can change which shade is used for each color on light and dark mode:
```vue [app.vue]
<style>
```css [main.css]
@import "tailwindcss";
@import "@nuxt/ui";
@@ -229,7 +220,6 @@ You can change which shade is used for each color on light and dark mode:
.dark {
--ui-primary: var(--ui-color-primary-200);
}
</style>
```
::
@@ -324,8 +314,7 @@ body {
::tip
You can customize these CSS variables to tailor the appearance of your application:
```vue [app.vue]
<style>
```css [main.css]
@import "tailwindcss";
@import "@nuxt/ui";
@@ -338,7 +327,6 @@ You can customize these CSS variables to tailor the appearance of your applicati
--ui-bg: var(--ui-color-neutral-950);
--ui-border: var(--ui-color-neutral-900);
}
</style>
```
::
@@ -359,15 +347,40 @@ Try the :prose-icon{name="i-lucide-swatch-book" class="text-[var(--ui-primary)]"
::tip
You can customize the default radius value using the default Tailwind CSS variables or a value of your choice:
```vue [app.vue]
<style>
```css [main.css]
@import "tailwindcss";
@import "@nuxt/ui";
:root {
--ui-radius: var(--radius-sm);
}
</style>
```
::
#### Container
Nuxt UI uses a global `--ui-container` CSS variable to define the width of the container:
```css
:root {
--ui-container: var(--container-7xl);
}
```
::tip
You can customize the default container width using the default Tailwind CSS variables or a value of your choice:
```css [main.css]
@import "tailwindcss";
@import "@nuxt/ui";
@theme {
--container-8xl: 90rem;
}
:root {
--ui-container: var(--container-8xl);
}
```
::
@@ -384,7 +397,7 @@ Components in Nuxt UI can have multiple `slots`, each representing a distinct HT
```ts [src/theme/card.ts]
export default {
slots: {
root: 'bg-[var(--ui-bg)] ring ring-[var(--ui-border)] divide-y divide-[var(--ui-border)] rounded-[calc(var(--ui-radius)*2)] shadow',
root: 'bg-[var(--ui-bg)] ring ring-[var(--ui-border)] divide-y divide-[var(--ui-border)] rounded-[calc(var(--ui-radius)*2)] shadow-sm',
header: 'p-4 sm:px-6',
body: 'p-4 sm:p-6',
footer: 'p-4 sm:px-6'
@@ -418,7 +431,7 @@ Some components don't have slots, they are just composed of a single root elemen
```ts [src/theme/container.ts]
export default {
base: 'max-w-7xl mx-auto px-4 sm:px-6 lg:px-8'
base: 'max-w-[var(--ui-container)] mx-auto px-4 sm:px-6 lg:px-8'
}
```

View File

@@ -12,15 +12,13 @@ links:
Nuxt UI automatically registers the [`@nuxt/fonts`](https://github.com/nuxt/fonts) module for you, so there's no additional setup required. To use a font in your Nuxt UI application, you can simply declare it in your CSS:
```vue [app.vue]
<style>
```css [main.css]
@import "tailwindcss";
@import "@nuxt/ui";
@theme {
--font-family-sans: 'Public Sans', sans-serif;
}
</style>
```
That's it! Nuxt Fonts will detect this and you should immediately see the web font loaded in your browser.

View File

@@ -0,0 +1,189 @@
---
navigation.title: Nuxt
title: Internationalization (i18n) in a Nuxt app
description: 'Learn how to internationalize your Nuxt app and support multi-directional support (LTR/RTL).'
select:
items:
- label: Nuxt
icon: i-logos-nuxt-icon
to: /getting-started/i18n/nuxt
- label: Vue
icon: i-logos-vue
to: /getting-started/i18n/vue
---
::note{to="/components/app"}
Nuxt UI provides an [App](/components/app) component that wraps your app to provide global configurations.
::
## Locale
Use the `locale` prop with the locale you want to use from `@nuxt/ui/locale`:
```vue [app.vue]
<script setup lang="ts">
import { fr } from '@nuxt/ui/locale'
</script>
<template>
<UApp :locale="fr">
<NuxtPage />
</UApp>
</template>
```
### Custom locale
You also have the option to add your own locale using `defineLocale`:
```vue [app.vue]
<script setup lang="ts">
const locale = defineLocale('My custom locale', 'en', {
// implement pairs
})
</script>
<template>
<UApp :locale="locale">
<NuxtPage />
</UApp>
</template>
```
::tip
Look at the second parameter, there you need to pass the iso code of the language. Example:
* `hi` Hindi (language)
* `de-AT`: German (language) as used in Austria (region)
::
### Dynamic locale
To dynamically switch between languages, you can use the [Nuxt I18n](https://i18n.nuxtjs.org/) module.
::steps{level="4"}
#### Install the Nuxt I18n package
::code-group{sync="pm"}
```bash [pnpm]
pnpm add @nuxtjs/i18n@next
```
```bash [yarn]
yarn add @nuxtjs/i18n@next
```
```bash [npm]
npm install @nuxtjs/i18n@next
```
```bash [bun]
bun add @nuxtjs/i18n@next
```
::
#### Add the Nuxt I18n module in your `nuxt.config.ts`{lang="ts-type"}
```ts [nuxt.config.ts]
export default defineNuxtConfig({
modules: [
'@nuxt/ui',
'@nuxtjs/i18n'
],
i18n: {
locales: [{
code: 'de',
name: 'Deutsch'
}, {
code: 'en',
name: 'English'
}, {
code: 'fr',
name: 'Français'
}]
}
})
```
#### Set the `locale` prop using `useI18n`
```vue [app.vue]
<script setup lang="ts">
import * as locales from '@nuxt/ui/locale'
const { locale } = useI18n()
</script>
<template>
<UApp :locale="locales[locale]">
<NuxtPage />
</UApp>
</template>
```
::
### Supported languages
:supported-languages
## Direction
Use the `dir` prop with `ltr` or `rtl` to set the global reading direction of your app:
```vue [app.vue]
<template>
<UApp dir="rtl">
<NuxtPage />
</UApp>
</template>
```
### Dynamic direction
To dynamically change the global reading direction of your app, you can use VueUse's [useTextDirection](https://vueuse.org/core/useTextDirection/) composable to detect and switch between LTR and RTL text directions.
::steps{level="4"}
#### Install the `@vueuse/core` package
::code-group{sync="pm"}
```bash [pnpm]
pnpm add @vueuse/core
```
```bash [yarn]
yarn add @vueuse/core
```
```bash [npm]
npm install @vueuse/core
```
```bash [bun]
bun add @vueuse/core
```
::
#### Set the `dir` prop using `useTextDirection`
```vue [app.vue]
<script setup lang="ts">
import { useTextDirection } from '@vueuse/core'
const textDirection = useTextDirection({ initialValue: 'ltr' })
const dir = computed(() => textDirection.value === 'rtl' ? 'rtl' : 'ltr')
</script>
<template>
<UApp :dir="dir">
<NuxtPage />
</UApp>
</template>
```
::

View File

@@ -0,0 +1,198 @@
---
navigation.title: Vue
title: Internationalization (i18n) in a Vue app
description: 'Learn how to internationalize your Vue app and support multi-directional support (LTR/RTL).'
select:
items:
- label: Nuxt
icon: i-logos-nuxt-icon
to: /getting-started/i18n/nuxt
- label: Vue
icon: i-logos-vue
to: /getting-started/i18n/vue
---
::note{to="/components/app"}
Nuxt UI provides an [App](/components/app) component that wraps your app to provide global configurations.
::
## Locale
Use the `locale` prop with the locale you want to use from `@nuxt/ui/locale`:
```vue [App.vue]
<script setup lang="ts">
import { fr } from '@nuxt/ui/locale'
</script>
<template>
<UApp :locale="fr">
<RouterView />
</UApp>
</template>
```
### Custom locale
You also have the option to add your locale using `defineLocale`:
```vue [App.vue]
<script setup lang="ts">
import { defineLocale } from '@nuxt/ui/runtime/composables/defineLocale'
const locale = defineLocale('My custom locale', 'en', {
// implement pairs
})
</script>
<template>
<UApp :locale="locale">
<RouterView />
</UApp>
</template>
```
::tip
Look at the second parameter, there you need to pass the iso code of the language. Example:
* `hi` Hindi (language)
* `de-AT`: German (language) as used in Austria (region)
::
### Dynamic locale
To dynamically switch between languages, you can use the [Vue I18n](https://vue-i18n.intlify.dev/) plugin.
::steps{level="4"}
#### Install the Vue I18n package
::code-group{sync="pm"}
```bash [pnpm]
pnpm add vue-i18n@10
```
```bash [yarn]
yarn add vue-i18n@10
```
```bash [npm]
npm install vue-i18n@10
```
```bash [bun]
bun add vue-i18n@10
```
::
#### Use the Vue I18n plugin in your `main.ts`
```ts [main.ts]{2,6-18,22}
import { createApp } from 'vue'
import { createI18n } from 'vue-i18n'
import ui from '@nuxt/ui/vue-plugin'
import App from './App.vue'
const i18n = createI18n({
legacy: false,
locale: 'en',
availableLocales: ['en', 'de'],
messages: {
en: {
// ...
},
de: {
// ...
}
}
})
const app = createApp(App)
app.use(i18n)
app.use(ui)
app.mount('#app')
```
#### Set the `locale` prop using `useI18n`
```vue [App.vue]
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import * as locales from '@nuxt/ui/locale'
const { locale } = useI18n()
</script>
<template>
<UApp :locale="locales[locale]">
<RouterView />
</UApp>
</template>
```
::
## Supported languages
:supported-languages
## Direction
Use the `dir` prop with `ltr` or `rtl` to set the global reading direction of your app:
```vue [App.vue]
<template>
<UApp dir="rtl">
<NuxtPage />
</UApp>
</template>
```
### Dynamic direction
To dynamically change the global reading direction of your app, you can use VueUse's [useTextDirection](https://vueuse.org/core/useTextDirection/) composable to detect and switch between LTR and RTL text directions.
::steps{level="4"}
#### Install the `@vueuse/core` package
::code-group{sync="pm"}
```bash [pnpm]
pnpm add @vueuse/core
```
```bash [yarn]
yarn add @vueuse/core
```
```bash [npm]
npm install @vueuse/core
```
```bash [bun]
bun add @vueuse/core
```
::
#### Set the `dir` prop using `useTextDirection`
```vue [App.vue]
<script setup lang="ts">
import { computed } from 'vue'
import { useTextDirection } from '@vueuse/core'
const textDirection = useTextDirection()
const dir = computed(() => textDirection.value === 'rtl' ? 'rtl' : 'ltr')
</script>
<template>
<UApp :dir="dir">
<RouterView />
</UApp>
</template>
```

View File

@@ -27,6 +27,14 @@ Use it as at the root of your app:
</template>
```
::tip{to="/getting-started/i18n/nuxt#locale"}
Learn how to use the `locale` prop to change the locale of your app.
::
::tip{to="/getting-started/i18n/nuxt#direction"}
Learn how to use the `dir` prop to change the global reading direction of your app.
::
## API
### Props

View File

@@ -64,7 +64,7 @@ It requires two props:
::
::
Errors are reported directly to the [FormField](/components/form-field) component based on the `name` prop. This means the validation rules defined for the `email` attribute in your schema will be applied to `<FormField name="email">`{lang="vue"}.
Errors are reported directly to the [FormField](/components/form-field) component based on the `name` or `error-pattern` prop. This means the validation rules defined for the `email` attribute in your schema will be applied to `<FormField name="email">`{lang="vue"}.
Nested validation rules are handled using dot notation. For example, a rule like `{ user: z.object({ email: z.string() }) }`{lang="ts"} will be applied to `<FormField name="user.email">`{lang="vue"}.

View File

@@ -694,7 +694,7 @@ This example uses [refDebounced](https://vueuse.org/shared/refDebounced/#refdebo
### With custom search
Use the `filter` prop with an array of fields to filter on.
Use the `filter` prop with an array of fields to filter on. Defaults to `[labelKey]`.
::component-example
---
@@ -703,6 +703,17 @@ name: 'input-menu-filter-fields-example'
---
::
### As a country picker
This example demonstrates using the InputMenu as a country picker with lazy loading - countries are only fetched when the menu is opened.
::component-example
---
collapse: true
name: 'input-menu-countries-example'
---
::
## API
### Props

View File

@@ -137,7 +137,7 @@ Each item can take a `children` array of objects with the following properties t
Use the `orientation` prop to change the orientation of the NavigationMenu.
When orientation is `vertical`, a [Collapsible](/components/collapsible) component is used to display children.
When orientation is `vertical`, a [Collapsible](/components/collapsible) component is used to display children. You can control the open state of each item using the `open` and `defaultOpen` properties.
::component-code
---
@@ -152,6 +152,7 @@ props:
items:
- - label: Guide
icon: i-lucide-book-open
defaultOpen: true
children:
- label: Introduction
description: Fully styled and customizable components for Nuxt.

View File

@@ -200,7 +200,9 @@ props:
### Search Input
Use the `search-input` prop to customize the search input. Defaults to `{ placeholder: 'Search...' }`{lang="ts-type"}.
Use the `search-input` prop to customize or hide the search input (with `false` value).
You can pass all the props of the [Input](/components/input) component to customize it.
::component-code
---
@@ -219,6 +221,7 @@ props:
icon: 'i-lucide-circle-help'
searchInput:
placeholder: 'Filter...'
icon: 'i-lucide-search'
items:
- label: Backlog
icon: 'i-lucide-circle-help'
@@ -232,10 +235,6 @@ props:
---
::
::tip
You can set the `search-input` prop to `false` to hide the search input.
::
### Content
Use the `content` prop to control how the SelectMenu content is rendered, like its `align` or `side` for example.
@@ -732,7 +731,7 @@ This example uses [refDebounced](https://vueuse.org/shared/refDebounced/#refdebo
### With custom search
Use the `filter` prop with an array of fields to filter on.
Use the `filter` prop with an array of fields to filter on. Defaults to `[labelKey]`.
::component-example
---
@@ -741,6 +740,17 @@ name: 'select-menu-filter-fields-example'
---
::
### As a country picker
This example demonstrates using the SelectMenu as a country picker with lazy loading - countries are only fetched when the menu is opened.
::component-example
---
collapse: true
name: 'select-menu-countries-example'
---
::
## API
### Props

View File

@@ -1,5 +1,4 @@
import { createResolver } from '@nuxt/kit'
import module from '../src/module'
import pkg from '../package.json'
const { resolve } = createResolver(import.meta.url)
@@ -10,7 +9,7 @@ export default defineNuxtConfig({
// ],
modules: [
module,
'../src/module',
'@nuxt/ui-pro',
'@nuxt/content',
'@nuxt/image',
@@ -57,6 +56,7 @@ export default defineNuxtConfig({
routeRules: {
'/': { redirect: '/getting-started', prerender: false },
'/getting-started/installation': { redirect: '/getting-started/installation/nuxt', prerender: false },
'/getting-started/i18n': { redirect: '/getting-started/i18n/nuxt', prerender: false },
'/composables': { redirect: '/composables/define-shortcuts', prerender: false },
'/components': { redirect: '/components/app', prerender: false }
},
@@ -70,7 +70,8 @@ export default defineNuxtConfig({
nitro: {
prerender: {
routes: [
'/getting-started'
'/getting-started',
'/api/countries.json'
// '/api/releases.json',
// '/api/pulls.json'
],

View File

@@ -3,13 +3,13 @@
"name": "@nuxt/ui-docs",
"type": "module",
"dependencies": {
"@iconify-json/lucide": "^1.2.12",
"@iconify-json/lucide": "^1.2.13",
"@iconify-json/simple-icons": "^1.2.11",
"@iconify-json/vscode-icons": "^1.2.2",
"@nuxt/content": "3.0.0-alpha.5",
"@nuxt/content": "3.0.0-alpha.6",
"@nuxt/image": "^1.8.1",
"@nuxt/ui": "latest",
"@nuxt/ui-pro": "https://pkg.pr.new/@nuxt/ui-pro@62862c8",
"@nuxt/ui-pro": "https://pkg.pr.new/@nuxt/ui-pro@7c62edd",
"@nuxthub/core": "^0.8.6",
"@nuxtjs/plausible": "^1.0.3",
"@octokit/rest": "^21.0.2",
@@ -27,6 +27,6 @@
"zod": "^3.23.8"
},
"devDependencies": {
"wrangler": "^3.85.0"
"wrangler": "^3.86.1"
}
}

View File

@@ -0,0 +1,202 @@
type Country = {
name: string
code: string
emoji: string
}
const countries: Country[] = [
{ name: 'Afghanistan', code: 'AF', emoji: '🇦🇫' },
{ name: 'Albania', code: 'AL', emoji: '🇦🇱' },
{ name: 'Algeria', code: 'DZ', emoji: '🇩🇿' },
{ name: 'Andorra', code: 'AD', emoji: '🇦🇩' },
{ name: 'Angola', code: 'AO', emoji: '🇦🇴' },
{ name: 'Antigua and Barbuda', code: 'AG', emoji: '🇦🇬' },
{ name: 'Argentina', code: 'AR', emoji: '🇦🇷' },
{ name: 'Armenia', code: 'AM', emoji: '🇦🇲' },
{ name: 'Australia', code: 'AU', emoji: '🇦🇺' },
{ name: 'Austria', code: 'AT', emoji: '🇦🇹' },
{ name: 'Azerbaijan', code: 'AZ', emoji: '🇦🇿' },
{ name: 'Bahamas', code: 'BS', emoji: '🇧🇸' },
{ name: 'Bahrain', code: 'BH', emoji: '🇧🇭' },
{ name: 'Bangladesh', code: 'BD', emoji: '🇧🇩' },
{ name: 'Barbados', code: 'BB', emoji: '🇧🇧' },
{ name: 'Belarus', code: 'BY', emoji: '🇧🇾' },
{ name: 'Belgium', code: 'BE', emoji: '🇧🇪' },
{ name: 'Belize', code: 'BZ', emoji: '🇧🇿' },
{ name: 'Benin', code: 'BJ', emoji: '🇧🇯' },
{ name: 'Bhutan', code: 'BT', emoji: '🇧🇹' },
{ name: 'Bolivia', code: 'BO', emoji: '🇧🇴' },
{ name: 'Bosnia and Herzegovina', code: 'BA', emoji: '🇧🇦' },
{ name: 'Botswana', code: 'BW', emoji: '🇧🇼' },
{ name: 'Brazil', code: 'BR', emoji: '🇧🇷' },
{ name: 'Brunei', code: 'BN', emoji: '🇧🇳' },
{ name: 'Bulgaria', code: 'BG', emoji: '🇧🇬' },
{ name: 'Burkina Faso', code: 'BF', emoji: '🇧🇫' },
{ name: 'Burundi', code: 'BI', emoji: '🇧🇮' },
{ name: 'Cambodia', code: 'KH', emoji: '🇰🇭' },
{ name: 'Cameroon', code: 'CM', emoji: '🇨🇲' },
{ name: 'Canada', code: 'CA', emoji: '🇨🇦' },
{ name: 'Cape Verde', code: 'CV', emoji: '🇨🇻' },
{ name: 'Central African Republic', code: 'CF', emoji: '🇨🇫' },
{ name: 'Chad', code: 'TD', emoji: '🇹🇩' },
{ name: 'Chile', code: 'CL', emoji: '🇨🇱' },
{ name: 'China', code: 'CN', emoji: '🇨🇳' },
{ name: 'Colombia', code: 'CO', emoji: '🇨🇴' },
{ name: 'Comoros', code: 'KM', emoji: '🇰🇲' },
{ name: 'Congo', code: 'CG', emoji: '🇨🇬' },
{ name: 'Costa Rica', code: 'CR', emoji: '🇨🇷' },
{ name: 'Croatia', code: 'HR', emoji: '🇭🇷' },
{ name: 'Cuba', code: 'CU', emoji: '🇨🇺' },
{ name: 'Cyprus', code: 'CY', emoji: '🇨🇾' },
{ name: 'Czech Republic', code: 'CZ', emoji: '🇨🇿' },
{ name: 'Denmark', code: 'DK', emoji: '🇩🇰' },
{ name: 'Djibouti', code: 'DJ', emoji: '🇩🇯' },
{ name: 'Dominica', code: 'DM', emoji: '🇩🇲' },
{ name: 'Dominican Republic', code: 'DO', emoji: '🇩🇴' },
{ name: 'East Timor', code: 'TL', emoji: '🇹🇱' },
{ name: 'Ecuador', code: 'EC', emoji: '🇪🇨' },
{ name: 'Egypt', code: 'EG', emoji: '🇪🇬' },
{ name: 'El Salvador', code: 'SV', emoji: '🇸🇻' },
{ name: 'Equatorial Guinea', code: 'GQ', emoji: '🇬🇶' },
{ name: 'Eritrea', code: 'ER', emoji: '🇪🇷' },
{ name: 'Estonia', code: 'EE', emoji: '🇪🇪' },
{ name: 'Ethiopia', code: 'ET', emoji: '🇪🇹' },
{ name: 'Fiji', code: 'FJ', emoji: '🇫🇯' },
{ name: 'Finland', code: 'FI', emoji: '🇫🇮' },
{ name: 'France', code: 'FR', emoji: '🇫🇷' },
{ name: 'Gabon', code: 'GA', emoji: '🇬🇦' },
{ name: 'Gambia', code: 'GM', emoji: '🇬🇲' },
{ name: 'Georgia', code: 'GE', emoji: '🇬🇪' },
{ name: 'Germany', code: 'DE', emoji: '🇩🇪' },
{ name: 'Ghana', code: 'GH', emoji: '🇬🇭' },
{ name: 'Greece', code: 'GR', emoji: '🇬🇷' },
{ name: 'Grenada', code: 'GD', emoji: '🇬🇩' },
{ name: 'Guatemala', code: 'GT', emoji: '🇬🇹' },
{ name: 'Guinea', code: 'GN', emoji: '🇬🇳' },
{ name: 'Guinea-Bissau', code: 'GW', emoji: '🇬🇼' },
{ name: 'Guyana', code: 'GY', emoji: '🇬🇾' },
{ name: 'Haiti', code: 'HT', emoji: '🇭🇹' },
{ name: 'Honduras', code: 'HN', emoji: '🇭🇳' },
{ name: 'Hungary', code: 'HU', emoji: '🇭🇺' },
{ name: 'Iceland', code: 'IS', emoji: '🇮🇸' },
{ name: 'India', code: 'IN', emoji: '🇮🇳' },
{ name: 'Indonesia', code: 'ID', emoji: '🇮🇩' },
{ name: 'Iran', code: 'IR', emoji: '🇮🇷' },
{ name: 'Iraq', code: 'IQ', emoji: '🇮🇶' },
{ name: 'Ireland', code: 'IE', emoji: '🇮🇪' },
{ name: 'Israel', code: 'IL', emoji: '🇮🇱' },
{ name: 'Italy', code: 'IT', emoji: '🇮🇹' },
{ name: 'Jamaica', code: 'JM', emoji: '🇯🇲' },
{ name: 'Japan', code: 'JP', emoji: '🇯🇵' },
{ name: 'Jordan', code: 'JO', emoji: '🇯🇴' },
{ name: 'Kazakhstan', code: 'KZ', emoji: '🇰🇿' },
{ name: 'Kenya', code: 'KE', emoji: '🇰🇪' },
{ name: 'Kiribati', code: 'KI', emoji: '🇰🇷' },
{ name: 'Kuwait', code: 'KW', emoji: '🇰🇼' },
{ name: 'Kyrgyzstan', code: 'KG', emoji: '🇰🇬' },
{ name: 'Laos', code: 'LA', emoji: '🇱🇦' },
{ name: 'Latvia', code: 'LV', emoji: '🇱🇻' },
{ name: 'Lebanon', code: 'LB', emoji: '🇱🇧' },
{ name: 'Lesotho', code: 'LS', emoji: '🇱🇸' },
{ name: 'Liberia', code: 'LR', emoji: '🇱🇷' },
{ name: 'Libya', code: 'LY', emoji: '🇱🇾' },
{ name: 'Liechtenstein', code: 'LI', emoji: '🇱🇮' },
{ name: 'Lithuania', code: 'LT', emoji: '🇱🇹' },
{ name: 'Luxembourg', code: 'LU', emoji: '🇱🇺' },
{ name: 'Madagascar', code: 'MG', emoji: '🇲🇬' },
{ name: 'Malawi', code: 'MW', emoji: '🇲🇼' },
{ name: 'Malaysia', code: 'MY', emoji: '🇲🇾' },
{ name: 'Maldives', code: 'MV', emoji: '🇲🇻' },
{ name: 'Mali', code: 'ML', emoji: '🇲🇱' },
{ name: 'Malta', code: 'MT', emoji: '🇲🇹' },
{ name: 'Marshall Islands', code: 'MH', emoji: '🇲🇭' },
{ name: 'Mauritania', code: 'MR', emoji: '🇲🇦' },
{ name: 'Mauritius', code: 'MU', emoji: '🇲🇺' },
{ name: 'Mexico', code: 'MX', emoji: '🇲🇽' },
{ name: 'Micronesia', code: 'FM', emoji: '🇫🇲' },
{ name: 'Moldova', code: 'MD', emoji: '🇲🇩' },
{ name: 'Monaco', code: 'MC', emoji: '🇲🇨' },
{ name: 'Mongolia', code: 'MN', emoji: '🇲🇳' },
{ name: 'Montenegro', code: 'ME', emoji: '🇲🇪' },
{ name: 'Morocco', code: 'MA', emoji: '🇲🇦' },
{ name: 'Mozambique', code: 'MZ', emoji: '🇲🇿' },
{ name: 'Myanmar', code: 'MM', emoji: '🇲🇲' },
{ name: 'Namibia', code: 'NA', emoji: '🇳🇦' },
{ name: 'Nauru', code: 'NR', emoji: '🇳🇷' },
{ name: 'Nepal', code: 'NP', emoji: '🇳🇵' },
{ name: 'Netherlands', code: 'NL', emoji: '🇳🇱' },
{ name: 'New Zealand', code: 'NZ', emoji: '🇳🇿' },
{ name: 'Nicaragua', code: 'NI', emoji: '🇳🇮' },
{ name: 'Niger', code: 'NE', emoji: '🇳🇪' },
{ name: 'Nigeria', code: 'NG', emoji: '🇳🇬' },
{ name: 'North Macedonia', code: 'MK', emoji: '🇲🇰' },
{ name: 'Norway', code: 'NO', emoji: '🇳🇴' },
{ name: 'Oman', code: 'OM', emoji: '🇴🇲' },
{ name: 'Pakistan', code: 'PK', emoji: '🇵🇰' },
{ name: 'Palau', code: 'PW', emoji: '🇵🇼' },
{ name: 'Palestine', code: 'PS', emoji: '🇵🇸' },
{ name: 'Panama', code: 'PA', emoji: '🇵🇦' },
{ name: 'Papua New Guinea', code: 'PG', emoji: '🇵🇬' },
{ name: 'Paraguay', code: 'PY', emoji: '🇵🇾' },
{ name: 'Peru', code: 'PE', emoji: '🇵🇪' },
{ name: 'Philippines', code: 'PH', emoji: '🇵🇭' },
{ name: 'Poland', code: 'PL', emoji: '🇵🇱' },
{ name: 'Portugal', code: 'PT', emoji: '🇵🇹' },
{ name: 'Qatar', code: 'QA', emoji: '🇶🇦' },
{ name: 'Romania', code: 'RO', emoji: '🇷🇴' },
{ name: 'Russia', code: 'RU', emoji: '🇷🇺' },
{ name: 'Rwanda', code: 'RW', emoji: '🇷🇼' },
{ name: 'Saint Kitts and Nevis', code: 'KN', emoji: '🇰🇳' },
{ name: 'Saint Lucia', code: 'LC', emoji: '🇱🇨' },
{ name: 'Saint Vincent and the Grenadines', code: 'VC', emoji: '🇻🇨' },
{ name: 'Samoa', code: 'WS', emoji: '🇼🇸' },
{ name: 'San Marino', code: 'SM', emoji: '🇸🇲' },
{ name: 'Sao Tome and Principe', code: 'ST', emoji: '🇸🇹' },
{ name: 'Saudi Arabia', code: 'SA', emoji: '🇸🇦' },
{ name: 'Senegal', code: 'SN', emoji: '🇸🇳' },
{ name: 'Serbia', code: 'RS', emoji: '🇷🇸' },
{ name: 'Seychelles', code: 'SC', emoji: '🇸🇨' },
{ name: 'Sierra Leone', code: 'SL', emoji: '🇸🇱' },
{ name: 'Singapore', code: 'SG', emoji: '🇸🇬' },
{ name: 'Slovakia', code: 'SK', emoji: '🇸🇰' },
{ name: 'Slovenia', code: 'SI', emoji: '🇸🇮' },
{ name: 'Solomon Islands', code: 'SB', emoji: '🇸🇧' },
{ name: 'Somalia', code: 'SO', emoji: '🇸🇴' },
{ name: 'South Africa', code: 'ZA', emoji: '🇿🇦' },
{ name: 'South Korea', code: 'KR', emoji: '🇰🇷' },
{ name: 'South Sudan', code: 'SS', emoji: '🇸🇸' },
{ name: 'Spain', code: 'ES', emoji: '🇪🇸' },
{ name: 'Sri Lanka', code: 'LK', emoji: '🇱🇰' },
{ name: 'Sudan', code: 'SD', emoji: '🇸🇩' },
{ name: 'Suriname', code: 'SR', emoji: '🇸🇷' },
{ name: 'Sweden', code: 'SE', emoji: '🇸🇪' },
{ name: 'Switzerland', code: 'CH', emoji: '🇨🇭' },
{ name: 'Syria', code: 'SY', emoji: '🇸🇾' },
{ name: 'Taiwan', code: 'TW', emoji: '🇹🇼' },
{ name: 'Tajikistan', code: 'TJ', emoji: '🇹🇯' },
{ name: 'Tanzania', code: 'TZ', emoji: '🇹🇿' },
{ name: 'Thailand', code: 'TH', emoji: '🇹🇭' },
{ name: 'Togo', code: 'TG', emoji: '🇹🇬' },
{ name: 'Tonga', code: 'TO', emoji: '🇹🇴' },
{ name: 'Trinidad and Tobago', code: 'TT', emoji: '🇹🇹' },
{ name: 'Tunisia', code: 'TN', emoji: '🇹🇳' },
{ name: 'Turkey', code: 'TR', emoji: '🇹🇷' },
{ name: 'Turkmenistan', code: 'TM', emoji: '🇹🇲' },
{ name: 'Tuvalu', code: 'TV', emoji: '🇹🇻' },
{ name: 'Uganda', code: 'UG', emoji: '🇺🇬' },
{ name: 'Ukraine', code: 'UA', emoji: '🇺🇦' },
{ name: 'United Arab Emirates', code: 'AE', emoji: '🇦🇪' },
{ name: 'United Kingdom', code: 'GB', emoji: '🇬🇧' },
{ name: 'United States', code: 'US', emoji: '🇺🇸' },
{ name: 'Uruguay', code: 'UY', emoji: '🇺🇾' },
{ name: 'Uzbekistan', code: 'UZ', emoji: '🇺🇿' },
{ name: 'Vanuatu', code: 'VU', emoji: '🇻🇺' },
{ name: 'Vatican City', code: 'VA', emoji: '🇻🇦' },
{ name: 'Venezuela', code: 'VE', emoji: '🇻🇪' },
{ name: 'Vietnam', code: 'VN', emoji: '🇻🇳' },
{ name: 'Yemen', code: 'YE', emoji: '🇾🇪' },
{ name: 'Zambia', code: 'ZM', emoji: '🇿🇲' },
{ name: 'Zimbabwe', code: 'ZW', emoji: '🇿🇼' }
]
export default eventHandler(async () => countries)

View File

@@ -30,7 +30,11 @@
"./vue-plugin": {
"types": "./vue-plugin.d.ts"
},
"./runtime/*": "./dist/runtime/*"
"./runtime/*": "./dist/runtime/*",
"./locale": {
"types": "./dist/runtime/locale/index.d.ts",
"import": "./dist/runtime/locale/index.js"
}
},
"imports": {
"#build/ui/*": "./.nuxt/ui/*.ts"
@@ -70,12 +74,12 @@
"@iconify/vue": "^4.1.2",
"@nuxt/devtools-kit": "^1.6.0",
"@nuxt/fonts": "^0.10.2",
"@nuxt/icon": "^1.6.1",
"@nuxt/icon": "^1.7.2",
"@nuxt/kit": "^3.14.159",
"@nuxt/schema": "^3.14.159",
"@nuxtjs/color-mode": "^3.5.2",
"@tailwindcss/postcss": "4.0.0-alpha.30",
"@tailwindcss/vite": "4.0.0-alpha.30",
"@tailwindcss/postcss": "4.0.0-alpha.33",
"@tailwindcss/vite": "4.0.0-alpha.33",
"@tanstack/vue-table": "^8.20.5",
"@unhead/vue": "^1.11.11",
"@vueuse/core": "^11.2.0",
@@ -96,11 +100,11 @@
"mlly": "^1.7.2",
"ohash": "^1.1.4",
"pathe": "^1.1.2",
"radix-vue": "^1.9.8",
"radix-vue": "^1.9.9",
"scule": "^1.3.0",
"sirv": "^3.0.0",
"tailwind-variants": "^0.2.1",
"tailwindcss": "4.0.0-alpha.30",
"tailwindcss": "4.0.0-alpha.33",
"tinyglobby": "^0.2.10",
"unplugin": "^1.15.0",
"unplugin-auto-import": "^0.18.3",
@@ -112,7 +116,7 @@
"@nuxt/module-builder": "^0.8.4",
"@nuxt/test-utils": "^3.14.4",
"@release-it/conventional-changelog": "^9.0.2",
"@standard-schema/spec": "1.0.0-beta.1",
"@standard-schema/spec": "1.0.0-beta.3",
"@vue/test-utils": "^2.4.6",
"embla-carousel": "^8.3.1",
"eslint": "^9.14.0",
@@ -135,6 +139,7 @@
},
"resolutions": {
"@nuxt/ui": "workspace:*",
"@nuxt/content": "3.0.0-alpha.5",
"happy-dom": "14.12.3",
"rollup": "^4.24.0"
}

View File

@@ -14,11 +14,11 @@
"vue-router": "^4.4.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.1.4",
"@vitejs/plugin-vue": "^5.1.5",
"typescript": "^5.6.3",
"unplugin-auto-import": "^0.18.3",
"unplugin-vue-components": "^0.27.4",
"vite": "^5.4.10",
"vite": "^5.4.11",
"vue-tsc": "^2.1.10"
}
}

View File

@@ -1,108 +0,0 @@
<script setup lang="ts">
import { z } from 'zod'
import type { FormSubmitEvent } from '@nuxt/ui'
const schema = z.object({
input: z.string().min(10),
inputMenu: z.any().refine(option => option?.value === 'option-2', {
message: 'Select Option 2'
}),
inputMenuMultiple: z.any().refine(values => !!values?.find((option: any) => option.value === 'option-2'), {
message: 'Include Option 2'
}),
textarea: z.string().min(10),
select: z.string().refine(value => value === 'option-2', {
message: 'Select Option 2'
}),
selectMenu: z.any().refine(option => option?.value === 'option-2', {
message: 'Select Option 2'
}),
selectMenuMultiple: z.any().refine(values => !!values?.find((option: any) => option.value === 'option-2'), {
message: 'Include Option 2'
}),
switch: z.boolean().refine(value => value === true, {
message: 'Toggle me'
}),
checkbox: z.boolean().refine(value => value === true, {
message: 'Check me'
}),
radioGroup: z.string().refine(value => value === 'option-2', {
message: 'Select Option 2'
}),
slider: z.number().max(20, { message: 'Must be less than 20' })
})
type Schema = z.output<typeof schema>
const state = reactive<Partial<Schema>>({})
const form = useTemplateRef('form')
const items = [
{ label: 'Option 1', value: 'option-1' },
{ label: 'Option 2', value: 'option-2' },
{ label: 'Option 3', value: 'option-3' }
]
function onSubmit(event: FormSubmitEvent<Schema>) {
console.log(event.data)
}
</script>
<template>
<UForm ref="form" :state="state" :schema="schema" class="gap-4 flex flex-col w-60" @submit="onSubmit">
<UFormField label="Input" name="input">
<UInput v-model="state.input" placeholder="john@lennon.com" />
</UFormField>
<UFormField label="Textarea" name="textarea">
<UTextarea v-model="state.textarea" />
</UFormField>
<UFormField name="select" label="Select">
<USelect v-model="state.select" class="w-44" :items="items" />
</UFormField>
<UFormField name="selectMenu" label="Select Menu">
<USelectMenu v-model="state.selectMenu" class="w-44" :items="items" />
</UFormField>
<UFormField name="selectMenuMultiple" label="Select Menu (Multiple)">
<USelectMenu v-model="state.selectMenuMultiple" class="w-44" multiple :items="items" />
</UFormField>
<UFormField name="inputMenu" label="Input Menu">
<UInputMenu v-model="state.inputMenu" :items="items" />
</UFormField>
<UFormField name="inputMenuMultiple" label="Input Menu (Multiple)">
<UInputMenu v-model="state.inputMenuMultiple" multiple :items="items" />
</UFormField>
<UFormField name="checkbox">
<UCheckbox v-model="state.checkbox" label="Check me" />
</UFormField>
<UFormField name="radioGroup">
<URadioGroup v-model="state.radioGroup" legend="Radio group" :items="items" />
</UFormField>
<UFormField name="switch">
<USwitch v-model="state.switch" label="Switch me" />
</UFormField>
<UFormField name="slider" label="Slider">
<USlider v-model="state.slider" />
</UFormField>
<div class="flex gap-2">
<UButton color="neutral" type="submit">
Submit
</UButton>
<UButton color="neutral" variant="outline" @click="form?.clear()">
Clear
</UButton>
</div>
</UForm>
</template>

View File

@@ -1,65 +0,0 @@
<script setup lang="ts">
import { z } from 'zod'
import type { FormSubmitEvent } from '@nuxt/ui'
const schema = z.object({
email: z.string().min(2),
password: z.string().min(8)
})
type Schema = z.output<typeof schema>
const nestedSchema = z.object({
phone: z.string().length(10)
})
type NestedSchema = z.output<typeof nestedSchema>
const state = reactive<Partial<Schema & { nested: Partial<NestedSchema> }>>({
nested: {}
})
const checked = ref(false)
function onSubmit(event: FormSubmitEvent<Schema>) {
console.log('Success', event.data)
}
function onError(event: any) {
console.log('Error', event)
}
</script>
<template>
<UForm
:state="state"
:schema="schema"
class="gap-4 flex flex-col w-60"
@submit="onSubmit"
@error="onError"
>
<UFormField label="Email" name="email">
<UInput v-model="state.email" placeholder="john@lennon.com" />
</UFormField>
<UFormField label="Password" name="password">
<UInput v-model="state.password" type="password" />
</UFormField>
<div>
<UCheckbox v-model="checked" name="check" label="Check me" @change="state.nested = {}" />
</div>
<UForm v-if="checked && state.nested" :state="state.nested" :schema="nestedSchema">
<UFormField label="Phone" name="phone">
<UInput v-model="state.nested.phone" />
</UFormField>
</UForm>
<div>
<UButton color="neutral" type="submit">
Submit
</UButton>
</div>
</UForm>
</template>

View File

@@ -1,83 +0,0 @@
<script setup lang="ts">
import { z } from 'zod'
import type { FormSubmitEvent } from '@nuxt/ui'
const schema = z.object({
email: z.string().min(2),
password: z.string().min(8)
})
type Schema = z.output<typeof schema>
const itemSchema = z.object({
name: z.string().min(1),
price: z.string().min(1)
})
type ItemSchema = z.output<typeof itemSchema>
const state = reactive<Partial<Schema & { items: Partial<ItemSchema>[] }>>({})
function addItem() {
if (!state.items) {
state.items = []
}
state.items.push({})
}
function removeItem() {
if (state.items) {
state.items.pop()
}
}
function onSubmit(event: FormSubmitEvent<Schema>) {
console.log('Success', event.data)
}
function onError(event: any) {
console.log('Error', event)
}
</script>
<template>
<UForm
:state="state"
:schema="schema"
class="gap-4 flex flex-col w-60"
@submit="onSubmit"
@error="onError"
>
<UFormField label="Email" name="email">
<UInput v-model="state.email" placeholder="john@lennon.com" />
</UFormField>
<UFormField label="Password" name="password">
<UInput v-model="state.password" type="password" />
</UFormField>
<UForm v-for="item, count in state.items" :key="count" :state="item" :schema="itemSchema" class="flex gap-2">
<UFormField label="Name" name="name">
<UInput v-model="item.name" />
</UFormField>
<UFormField label="Price" name="price">
<UInput v-model="item.price" />
</UFormField>
</UForm>
<div class="flex gap-2">
<UButton color="neutral" variant="subtle" size="sm" @click="addItem()">
Add Item
</UButton>
<UButton color="neutral" variant="ghost" size="sm" @click="removeItem()">
Remove Item
</UButton>
</div>
<div>
<UButton color="neutral" type="submit">
Submit
</UButton>
</div>
</UForm>
</template>

View File

@@ -1,6 +1,9 @@
<script setup lang="ts">
import { z } from 'zod'
import type { FormSubmitEvent } from '@nuxt/ui'
import FormExampleElements from '../../../../docs/app/components/content/examples/form/FormExampleElements.vue'
import FormExampleNestedList from '../../../../docs/app/components/content/examples/form/FormExampleNestedList.vue'
import FormExampleNested from '../../../../docs/app/components/content/examples/form/FormExampleNested.vue'
const schema = z.object({
email: z.string().email(),
@@ -8,18 +11,20 @@ const schema = z.object({
tos: z.literal(true)
})
type Schema = z.output<typeof schema>
type Schema = z.input<typeof schema>
const state = reactive<Partial<Schema>>({})
const state2 = reactive<Partial<Schema>>({})
function onSubmit(event: FormSubmitEvent<Schema>) {
console.log(event.data)
}
const validateOn = ref(['input', 'change', 'blur'])
const disabled = ref(false)
</script>
<template>
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-8">
<div class="flex gap-4">
<UForm
:state="state"
@@ -40,75 +45,24 @@ function onSubmit(event: FormSubmitEvent<Schema>) {
</UFormField>
<div>
<UButton color="neutral" type="submit">
<UButton type="submit">
Submit
</UButton>
</div>
</UForm>
<UForm
:state="state2"
:schema="schema"
class="gap-4 flex flex-col w-60"
:validate-on-input-delay="2000"
@submit="onSubmit"
>
<UFormField label="Email" name="email">
<UInput v-model="state2.email" placeholder="john@lennon.com" />
</UFormField>
<UFormField
label="Password"
name="password"
:validate-on-input-delay="50"
eager-validation
>
<UInput v-model="state2.password" type="password" />
</UFormField>
<div>
<UButton color="neutral" type="submit">
Submit
</UButton>
</div>
</UForm>
<FormNestedExample />
<FormNestedListExample />
<FormExampleNested />
<FormExampleNestedList />
</div>
<USeparator class="my-8" />
<div class="border border-[var(--ui-border)] rounded-lg">
<div class="py-2 px-4 flex gap-4 items-center">
<UFormField label="Validate on" class="flex items-center gap-2">
<USelectMenu v-model="validateOn" :items="['input', 'change', 'blur']" multiple class="w-48" />
</UFormField>
<UCheckbox v-model="disabled" label="Disabled" />
</div>
<div class="flex gap-4 flex-wrap">
<div>
<p class="text-lg font-bold underline mb-4">
Validate on input
</p>
<FormElementsExample :validate-on="['input']" />
</div>
<div>
<p class="text-lg font-bold underline mb-4">
Validate on change
</p>
<FormElementsExample :validate-on="['change']" />
</div>
<div>
<p class="text-lg font-bold underline mb-4">
Validate on blur
</p>
<FormElementsExample :validate-on="['blur']" />
</div>
<div>
<p class="text-lg font-bold underline mb-4">
Default
</p>
<FormElementsExample />
</div>
<div>
<p class="text-lg font-bold underline mb-4">
Disabled
</p>
<FormElementsExample disabled />
</div>
<FormExampleElements :validate-on="validateOn" :disabled="disabled" class="border-t border-[var(--ui-border)] p-4" />
</div>
</div>
</template>

View File

@@ -8,7 +8,7 @@ const orientations = Object.keys(theme.variants.orientation)
const color = ref(theme.defaultVariants.color)
const highlightColor = ref()
const variant = ref(theme.defaultVariants.variant)
const orientation = ref('horizontal' as const)
const orientation = ref('vertical' as const)
const highlight = ref(true)
const items = [
@@ -16,6 +16,7 @@ const items = [
label: 'Documentation',
icon: 'i-lucide-book-open',
badge: 10,
defaultOpen: true,
children: [{
label: 'Introduction',
description: 'Fully styled and customizable components for Nuxt.',

View File

@@ -8,7 +8,7 @@
"generate": "nuxi generate"
},
"dependencies": {
"@iconify-json/lucide": "^1.2.12",
"@iconify-json/lucide": "^1.2.13",
"@nuxt/ui": "latest",
"nuxt": "^3.14.159"
}

1661
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,36 +0,0 @@
import { promises as fsp } from 'node:fs'
import { resolve } from 'node:path'
import { execSync } from 'node:child_process'
async function loadPackage(dir: string) {
const pkgPath = resolve(dir, 'package.json')
const data = JSON.parse(await fsp.readFile(pkgPath, 'utf-8').catch(() => '{}'))
const save = () => fsp.writeFile(pkgPath, JSON.stringify(data, null, 2) + '\n')
return {
dir,
data,
save
}
}
async function main() {
const pkg = await loadPackage(process.cwd())
const commit = execSync('git rev-parse --short HEAD').toString('utf-8').trim()
const date = Math.round(Date.now() / (1000 * 60))
pkg.data.name = `${pkg.data.name}-edge`
pkg.data.version = `${pkg.data.version}-${date}.${commit}`
pkg.save()
}
main().catch((err) => {
console.error(err)
process.exit(1)
})

View File

@@ -1,19 +0,0 @@
#!/bin/bash
# Restore all git changes
git restore -s@ -SW -- .
# Bump versions to edge
pnpm jiti ./scripts/bump-edge
# Update token
if [[ ! -z ${NODE_AUTH_TOKEN} ]] ; then
echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" >> ~/.npmrc
echo "registry=https://registry.npmjs.org/" >> ~/.npmrc
echo "always-auth=true" >> ~/.npmrc
npm whoami
fi
# Release package
echo "Publishing @nuxt/ui"
npm publish -q --access public

View File

@@ -1,16 +0,0 @@
#!/bin/bash
# Restore all git changes
git restore -s@ -SW -- .
# Update token
if [[ ! -z ${NODE_AUTH_TOKEN} ]] ; then
echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" >> ~/.npmrc
echo "registry=https://registry.npmjs.org/" >> ~/.npmrc
echo "always-auth=true" >> ~/.npmrc
npm whoami
fi
# Release package
echo "Publishing @nuxt/ui"
npm publish -q --access public

View File

@@ -176,16 +176,18 @@ export default defineNuxtModule<ModuleOptions>({
nuxt.options.routeRules = defu(nuxt.options.routeRules, { '/__nuxt_ui__/**': { ssr: false } })
extendPages((pages) => {
pages.unshift({
name: 'ui-devtools',
path: '/__nuxt_ui__/components/:slug',
file: resolve('./devtools/runtime/DevtoolsRenderer.vue'),
meta: {
// https://github.com/nuxt/nuxt/pull/29366
// isolate: true
layout: false
}
})
if (pages.length) {
pages.unshift({
name: 'ui-devtools',
path: '/__nuxt_ui__/components/:slug',
file: resolve('./devtools/runtime/DevtoolsRenderer.vue'),
meta: {
// https://github.com/nuxt/nuxt/pull/29366
// isolate: true
layout: false
}
})
}
})
addCustomTab({

View File

@@ -3,6 +3,7 @@ import { tv, type VariantProps } from 'tailwind-variants'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/alert'
import { useLocale } from '../composables/useLocale'
import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
import type { AvatarProps, ButtonProps } from '../types'
@@ -74,6 +75,7 @@ const emits = defineEmits<AlertEmits>()
const slots = defineSlots<AlertSlots>()
const appConfig = useAppConfig()
const { t } = useLocale()
const multiline = computed(() => !!props.title && !!props.description)
@@ -123,7 +125,7 @@ const ui = computed(() => alert({
size="md"
color="neutral"
variant="link"
aria-label="Close"
:aria-label="t('ui.alert.close')"
v-bind="typeof close === 'object' ? close : undefined"
:class="ui.close({ class: props.ui?.close })"
@click="emits('update:open', false)"

View File

@@ -1,11 +1,13 @@
<script lang="ts">
import type { ConfigProviderProps, TooltipProviderProps } from 'radix-vue'
import { localeContextInjectionKey } from '../composables/useLocale'
import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
import type { ToasterProps } from '../types'
import type { ToasterProps, Locale } from '../types'
export interface AppProps extends Omit<ConfigProviderProps, 'useId'> {
tooltip?: TooltipProviderProps
toaster?: ToasterProps | null
locale?: Locale
}
export interface AppSlots {
@@ -20,7 +22,7 @@ extendDevtoolsMeta({ ignore: true })
</script>
<script setup lang="ts">
import { toRef, useId } from 'vue'
import { toRef, useId, provide, computed } from 'vue'
import { ConfigProvider, TooltipProvider, useForwardProps } from 'radix-vue'
import { reactivePick } from '@vueuse/core'
import UToaster from './Toaster.vue'
@@ -33,6 +35,8 @@ defineSlots<AppSlots>()
const configProviderProps = useForwardProps(reactivePick(props, 'dir', 'scrollBody'))
const tooltipProps = toRef(() => props.tooltip)
const toasterProps = toRef(() => props.toaster)
provide(localeContextInjectionKey, computed(() => props.locale))
</script>
<template>

View File

@@ -10,6 +10,7 @@ import type { FadeOptionsType } from 'embla-carousel-fade'
import type { WheelGesturesPluginOptions } from 'embla-carousel-wheel-gestures'
import _appConfig from '#build/app.config'
import theme from '#build/ui/carousel'
import { useLocale } from '../composables/useLocale'
import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
import type { ButtonProps } from '../types'
import type { AcceptableValue, PartialString } from '../types/utils'
@@ -134,6 +135,7 @@ const props = withDefaults(defineProps<CarouselProps<T>>(), {
defineSlots<CarouselSlots<T>>()
const appConfig = useAppConfig()
const { t } = useLocale()
const rootProps = useForwardProps(reactivePick(props, 'active', 'align', 'breakpoints', 'containScroll', 'dragFree', 'dragThreshold', 'duration', 'inViewThreshold', 'loop', 'skipSnaps', 'slidesToScroll', 'startIndex', 'watchDrag', 'watchResize', 'watchSlides', 'watchFocus'))
const ui = computed(() => carousel({
@@ -279,7 +281,7 @@ defineExpose({
size="md"
color="neutral"
variant="outline"
aria-label="Prev"
:aria-label="t('ui.carousel.prev')"
v-bind="typeof prev === 'object' ? prev : undefined"
:class="ui.prev({ class: props.ui?.prev })"
@click="scrollPrev"
@@ -290,7 +292,7 @@ defineExpose({
size="md"
color="neutral"
variant="outline"
aria-label="Next"
:aria-label="t('ui.carousel.next')"
v-bind="typeof next === 'object' ? next : undefined"
:class="ui.next({ class: props.ui?.next })"
@click="scrollNext"
@@ -300,7 +302,7 @@ defineExpose({
<div v-if="dots" :class="ui.dots({ class: props.ui?.dots })">
<template v-for="(_, index) in scrollSnaps" :key="index">
<button
:aria-label="`Go to slide ${index + 1}`"
:aria-label="t('ui.carousel.goto', { slide: index + 1 })"
:class="ui.dot({ class: props.ui?.dot, active: selectedIndex === index })"
@click="scrollTo(index)"
/>

View File

@@ -7,6 +7,7 @@ import type { UseFuseOptions } from '@vueuse/integrations/useFuse'
import _appConfig from '#build/app.config'
import theme from '#build/ui/command-palette'
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
import { useLocale } from '../composables/useLocale'
import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
import type { AvatarProps, ButtonProps, ChipProps, KbdProps, InputProps } from '../types'
import type { DynamicSlots, PartialString } from '../types/utils'
@@ -144,6 +145,7 @@ const slots = defineSlots<CommandPaletteSlots<G, T>>()
const searchTerm = defineModel<string>('searchTerm', { default: '' })
const appConfig = useAppConfig()
const { t } = useLocale()
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'disabled', 'multiple', 'modelValue', 'defaultValue', 'selectedValue', 'resetSearchTermOnBlur'), emits)
const inputProps = useForwardProps(reactivePick(props, 'loading', 'loadingIcon', 'placeholder'))
@@ -245,7 +247,7 @@ const groups = computed(() => {
size="md"
color="neutral"
variant="ghost"
aria-label="Close"
:aria-label="t('ui.commandPalette.close')"
v-bind="typeof close === 'object' ? close : undefined"
:class="ui.close({ class: props.ui?.close })"
@click="emits('update:open', false)"
@@ -259,7 +261,7 @@ const groups = computed(() => {
<ComboboxContent :class="ui.content({ class: props.ui?.content })" :dismissable="false">
<ComboboxEmpty :class="ui.empty({ class: props.ui?.empty })">
<slot name="empty" :search-term="searchTerm">
{{ searchTerm ? `No results for ${searchTerm}` : 'No results' }}
{{ searchTerm ? t('ui.commandPalette.noMatch', { searchTerm }) : t('ui.commandPalette.noData') }}
</slot>
</ComboboxEmpty>

View File

@@ -34,6 +34,8 @@ import ULink from './Link.vue'
import UAvatar from './Avatar.vue'
import UIcon from './Icon.vue'
import UKbd from './Kbd.vue'
// eslint-disable-next-line import/no-self-import
import UContextMenuContent from './ContextMenuContent.vue'
const props = defineProps<ContextMenuContentProps<T>>()
const emits = defineEmits<ContextMenuContentEmits>()

View File

@@ -40,6 +40,8 @@ import ULink from './Link.vue'
import UAvatar from './Avatar.vue'
import UIcon from './Icon.vue'
import UKbd from './Kbd.vue'
// eslint-disable-next-line import/no-self-import
import UDropdownMenuContent from './DropdownMenuContent.vue'
const props = defineProps<DropdownMenuContentProps<T>>()
const emits = defineEmits<DropdownMenuContentEmits>()

View File

@@ -94,13 +94,13 @@ onUnmounted(() => {
const errors = ref<FormErrorWithId[]>([])
provide('form-errors', errors)
const inputs = ref<Record<string, string>>({})
const inputs = ref<Record<string, { id?: string, pattern?: RegExp }>>({})
provide(formInputsInjectionKey, inputs)
function resolveErrorIds(errs: FormError[]): FormErrorWithId[] {
return errs.map(err => ({
...err,
id: inputs.value[err.name]
id: inputs.value[err.name]?.id
}))
}
@@ -129,7 +129,7 @@ async function getErrors(): Promise<FormErrorWithId[]> {
}
async function _validate(opts: { name?: string | string[], silent?: boolean, nested?: boolean } = { silent: false, nested: true }): Promise<T | false> {
const names = opts.name && !Array.isArray(opts.name) ? [opts.name] : opts.name
const names = opts.name && !Array.isArray(opts.name) ? [opts.name] : opts.name as string[]
const nestedValidatePromises = !names && opts.nested
? Array.from(nestedForms.value.values()).map(
@@ -143,9 +143,16 @@ async function _validate(opts: { name?: string | string[], silent?: boolean, nes
: []
if (names) {
const otherErrors = errors.value.filter(error => !names!.includes(error.name))
const pathErrors = (await getErrors()).filter(error => names!.includes(error.name)
)
const otherErrors = errors.value.filter(error => !names.some((name) => {
const pattern = inputs.value?.[name]?.pattern
return name === error.name || (pattern && error.name.match(pattern))
}))
const pathErrors = (await getErrors()).filter(error => names.some((name) => {
const pattern = inputs.value?.[name]?.pattern
return name === error.name || (pattern && error.name.match(pattern))
}))
errors.value = otherErrors.concat(pathErrors)
} else {
errors.value = await getErrors()
@@ -196,7 +203,7 @@ provide(formOptionsInjectionKey, computed(() => ({
validateOnInputDelay: props.validateOnInputDelay
})))
defineExpose<{ $el: HTMLFormElement | HTMLDivElement } & Form<T>>({
defineExpose<Form<T>>({
validate: _validate,
errors,
@@ -230,7 +237,7 @@ defineExpose<{ $el: HTMLFormElement | HTMLDivElement } & Form<T>>({
},
disabled
} as { $el: HTMLFormElement | HTMLDivElement } & Form<T>)
})
</script>
<template>

View File

@@ -12,7 +12,10 @@ const formField = tv({ extend: tv(theme), ...(appConfig.ui?.formField || {}) })
type FormFieldVariants = VariantProps<typeof formField>
export interface FormFieldProps {
/** The name of the FormField. Also used to match form errors. */
name?: string
/** A regular expression to match form error names. */
errorPattern?: RegExp
label?: string
description?: string
help?: string
@@ -54,7 +57,7 @@ const ui = computed(() => formField({
const formErrors = inject<Ref<FormError[]> | null>('form-errors', null)
const error = computed(() => props.error || formErrors?.value?.find(error => error.name === props.name)?.message)
const error = computed(() => props.error || formErrors?.value?.find(error => error.name === props.name || (props.errorPattern && error.name.match(props.errorPattern)))?.message)
const id = ref(useId())
@@ -65,7 +68,8 @@ provide(formFieldInjectionKey, computed(() => ({
name: props.name,
size: props.size,
eagerValidation: props.eagerValidation,
validateOnInputDelay: props.validateOnInputDelay
validateOnInputDelay: props.validateOnInputDelay,
errorPattern: props.errorPattern
}) as FormFieldInjectedOptions<FormFieldProps>))
</script>

View File

@@ -6,6 +6,7 @@ import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/input-menu'
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
import { useLocale } from '../composables/useLocale'
import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
import type { AvatarProps, ChipProps, InputProps } from '../types'
import type { AcceptableValue, ArrayOrWrapped, PartialString, MaybeArrayOfArray, MaybeArrayOfArrayItem, SelectModelValue, SelectModelValueEmits, SelectItemKey } from '../types/utils'
@@ -78,9 +79,10 @@ export interface InputMenuProps<T extends MaybeArrayOfArrayItem<I>, I extends Ma
*/
portal?: boolean
/**
* Whether to filter items or not, can be an array of fields to filter.
* When `false`, items will not be filtered which is useful for custom filtering.
* @defaultValue ['label']
* Whether to filter items or not, can be an array of fields to filter. Defaults to `[labelKey]`.
* When `false`, items will not be filtered which is useful for custom filtering (useAsyncData, useFetch, etc.).
* `['label']`{lang="ts-type"}
* @defaultValue true
*/
filter?: boolean | string[]
/**
@@ -148,7 +150,7 @@ const props = withDefaults(defineProps<InputMenuProps<T, I, V, M>>(), {
type: 'text',
autofocusDelay: 0,
portal: true,
filter: () => ['label'],
filter: true,
labelKey: 'label' as never
})
const emits = defineEmits<InputMenuEmits<T, V, M>>()
@@ -157,6 +159,7 @@ const slots = defineSlots<InputMenuSlots<T>>()
const searchTerm = defineModel<string>('searchTerm', { default: '' })
const appConfig = useAppConfig()
const { t } = useLocale()
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'selectedValue', 'open', 'defaultOpen', 'resetSearchTermOnBlur'), emits)
const contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffset: 8, position: 'popper' }) as ComboboxContentProps)
const arrowProps = toRef(() => props.arrow as ComboboxArrowProps)
@@ -347,7 +350,7 @@ defineExpose({
<ComboboxContent :class="ui.content({ class: props.ui?.content })" v-bind="contentProps">
<ComboboxEmpty :class="ui.empty({ class: props.ui?.empty })">
<slot name="empty" :search-term="searchTerm">
{{ searchTerm ? `No results for ${searchTerm}` : 'No results' }}
{{ searchTerm ? t('ui.inputMenu.noMatch', { searchTerm }) : t('ui.inputMenu.noData') }}
</slot>
</ComboboxEmpty>

View File

@@ -98,6 +98,7 @@ import { isEqual } from 'ohash'
import { useForwardProps } from 'radix-vue'
import { reactiveOmit } from '@vueuse/core'
import { useRoute } from '#imports'
import ULinkBase from './LinkBase.vue'
defineOptions({ inheritAttrs: false })

View File

@@ -4,6 +4,7 @@ import type { DialogRootProps, DialogRootEmits, DialogContentProps } from 'radix
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/modal'
import { useLocale } from '../composables/useLocale'
import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
import type { ButtonProps } from '../types'
@@ -95,7 +96,8 @@ const contentEvents = computed(() => {
if (props.preventClose) {
return {
pointerDownOutside: (e: Event) => e.preventDefault(),
interactOutside: (e: Event) => e.preventDefault()
interactOutside: (e: Event) => e.preventDefault(),
escapeKeyDown: (e: Event) => e.preventDefault()
}
}
@@ -103,6 +105,7 @@ const contentEvents = computed(() => {
})
const appConfig = useAppConfig()
const { t } = useLocale()
const ui = computed(() => modal({
transition: props.transition,
@@ -143,7 +146,7 @@ const ui = computed(() => modal({
size="md"
color="neutral"
variant="ghost"
aria-label="Close"
:aria-label="t('ui.modal.close')"
v-bind="typeof close === 'object' ? close : undefined"
:class="ui.close({ class: props.ui?.close })"
/>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { tv, type VariantProps } from 'tailwind-variants'
import type { NavigationMenuRootProps, NavigationMenuRootEmits, NavigationMenuContentProps } from 'radix-vue'
import type { NavigationMenuRootProps, NavigationMenuRootEmits, NavigationMenuContentProps, CollapsibleRootProps } from 'radix-vue'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/navigation-menu'
@@ -17,7 +17,7 @@ export interface NavigationMenuChildItem extends Omit<NavigationMenuItem, 'child
description?: string
}
export interface NavigationMenuItem extends Omit<LinkProps, 'raw' | 'custom'> {
export interface NavigationMenuItem extends Omit<LinkProps, 'raw' | 'custom'>, Pick<CollapsibleRootProps, 'defaultOpen' | 'open'> {
label?: string
icon?: string
avatar?: AvatarProps
@@ -208,6 +208,8 @@ const lists = computed(() => props.items?.length ? (Array.isArray(props.items[0]
:key="`list-${listIndex}-${index}`"
as="li"
:value="item.value || String(index)"
:default-open="item.defaultOpen"
:open="item.open"
:class="ui.item({ class: props.ui?.item })"
>
<ULink v-slot="{ active, ...slotProps }" v-bind="pickLinkProps(item)" custom>

View File

@@ -31,6 +31,11 @@ export interface PopoverProps extends PopoverRootProps, Pick<HoverCardRootProps,
* @defaultValue true
*/
portal?: boolean
/**
* When `true`, the popover will not close when clicking outside.
* @defaultValue false
*/
preventClose?: boolean
class?: any
ui?: Partial<typeof popover.slots>
}
@@ -64,6 +69,17 @@ const slots = defineSlots<PopoverSlots>()
const pick = props.mode === 'hover' ? reactivePick(props, 'defaultOpen', 'open', 'openDelay', 'closeDelay') : reactivePick(props, 'defaultOpen', 'open', 'modal')
const rootProps = useForwardPropsEmits(pick, emits)
const contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffset: 8 }) as PopoverContentProps)
const contentEvents = computed(() => {
if (props.preventClose) {
return {
pointerDownOutside: (e: Event) => e.preventDefault(),
interactOutside: (e: Event) => e.preventDefault(),
escapeKeyDown: (e: Event) => e.preventDefault()
}
}
return {}
})
const arrowProps = toRef(() => props.arrow as PopoverArrowProps)
// eslint-disable-next-line vue/no-dupe-keys
@@ -81,7 +97,7 @@ const Component = computed(() => props.mode === 'hover' ? HoverCard : Popover)
</Component.Trigger>
<Component.Portal :disabled="!portal">
<Component.Content v-bind="contentProps" :class="ui.content({ class: [props.class, props.ui?.content] })">
<Component.Content v-bind="contentProps" :class="ui.content({ class: [props.class, props.ui?.content] })" v-on="contentEvents">
<slot name="content" />
<Component.Arrow v-if="!!arrow" v-bind="arrowProps" :class="ui.arrow({ class: props.ui?.arrow })" />

View File

@@ -5,6 +5,7 @@ import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/select-menu'
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
import { useLocale } from '../composables/useLocale'
import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
import type { AvatarProps, ChipProps, InputProps } from '../types'
import type { AcceptableValue, ArrayOrWrapped, PartialString, MaybeArrayOfArray, MaybeArrayOfArrayItem, SelectModelValue, SelectModelValueEmits, SelectItemKey } from '../types/utils'
@@ -36,9 +37,10 @@ export interface SelectMenuProps<T extends MaybeArrayOfArrayItem<I>, I extends M
/**
* Wether to display the search input or not.
* Can be an object to pass additional props to the input.
* @defaultValue { placeholder: 'Search...' }
* `{ placeholder: 'Search...', variant: 'none' }`{lang="ts-type"}
* @defaultValue true
*/
searchInput?: boolean | { placeholder?: string }
searchInput?: boolean | InputProps
color?: SelectMenuVariants['color']
variant?: SelectMenuVariants['variant']
size?: SelectMenuVariants['size']
@@ -69,9 +71,10 @@ export interface SelectMenuProps<T extends MaybeArrayOfArrayItem<I>, I extends M
*/
portal?: boolean
/**
* Whether to filter items or not, can be an array of fields to filter.
* Whether to filter items or not, can be an array of fields to filter. Defaults to `[labelKey]`.
* When `false`, items will not be filtered which is useful for custom filtering (useAsyncData, useFetch, etc.).
* @defaultValue ['label']
* `['label']`{lang="ts-type"}
* @defaultValue true
*/
filter?: boolean | string[]
/**
@@ -131,13 +134,14 @@ import { get, escapeRegExp } from '../utils'
import UIcon from './Icon.vue'
import UAvatar from './Avatar.vue'
import UChip from './Chip.vue'
import UInput from './Input.vue'
const props = withDefaults(defineProps<SelectMenuProps<T, I, V, M>>(), {
search: true,
portal: true,
autofocusDelay: 0,
searchInput: () => ({ placeholder: 'Search...' }),
filter: () => ['label'],
searchInput: true,
filter: true,
labelKey: 'label' as never
})
@@ -147,9 +151,12 @@ const slots = defineSlots<SelectMenuSlots<T>>()
const searchTerm = defineModel<string>('searchTerm', { default: '' })
const appConfig = useAppConfig()
const { t } = useLocale()
const rootProps = useForwardPropsEmits(reactivePick(props, 'modelValue', 'defaultValue', 'selectedValue', 'open', 'defaultOpen', 'resetSearchTermOnBlur'), emits)
const contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffset: 8, position: 'popper' }) as ComboboxContentProps)
const arrowProps = toRef(() => props.arrow as ComboboxArrowProps)
const searchInputProps = toRef(() => defu(props.searchInput, { placeholder: 'Search...', variant: 'none' }) as InputProps)
// This is a hack due to generic boolean casting (see https://github.com/nuxt/ui/issues/2541)
const multiple = toRef(() => typeof props.multiple === 'string' ? true : props.multiple)
@@ -274,17 +281,13 @@ function onUpdateOpen(value: boolean) {
<ComboboxPortal :disabled="!portal">
<ComboboxContent :class="ui.content({ class: props.ui?.content })" v-bind="contentProps">
<ComboboxInput
v-if="!!searchInput"
autofocus
autocomplete="off"
v-bind="typeof searchInput === 'object' ? searchInput : {}"
:class="ui.input({ class: props.ui?.input })"
/>
<ComboboxInput v-if="!!searchInput" as-child>
<UInput autofocus autocomplete="off" v-bind="searchInputProps" :class="ui.input({ class: props.ui?.input })" />
</ComboboxInput>
<ComboboxEmpty :class="ui.empty({ class: props.ui?.empty })">
<slot name="empty" :search-term="searchTerm">
{{ searchTerm ? `No results for ${searchTerm}` : 'No results' }}
{{ searchTerm ? t('ui.selectMenu.noMatch', { searchTerm }) : t('ui.selectMenu.noData') }}
</slot>
</ComboboxEmpty>

View File

@@ -4,6 +4,7 @@ import type { DialogRootProps, DialogRootEmits, DialogContentProps } from 'radix
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/slideover'
import { useLocale } from '../composables/useLocale'
import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
import type { ButtonProps } from '../types'
@@ -94,7 +95,8 @@ const contentEvents = computed(() => {
if (props.preventClose) {
return {
pointerDownOutside: (e: Event) => e.preventDefault(),
interactOutside: (e: Event) => e.preventDefault()
interactOutside: (e: Event) => e.preventDefault(),
escapeKeyDown: (e: Event) => e.preventDefault()
}
}
@@ -102,6 +104,7 @@ const contentEvents = computed(() => {
})
const appConfig = useAppConfig()
const { t } = useLocale()
const ui = computed(() => slideover({
transition: props.transition,
@@ -142,7 +145,7 @@ const ui = computed(() => slideover({
size="md"
color="neutral"
variant="ghost"
aria-label="Close"
:aria-label="t('ui.slideover.close')"
v-bind="typeof close === 'object' ? close : undefined"
:class="ui.close({ class: props.ui?.close })"
/>

View File

@@ -25,6 +25,7 @@ import type {
} from '@tanstack/vue-table'
import _appConfig from '#build/app.config'
import theme from '#build/ui/table'
import { useLocale } from '../composables/useLocale'
const appConfig = _appConfig as AppConfig & { ui: { table: Partial<typeof theme> } }
@@ -41,6 +42,7 @@ export interface TableData {
export interface TableProps<T> {
data?: T[]
columns?: TableColumn<T>[]
caption?: string
/**
* Whether the table should have a sticky header.
* @defaultValue false
@@ -95,6 +97,7 @@ type DynamicCellSlots<T, K = keyof T> = Record<string, (props: CellContext<T, un
export type TableSlots<T> = {
expanded: (props: { row: Row<T> }) => any
empty: (props?: {}) => any
caption: (props?: {}) => any
} & DynamicHeaderSlots<T> & DynamicCellSlots<T>
</script>
@@ -114,6 +117,7 @@ import { upperFirst } from 'scule'
const props = defineProps<TableProps<T>>()
defineSlots<TableSlots<T>>()
const { t } = useLocale()
const data = computed(() => props.data ?? [])
const columns = computed<TableColumn<T>[]>(() => props.columns ?? Object.keys(data.value[0] ?? {}).map((accessorKey: string) => ({ accessorKey, header: upperFirst(accessorKey) })))
@@ -190,6 +194,12 @@ defineExpose({
<template>
<div :class="ui.root({ class: [props.class, props.ui?.root] })">
<table :class="ui.base({ class: [props.ui?.base] })">
<caption v-if="caption" :class="ui.caption({ class: [props.ui?.caption] })">
<slot name="caption">
{{ caption }}
</slot>
</caption>
<thead :class="ui.thead({ class: [props.ui?.thead] })">
<tr v-for="headerGroup in tableApi.getHeaderGroups()" :key="headerGroup.id" :class="ui.tr({ class: [props.ui?.tr] })">
<th
@@ -231,7 +241,7 @@ defineExpose({
<tr v-else :class="ui.tr({ class: [props.ui?.tr] })">
<td :colspan="columns?.length" :class="ui.empty({ class: props.ui?.empty })">
<slot name="empty">
No results
{{ t('ui.table.noData') }}
</slot>
</td>
</tr>

View File

@@ -4,6 +4,7 @@ import type { ToastRootProps, ToastRootEmits } from 'radix-vue'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/toast'
import { useLocale } from '../composables/useLocale'
import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
import type { AvatarProps, ButtonProps } from '../types'
@@ -74,6 +75,7 @@ const emits = defineEmits<ToastEmits>()
const slots = defineSlots<ToastSlots>()
const appConfig = useAppConfig()
const { t } = useLocale()
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'defaultOpen', 'open', 'duration', 'type'), emits)
const multiline = computed(() => !!props.title && !!props.description)
@@ -151,7 +153,7 @@ defineExpose({
size="md"
color="neutral"
variant="link"
aria-label="Close"
:aria-label="t('ui.toast.close')"
v-bind="typeof close === 'object' ? close : undefined"
:class="ui.close({ class: props.ui?.close })"
@click.stop

View File

@@ -19,6 +19,11 @@ export interface ToasterProps extends Omit<ToastProviderProps, 'swipeDirection'>
* @defaultValue true
*/
expand?: boolean
/**
* Render the toaster in a portal.
* @defaultValue true
*/
portal?: boolean
class?: any
ui?: Partial<typeof toaster.slots>
}
@@ -44,6 +49,7 @@ import UToast from './Toast.vue'
const props = withDefaults(defineProps<ToasterProps>(), {
expand: true,
portal: true,
duration: 5000
})
defineSlots<ToasterSlots>()
@@ -120,18 +126,20 @@ function getOffset(index: number) {
@click="toast.click && toast.click(toast)"
/>
<ToastViewport
:data-expanded="expanded"
:class="ui.viewport({ class: [props.class, props.ui?.viewport] })"
:style="{
'--scale-factor': '0.05',
'--translate-factor': position?.startsWith('top') ? '1px' : '-1px',
'--gap': position?.startsWith('top') ? '16px' : '-16px',
'--front-height': `${frontHeight}px`,
'--height': `${height}px`
}"
@mouseenter="hovered = true"
@mouseleave="hovered = false"
/>
<Teleport to="body" :disabled="!portal">
<ToastViewport
:data-expanded="expanded"
:class="ui.viewport({ class: [props.class, props.ui?.viewport] })"
:style="{
'--scale-factor': '0.05',
'--translate-factor': position?.startsWith('top') ? '1px' : '-1px',
'--gap': position?.startsWith('top') ? '16px' : '-16px',
'--front-height': `${frontHeight}px`,
'--height': `${height}px`
}"
@mouseenter="hovered = true"
@mouseleave="hovered = false"
/>
</Teleport>
</ToastProvider>
</template>

View File

@@ -0,0 +1,9 @@
import type { Locale, LocalePair } from '../types/locale'
export function defineLocale(name: string, code: string, pair: LocalePair): Locale {
return {
name,
code,
ui: pair
}
}

View File

@@ -19,7 +19,7 @@ export const formOptionsInjectionKey: InjectionKey<ComputedRef<FormInjectedOptio
export const formBusInjectionKey: InjectionKey<UseEventBusReturn<FormEvent, string>> = Symbol('nuxt-ui.form-events')
export const formFieldInjectionKey: InjectionKey<ComputedRef<FormFieldInjectedOptions<FormFieldProps>>> = Symbol('nuxt-ui.form-field')
export const inputIdInjectionKey: InjectionKey<Ref<string | undefined>> = Symbol('nuxt-ui.input-id')
export const formInputsInjectionKey: InjectionKey<Ref<Record<string, string>>> = Symbol('nuxt-ui.form-inputs')
export const formInputsInjectionKey: InjectionKey<Ref<Record<string, { id?: string, pattern?: RegExp }>>> = Symbol('nuxt-ui.form-inputs')
export const formLoadingInjectionKey: InjectionKey<Readonly<Ref<boolean>>> = Symbol('nuxt-ui.form-loading')
export function useFormField<T>(props?: Props<T>, opts?: { bind?: boolean }) {
@@ -38,7 +38,7 @@ export function useFormField<T>(props?: Props<T>, opts?: { bind?: boolean }) {
inputId.value = props?.id
}
if (formInputs && formField.value.name && inputId.value) {
formInputs.value[formField.value.name] = inputId.value
formInputs.value[formField.value.name] = { id: inputId.value, pattern: formField.value.errorPattern }
}
}

View File

@@ -0,0 +1,13 @@
import { computed, inject, ref } from 'vue'
import type { InjectionKey, Ref } from 'vue'
import type { Locale } from '../types/locale'
import { buildLocaleContext } from '../utils/locale'
import { en } from '../locale'
export const localeContextInjectionKey: InjectionKey<Ref<Locale | undefined>> = Symbol('nuxt-ui.locale-context')
export const useLocale = (localeOverrides?: Ref<Locale | undefined>) => {
const locale = localeOverrides || inject(localeContextInjectionKey, ref())!
return buildLocaleContext(computed(() => locale.value || en))
}

View File

@@ -31,7 +31,8 @@
--ui-border-accented: var(--ui-color-neutral-300);
--ui-border-inverted: var(--ui-color-neutral-900);
--ui-radius: var(--radius);
--ui-radius: var(--radius-sm);
--ui-container: var(--container-7xl);
}
.dark {

37
src/runtime/locale/ar.ts Normal file
View File

@@ -0,0 +1,37 @@
import { defineLocale } from '../composables/defineLocale'
export default defineLocale('العربية', 'ar', {
inputMenu: {
noMatch: 'لا توجد نتائج مطابقة',
noData: 'لا توجد بيانات'
},
commandPalette: {
noMatch: 'لا توجد نتائج مطابقة',
noData: 'لا توجد بيانات',
close: 'إغلاق'
},
selectMenu: {
noMatch: 'لا توجد نتائج مطابقة',
noData: 'لا توجد بيانات'
},
toast: {
close: 'إغلاق'
},
carousel: {
prev: 'السابق',
next: 'التالي',
goto: 'الذهاب إلي شريحة {slide}'
},
modal: {
close: 'إغلاق'
},
slideover: {
close: 'إغلاق'
},
alert: {
close: 'إغلاق'
},
table: {
noData: 'لا توجد بيانات'
}
})

37
src/runtime/locale/cs.ts Normal file
View File

@@ -0,0 +1,37 @@
import { defineLocale } from '../composables/defineLocale'
export default defineLocale('Čeština', 'cs', {
inputMenu: {
noMatch: 'Žádná shoda',
noData: 'Žádná data'
},
commandPalette: {
noMatch: 'Žádná shoda',
noData: 'Žádná data',
close: 'Zavřít'
},
selectMenu: {
noMatch: 'Žádná shoda',
noData: 'Žádná data'
},
toast: {
close: 'Zavřít'
},
carousel: {
prev: 'Předchozí',
next: 'Další',
goto: 'Přejít na {slide}'
},
modal: {
close: 'Zavřít'
},
slideover: {
close: 'Zavřít'
},
alert: {
close: 'Zavřít'
},
table: {
noData: 'Žádná data'
}
})

37
src/runtime/locale/de.ts Normal file
View File

@@ -0,0 +1,37 @@
import { defineLocale } from '../composables/defineLocale'
export default defineLocale('Deutsch', 'de', {
inputMenu: {
noMatch: 'Nichts gefunden',
noData: 'Keine Daten'
},
commandPalette: {
noMatch: 'Nichts gefunden',
noData: 'Keine Daten',
close: 'Schließen'
},
selectMenu: {
noMatch: 'Nichts gefunden',
noData: 'Keine Daten'
},
toast: {
close: 'Schließen'
},
carousel: {
prev: 'Weiter',
next: 'Zurück',
goto: 'Gehe zu {slide}'
},
modal: {
close: 'Schließen'
},
slideover: {
close: 'Schließen'
},
alert: {
close: 'Schließen'
},
table: {
noData: 'Keine Daten'
}
})

37
src/runtime/locale/en.ts Normal file
View File

@@ -0,0 +1,37 @@
import { defineLocale } from '../composables/defineLocale'
export default defineLocale('English', 'en', {
inputMenu: {
noMatch: 'No matching data',
noData: 'No data'
},
commandPalette: {
noMatch: 'No matching data',
noData: 'No data',
close: 'Close'
},
selectMenu: {
noMatch: 'No matching data',
noData: 'No data'
},
toast: {
close: 'Close'
},
carousel: {
prev: 'Prev',
next: 'Next',
goto: 'Go to slide {slide}'
},
modal: {
close: 'Close'
},
slideover: {
close: 'Close'
},
alert: {
close: 'Close'
},
table: {
noData: 'No data'
}
})

37
src/runtime/locale/fr.ts Normal file
View File

@@ -0,0 +1,37 @@
import { defineLocale } from '../composables/defineLocale'
export default defineLocale('Français', 'fr', {
inputMenu: {
noMatch: 'Aucune donnée correspondante',
noData: 'Aucune donnée'
},
commandPalette: {
noMatch: 'Aucune donnée correspondante',
noData: 'Aucune donnée',
close: 'Fermer'
},
selectMenu: {
noMatch: 'Aucune donnée correspondante',
noData: 'Aucune donnée'
},
toast: {
close: 'Fermer'
},
carousel: {
prev: 'Précédent',
next: 'Suivant',
goto: 'Aller à {slide}'
},
modal: {
close: 'Fermer'
},
slideover: {
close: 'Fermer'
},
alert: {
close: 'Fermer'
},
table: {
noData: 'Aucune donnée'
}
})

View File

@@ -0,0 +1,9 @@
export { default as ar } from './ar'
export { default as cs } from './cs'
export { default as de } from './de'
export { default as en } from './en'
export { default as fr } from './fr'
export { default as it } from './it'
export { default as ru } from './ru'
export { default as zh_hans } from './zh_hans'
export { default as zh_hant } from './zh_hant'

37
src/runtime/locale/it.ts Normal file
View File

@@ -0,0 +1,37 @@
import { defineLocale } from '../composables/defineLocale'
export default defineLocale('Italiano', 'it', {
inputMenu: {
noMatch: 'Nessun dato corrispondente',
noData: 'Nessun dato'
},
commandPalette: {
noMatch: 'Nessun dato corrispondente',
noData: 'Nessun dato',
close: 'Chiudi'
},
selectMenu: {
noMatch: 'Nessun dato corrispondente',
noData: 'Nessun dato'
},
toast: {
close: 'Chiudi'
},
carousel: {
prev: 'Precedente',
next: 'Successiva',
goto: 'Vai alla slide {slide}'
},
modal: {
close: 'Chiudi'
},
slideover: {
close: 'Chiudi'
},
alert: {
close: 'Chiudi'
},
table: {
noData: 'Nessuno dato'
}
})

37
src/runtime/locale/ru.ts Normal file
View File

@@ -0,0 +1,37 @@
import { defineLocale } from '../composables/defineLocale'
export default defineLocale('Русский', 'ru', {
inputMenu: {
noMatch: 'Совпадений не найдено',
noData: 'Нет данных'
},
commandPalette: {
noMatch: 'Совпадений не найдено',
noData: 'Нет данных',
close: 'Закрыть'
},
selectMenu: {
noMatch: 'Совпадений не найдено',
noData: 'Нет данных'
},
toast: {
close: 'Закрыть'
},
carousel: {
prev: 'Назад',
next: 'Далее',
goto: 'Перейти к {slide}'
},
modal: {
close: 'Закрыть'
},
slideover: {
close: 'Закрыть'
},
alert: {
close: 'Закрыть'
},
table: {
noData: 'Нет данных'
}
})

View File

@@ -0,0 +1,37 @@
import { defineLocale } from '../composables/defineLocale'
export default defineLocale('简体中文', 'zh-Hans', {
inputMenu: {
noMatch: '没有匹配的数据',
noData: '没有数据'
},
commandPalette: {
noMatch: '没有匹配的数据',
noData: '没有数据',
close: '关闭'
},
selectMenu: {
noMatch: '没有匹配的数据',
noData: '没有数据'
},
toast: {
close: '关闭'
},
carousel: {
prev: '上一页',
next: '下一页',
goto: '跳转到第 {slide} 页'
},
modal: {
close: '关闭'
},
slideover: {
close: '关闭'
},
alert: {
close: '关闭'
},
table: {
noData: '没有数据'
}
})

View File

@@ -0,0 +1,37 @@
import { defineLocale } from '../composables/defineLocale'
export default defineLocale('繁体中文', 'zh-Hant', {
inputMenu: {
noMatch: '沒有匹配的資料',
noData: '沒有資料'
},
commandPalette: {
noMatch: '沒有匹配的資料',
noData: '沒有資料',
close: '關閉'
},
selectMenu: {
noMatch: '沒有匹配的資料',
noData: '沒有資料'
},
toast: {
close: '關閉'
},
carousel: {
prev: '上一頁',
next: '下一頁',
goto: '跳轉到第 {slide} 頁'
},
modal: {
close: '關閉'
},
slideover: {
close: '關閉'
},
alert: {
close: '關閉'
},
table: {
noData: '沒有資料'
}
})

View File

@@ -1,4 +1,4 @@
import type { StandardSchema } from '@standard-schema/spec'
import type { v1 } from '@standard-schema/spec'
import type { ComputedRef, Ref } from 'vue'
import type { ZodSchema } from 'zod'
import type { Schema as JoiSchema } from 'joi'
@@ -26,7 +26,7 @@ export type FormSchema<T extends Record<string, any>> =
| ValibotSafeParserAsync<any, any>
| JoiSchema<T>
| SuperstructSchema<any, any>
| StandardSchema
| v1.StandardSchema
export type FormInputEvents = 'input' | 'blur' | 'change'
@@ -82,6 +82,7 @@ export interface FormFieldInjectedOptions<T> {
error?: string | boolean
eagerValidation?: boolean
validateOnInputDelay?: number
errorPattern?: RegExp
}
export class FormValidationException extends Error {

View File

@@ -42,3 +42,4 @@ export * from '../components/Toast.vue'
export * from '../components/Toaster.vue'
export * from '../components/Tooltip.vue'
export * from './form'
export * from './locale'

View File

@@ -0,0 +1,41 @@
export type LocalePair = {
inputMenu: {
noMatch: string
noData: string
}
commandPalette: {
noMatch: string
noData: string
close: string
}
selectMenu: {
noMatch: string
noData: string
}
toast: {
close: string
}
carousel: {
prev: string
next: string
goto: string
}
modal: {
close: string
}
slideover: {
close: string
}
alert: {
close: string
}
table: {
noData: string
}
}
export type Locale = {
name: string
code: string
ui: LocalePair
}

View File

@@ -1,4 +1,4 @@
import type { StandardSchema } from '@standard-schema/spec'
import type { v1 } from '@standard-schema/spec'
import type { ZodSchema } from 'zod'
import type { ValidationError as JoiError, Schema as JoiSchema } from 'joi'
import type { ObjectSchema as YupObjectSchema, ValidationError as YupError } from 'yup'
@@ -94,15 +94,15 @@ export async function getValibotErrors(
})) || []
}
export function isStandardSchema(schema: any): schema is StandardSchema {
export function isStandardSchema(schema: any): schema is v1.StandardSchema {
return '~standard' in schema
}
export async function getStandardErrors(
state: any,
schema: StandardSchema
schema: v1.StandardSchema
): Promise<FormError[]> {
const result = await schema['~validate']({ value: state })
const result = await schema['~standard'].validate(state)
return result.issues?.map(issue => ({
name: issue.path?.map(item => typeof item === 'object' ? item.key : item).join('.') || '',
message: issue.message

View File

@@ -0,0 +1,40 @@
import type { Ref } from 'vue'
import type { Locale } from '../types/locale'
import type { MaybeRef } from '@vueuse/core'
import { computed, isRef, ref, unref } from 'vue'
import { get } from './index'
export type TranslatorOption = Record<string, string | number>
export type Translator = (path: string, option?: TranslatorOption) => string
export type LocaleContext = {
locale: Ref<Locale>
lang: Ref<string>
code: Ref<string>
t: Translator
}
export function buildTranslator(locale: MaybeRef<Locale>): Translator {
return (path, option) => translate(path, option, unref(locale))
}
export function translate(path: string, option: undefined | TranslatorOption, locale: Locale): string {
const prop: string = get(locale, path, path)
return prop.replace(
/\{(\w+)\}/g,
(_, key) => `${option?.[key] ?? `{${key}}`}`
)
}
export function buildLocaleContext(locale: MaybeRef<Locale>): LocaleContext {
const lang = computed(() => unref(locale).name)
const code = computed(() => unref(locale).code)
const localeRef = isRef(locale) ? locale : ref(locale)
return {
lang,
code,
locale: localeRef,
t: buildTranslator(locale)
}
}

View File

@@ -1,6 +1,6 @@
export default {
slots: {
root: 'bg-[var(--ui-bg)] ring ring-[var(--ui-border)] divide-y divide-[var(--ui-border)] rounded-[calc(var(--ui-radius)*2)] shadow',
root: 'bg-[var(--ui-bg)] ring ring-[var(--ui-border)] divide-y divide-[var(--ui-border)] rounded-[calc(var(--ui-radius)*2)] shadow-sm',
header: 'p-4 sm:px-6',
body: 'p-4 sm:p-6',
footer: 'p-4 sm:px-6'

View File

@@ -1,3 +1,3 @@
export default {
base: 'max-w-7xl mx-auto px-4 sm:px-6 lg:px-8'
base: 'max-w-[var(--ui-container)] mx-auto px-4 sm:px-6 lg:px-8'
}

View File

@@ -16,7 +16,7 @@ export default (options: Required<ModuleOptions>) => ({
...buttonGroupVariantWithRoot,
size: {
xs: {
base: 'px-2 py-1 text-xs gap-1',
base: 'px-2 py-1 text-xs',
leading: 'ps-2',
trailing: 'pe-2',
leadingIcon: 'size-4',
@@ -24,7 +24,7 @@ export default (options: Required<ModuleOptions>) => ({
trailingIcon: 'size-4'
},
sm: {
base: 'px-2.5 py-1.5 text-xs gap-1.5',
base: 'px-2.5 py-1.5 text-xs',
leading: 'ps-2.5',
trailing: 'pe-2.5',
leadingIcon: 'size-4',
@@ -32,7 +32,7 @@ export default (options: Required<ModuleOptions>) => ({
trailingIcon: 'size-4'
},
md: {
base: 'px-2.5 py-1.5 text-sm gap-1.5',
base: 'px-2.5 py-1.5 text-sm',
leading: 'ps-2.5',
trailing: 'pe-2.5',
leadingIcon: 'size-5',
@@ -40,7 +40,7 @@ export default (options: Required<ModuleOptions>) => ({
trailingIcon: 'size-5'
},
lg: {
base: 'px-3 py-2 text-sm gap-2',
base: 'px-3 py-2 text-sm',
leading: 'ps-3',
trailing: 'pe-3',
leadingIcon: 'size-5',
@@ -48,7 +48,7 @@ export default (options: Required<ModuleOptions>) => ({
trailingIcon: 'size-5'
},
xl: {
base: 'px-3 py-2 text-base gap-2',
base: 'px-3 py-2 text-base',
leading: 'ps-3',
trailing: 'pe-3',
leadingIcon: 'size-6',
@@ -60,8 +60,8 @@ export default (options: Required<ModuleOptions>) => ({
outline: 'text-[var(--ui-text-highlighted)] bg-[var(--ui-bg)] ring ring-inset ring-[var(--ui-border-accented)]',
soft: 'text-[var(--ui-text-highlighted)] bg-[var(--ui-bg-elevated)]/50 hover:bg-[var(--ui-bg-elevated)] focus:bg-[var(--ui-bg-elevated)] disabled:bg-[var(--ui-bg-elevated)]/50',
subtle: 'text-[var(--ui-text-highlighted)] bg-[var(--ui-bg-elevated)] ring ring-inset ring-[var(--ui-border-accented)]',
ghost: 'text-[var(--ui-text-highlighted)] hover:bg-[var(--ui-bg-elevated)] focus:bg-[var(--ui-bg-elevated)] disabled:bg-transparent dark:disabled:bg-transparent',
none: 'text-[var(--ui-text-highlighted)]'
ghost: 'text-[var(--ui-text-highlighted)] bg-transparent hover:bg-[var(--ui-bg-elevated)] focus:bg-[var(--ui-bg-elevated)] disabled:bg-transparent dark:disabled:bg-transparent',
none: 'text-[var(--ui-text-highlighted)] bg-transparent'
},
color: {
...Object.fromEntries((options.theme.colors || []).map((color: string) => [color, ''])),

View File

@@ -10,7 +10,7 @@ export default (options: Required<ModuleOptions>) => ({
linkLeadingAvatar: 'shrink-0',
linkLeadingAvatarSize: '2xs',
linkTrailing: 'ms-auto inline-flex',
linkTrailingBadge: 'shrink-0 rounded',
linkTrailingBadge: 'shrink-0 rounded-[calc(var(--ui-radius)]',
linkTrailingBadgeSize: 'sm',
linkTrailingIcon: 'size-5 transform shrink-0 group-data-[state=open]:rotate-180 transition-transform duration-200',
linkLabel: 'truncate',

View File

@@ -7,26 +7,7 @@ export default (options: Required<ModuleOptions>) => {
slots: {
value: 'truncate',
placeholder: 'truncate text-[var(--ui-text-dimmed)]',
input: 'placeholder:text-[var(--ui-text-dimmed)] border-0 border-b border-[var(--ui-border)] focus:outline-none'
},
variants: {
size: {
xs: {
input: 'text-xs px-2 py-1'
},
sm: {
input: 'text-xs px-2.5 py-1.5'
},
md: {
input: 'text-sm px-2.5 py-1.5'
},
lg: {
input: 'text-sm px-3 py-2'
},
xl: {
input: 'text-base px-3 py-2'
}
}
input: 'border-b border-[var(--ui-border)]'
}
}, select(options))
}

View File

@@ -4,6 +4,7 @@ export default (options: Required<ModuleOptions>) => ({
slots: {
root: 'relative overflow-auto',
base: 'min-w-full overflow-clip',
caption: 'sr-only',
thead: 'relative [&>tr]:after:absolute [&>tr]:after:inset-x-0 [&>tr]:after:bottom-0 [&>tr]:after:h-px [&>tr]:after:bg-[var(--ui-border-accented)]',
tbody: 'divide-y divide-[var(--ui-border)]',
tr: 'data-[selected=true]:bg-[var(--ui-bg-elevated)]/50',

View File

@@ -21,7 +21,7 @@ export default (options: Required<ModuleOptions>) => ({
pill: {
list: 'bg-[var(--ui-bg-elevated)] rounded-[calc(var(--ui-radius)*2)]',
trigger: 'flex-1 w-full',
indicator: 'rounded-[calc(var(--ui-radius)*1.5)] shadow-sm'
indicator: 'rounded-[calc(var(--ui-radius)*1.5)] shadow-xs'
},
link: {
list: 'border-[var(--ui-border)]',

View File

@@ -1,6 +1,6 @@
export default {
slots: {
content: 'flex items-center gap-1 bg-[var(--ui-bg)] text-[var(--ui-text-highlighted)] shadow rounded-[var(--ui-radius)] ring ring-[var(--ui-border)] h-6 px-2 py-1 text-xs select-none data-[state=delayed-open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in]',
content: 'flex items-center gap-1 bg-[var(--ui-bg)] text-[var(--ui-text-highlighted)] shadow-sm rounded-[var(--ui-radius)] ring ring-[var(--ui-border)] h-6 px-2 py-1 text-xs select-none data-[state=delayed-open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in]',
arrow: 'fill-[var(--ui-border)]',
text: 'truncate',
kbds: `hidden lg:inline-flex items-center shrink-0 gap-0.5 before:content-['·'] before:me-0.5`,

View File

@@ -62,7 +62,7 @@ describe('CommandPalette', () => {
it.each([
// Props
['with groups', { props }],
['without results', {}],
['without data', {}],
['with modelValue', { props: { ...props, modelValue: groups[2].items[0] } }],
['with defaultValue', { props: { ...props, defaultValue: groups[2].items[0] } }],
['with labelKey', { props: { ...props, labelKey: 'icon' } }],

View File

@@ -363,4 +363,34 @@ describe('Form', () => {
expect(wrapper.setupState.onError).toHaveBeenCalledTimes(0)
})
})
test('form field errorPattern works', async () => {
const wrapper = await mountSuspended({
components: {
UFormField,
UForm,
UInput
},
setup() {
const form = ref()
const state = reactive({})
function validate() {
return [{ name: 'email.1', message: 'Error message' }]
}
return { state, validate, form }
},
template: `
<UForm ref="form" :state="state" :validate="validate">
<UFormField id="emailField" :error-pattern="/(email)\\..*/">
<UInput id="emailInput" v-model="state.email" />
</UFormField>
</UForm>
`
})
const form = wrapper.setupState.form
form.value.submit()
await flushPromises()
expect(wrapper.html()).toContain('Error message')
})
})

View File

@@ -49,6 +49,7 @@ describe('SelectMenu', () => {
['with placeholder', { props: { ...props, placeholder: 'Search...' } }],
['without searchInput', { props: { ...props, searchInput: false } }],
['with searchInput placeholder', { props: { ...props, searchInput: { placeholder: 'Filter items...' } } }],
['with searchInput icon', { props: { ...props, searchInput: { icon: 'i-lucide-search' } } }],
['with disabled', { props: { ...props, disabled: true } }],
['with required', { props: { ...props, required: true } }],
['with icon', { props: { icon: 'i-lucide-search' } }],

View File

@@ -145,7 +145,8 @@ describe('Table', () => {
it.each([
// Props
['with data', { props }],
['without results', {}],
['without data', {}],
['with caption', { props: { ...props, caption: 'Table caption' } }],
['with columns', { props: { ...props, columns } }],
['with sticky', { props: { ...props, sticky: true } }],
['with loading', { props: { ...props, loading: true } }],
@@ -157,7 +158,8 @@ describe('Table', () => {
['with header slot', { props, slots: { 'id-header': () => 'ID Header slot' } }],
['with cell slot', { props, slots: { 'id-cell': () => 'ID Cell slot' } }],
['with expanded slot', { props, slots: { expanded: () => 'Expanded slot' } }],
['with empty slot', { props, slots: { empty: () => 'Empty slot' } }]
['with empty slot', { props, slots: { empty: () => 'Empty slot' } }],
['with caption slot', { props, slots: { caption: () => 'Caption slot' } }]
])('renders %s correctly', async (nameOrHtml: string, options: { props?: TableProps<typeof data[number]>, slots?: Partial<TableSlots<typeof data[number]>> }) => {
const html = await ComponentRender(nameOrHtml, options, Table)
expect(html).toMatchSnapshot()

View File

@@ -12,7 +12,7 @@ const ToastWrapper = defineComponent({
ClientOnly
},
inheritAttrs: false,
template: `<UToaster>
template: `<UToaster :portal="false">
<ClientOnly>
<UToast v-bind="$attrs">
<template v-for="(_, name) in $slots" #[name]="slotData">

View File

@@ -2,7 +2,7 @@
exports[`ButtonGroup > renders orientation vertical with default slot correctly 1`] = `
"<div class="relative flex flex-col -space-y-px">
<div class="relative inline-flex items-center group"><input type="text" class="w-full rounded-[calc(var(--ui-radius)*1.5)] border-0 placeholder:text-[var(--ui-text-dimmed)] focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 transition-colors group-not-only:group-first:rounded-b-none group-not-only:group-last:rounded-t-none group-not-last:group-not-first:rounded-none px-2.5 py-1.5 text-sm gap-1.5 text-[var(--ui-text-highlighted)] bg-[var(--ui-bg)] ring ring-inset ring-[var(--ui-border-accented)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--ui-primary)]" autocomplete="off">
<div class="relative inline-flex items-center group"><input type="text" class="w-full rounded-[calc(var(--ui-radius)*1.5)] border-0 placeholder:text-[var(--ui-text-dimmed)] focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 transition-colors group-not-only:group-first:rounded-b-none group-not-only:group-last:rounded-t-none group-not-last:group-not-first:rounded-none px-2.5 py-1.5 text-sm text-[var(--ui-text-highlighted)] bg-[var(--ui-bg)] ring ring-inset ring-[var(--ui-border-accented)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--ui-primary)]" autocomplete="off">
<!--v-if-->
<!--v-if-->
</div> <button type="button" class="rounded-[calc(var(--ui-radius)*1.5)] font-medium inline-flex items-center focus:outline-none disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75 transition-colors not-only:first:rounded-b-none not-only:last:rounded-t-none not-last:not-first:rounded-none px-2.5 py-1.5 text-sm gap-1.5 text-[var(--ui-bg)] bg-[var(--ui-primary)] hover:bg-[var(--ui-primary)]/75 disabled:bg-[var(--ui-primary)] aria-disabled:bg-[var(--ui-primary)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ui-primary)]">
@@ -18,7 +18,7 @@ exports[`ButtonGroup > renders with class correctly 1`] = `"<div class="inline-f
exports[`ButtonGroup > renders with default slot correctly 1`] = `
"<div class="relative inline-flex -space-x-px">
<div class="relative inline-flex items-center group"><input type="text" class="w-full rounded-[calc(var(--ui-radius)*1.5)] border-0 placeholder:text-[var(--ui-text-dimmed)] focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 transition-colors group-not-only:group-first:rounded-e-none group-not-only:group-last:rounded-s-none group-not-last:group-not-first:rounded-none px-2.5 py-1.5 text-sm gap-1.5 text-[var(--ui-text-highlighted)] bg-[var(--ui-bg)] ring ring-inset ring-[var(--ui-border-accented)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--ui-primary)]" autocomplete="off">
<div class="relative inline-flex items-center group"><input type="text" class="w-full rounded-[calc(var(--ui-radius)*1.5)] border-0 placeholder:text-[var(--ui-text-dimmed)] focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 transition-colors group-not-only:group-first:rounded-e-none group-not-only:group-last:rounded-s-none group-not-last:group-not-first:rounded-none px-2.5 py-1.5 text-sm text-[var(--ui-text-highlighted)] bg-[var(--ui-bg)] ring ring-inset ring-[var(--ui-border-accented)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--ui-primary)]" autocomplete="off">
<!--v-if-->
<!--v-if-->
</div> <button type="button" class="rounded-[calc(var(--ui-radius)*1.5)] font-medium inline-flex items-center focus:outline-none disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75 transition-colors not-only:first:rounded-e-none not-only:last:rounded-s-none not-last:not-first:rounded-none px-2.5 py-1.5 text-sm gap-1.5 text-[var(--ui-bg)] bg-[var(--ui-primary)] hover:bg-[var(--ui-primary)]/75 disabled:bg-[var(--ui-primary)] aria-disabled:bg-[var(--ui-primary)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ui-primary)]">
@@ -30,7 +30,7 @@ exports[`ButtonGroup > renders with default slot correctly 1`] = `
exports[`ButtonGroup > renders with size lg correctly 1`] = `
"<div class="relative inline-flex -space-x-px">
<div class="relative inline-flex items-center group"><input type="text" class="w-full rounded-[calc(var(--ui-radius)*1.5)] border-0 placeholder:text-[var(--ui-text-dimmed)] focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 transition-colors group-not-only:group-first:rounded-e-none group-not-only:group-last:rounded-s-none group-not-last:group-not-first:rounded-none px-3 py-2 text-sm gap-2 text-[var(--ui-text-highlighted)] bg-[var(--ui-bg)] ring ring-inset ring-[var(--ui-border-accented)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--ui-primary)]" autocomplete="off">
<div class="relative inline-flex items-center group"><input type="text" class="w-full rounded-[calc(var(--ui-radius)*1.5)] border-0 placeholder:text-[var(--ui-text-dimmed)] focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 transition-colors group-not-only:group-first:rounded-e-none group-not-only:group-last:rounded-s-none group-not-last:group-not-first:rounded-none px-3 py-2 text-sm text-[var(--ui-text-highlighted)] bg-[var(--ui-bg)] ring ring-inset ring-[var(--ui-border-accented)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--ui-primary)]" autocomplete="off">
<!--v-if-->
<!--v-if-->
</div> <button type="button" class="rounded-[calc(var(--ui-radius)*1.5)] font-medium inline-flex items-center focus:outline-none disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75 transition-colors not-only:first:rounded-e-none not-only:last:rounded-s-none not-last:not-first:rounded-none px-3 py-2 text-sm gap-2 text-[var(--ui-bg)] bg-[var(--ui-primary)] hover:bg-[var(--ui-primary)]/75 disabled:bg-[var(--ui-primary)] aria-disabled:bg-[var(--ui-primary)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ui-primary)]">
@@ -42,7 +42,7 @@ exports[`ButtonGroup > renders with size lg correctly 1`] = `
exports[`ButtonGroup > renders with size md correctly 1`] = `
"<div class="relative inline-flex -space-x-px">
<div class="relative inline-flex items-center group"><input type="text" class="w-full rounded-[calc(var(--ui-radius)*1.5)] border-0 placeholder:text-[var(--ui-text-dimmed)] focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 transition-colors group-not-only:group-first:rounded-e-none group-not-only:group-last:rounded-s-none group-not-last:group-not-first:rounded-none px-2.5 py-1.5 text-sm gap-1.5 text-[var(--ui-text-highlighted)] bg-[var(--ui-bg)] ring ring-inset ring-[var(--ui-border-accented)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--ui-primary)]" autocomplete="off">
<div class="relative inline-flex items-center group"><input type="text" class="w-full rounded-[calc(var(--ui-radius)*1.5)] border-0 placeholder:text-[var(--ui-text-dimmed)] focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 transition-colors group-not-only:group-first:rounded-e-none group-not-only:group-last:rounded-s-none group-not-last:group-not-first:rounded-none px-2.5 py-1.5 text-sm text-[var(--ui-text-highlighted)] bg-[var(--ui-bg)] ring ring-inset ring-[var(--ui-border-accented)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--ui-primary)]" autocomplete="off">
<!--v-if-->
<!--v-if-->
</div> <button type="button" class="rounded-[calc(var(--ui-radius)*1.5)] font-medium inline-flex items-center focus:outline-none disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75 transition-colors not-only:first:rounded-e-none not-only:last:rounded-s-none not-last:not-first:rounded-none px-2.5 py-1.5 text-sm gap-1.5 text-[var(--ui-bg)] bg-[var(--ui-primary)] hover:bg-[var(--ui-primary)]/75 disabled:bg-[var(--ui-primary)] aria-disabled:bg-[var(--ui-primary)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ui-primary)]">
@@ -54,7 +54,7 @@ exports[`ButtonGroup > renders with size md correctly 1`] = `
exports[`ButtonGroup > renders with size sm correctly 1`] = `
"<div class="relative inline-flex -space-x-px">
<div class="relative inline-flex items-center group"><input type="text" class="w-full rounded-[calc(var(--ui-radius)*1.5)] border-0 placeholder:text-[var(--ui-text-dimmed)] focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 transition-colors group-not-only:group-first:rounded-e-none group-not-only:group-last:rounded-s-none group-not-last:group-not-first:rounded-none px-2.5 py-1.5 text-xs gap-1.5 text-[var(--ui-text-highlighted)] bg-[var(--ui-bg)] ring ring-inset ring-[var(--ui-border-accented)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--ui-primary)]" autocomplete="off">
<div class="relative inline-flex items-center group"><input type="text" class="w-full rounded-[calc(var(--ui-radius)*1.5)] border-0 placeholder:text-[var(--ui-text-dimmed)] focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 transition-colors group-not-only:group-first:rounded-e-none group-not-only:group-last:rounded-s-none group-not-last:group-not-first:rounded-none px-2.5 py-1.5 text-xs text-[var(--ui-text-highlighted)] bg-[var(--ui-bg)] ring ring-inset ring-[var(--ui-border-accented)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--ui-primary)]" autocomplete="off">
<!--v-if-->
<!--v-if-->
</div> <button type="button" class="rounded-[calc(var(--ui-radius)*1.5)] font-medium inline-flex items-center focus:outline-none disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75 transition-colors not-only:first:rounded-e-none not-only:last:rounded-s-none not-last:not-first:rounded-none px-2.5 py-1.5 text-xs gap-1.5 text-[var(--ui-bg)] bg-[var(--ui-primary)] hover:bg-[var(--ui-primary)]/75 disabled:bg-[var(--ui-primary)] aria-disabled:bg-[var(--ui-primary)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ui-primary)]">
@@ -66,7 +66,7 @@ exports[`ButtonGroup > renders with size sm correctly 1`] = `
exports[`ButtonGroup > renders with size xl correctly 1`] = `
"<div class="relative inline-flex -space-x-px">
<div class="relative inline-flex items-center group"><input type="text" class="w-full rounded-[calc(var(--ui-radius)*1.5)] border-0 placeholder:text-[var(--ui-text-dimmed)] focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 transition-colors group-not-only:group-first:rounded-e-none group-not-only:group-last:rounded-s-none group-not-last:group-not-first:rounded-none px-3 py-2 text-base gap-2 text-[var(--ui-text-highlighted)] bg-[var(--ui-bg)] ring ring-inset ring-[var(--ui-border-accented)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--ui-primary)]" autocomplete="off">
<div class="relative inline-flex items-center group"><input type="text" class="w-full rounded-[calc(var(--ui-radius)*1.5)] border-0 placeholder:text-[var(--ui-text-dimmed)] focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 transition-colors group-not-only:group-first:rounded-e-none group-not-only:group-last:rounded-s-none group-not-last:group-not-first:rounded-none px-3 py-2 text-base text-[var(--ui-text-highlighted)] bg-[var(--ui-bg)] ring ring-inset ring-[var(--ui-border-accented)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--ui-primary)]" autocomplete="off">
<!--v-if-->
<!--v-if-->
</div> <button type="button" class="rounded-[calc(var(--ui-radius)*1.5)] font-medium inline-flex items-center focus:outline-none disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75 transition-colors not-only:first:rounded-e-none not-only:last:rounded-s-none not-last:not-first:rounded-none px-3 py-2 text-base gap-2 text-[var(--ui-bg)] bg-[var(--ui-primary)] hover:bg-[var(--ui-primary)]/75 disabled:bg-[var(--ui-primary)] aria-disabled:bg-[var(--ui-primary)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ui-primary)]">
@@ -78,7 +78,7 @@ exports[`ButtonGroup > renders with size xl correctly 1`] = `
exports[`ButtonGroup > renders with size xs correctly 1`] = `
"<div class="relative inline-flex -space-x-px">
<div class="relative inline-flex items-center group"><input type="text" class="w-full rounded-[calc(var(--ui-radius)*1.5)] border-0 placeholder:text-[var(--ui-text-dimmed)] focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 transition-colors group-not-only:group-first:rounded-e-none group-not-only:group-last:rounded-s-none group-not-last:group-not-first:rounded-none px-2 py-1 text-xs gap-1 text-[var(--ui-text-highlighted)] bg-[var(--ui-bg)] ring ring-inset ring-[var(--ui-border-accented)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--ui-primary)]" autocomplete="off">
<div class="relative inline-flex items-center group"><input type="text" class="w-full rounded-[calc(var(--ui-radius)*1.5)] border-0 placeholder:text-[var(--ui-text-dimmed)] focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 transition-colors group-not-only:group-first:rounded-e-none group-not-only:group-last:rounded-s-none group-not-last:group-not-first:rounded-none px-2 py-1 text-xs text-[var(--ui-text-highlighted)] bg-[var(--ui-bg)] ring ring-inset ring-[var(--ui-border-accented)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--ui-primary)]" autocomplete="off">
<!--v-if-->
<!--v-if-->
</div> <button type="button" class="rounded-[calc(var(--ui-radius)*1.5)] font-medium inline-flex items-center focus:outline-none disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75 transition-colors not-only:first:rounded-e-none not-only:last:rounded-s-none not-last:not-first:rounded-none px-2 py-1 text-xs gap-1 text-[var(--ui-bg)] bg-[var(--ui-primary)] hover:bg-[var(--ui-primary)]/75 disabled:bg-[var(--ui-primary)] aria-disabled:bg-[var(--ui-primary)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ui-primary)]">

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