Compare commits

..

44 Commits

Author SHA1 Message Date
Benjamin Canac
bc10a1cabe chore(release): v3.0.0-alpha.12 2025-01-27 18:49:23 +01:00
renovate[bot]
dcd86144a2 chore(deps): lock file maintenance (v3) (#3183)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-27 17:37:33 +01:00
renovate[bot]
761680b5cb chore(deps): update all non-major dependencies (v3) (#3185)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-27 17:27:48 +01:00
renovate[bot]
3183e4afe3 chore(deps): update nuxt framework to ^3.15.3 (v3) (#3176)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-27 17:18:45 +01:00
Benjamin Canac
cd16b95c98 fix(components): prevent multiple appConfig identifier import (#3186) 2025-01-27 13:26:21 +01:00
Benjamin Canac
d27be06164 chore(Avatar): cast ImageComponent to string 2025-01-27 13:02:56 +01:00
Sagiv
f3958773d6 feat(locale): add Hebrew language (#3181) 2025-01-27 10:50:48 +01:00
renovate[bot]
2006ec0646 chore(deps): update all non-major dependencies (v3) (#3154)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-27 10:45:31 +01:00
Benjamin Canac
3320e0473c chore(renovate): run pnpm dedupe post update 2025-01-25 12:22:30 +01:00
Romain Hamel
c0b485d563 feat(Form): form validation properties (#3137) 2025-01-24 19:10:44 +01:00
Nándor Dudás
891ba1fec6 feat(locale): add Hungarian language (#3129)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-01-24 19:07:55 +01:00
Howard Guo
1a95104631 fix(locale): remove emoji fallback for Chinese (#3134) 2025-01-24 18:37:33 +01:00
Benjamin Canac
ac86ee01b9 feat(NavigationMenu): add contentOrientation prop 2025-01-24 18:35:04 +01:00
Gerben Mulder
8f7f579da0 fix(Form): standard schema validation no longer wrapped in value object (#3104) 2025-01-24 18:21:52 +01:00
Nexos Creator
8e96daa5cc feat(locale): add Hindi language (#3170) 2025-01-24 17:31:14 +01:00
Benjamin Canac
36d7402be1 fix(Avatar): hide fallback when image is loaded
Resolves nuxt/ui-pro#727
2025-01-24 17:29:53 +01:00
Benjamin Canac
63b7de4159 docs(navigation-menu): add missing github icon 2025-01-24 14:46:06 +01:00
Benjamin Canac
f8b4de587e docs(installation): add devtools.enabled option 2025-01-24 14:45:56 +01:00
Benjamin Canac
890c3d0840 docs(getting-started): wrong heading for devtools 2025-01-24 14:41:53 +01:00
Benjamin Canac
7441b6451d playground(nuxt.config): enable @nuxt/fonts outside devtools prepare 2025-01-24 14:36:45 +01:00
Benjamin Canac
2b7ff3edf6 fix(NavigationMenu): handle children recursively in vertical orientation
Resolves #3128
2025-01-24 14:07:29 +01:00
Benjamin Canac
9b5a957cdd fix(ContextMenu/DropdownMenu): remove unnecessary bindings in html 2025-01-24 13:03:44 +01:00
Benjamin Canac
00c5f26111 fix(Avatar): handle loading manually to support @nuxt/image
Resolves nuxt/ui-pro#727
2025-01-24 12:14:48 +01:00
Benjamin Canac
aafddd8eed fix(useToast): add in queue and improve unique ids
Resolves #2686
2025-01-24 11:16:02 +01:00
Benjamin Canac
8d941e1360 docs(deps): update @nuxt/ui-pro 2025-01-23 11:18:13 +01:00
Konstantin
ba3d5e2c7d docs(table): describe meta field on columns (#3160) 2025-01-23 10:49:07 +01:00
Benjamin Canac
1b989c419d docs(ComponentExample): pass width to iframe only without iframeMobile 2025-01-22 18:06:17 +01:00
Benjamin Canac
b8b7a8366d docs(app): increase content search result limit 2025-01-22 17:47:37 +01:00
Benjamin Canac
fb94ee379c docs(app): replace heroicons icons by lucide 2025-01-22 17:40:17 +01:00
Benjamin Canac
a5ed62f83a docs(deps): update @nuxt/ui-pro 2025-01-22 15:37:14 +01:00
Benjamin Canac
12b6c78a17 docs(app): prevent ui-pro / vue switch when disabled 2025-01-22 15:37:14 +01:00
renovate[bot]
9cafd1295e chore(deps): update tailwindcss to v4.0.0 (v3) (#3155)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-22 15:15:24 +01:00
Benjamin Canac
545c3917a1 docs(ComponentTheme): prevent async data override on generate 2025-01-22 10:35:11 +01:00
renovate[bot]
3e2e5a075d chore(deps): update all non-major dependencies (v3) (#3150)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-21 15:54:27 +01:00
Aaron Dewes
629dcfab16 docs(input): fix aria-label on examples (#3149) 2025-01-21 12:12:14 +01:00
renovate[bot]
53d636aa9b chore(deps): update devdependency vitest to v3 (v3) (#3117)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-20 18:29:25 +01:00
renovate[bot]
eb068b2f90 chore(deps): lock file maintenance (v3) (#3145)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-20 18:10:25 +01:00
Benjamin Canac
75a470d588 chore(deps): dedupe 2025-01-20 14:49:13 +01:00
Benjamin Canac
90dc03cd03 chore(Select): clean props 2025-01-20 14:48:28 +01:00
Romain Hamel
088dc9bf38 docs(form): fix nested form example schema (#3135) 2025-01-20 11:53:05 +01:00
Romain Hamel
b95b91391a feat(FormField): set aria-describedby and aria-invalid attributes (#3123) 2025-01-20 11:46:09 +01:00
renovate[bot]
b8d99726ef chore(deps): update all non-major dependencies (v3) (#3091)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-20 11:17:39 +01:00
Hugo Richard
b88f67ccfe docs(app): add h-64 to safelist (#3115) 2025-01-20 11:13:44 +01:00
renovate[bot]
55b233dc3d chore(deps): update nuxt framework to ^3.15.2 (v3) (#3074)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-20 10:56:59 +01:00
134 changed files with 3367 additions and 3905 deletions

View File

@@ -1,5 +1,47 @@
# Changelog
## [3.0.0-alpha.12](https://github.com/nuxt/ui/compare/v3.0.0-alpha.11...v3.0.0-alpha.12) (2025-01-27)
### ⚠ BREAKING CHANGES
* **ColorPicker:** migrate from `color` to `colortranslator` (#3097)
* **Form:** include nested state in submit data (#3028)
### Features
* **css:** add `light` variant to reverse colors ([75f7064](https://github.com/nuxt/ui/commit/75f7064b409a47d068007d0b4f3af007fb24c679))
* **FormField:** set `aria-describedby` and `aria-invalid` attributes ([#3123](https://github.com/nuxt/ui/issues/3123)) ([b95b913](https://github.com/nuxt/ui/commit/b95b91391af21ee0fd96c69fb6ccf99b3126bc79))
* **Form:** form validation properties ([#3137](https://github.com/nuxt/ui/issues/3137)) ([c0b485d](https://github.com/nuxt/ui/commit/c0b485d56376d6655d15d6241daeef19f25db25f))
* **locale:** add Hebrew language ([#3181](https://github.com/nuxt/ui/issues/3181)) ([f395877](https://github.com/nuxt/ui/commit/f3958773d610d64fe15cf57525044eec22dc1f96))
* **locale:** add Hindi language ([#3170](https://github.com/nuxt/ui/issues/3170)) ([8e96daa](https://github.com/nuxt/ui/commit/8e96daa5cc57e1a2c7605d54f8640f8e012a645d))
* **locale:** add Hungarian language ([#3129](https://github.com/nuxt/ui/issues/3129)) ([891ba1f](https://github.com/nuxt/ui/commit/891ba1fec64255ba4db0f4447e044cc9140ced94))
* **locale:** add Khmer language ([#3119](https://github.com/nuxt/ui/issues/3119)) ([64421a1](https://github.com/nuxt/ui/commit/64421a190ff43563cc73f64b6a9141d69e3f5ca5))
* **locale:** add Norwegian Bokmål language ([#3095](https://github.com/nuxt/ui/issues/3095)) ([9ccfe8f](https://github.com/nuxt/ui/commit/9ccfe8fbb3284a5bdd0766ba5831135d298b563f))
* **NavigationMenu:** add `collapsed` prop ([3fc2210](https://github.com/nuxt/ui/commit/3fc2210e0392b63b065e4f4899ff864f1a3717b1))
* **NavigationMenu:** add `contentOrientation` prop ([ac86ee0](https://github.com/nuxt/ui/commit/ac86ee01b9fc9b5dc882b210d88b8fef73148e42))
* **NavigationMenu:** handle `label` type in items ([27fdc8e](https://github.com/nuxt/ui/commit/27fdc8e260bb8d2ca815c84cfdc30b6ca3baa038)), closes [#2993](https://github.com/nuxt/ui/issues/2993)
### Bug Fixes
* **Alert:** allow actions wrap ([#3083](https://github.com/nuxt/ui/issues/3083)) ([e7c10bc](https://github.com/nuxt/ui/commit/e7c10bcb0dbbfbbe48bbdea7cbd99d4535be1adb))
* **Avatar:** handle loading manually to support `@nuxt/image` ([00c5f26](https://github.com/nuxt/ui/commit/00c5f261117fd986c8be70ecdc21762023e7ebc0)), closes [nuxt/ui-pro#727](https://github.com/nuxt/ui-pro/issues/727)
* **Avatar:** hide fallback when image is loaded ([36d7402](https://github.com/nuxt/ui/commit/36d7402be1f823c753c7cd44cca82bbb5fd4cddd)), closes [nuxt/ui-pro#727](https://github.com/nuxt/ui-pro/issues/727)
* **Button:** wrong avatar size with `block` prop ([ba1dd13](https://github.com/nuxt/ui/commit/ba1dd13173835c9b72b862eb9f875a8cd79c5604))
* **colors:** move css variables to `base` layer ([533ccec](https://github.com/nuxt/ui/commit/533ccec11007ec9078fd8daefd88f6b146991939)), closes [#3075](https://github.com/nuxt/ui/issues/3075)
* **components:** prevent multiple `appConfig` identifier import ([#3186](https://github.com/nuxt/ui/issues/3186)) ([cd16b95](https://github.com/nuxt/ui/commit/cd16b95c98c0ec29bc0586ba890555f79be00290))
* **ContextMenu/DropdownMenu:** remove unnecessary bindings in html ([9b5a957](https://github.com/nuxt/ui/commit/9b5a957cdd01baafaa981864ad7d03902ad6918d))
* **Form:** include nested state in submit data ([#3028](https://github.com/nuxt/ui/issues/3028)) ([de9ecb1](https://github.com/nuxt/ui/commit/de9ecb1d767060f88c1dbdf69b9c04d5731b049d))
* **Form:** standard schema validation no longer wrapped in `value` object ([#3104](https://github.com/nuxt/ui/issues/3104)) ([8f7f579](https://github.com/nuxt/ui/commit/8f7f579da0fc58575184dc445ff0dda0c0ca1298))
* **locale:** remove emoji fallback for Chinese ([#3134](https://github.com/nuxt/ui/issues/3134)) ([1a95104](https://github.com/nuxt/ui/commit/1a951046319eaf85c2adb44928a0255dedef093d))
* **locale:** year translation missing `ñ` in `es` ([#3090](https://github.com/nuxt/ui/issues/3090)) ([1bf370e](https://github.com/nuxt/ui/commit/1bf370e6fd27fab644689335b7356bbf4c359663))
* **NavigationMenu:** handle children recursively in vertical orientation ([2b7ff3e](https://github.com/nuxt/ui/commit/2b7ff3edf6620d7ed4a491d89f0e616b5916984b)), closes [#3128](https://github.com/nuxt/ui/issues/3128)
* **NavigationMenu:** highlight open items on `horizontal` orientation only ([931211a](https://github.com/nuxt/ui/commit/931211a634183a8122ce0be874cc1f9048768d88))
* **useToast:** add in queue and improve unique ids ([aafddd8](https://github.com/nuxt/ui/commit/aafddd8eed0f3fc7c7228c2db4718ba54f3fc522)), closes [#2686](https://github.com/nuxt/ui/issues/2686)
### Code Refactoring
* **ColorPicker:** migrate from `color` to `colortranslator` ([#3097](https://github.com/nuxt/ui/issues/3097)) ([51e5e65](https://github.com/nuxt/ui/commit/51e5e65be7f834ec226be28d95a1b547b85b329c))
## [3.0.0-alpha.11](https://github.com/nuxt/ui/compare/v3.0.0-alpha.10...v3.0.0-alpha.11) (2025-01-13)
### ⚠ BREAKING CHANGES

View File

@@ -6,8 +6,8 @@
},
"dependencies": {
"citty": "^0.1.6",
"consola": "^3.3.3",
"pathe": "^2.0.1",
"consola": "^3.4.0",
"pathe": "^2.0.2",
"scule": "^1.3.0"
}
}

View File

@@ -35,9 +35,9 @@ import _appConfig from '#build/app.config'
import theme from '#build/${path}/${prose ? 'prose/' : ''}${content ? 'content/' : ''}${kebabName}'
import { tv } from '${pro ? '#ui/utils/tv' : '../utils/tv'}'
const appConfig = _appConfig as AppConfig & { ${key}: { ${prose ? 'prose: { ' : ''}${camelName}: Partial<typeof theme> } }${prose ? ' }' : ''}
const appConfig${camelName} = _appConfig as AppConfig & { ${key}: { ${prose ? 'prose: { ' : ''}${camelName}: Partial<typeof theme> } }${prose ? ' }' : ''}
const ${camelName} = tv({ extend: tv(theme), ...(appConfig.${key}?.${prose ? 'prose?.' : ''}${camelName} || {}) })
const ${camelName} = tv({ extend: tv(theme), ...(appConfig${camelName}.${key}?.${prose ? 'prose?.' : ''}${camelName} || {}) })
export interface ${upperName}Props {
/**
@@ -78,9 +78,9 @@ import _appConfig from '#build/app.config'
import theme from '#build/${path}/${prose ? 'prose/' : ''}${content ? 'content/' : ''}${kebabName}'
import { tv } from '${pro ? '#ui/utils/tv' : '../utils/tv'}'
const appConfig = _appConfig as AppConfig & { ${key}: { ${prose ? 'prose: { ' : ''}${camelName}: Partial<typeof theme> } }${prose ? ' }' : ''}
const appConfig${camelName} = _appConfig as AppConfig & { ${key}: { ${prose ? 'prose: { ' : ''}${camelName}: Partial<typeof theme> } }${prose ? ' }' : ''}
const ${camelName} = tv({ extend: tv(theme), ...(appConfig.${key}?.${prose ? 'prose?.' : ''}${camelName} || {}) })
const ${camelName} = tv({ extend: tv(theme), ...(appConfig${camelName}.${key}?.${prose ? 'prose?.' : ''}${camelName} || {}) })
type ${upperName}Variants = VariantProps<typeof ${camelName}>

View File

@@ -12,7 +12,7 @@
"dependencies": {
"@nuxt/ui": "latest",
"knitwork": "^1.2.0",
"nuxt": "^3.15.1",
"nuxt": "^3.15.3",
"prettier": "^3.4.2",
"zod": "^3.24.1"
}

View File

@@ -110,7 +110,7 @@ provide('navigation', mappedNavigation)
items: modules
}]"
:navigation="filteredNavigation"
:fuse="{ resultLimit: 42 }"
:fuse="{ resultLimit: 100 }"
/>
</ClientOnly>
</template>
@@ -152,5 +152,5 @@ html[data-module="ui"] .ui-pro-only {
display: none;
}
/* Safelist (do not remove): [&>div]:*:my-0 [&>div]:*:w-full */
/* Safelist (do not remove): [&>div]:*:my-0 [&>div]:*:w-full h-64 */
</style>

View File

@@ -132,11 +132,18 @@ const optionsValues = ref(props.options?.reduce((acc, option) => {
return acc
}, {} as Record<string, any>) || {})
const urlSearchParams = computed(() => new URLSearchParams({
...optionsValues.value,
...componentProps,
width: Math.round(width.value).toString()
}).toString())
const urlSearchParams = computed(() => {
const params = {
...optionsValues.value,
...componentProps
}
if (!props.iframeMobile) {
params.width = Math.round(width.value).toString()
}
return new URLSearchParams(params).toString()
})
</script>
<template>

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import json5 from 'json5'
import { camelCase } from 'scule'
import { hash } from 'ohash'
import * as theme from '#build/ui'
import * as themePro from '#build/ui-pro'
@@ -77,7 +78,7 @@ const component = computed(() => {
}
})
const { data: ast } = await useAsyncData(`component-theme-${name}`, async () => {
const { data: ast } = await useAsyncData(`component-theme-${name}-${hash({ props })}`, async () => {
const md = `
::code-collapse{class="nuxt-only"}

View File

@@ -15,14 +15,14 @@ function getEmojiFlag(locale: string): string {
el: 'gr',
et: 'ee',
en: 'gb',
hi: 'in',
ja: 'jp',
kh: 'km',
ko: 'kr',
nb: 'no',
sv: 'se',
uk: 'ua',
vi: 'vn',
zh: 'cn'
vi: 'vn'
}
const baseLanguage = locale.split('-')[0]?.toLowerCase() || locale

View File

@@ -4,7 +4,7 @@ import type { FormSubmitEvent } from '@nuxt/ui'
const schema = z.object({
name: z.string().min(2),
news: z.boolean()
news: z.boolean().default(false)
})
type Schema = z.output<typeof schema>
@@ -36,7 +36,7 @@ async function onSubmit(event: FormSubmitEvent<any>) {
</UFormField>
<div>
<UCheckbox v-model="state.news" name="news" label="Register to our newsletter" />
<UCheckbox v-model="state.news" name="news" label="Register to our newsletter" @update:model-value="state.email = undefined" />
</div>
<UForm v-if="state.news" :state="state" :schema="nestedSchema">

View File

@@ -51,7 +51,7 @@ const text = computed(() => {
variant="link"
size="sm"
:icon="show ? 'i-lucide-eye-off' : 'i-lucide-eye'"
aria-label="show ? 'Hide password' : 'Show password'"
:aria-label="show ? 'Hide password' : 'Show password'"
:aria-pressed="show"
aria-controls="password"
@click="show = !show"

View File

@@ -16,7 +16,7 @@ const password = ref('password')
variant="link"
size="sm"
:icon="show ? 'i-lucide-eye-off' : 'i-lucide-eye'"
aria-label="show ? 'Hide password' : 'Show password'"
:aria-label="show ? 'Hide password' : 'Show password'"
:aria-pressed="show"
aria-controls="password"
@click="show = !show"

View File

@@ -51,7 +51,8 @@ const items = [
]
},
{
label: 'GitHub'
label: 'GitHub',
icon: 'i-simple-icons-github'
}
]
</script>

View File

@@ -10,7 +10,13 @@ export function useSharedData() {
icon: 'i-simple-icons-vuedotjs',
value: 'vue',
disabled: module.value === 'ui-pro',
onSelect: () => framework.value = 'vue'
onSelect: () => {
if (module.value === 'ui-pro') {
return
}
framework.value = 'vue'
}
}].map(f => ({ ...f, active: framework.value === f.value })))
const module = useCookie('nuxt-ui-module', { default: () => 'ui' })
@@ -24,7 +30,13 @@ export function useSharedData() {
icon: 'i-lucide-panels-top-left',
value: 'ui-pro',
disabled: framework.value === 'vue',
onSelect: () => module.value = 'ui-pro'
onSelect: () => {
if (framework.value === 'vue') {
return
}
module.value = 'ui-pro'
}
}].map(m => ({ ...m, active: module.value === m.value })))
return {

View File

@@ -113,7 +113,7 @@ provide('navigation', mappedNavigation)
items: modules
}]"
:navigation="filteredNavigation"
:fuse="{ resultLimit: 42 }"
:fuse="{ resultLimit: 100 }"
/>
</ClientOnly>
</UApp>

View File

@@ -87,12 +87,12 @@ const communityLinks = computed(() => [{
to: `https://github.com/nuxt/${page.value?.module === 'ui-pro' ? 'ui-pro' : 'ui'}`,
target: '_blank'
}, {
icon: 'i-heroicons-lifebuoy',
icon: 'i-lucide-life-buoy',
label: 'Contribution',
to: '/getting-started/contribution'
}, {
label: 'Roadmap',
icon: 'i-heroicons-map',
icon: 'i-lucide-map',
to: '/roadmap'
}])

View File

@@ -76,7 +76,7 @@ You can now use Nuxt UI in any Vue project without Nuxt by adding the Vite and V
Learn how to install and configure Nuxt UI in a Vue project in the **Vue installation guide**.
::
## Nuxt DevTools Integration
### Nuxt DevTools Integration
Nuxt UI is deeply integrated with Nuxt Devtools, providing a powerful development experience:

View File

@@ -214,6 +214,23 @@ export default defineNuxtConfig({
This option adds the `transition-colors` class on components with hover or active states.
::
### `devtools.enabled`
Use the `devtools.enabled` option to enable or disable the Nuxt UI devtools.
- Default: `true`{lang="ts-type"}
```ts [nuxt.config.ts]
export default defineNuxtConfig({
modules: ['@nuxt/ui'],
ui: {
devtools: {
enabled: false
}
}
})
```
## Continuous Releases
Nuxt UI v3 uses [pkg.pr.new](https://github.com/stackblitz-labs/pkg.pr.new) for continuous preview releases, providing developers with instant access to the latest features and bug fixes without waiting for official releases.

View File

@@ -195,9 +195,13 @@ This will give you access to the following:
| Name | Type |
| ---- | ---- |
| `submit()`{lang="ts-type"} | `Promise<void>`{lang="ts-type"} <br> <div class="text-[var(--ui-text-toned)] mt-1"><p>Triggers form submission.</p> |
| `validate(opts: { name?: string \| string[], silent?: boolean, nested?: boolean, transform?: boolean })`{lang="ts-type"} | `Promise<T>`{lang="ts-type"} <br> <div class="text-[var(--ui-text-toned)] mt-1"><p>Triggers form validation. Will raise any errors unless `opts.silent` is set to true.</p> |
| `clear(path?: string)`{lang="ts-type"} | `void` <br> <div class="text-[var(--ui-text-toned)] mt-1"><p>Clears form errors associated with a specific path. If no path is provided, clears all form errors.</p> |
| `getErrors(path?: string)`{lang="ts-type"} | `FormError[]`{lang="ts-type"} <br> <div class="text-[var(--ui-text-toned)] mt-1"><p>Retrieves form errors associated with a specific path. If no path is provided, returns all form errors.</p></div> |
| `setErrors(errors: FormError[], path?: string)`{lang="ts-type"} | `void` <br> <div class="text-[var(--ui-text-toned)] mt-1"><p>Sets form errors for a given path. If no path is provided, overrides all errors.</p> |
| `validate(opts: { name?: keyof T \| (keyof T)[], silent?: boolean, nested?: boolean, transform?: boolean })`{lang="ts-type"} | `Promise<T>`{lang="ts-type"} <br> <div class="text-[var(--ui-text-toned)] mt-1"><p>Triggers form validation. Will raise any errors unless `opts.silent` is set to true.</p> |
| `clear(path?: keyof T)`{lang="ts-type"} | `void` <br> <div class="text-[var(--ui-text-toned)] mt-1"><p>Clears form errors associated with a specific path. If no path is provided, clears all form errors.</p> |
| `getErrors(path?: keyof T)`{lang="ts-type"} | `FormError[]`{lang="ts-type"} <br> <div class="text-[var(--ui-text-toned)] mt-1"><p>Retrieves form errors associated with a specific path. If no path is provided, returns all form errors.</p></div> |
| `setErrors(errors: FormError[], path?: keyof T)`{lang="ts-type"} | `void` <br> <div class="text-[var(--ui-text-toned)] mt-1"><p>Sets form errors for a given path. If no path is provided, overrides all errors.</p> |
| `errors`{lang="ts-type"} | `Ref<FormError[]>`{lang="ts-type"} <br> <div class="text-[var(--ui-text-toned)] mt-1"><p>A reference to the array containing validation errors. Use this to access or manipulate the error information.</p> |
| `disabled`{lang="ts-type"} | `Ref<boolean>`{lang="ts-type"} |
| `dirty`{lang="ts-type"} | `Ref<boolean>`{lang="ts-type"} `true` if at least one form field has been updated by the user.|
| `dirtyFields`{lang="ts-type"} | `DeepReadonly<Set<keyof T>>`{lang="ts-type"} Tracks fields that have been modified by the user. |
| `touchedFields`{lang="ts-type"} | `DeepReadonly<Set<keyof T>>`{lang="ts-type"} Tracks fields that the user interacted with. |
| `blurredFields`{lang="ts-type"} | `DeepReadonly<Set<keyof T>>`{lang="ts-type"} Tracks fields blurred by the user. |

View File

@@ -613,6 +613,85 @@ props:
The arrow is animated to follow the active item.
::
### Content Orientation
Use the `content-orientation` prop to change the orientation of the content.
::warning
This prop only works when `orientation` is `horizontal`.
::
::component-code
---
collapse: true
ignore:
- items
- arrow
- class
external:
- items
props:
arrow: true
contentOrientation: 'vertical'
items:
- label: Guide
icon: i-lucide-book-open
to: /getting-started
children:
- label: Introduction
description: Fully styled and customizable components for Nuxt.
icon: i-lucide-house
- label: Installation
description: Learn how to install and configure Nuxt UI in your application.
icon: i-lucide-cloud-download
- label: 'Icons'
icon: 'i-lucide-smile'
description: 'You have nothing to do, @nuxt/icon will handle it automatically.'
- label: Composables
icon: i-lucide-database
to: /composables
children:
- label: defineShortcuts
icon: i-lucide-file-text
description: Define shortcuts for your application.
to: /composables/define-shortcuts
- label: useModal
icon: i-lucide-file-text
description: Display a modal within your application.
to: /composables/use-modal
- label: useSlideover
icon: i-lucide-file-text
description: Display a slideover within your application.
to: /composables/use-slideover
- label: useToast
icon: i-lucide-file-text
description: Display a toast within your application.
to: /composables/use-toast
- label: Components
icon: i-lucide-box
to: /components
active: true
children:
- label: Link
icon: i-lucide-file-text
description: Use NuxtLink with superpowers.
to: /components/link
- label: Modal
icon: i-lucide-file-text
description: Display a modal within your application.
to: /components/modal
- label: NavigationMenu
icon: i-lucide-file-text
description: Display a list of links.
to: /components/navigation-menu
- label: Pagination
icon: i-lucide-file-text
description: Display a list of pages.
to: /components/pagination
class: 'w-full justify-center'
---
::
### Unmount
Use the `unmount-on-hide` prop to control the content unmounting behavior. Defaults to `true`.

View File

@@ -77,6 +77,10 @@ Use the `columns` prop as an array of [ColumnDef](https://tanstack.com/table/lat
- `accessorKey`: [The key of the row object to use when extracting the value for the column.]{class="text-[var(--ui-text-muted)]"}
- `header`: [The header to display for the column. If a string is passed, it can be used as a default for the column ID. If a function is passed, it will be passed a props object for the header and should return the rendered header value (the exact type depends on the adapter being used).]{class="text-[var(--ui-text-muted)]"}
- `cell`: [The cell to display each row for the column. If a function is passed, it will be passed a props object for the cell and should return the rendered cell value (the exact type depends on the adapter being used).]{class="text-[var(--ui-text-muted)]"}
- `meta`: [Extra properties for the column.]{class="text-[var(--ui-text-muted)]"}
- `class`:
- `td`: [The classes to apply to the `td` element.]{class="text-[var(--ui-text-muted)]"}
- `th`: [The classes to apply to the `th` element.]{class="text-[var(--ui-text-muted)]"}
In order to render components or other HTML elements, you will need to use the Vue [`h` function](https://vuejs.org/api/render-function.html#h) inside the `header` and `cell` props. This is different from other components that use slots but allows for more flexibility.

View File

@@ -4,21 +4,21 @@
"type": "module",
"dependencies": {
"@iconify-json/logos": "^1.2.4",
"@iconify-json/lucide": "^1.2.22",
"@iconify-json/simple-icons": "^1.2.19",
"@iconify-json/lucide": "^1.2.25",
"@iconify-json/simple-icons": "^1.2.22",
"@iconify-json/vscode-icons": "^1.2.10",
"@nuxt/content": "^3.0.0",
"@nuxt/content": "^3.0.1",
"@nuxt/image": "^1.9.0",
"@nuxt/ui": "latest",
"@nuxt/ui-pro": "https://pkg.pr.new/@nuxt/ui-pro@2dbbff0",
"@nuxthub/core": "^0.8.11",
"@nuxt/ui-pro": "https://pkg.pr.new/@nuxt/ui-pro@880e49c",
"@nuxthub/core": "^0.8.14",
"@nuxtjs/plausible": "^1.2.0",
"@octokit/rest": "^21.1.0",
"@vueuse/nuxt": "^12.4.0",
"@vueuse/nuxt": "^12.5.0",
"joi": "^17.13.3",
"nuxt": "^3.15.1",
"nuxt-component-meta": "^0.9.0",
"nuxt-og-image": "^4.0.2",
"nuxt": "^3.15.3",
"nuxt-component-meta": "^0.10.0",
"nuxt-og-image": "^4.1.2",
"prettier": "^3.4.2",
"shiki-transformer-color-highlight": "^0.2.0",
"superstruct": "^2.0.2",
@@ -28,6 +28,6 @@
"zod": "^3.24.1"
},
"devDependencies": {
"wrangler": "^3.101.0"
"wrangler": "^3.105.1"
}
}

View File

@@ -1,8 +1,8 @@
{
"name": "@nuxt/ui",
"description": "A UI Library for Modern Web Apps, powered by Vue & Tailwind CSS.",
"version": "3.0.0-alpha.11",
"packageManager": "pnpm@9.15.3",
"version": "3.0.0-alpha.12",
"packageManager": "pnpm@9.15.4",
"repository": {
"type": "git",
"url": "git+https://github.com/nuxt/ui.git"
@@ -60,7 +60,7 @@
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground && nuxi prepare docs && nuxi prepare devtools && vite build playground-vue",
"devtools": "NUXT_UI_DEVTOOLS_LOCAL=true nuxi dev playground",
"devtools:build": "nuxi generate devtools",
"devtools:prepare": "nuxt-component-meta playground --outputDir ../src/devtools/.component-meta/",
"devtools:prepare": "DEVTOOLS=true nuxt-component-meta playground --outputDir ../src/devtools/.component-meta/",
"docs": "DEV=true nuxi dev docs",
"docs:build": "nuxi build docs",
"docs:prepare": "nuxt-component-meta docs",
@@ -74,22 +74,22 @@
},
"dependencies": {
"@iconify/vue": "^4.3.0",
"@internationalized/date": "^3.6.0",
"@internationalized/date": "^3.7.0",
"@internationalized/number": "^3.6.0",
"@nuxt/devtools-kit": "^1.7.0",
"@nuxt/fonts": "^0.10.3",
"@nuxt/icon": "^1.10.3",
"@nuxt/kit": "^3.15.1",
"@nuxt/schema": "^3.15.1",
"@nuxt/kit": "^3.15.3",
"@nuxt/schema": "^3.15.3",
"@nuxtjs/color-mode": "^3.5.2",
"@tailwindcss/postcss": "4.0.0-beta.9",
"@tailwindcss/vite": "4.0.0-beta.9",
"@tailwindcss/postcss": "4.0.0",
"@tailwindcss/vite": "4.0.0",
"@tanstack/vue-table": "^8.20.5",
"@unhead/vue": "^1.11.16",
"@vueuse/core": "^12.4.0",
"@vueuse/integrations": "^12.4.0",
"@unhead/vue": "^1.11.18",
"@vueuse/core": "^12.5.0",
"@vueuse/integrations": "^12.5.0",
"colortranslator": "^4.1.0",
"consola": "^3.3.3",
"consola": "^3.4.0",
"defu": "^6.1.4",
"embla-carousel-auto-height": "^8.5.2",
"embla-carousel-auto-scroll": "^8.5.2",
@@ -104,12 +104,12 @@
"magic-string": "^0.30.17",
"mlly": "^1.7.4",
"ohash": "^1.1.4",
"pathe": "^2.0.1",
"pathe": "^2.0.2",
"reka-ui": "1.0.0-alpha.8",
"scule": "^1.3.0",
"sirv": "^3.0.0",
"tailwind-variants": "^0.3.0",
"tailwindcss": "4.0.0-beta.9",
"tailwind-variants": "^0.3.1",
"tailwindcss": "4.0.0",
"tinyglobby": "^0.2.10",
"unplugin": "^2.1.2",
"unplugin-auto-import": "^19.0.0",
@@ -121,19 +121,19 @@
"@nuxt/module-builder": "^0.8.4",
"@nuxt/test-utils": "^3.15.4",
"@release-it/conventional-changelog": "^10.0.0",
"@standard-schema/spec": "1.0.0-rc.0",
"@standard-schema/spec": "1.0.0",
"@vue/test-utils": "^2.4.6",
"embla-carousel": "^8.5.2",
"eslint": "^9.18.0",
"eslint": "^9.19.0",
"happy-dom": "^15.7.4",
"joi": "^17.13.3",
"knitwork": "^1.2.0",
"nuxt": "^3.15.1",
"nuxt-component-meta": "^0.9.0",
"release-it": "^18.1.1",
"nuxt": "^3.15.3",
"nuxt-component-meta": "^0.10.0",
"release-it": "^18.1.2",
"superstruct": "^2.0.2",
"valibot": "^0.42.1",
"vitest": "^2.1.8",
"vitest": "^3.0.4",
"vitest-environment-nuxt": "^1.0.1",
"vue-tsc": "^2.2.0",
"yup": "^1.6.1",

View File

@@ -18,7 +18,7 @@
"typescript": "^5.7.2",
"unplugin-auto-import": "^19.0.0",
"unplugin-vue-components": "^28.0.0",
"vite": "^6.0.7",
"vite": "^6.0.11",
"vue-tsc": "^2.2.0"
}
}

View File

@@ -4,11 +4,13 @@ import theme from '#build/ui/navigation-menu'
const colors = Object.keys(theme.variants.color)
const variants = Object.keys(theme.variants.variant)
const orientations = Object.keys(theme.variants.orientation)
const contentOrientations = Object.keys(theme.variants.contentOrientation)
const color = ref(theme.defaultVariants.color)
const highlightColor = ref()
const variant = ref(theme.defaultVariants.variant)
const orientation = ref('vertical' as const)
const orientation = ref('horizontal' as const)
const contentOrientation = ref('horizontal' as const)
const highlight = ref(true)
const collapsed = ref(false)
@@ -93,6 +95,7 @@ const items = [
<USelect v-model="color" :items="colors" placeholder="Color" />
<USelect v-model="variant" :items="variants" placeholder="Variant" />
<USelect v-model="orientation" :items="orientations" placeholder="Orientation" />
<USelect v-model="contentOrientation" :items="contentOrientations" placeholder="Content orientation" />
<USwitch v-model="collapsed" label="Collapsed" />
<USwitch v-model="highlight" label="Highlight" />
<USelect v-model="highlightColor" :items="colors" placeholder="Highlight color" />
@@ -105,6 +108,7 @@ const items = [
:color="color"
:variant="variant"
:orientation="orientation"
:viewport-orientation="contentOrientation"
:highlight="highlight"
:highlight-color="highlightColor"
:class="highlight && 'data-[orientation=horizontal]:border-b border-[var(--ui-border)]'"

View File

@@ -13,7 +13,7 @@ export default defineNuxtConfig({
},
ui: {
fonts: false
fonts: !process.env.DEVTOOLS
},
future: {

View File

@@ -8,10 +8,10 @@
"generate": "nuxi generate"
},
"dependencies": {
"@iconify-json/lucide": "^1.2.22",
"@iconify-json/simple-icons": "^1.2.19",
"@iconify-json/lucide": "^1.2.25",
"@iconify-json/simple-icons": "^1.2.22",
"@nuxt/ui": "latest",
"@nuxthub/core": "^0.8.11",
"nuxt": "^3.15.1"
"@nuxthub/core": "^0.8.14",
"nuxt": "^3.15.3"
}
}

4510
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -26,5 +26,6 @@
}, {
"matchDepTypes": ["resolutions"],
"enabled": false
}]
}],
"postUpdateOptions": ["pnpmDedupe"]
}

View File

@@ -7,9 +7,9 @@ import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
import { tv } from '../utils/tv'
import type { DynamicSlots } from '../types/utils'
const appConfig = _appConfig as AppConfig & { ui: { accordion: Partial<typeof theme> } }
const appConfigAccordion = _appConfig as AppConfig & { ui: { accordion: Partial<typeof theme> } }
const accordion = tv({ extend: tv(theme), ...(appConfig.ui?.accordion || {}) })
const accordion = tv({ extend: tv(theme), ...(appConfigAccordion.ui?.accordion || {}) })
export interface AccordionItem {
label?: string

View File

@@ -7,9 +7,9 @@ import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
import { tv } from '../utils/tv'
import type { AvatarProps, ButtonProps } from '../types'
const appConfig = _appConfig as AppConfig & { ui: { alert: Partial<typeof theme> } }
const appConfigAlert = _appConfig as AppConfig & { ui: { alert: Partial<typeof theme> } }
const alert = tv({ extend: tv(theme), ...(appConfig.ui?.alert || {}) })
const alert = tv({ extend: tv(theme), ...(appConfigAlert.ui?.alert || {}) })
type AlertVariants = VariantProps<typeof alert>

View File

@@ -7,9 +7,9 @@ import theme from '#build/ui/avatar'
import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
import { tv } from '../utils/tv'
const appConfig = _appConfig as AppConfig & { ui: { avatar: Partial<typeof theme> } }
const appConfigAvatar = _appConfig as AppConfig & { ui: { avatar: Partial<typeof theme> } }
const avatar = tv({ extend: tv(theme), ...(appConfig.ui?.avatar || {}) })
const avatar = tv({ extend: tv(theme), ...(appConfigAvatar.ui?.avatar || {}) })
type AvatarVariants = VariantProps<typeof avatar>
@@ -36,21 +36,24 @@ extendDevtoolsMeta<AvatarProps>({ defaultProps: { src: 'https://avatars.githubus
</script>
<script setup lang="ts">
import { computed } from 'vue'
import { AvatarRoot, AvatarImage, AvatarFallback, useForwardProps } from 'reka-ui'
import { reactivePick } from '@vueuse/core'
import { ref, computed, useAttrs, onMounted } from 'vue'
import { AvatarRoot, AvatarFallback, useForwardProps } from 'reka-ui'
import { reactivePick, useImage } from '@vueuse/core'
import ImageComponent from '#build/ui-image-component'
import { useAvatarGroup } from '../composables/useAvatarGroup'
import UIcon from './Icon.vue'
import ImageComponent from '#build/ui-image-component'
defineOptions({ inheritAttrs: false })
const props = withDefaults(defineProps<AvatarProps>(), { as: 'span' })
const attrs = useAttrs()
const fallbackProps = useForwardProps(reactivePick(props, 'delayMs'))
const fallback = computed(() => props.text || (props.alt || '').split(' ').map(word => word.charAt(0)).join('').substring(0, 2))
const imageLoaded = ref(false)
const { size } = useAvatarGroup(props)
// eslint-disable-next-line vue/no-dupe-keys
@@ -69,22 +72,40 @@ const sizePx = computed(() => ({
'2xl': 44,
'3xl': 48
})[props.size || 'md'])
// Reproduces Reka UI's [AvatarImage](https://reka-ui.com/docs/components/avatar#image) component behavior which cannot be used with NuxtImg component
onMounted(() => {
if (!props.src || (ImageComponent as unknown as string) !== 'img') {
return
}
const { then } = useImage({ ...props, ...attrs, src: props.src! })
then((img) => {
if (img.isReady.value) {
imageLoaded.value = true
}
})
})
</script>
<template>
<AvatarRoot :as="as" :class="ui.root({ class: [props.class, props.ui?.root] })">
<AvatarImage
<component
:is="ImageComponent"
v-if="src"
:as="ImageComponent"
v-show="imageLoaded"
role="img"
:src="src"
:alt="alt"
:width="sizePx"
:height="sizePx"
v-bind="$attrs"
v-bind="attrs"
:class="ui.image({ class: props.ui?.image })"
@load="imageLoaded = true"
/>
<AvatarFallback as-child v-bind="{ ...fallbackProps, ...$attrs }">
<AvatarFallback v-if="!imageLoaded" as-child v-bind="{ ...fallbackProps, ...$attrs }">
<slot>
<UIcon v-if="icon" :name="icon" :class="ui.icon({ class: props.ui?.icon })" />
<span v-else :class="ui.fallback({ class: props.ui?.fallback })">{{ fallback || '&nbsp;' }}</span>

View File

@@ -6,9 +6,9 @@ import theme from '#build/ui/avatar-group'
import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
import { tv } from '../utils/tv'
const appConfig = _appConfig as AppConfig & { ui: { avatarGroup: Partial<typeof theme> } }
const appConfigAvatarGroup = _appConfig as AppConfig & { ui: { avatarGroup: Partial<typeof theme> } }
const avatarGroup = tv({ extend: tv(theme), ...(appConfig.ui?.avatarGroup || {}) })
const avatarGroup = tv({ extend: tv(theme), ...(appConfigAvatarGroup.ui?.avatarGroup || {}) })
type AvatarGroupVariants = VariantProps<typeof avatarGroup>

View File

@@ -8,9 +8,9 @@ import type { UseComponentIconsProps } from '../composables/useComponentIcons'
import { tv } from '../utils/tv'
import type { AvatarProps } from '../types'
const appConfig = _appConfig as AppConfig & { ui: { badge: Partial<typeof theme> } }
const appConfigBadge = _appConfig as AppConfig & { ui: { badge: Partial<typeof theme> } }
const badge = tv({ extend: tv(theme), ...(appConfig.ui?.badge || {}) })
const badge = tv({ extend: tv(theme), ...(appConfigBadge.ui?.badge || {}) })
type BadgeVariants = VariantProps<typeof badge>

View File

@@ -7,9 +7,9 @@ import { tv } from '../utils/tv'
import type { AvatarProps, LinkProps } from '../types'
import type { DynamicSlots, PartialString } from '../types/utils'
const appConfig = _appConfig as AppConfig & { ui: { breadcrumb: Partial<typeof theme> } }
const appConfigBreadcrumb = _appConfig as AppConfig & { ui: { breadcrumb: Partial<typeof theme> } }
const breadcrumb = tv({ extend: tv(theme), ...(appConfig.ui?.breadcrumb || {}) })
const breadcrumb = tv({ extend: tv(theme), ...(appConfigBreadcrumb.ui?.breadcrumb || {}) })
export interface BreadcrumbItem extends Omit<LinkProps, 'raw' | 'custom'> {
label?: string

View File

@@ -10,9 +10,9 @@ import { tv } from '../utils/tv'
import type { AvatarProps } from '../types'
import type { PartialString } from '../types/utils'
const appConfig = _appConfig as AppConfig & { ui: { button: Partial<typeof theme> } }
const appConfigButton = _appConfig as AppConfig & { ui: { button: Partial<typeof theme> } }
const button = tv({ extend: tv(theme), ...(appConfig.ui?.button || {}) })
const button = tv({ extend: tv(theme), ...(appConfigButton.ui?.button || {}) })
type ButtonVariants = VariantProps<typeof button>

View File

@@ -6,9 +6,9 @@ import theme from '#build/ui/button-group'
import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
import { tv } from '../utils/tv'
const appConfig = _appConfig as AppConfig & { ui: { buttonGroup: Partial<typeof theme> } }
const appConfigButtonGroup = _appConfig as AppConfig & { ui: { buttonGroup: Partial<typeof theme> } }
const buttonGroup = tv({ extend: tv(theme), ...(appConfig.ui?.buttonGroup) })
const buttonGroup = tv({ extend: tv(theme), ...(appConfigButtonGroup.ui?.buttonGroup) })
type ButtonGroupVariants = VariantProps<typeof buttonGroup>

View File

@@ -7,9 +7,9 @@ import _appConfig from '#build/app.config'
import theme from '#build/ui/calendar'
import { tv } from '../utils/tv'
const appConfig = _appConfig as AppConfig & { ui: { calendar: Partial<typeof theme> } }
const appConfigCalendar = _appConfig as AppConfig & { ui: { calendar: Partial<typeof theme> } }
const calendar = tv({ extend: tv(theme), ...(appConfig.ui?.calendar || {}) })
const calendar = tv({ extend: tv(theme), ...(appConfigCalendar.ui?.calendar || {}) })
type CalendarVariants = VariantProps<typeof calendar>
@@ -77,6 +77,7 @@ import { computed } from 'vue'
import { useForwardPropsEmits } from 'reka-ui'
import { Calendar as SingleCalendar, RangeCalendar } from 'reka-ui/namespaced'
import { reactiveOmit } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { useLocale } from '../composables/useLocale'
import UButton from './Button.vue'
@@ -88,6 +89,7 @@ const props = withDefaults(defineProps<CalendarProps<R, M>>(), {
const emits = defineEmits<CalendarEmits<R, M>>()
defineSlots<CalendarSlots>()
const appConfig = useAppConfig()
const { code: locale, dir, t } = useLocale()
const rootProps = useForwardPropsEmits(reactiveOmit(props, 'range', 'modelValue', 'defaultValue', 'color', 'size', 'monthControls', 'yearControls', 'class', 'ui'), emits)

View File

@@ -5,9 +5,9 @@ import theme from '#build/ui/card'
import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
import { tv } from '../utils/tv'
const appConfig = _appConfig as AppConfig & { ui: { card: Partial<typeof theme> } }
const appConfigCard = _appConfig as AppConfig & { ui: { card: Partial<typeof theme> } }
const card = tv({ extend: tv(theme), ...(appConfig.ui?.card || {}) })
const card = tv({ extend: tv(theme), ...(appConfigCard.ui?.card || {}) })
export interface CardProps {
/**

View File

@@ -16,9 +16,9 @@ import { tv } from '../utils/tv'
import type { ButtonProps } from '../types'
import type { PartialString } from '../types/utils'
const appConfig = _appConfig as AppConfig & { ui: { carousel: Partial<typeof theme> } }
const appConfigCarousel = _appConfig as AppConfig & { ui: { carousel: Partial<typeof theme> } }
const carousel = tv({ extend: tv(theme), ...(appConfig.ui?.carousel || {}) })
const carousel = tv({ extend: tv(theme), ...(appConfigCarousel.ui?.carousel || {}) })
type CarouselVariants = VariantProps<typeof carousel>

View File

@@ -7,9 +7,9 @@ import theme from '#build/ui/checkbox'
import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
import { tv } from '../utils/tv'
const appConfig = _appConfig as AppConfig & { ui: { checkbox: Partial<typeof theme> } }
const appConfigCheckbox = _appConfig as AppConfig & { ui: { checkbox: Partial<typeof theme> } }
const checkbox = tv({ extend: tv(theme), ...(appConfig.ui?.checkbox || {}) })
const checkbox = tv({ extend: tv(theme), ...(appConfigCheckbox.ui?.checkbox || {}) })
type CheckboxVariants = VariantProps<typeof checkbox>
@@ -66,7 +66,7 @@ const modelValue = defineModel<boolean | 'indeterminate'>({ default: undefined }
const rootProps = useForwardProps(reactivePick(props, 'required', 'value', 'defaultValue'))
const appConfig = useAppConfig()
const { id: _id, emitFormChange, emitFormInput, size, color, name, disabled } = useFormField<CheckboxProps>(props)
const { id: _id, emitFormChange, emitFormInput, size, color, name, disabled, ariaAttrs } = useFormField<CheckboxProps>(props)
const id = _id.value ?? useId()
const ui = computed(() => checkbox({
@@ -92,7 +92,7 @@ function onUpdate(value: any) {
<div :class="ui.container({ class: props.ui?.container })">
<CheckboxRoot
:id="id"
v-bind="rootProps"
v-bind="{ ...rootProps, ...ariaAttrs }"
v-model="modelValue"
:name="name"
:disabled="disabled"

View File

@@ -6,9 +6,9 @@ import theme from '#build/ui/chip'
import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
import { tv } from '../utils/tv'
const appConfig = _appConfig as AppConfig & { ui: { chip: Partial<typeof theme> } }
const appConfigChip = _appConfig as AppConfig & { ui: { chip: Partial<typeof theme> } }
const chip = tv({ extend: tv(theme), ...(appConfig.ui?.chip || {}) })
const chip = tv({ extend: tv(theme), ...(appConfigChip.ui?.chip || {}) })
type ChipVariants = VariantProps<typeof chip>

View File

@@ -6,9 +6,9 @@ import theme from '#build/ui/collapsible'
import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
import { tv } from '../utils/tv'
const appConfig = _appConfig as AppConfig & { ui: { collapsible: Partial<typeof theme> } }
const appConfigCollapsible = _appConfig as AppConfig & { ui: { collapsible: Partial<typeof theme> } }
const collapsible = tv({ extend: tv(theme), ...(appConfig.ui?.collapsible || {}) })
const collapsible = tv({ extend: tv(theme), ...(appConfigCollapsible.ui?.collapsible || {}) })
export interface CollapsibleProps extends Pick<CollapsibleRootProps, 'defaultOpen' | 'open' | 'disabled' | 'unmountOnHide'> {
/**

View File

@@ -7,9 +7,9 @@ import theme from '#build/ui/color-picker'
import { tv } from '../utils/tv'
import type { HSLObject } from 'colortranslator'
const appConfig = _appConfig as AppConfig & { ui: { colorPicker: Partial<typeof theme> } }
const appConfigColorPicker = _appConfig as AppConfig & { ui: { colorPicker: Partial<typeof theme> } }
const colorPicker = tv({ extend: tv(theme), ...(appConfig.ui?.colorPicker || {}) })
const colorPicker = tv({ extend: tv(theme), ...(appConfigColorPicker.ui?.colorPicker || {}) })
type ColorPickerVariants = VariantProps<typeof colorPicker>

View File

@@ -11,9 +11,9 @@ import { tv } from '../utils/tv'
import type { AvatarProps, ButtonProps, ChipProps, KbdProps, InputProps } from '../types'
import type { DynamicSlots, PartialString } from '../types/utils'
const appConfig = _appConfig as AppConfig & { ui: { commandPalette: Partial<typeof theme> } }
const appConfigCommandPalette = _appConfig as AppConfig & { ui: { commandPalette: Partial<typeof theme> } }
const commandPalette = tv({ extend: tv(theme), ...(appConfig.ui?.commandPalette || {}) })
const commandPalette = tv({ extend: tv(theme), ...(appConfigCommandPalette.ui?.commandPalette || {}) })
export interface CommandPaletteItem {
prefix?: string

View File

@@ -5,9 +5,9 @@ import theme from '#build/ui/container'
import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
import { tv } from '../utils/tv'
const appConfig = _appConfig as AppConfig & { ui: { container: Partial<typeof theme> } }
const appConfigContainer = _appConfig as AppConfig & { ui: { container: Partial<typeof theme> } }
const container = tv({ extend: tv(theme), ...(appConfig.ui?.container || {}) })
const container = tv({ extend: tv(theme), ...(appConfigContainer.ui?.container || {}) })
export interface ContainerProps {
/**

View File

@@ -9,9 +9,9 @@ import { tv } from '../utils/tv'
import type { AvatarProps, KbdProps, LinkProps } from '../types'
import type { DynamicSlots, PartialString } from '../types/utils'
const appConfig = _appConfig as AppConfig & { ui: { contextMenu: Partial<typeof theme> } }
const appConfigContextMenu = _appConfig as AppConfig & { ui: { contextMenu: Partial<typeof theme> } }
const contextMenu = tv({ extend: tv(theme), ...(appConfig.ui?.contextMenu || {}) })
const contextMenu = tv({ extend: tv(theme), ...(appConfigContextMenu.ui?.contextMenu || {}) })
type ContextMenuVariants = VariantProps<typeof contextMenu>

View File

@@ -42,7 +42,7 @@ const emits = defineEmits<ContextMenuContentEmits>()
const slots = defineSlots<ContextMenuSlots<T>>()
const appConfig = useAppConfig()
const contentProps = useForwardPropsEmits(reactiveOmit(props, 'sub', 'items', 'portal', 'class', 'ui'), emits)
const contentProps = useForwardPropsEmits(reactiveOmit(props, 'sub', 'items', 'portal', 'labelKey', 'checkedIcon', 'loadingIcon', 'class', 'ui', 'uiOverride'), emits)
const proxySlots = omit(slots, ['default']) as Record<string, ContextMenuSlots<T>[string]>
const [DefineItemTemplate, ReuseItemTemplate] = createReusableTemplate<{ item: ContextMenuItem, active?: boolean, index: number }>()

View File

@@ -7,9 +7,9 @@ import theme from '#build/ui/drawer'
import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
import { tv } from '../utils/tv'
const appConfig = _appConfig as AppConfig & { ui: { drawer: Partial<typeof theme> } }
const appConfigDrawer = _appConfig as AppConfig & { ui: { drawer: Partial<typeof theme> } }
const drawer = tv({ extend: tv(theme), ...(appConfig.ui?.drawer || {}) })
const drawer = tv({ extend: tv(theme), ...(appConfigDrawer.ui?.drawer || {}) })
export interface DrawerProps extends Pick<DrawerRootProps, 'activeSnapPoint' | 'closeThreshold' | 'defaultOpen' | 'direction' | 'fadeFromIndex' | 'fixed' | 'modal' | 'nested' | 'direction' | 'open' | 'scrollLockTimeout' | 'shouldScaleBackground' | 'snapPoints'> {
/**

View File

@@ -9,9 +9,9 @@ import { tv } from '../utils/tv'
import type { AvatarProps, KbdProps, LinkProps } from '../types'
import type { DynamicSlots, PartialString } from '../types/utils'
const appConfig = _appConfig as AppConfig & { ui: { dropdownMenu: Partial<typeof theme> } }
const appConfigDropdownMenu = _appConfig as AppConfig & { ui: { dropdownMenu: Partial<typeof theme> } }
const dropdownMenu = tv({ extend: tv(theme), ...(appConfig.ui?.dropdownMenu || {}) })
const dropdownMenu = tv({ extend: tv(theme), ...(appConfigDropdownMenu.ui?.dropdownMenu || {}) })
type DropdownMenuVariants = VariantProps<typeof dropdownMenu>

View File

@@ -48,7 +48,7 @@ const emits = defineEmits<DropdownMenuContentEmits>()
const slots = defineSlots<DropdownMenuContentSlots<T>>()
const appConfig = useAppConfig()
const contentProps = useForwardPropsEmits(reactiveOmit(props, 'sub', 'items', 'portal', 'class', 'ui'), emits)
const contentProps = useForwardPropsEmits(reactiveOmit(props, 'sub', 'items', 'portal', 'labelKey', 'checkedIcon', 'loadingIcon', 'class', 'ui', 'uiOverride'), emits)
const proxySlots = omit(slots, ['default']) as Record<string, DropdownMenuContentSlots<T>[string]>
const [DefineItemTemplate, ReuseItemTemplate] = createReusableTemplate<{ item: DropdownMenuItem, active?: boolean, index: number }>()

View File

@@ -5,10 +5,11 @@ import theme from '#build/ui/form'
import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
import { tv } from '../utils/tv'
import type { FormSchema, FormError, FormInputEvents, FormErrorEvent, FormSubmitEvent, FormEvent, Form, FormErrorWithId } from '../types/form'
import type { DeepReadonly } from 'vue'
const appConfig = _appConfig as AppConfig & { ui: { form: Partial<typeof theme> } }
const appConfigForm = _appConfig as AppConfig & { ui: { form: Partial<typeof theme> } }
const form = tv({ extend: tv(theme), ...(appConfig.ui?.form || {}) })
const form = tv({ extend: tv(theme), ...(appConfigForm.ui?.form || {}) })
export interface FormProps<T extends object> {
id?: string | number
@@ -52,7 +53,7 @@ defineSlots<FormSlots>()
const formId = props.id ?? useId() as string
const bus = useEventBus<FormEvent>(`form-${formId}`)
const bus = useEventBus<FormEvent<T>>(`form-${formId}`)
const parentBus = inject(
formBusInjectionKey,
undefined
@@ -68,8 +69,24 @@ onMounted(async () => {
nestedForms.value.set(event.formId, { validate: event.validate })
} else if (event.type === 'detach') {
nestedForms.value.delete(event.formId)
} else if (props.validateOn?.includes(event.type as FormInputEvents)) {
await _validate({ name: event.name, silent: true, nested: false })
} else if (props.validateOn?.includes(event.type)) {
if (event.type !== 'input') {
await _validate({ name: event.name, silent: true, nested: false })
} else if (event.eager || blurredFields.has(event.name)) {
await _validate({ name: event.name, silent: true, nested: false })
}
}
if (event.type === 'blur') {
blurredFields.add(event.name)
}
if (event.type === 'change' || event.type === 'input' || event.type === 'blur' || event.type === 'focus') {
touchedFields.add(event.name)
}
if (event.type === 'change' || event.type === 'input') {
dirtyFields.add(event.name)
}
})
})
@@ -94,8 +111,12 @@ onUnmounted(() => {
const errors = ref<FormErrorWithId[]>([])
provide('form-errors', errors)
const inputs = ref<Record<string, { id?: string, pattern?: RegExp }>>({})
provide(formInputsInjectionKey, inputs)
const inputs = ref<{ [P in keyof T]?: { id?: string, pattern?: RegExp } }>({})
provide(formInputsInjectionKey, inputs as any)
const dirtyFields = new Set<keyof T>()
const touchedFields = new Set<keyof T>()
const blurredFields = new Set<keyof T>()
function resolveErrorIds(errs: FormError[]): FormErrorWithId[] {
return errs.map(err => ({
@@ -121,8 +142,8 @@ async function getErrors(): Promise<FormErrorWithId[]> {
return resolveErrorIds(errs)
}
async function _validate(opts: { name?: string | string[], silent?: boolean, nested?: boolean, transform?: boolean } = { silent: false, nested: true, transform: false }): Promise<T | false> {
const names = opts.name && !Array.isArray(opts.name) ? [opts.name] : opts.name as string[]
async function _validate(opts: { name?: keyof T | (keyof T)[], silent?: boolean, nested?: boolean, transform?: boolean } = { silent: false, nested: true, transform: false }): Promise<T | false> {
const names = opts.name && !Array.isArray(opts.name) ? [opts.name] : opts.name as (keyof T)[]
const nestedValidatePromises = !names && opts.nested
? Array.from(nestedForms.value.values()).map(
@@ -203,7 +224,7 @@ defineExpose<Form<T>>({
validate: _validate,
errors,
setErrors(errs: FormError[], name?: string) {
setErrors(errs: FormError[], name?: keyof T) {
if (name) {
errors.value = errors.value
.filter(error => error.name !== name)
@@ -217,7 +238,7 @@ defineExpose<Form<T>>({
await onSubmitWrapper(new Event('submit'))
},
getErrors(name?: string) {
getErrors(name?: keyof T) {
if (name) {
return errors.value.filter(err => err.name === name)
}
@@ -232,7 +253,12 @@ defineExpose<Form<T>>({
}
},
disabled
disabled,
dirty: computed(() => !!dirtyFields.size),
dirtyFields: readonly(dirtyFields) as DeepReadonly<Set<keyof T>>,
blurredFields: readonly(blurredFields) as DeepReadonly<Set<keyof T>>,
touchedFields: readonly(touchedFields) as DeepReadonly<Set<keyof T>>
})
</script>

View File

@@ -6,9 +6,9 @@ import theme from '#build/ui/form-field'
import { tv } from '../utils/tv'
import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
const appConfig = _appConfig as AppConfig & { ui: { formField: Partial<typeof theme> } }
const appConfigFormField = _appConfig as AppConfig & { ui: { formField: Partial<typeof theme> } }
const formField = tv({ extend: tv(theme), ...(appConfig.ui?.formField || {}) })
const formField = tv({ extend: tv(theme), ...(appConfigFormField.ui?.formField || {}) })
type FormFieldVariants = VariantProps<typeof formField>
@@ -66,6 +66,9 @@ const formErrors = inject<Ref<FormError[]> | null>('form-errors', null)
const error = computed(() => props.error || formErrors?.value?.find(error => error.name === props.name || (props.errorPattern && error.name.match(props.errorPattern)))?.message)
const id = ref(useId())
// Copies id's initial value to bind aria-attributes such as aria-describedby.
// This is required for the RadioGroup component which unsets the id value.
const ariaId = id.value
provide(inputIdInjectionKey, id)
@@ -75,7 +78,10 @@ provide(formFieldInjectionKey, computed(() => ({
size: props.size,
eagerValidation: props.eagerValidation,
validateOnInputDelay: props.validateOnInputDelay,
errorPattern: props.errorPattern
errorPattern: props.errorPattern,
hint: props.hint,
description: props.description,
ariaId
}) as FormFieldInjectedOptions<FormFieldProps>))
</script>
@@ -88,14 +94,14 @@ provide(formFieldInjectionKey, computed(() => ({
{{ label }}
</slot>
</Label>
<span v-if="hint || !!slots.hint" :class="ui.hint({ class: props.ui?.hint })">
<span v-if="hint || !!slots.hint" :id="`${ariaId}-hint`" :class="ui.hint({ class: props.ui?.hint })">
<slot name="hint" :hint="hint">
{{ hint }}
</slot>
</span>
</div>
<p v-if="description || !!slots.description" :class="ui.description({ class: props.ui?.description })">
<p v-if="description || !!slots.description" :id="`${ariaId}-description`" :class="ui.description({ class: props.ui?.description })">
<slot name="description" :description="description">
{{ description }}
</slot>
@@ -105,7 +111,7 @@ provide(formFieldInjectionKey, computed(() => ({
<div :class="[(label || !!slots.label || description || !!slots.description) && ui.container({ class: props.ui?.container })]">
<slot :error="error" />
<p v-if="(typeof error === 'string' && error) || !!slots.error" :class="ui.error({ class: props.ui?.error })">
<p v-if="(typeof error === 'string' && error) || !!slots.error" :id="`${ariaId}-error`" :class="ui.error({ class: props.ui?.error })">
<slot name="error" :error="error">
{{ error }}
</slot>

View File

@@ -9,9 +9,9 @@ import { tv } from '../utils/tv'
import type { AvatarProps } from '../types'
import type { PartialString } from '../types/utils'
const appConfig = _appConfig as AppConfig & { ui: { input: Partial<typeof theme> } }
const appConfigInput = _appConfig as AppConfig & { ui: { input: Partial<typeof theme> } }
const input = tv({ extend: tv(theme), ...(appConfig.ui?.input || {}) })
const input = tv({ extend: tv(theme), ...(appConfigInput.ui?.input || {}) })
type InputVariants = VariantProps<typeof input>
@@ -75,7 +75,7 @@ const slots = defineSlots<InputSlots>()
const [modelValue, modelModifiers] = defineModel<string | number>()
const { emitFormBlur, emitFormInput, emitFormChange, size: formGroupSize, color, id, name, highlight, disabled } = useFormField<InputProps>(props, { deferInputValidation: true })
const { emitFormBlur, emitFormInput, emitFormChange, size: formGroupSize, color, id, name, highlight, disabled, emitFormFocus, ariaAttrs } = useFormField<InputProps>(props, { deferInputValidation: true })
const { orientation, size: buttonGroupSize } = useButtonGroup<InputProps>(props)
const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(props)
@@ -166,10 +166,11 @@ onMounted(() => {
:disabled="disabled"
:required="required"
:autocomplete="autocomplete"
v-bind="$attrs"
v-bind="{ ...$attrs, ...ariaAttrs }"
@input="onInput"
@blur="onBlur"
@change="onChange"
@focus="emitFormFocus"
>
<slot />

View File

@@ -11,9 +11,9 @@ import { tv } from '../utils/tv'
import type { AvatarProps, ChipProps, InputProps } from '../types'
import type { PartialString, MaybeArrayOfArray, MaybeArrayOfArrayItem, SelectModelValue, SelectModelValueEmits, SelectItemKey } from '../types/utils'
const appConfig = _appConfig as AppConfig & { ui: { inputMenu: Partial<typeof theme> } }
const appConfigInputMenu = _appConfig as AppConfig & { ui: { inputMenu: Partial<typeof theme> } }
const inputMenu = tv({ extend: tv(theme), ...(appConfig.ui?.inputMenu || {}) })
const inputMenu = tv({ extend: tv(theme), ...(appConfigInputMenu.ui?.inputMenu || {}) })
export interface InputMenuItem {
label?: string
@@ -178,7 +178,7 @@ const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', '
const contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffset: 8, collisionPadding: 8, position: 'popper' }) as ComboboxContentProps)
const arrowProps = toRef(() => props.arrow as ComboboxArrowProps)
const { emitFormBlur, emitFormChange, emitFormInput, size: formGroupSize, color, id, name, highlight, disabled } = useFormField<InputProps>(props)
const { emitFormBlur, emitFormFocus, emitFormChange, emitFormInput, size: formGroupSize, color, id, name, highlight, disabled, ariaAttrs } = useFormField<InputProps>(props)
const { orientation, size: buttonGroupSize } = useButtonGroup<InputProps>(props)
const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(toRef(() => defu(props, { trailingIcon: appConfig.ui.icons.chevronDown })))
@@ -279,6 +279,7 @@ function onBlur(event: FocusEvent) {
function onFocus(event: FocusEvent) {
emits('focus', event)
emitFormFocus()
}
function onUpdateOpen(value: boolean) {
@@ -365,7 +366,7 @@ defineExpose({
<ComboboxInput v-model="searchTerm" :display-value="displayValue" as-child>
<TagsInputInput
ref="inputRef"
v-bind="$attrs"
v-bind="{ ...$attrs, ...ariaAttrs }"
:placeholder="placeholder"
:required="required"
:class="ui.tagsInput({ class: props.ui?.tagsInput })"
@@ -379,7 +380,7 @@ defineExpose({
ref="inputRef"
v-model="searchTerm"
:display-value="displayValue"
v-bind="$attrs"
v-bind="{ ...$attrs, ...ariaAttrs }"
:type="type"
:placeholder="placeholder"
:required="required"

View File

@@ -7,9 +7,9 @@ import theme from '#build/ui/input-number'
import { tv } from '../utils/tv'
import type { ButtonProps } from '../types'
const appConfig = _appConfig as AppConfig & { ui: { inputNumber: Partial<typeof theme> } }
const appConfigInputNumber = _appConfig as AppConfig & { ui: { inputNumber: Partial<typeof theme> } }
const inputNumber = tv({ extend: tv(theme), ...(appConfig.ui?.inputNumber || {}) })
const inputNumber = tv({ extend: tv(theme), ...(appConfigInputNumber.ui?.inputNumber || {}) })
type InputNumberVariants = VariantProps<typeof inputNumber>
@@ -78,6 +78,7 @@ export interface InputNumberSlots {
import { onMounted, ref, computed } from 'vue'
import { NumberFieldRoot, NumberFieldInput, NumberFieldDecrement, NumberFieldIncrement, useForwardPropsEmits } from 'reka-ui'
import { reactivePick } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { useFormField } from '../composables/useFormField'
import { useLocale } from '../composables/useLocale'
import UButton from './Button.vue'
@@ -92,7 +93,8 @@ defineSlots<InputNumberSlots>()
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'min', 'max', 'step', 'formatOptions'), emits)
const { emitFormBlur, emitFormChange, emitFormInput, id, color, size, name, highlight, disabled } = useFormField<InputNumberProps>(props)
const appConfig = useAppConfig()
const { emitFormBlur, emitFormFocus, emitFormChange, emitFormInput, id, color, size, name, highlight, disabled, ariaAttrs } = useFormField<InputNumberProps>(props)
const { t, code: codeLocale } = useLocale()
const locale = computed(() => props.locale || codeLocale.value)
@@ -152,12 +154,13 @@ defineExpose({
@update:model-value="onUpdate"
>
<NumberFieldInput
v-bind="$attrs"
v-bind="{ ...$attrs, ...ariaAttrs }"
ref="inputRef"
:placeholder="placeholder"
:required="required"
:class="ui.base({ class: props.ui?.base })"
@blur="onBlur"
@focus="emitFormFocus"
/>
<div :class="ui.increment({ class: props.ui?.increment })">

View File

@@ -7,9 +7,9 @@ import type { KbdKey } from '../composables/useKbd'
import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
import { tv } from '../utils/tv'
const appConfig = _appConfig as AppConfig & { ui: { kbd: Partial<typeof theme> } }
const appConfigKbd = _appConfig as AppConfig & { ui: { kbd: Partial<typeof theme> } }
const kbd = tv({ extend: tv(theme), ...(appConfig.ui?.kbd || {}) })
const kbd = tv({ extend: tv(theme), ...(appConfigKbd.ui?.kbd || {}) })
type KbdVariants = VariantProps<typeof kbd>

View File

@@ -53,9 +53,9 @@ interface NuxtLinkProps extends Omit<RouterLinkProps, 'to'> {
noPrefetch?: boolean
}
const appConfig = _appConfig as AppConfig & { ui: { link: Partial<typeof theme> } }
const appConfigLink = _appConfig as AppConfig & { ui: { link: Partial<typeof theme> } }
const link = tv({ extend: tv(theme), ...(appConfig.ui?.link || {}) })
const link = tv({ extend: tv(theme), ...(appConfigLink.ui?.link || {}) })
export interface LinkProps extends NuxtLinkProps {
/**

View File

@@ -7,9 +7,9 @@ import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
import { tv } from '../utils/tv'
import type { ButtonProps } from '../types'
const appConfig = _appConfig as AppConfig & { ui: { modal: Partial<typeof theme> } }
const appConfigModal = _appConfig as AppConfig & { ui: { modal: Partial<typeof theme> } }
const modal = tv({ extend: tv(theme), ...(appConfig.ui?.modal || {}) })
const modal = tv({ extend: tv(theme), ...(appConfigModal.ui?.modal || {}) })
export interface ModalProps extends DialogRootProps {
title?: string
@@ -73,7 +73,7 @@ extendDevtoolsMeta({ example: 'ModalExample' })
</script>
<script setup lang="ts">
import { computed, toRef, provide } from 'vue'
import { computed, toRef } from 'vue'
import { DialogRoot, DialogTrigger, DialogPortal, DialogOverlay, DialogContent, DialogTitle, DialogDescription, DialogClose, useForwardPropsEmits } from 'reka-ui'
import { reactivePick } from '@vueuse/core'
import { useAppConfig } from '#imports'
@@ -112,9 +112,6 @@ const ui = computed(() => modal({
transition: props.transition,
fullscreen: props.fullscreen
}))
// Blocks ButtonGroup injections to avoid side-effects if the modal is within a button group.
provide(buttonGroupInjectionKey, undefined)
</script>
<template>

View File

@@ -9,9 +9,9 @@ import { tv } from '../utils/tv'
import type { AvatarProps, BadgeProps, LinkProps } from '../types'
import type { DynamicSlots, MaybeArrayOfArray, MaybeArrayOfArrayItem, PartialString } from '../types/utils'
const appConfig = _appConfig as AppConfig & { ui: { navigationMenu: Partial<typeof theme> } }
const appConfigNavigationMenu = _appConfig as AppConfig & { ui: { navigationMenu: Partial<typeof theme> } }
const navigationMenu = tv({ extend: tv(theme), ...(appConfig.ui?.navigationMenu || {}) })
const navigationMenu = tv({ extend: tv(theme), ...(appConfigNavigationMenu.ui?.navigationMenu || {}) })
export interface NavigationMenuChildItem extends Omit<NavigationMenuItem, 'children' | 'type'> {
/** Description is only used when `orientation` is `horizontal`. */
@@ -72,6 +72,12 @@ export interface NavigationMenuProps<T> extends Pick<NavigationMenuRootProps, 'm
highlightColor?: NavigationMenuVariants['highlightColor']
/** The content of the menu. */
content?: Omit<NavigationMenuContentProps, 'as' | 'asChild' | 'forceMount'>
/**
* The orientation of the content.
* Only works when `orientation` is `horizontal`.
* @defaultValue 'horizontal'
*/
contentOrientation?: NavigationMenuVariants['contentOrientation']
/**
* Display an arrow alongside the menu.
* @defaultValue false
@@ -142,6 +148,7 @@ extendDevtoolsMeta({
import { computed, toRef } from 'vue'
import { NavigationMenuRoot, NavigationMenuList, NavigationMenuItem, NavigationMenuTrigger, NavigationMenuContent, NavigationMenuLink, NavigationMenuIndicator, NavigationMenuViewport, useForwardPropsEmits } from 'reka-ui'
import { createReusableTemplate } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { get } from '../utils'
import { pickLinkProps } from '../utils/link'
import ULinkBase from './LinkBase.vue'
@@ -153,6 +160,7 @@ import UCollapsible from './Collapsible.vue'
const props = withDefaults(defineProps<NavigationMenuProps<I>>(), {
orientation: 'horizontal',
contentOrientation: 'horizontal',
delayDuration: 0,
unmountOnHide: true,
labelKey: 'label'
@@ -175,10 +183,13 @@ const rootProps = useForwardPropsEmits(computed(() => ({
const contentProps = toRef(() => props.content)
const [DefineItemTemplate, ReuseItemTemplate] = createReusableTemplate<{ item: NavigationMenuItem, active?: boolean, index: number }>()
const appConfig = useAppConfig()
const [DefineLinkTemplate, ReuseLinkTemplate] = createReusableTemplate<{ item: NavigationMenuItem, index: number, active?: boolean }>()
const [DefineItemTemplate, ReuseItemTemplate] = createReusableTemplate<{ item: NavigationMenuItem, index: number }>()
const ui = computed(() => navigationMenu({
orientation: props.orientation,
contentOrientation: props.contentOrientation,
collapsed: props.collapsed,
color: props.color,
variant: props.variant,
@@ -190,7 +201,7 @@ const lists = computed(() => props.items?.length ? (Array.isArray(props.items[0]
</script>
<template>
<DefineItemTemplate v-slot="{ item, active, index }">
<DefineLinkTemplate v-slot="{ item, active, index }">
<slot :name="item.slot || 'item'" :item="(item as T)" :index="index">
<slot :name="item.slot ? `${item.slot}-leading` : 'item-leading'" :item="(item as T)" :active="active" :index="index">
<UAvatar v-if="item.avatar" :size="((props.ui?.linkLeadingAvatarSize || ui.linkLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.linkLeadingAvatar({ class: props.ui?.linkLeadingAvatar, active, disabled: !!item.disabled })" />
@@ -224,80 +235,73 @@ const lists = computed(() => props.items?.length ? (Array.isArray(props.items[0]
</slot>
</span>
</slot>
</DefineItemTemplate>
</DefineLinkTemplate>
<NavigationMenuRoot v-bind="rootProps" :data-collapsed="collapsed" :class="ui.root({ class: [props.class, props.ui?.root] })">
<template v-for="(list, listIndex) in lists" :key="`list-${listIndex}`">
<NavigationMenuList :class="ui.list({ class: props.ui?.list })">
<DefineItemTemplate v-slot="{ item, index }">
<component
:is="(orientation === 'vertical' && item.children?.length) ? UCollapsible : NavigationMenuItem"
as="li"
:value="item.value || String(index)"
:default-open="item.defaultOpen"
:unmount-on-hide="(orientation === 'vertical' && item.children?.length) ? unmountOnHide : undefined"
:open="item.open"
>
<div v-if="orientation === 'vertical' && item.type === 'label'" :class="ui.label({ class: props.ui?.label })">
<ReuseLinkTemplate :item="(item as T)" :index="index" />
</div>
<ULink v-else-if="item.type !== 'label'" v-slot="{ active, ...slotProps }" v-bind="(orientation === 'vertical' && item.children?.length) ? {} : pickLinkProps(item as Omit<NavigationMenuItem, 'type'>)" custom>
<component
:is="(orientation === 'vertical' && item.children?.length) ? UCollapsible : NavigationMenuItem"
v-for="(item, index) in list"
:key="`list-${listIndex}-${index}`"
as="li"
:value="item.value || String(index)"
:default-open="item.defaultOpen"
:unmount-on-hide="(orientation === 'vertical' && item.children?.length) ? unmountOnHide : undefined"
:open="item.open"
:class="ui.item({ class: props.ui?.item })"
:is="(orientation === 'horizontal' && (item.children?.length || !!slots[item.slot ? `${item.slot}-content` : 'item-content'])) ? NavigationMenuTrigger : NavigationMenuLink"
as-child
:active="active"
:disabled="item.disabled"
@select="item.onSelect"
>
<div v-if="orientation === 'vertical' && item.type === 'label'" :class="ui.label({ class: props.ui?.label })">
<ReuseItemTemplate :item="(item as T)" :index="index" />
</div>
<ULink v-else-if="item.type !== 'label'" v-slot="{ active, ...slotProps }" v-bind="(orientation === 'vertical' && item.children?.length) ? {} : pickLinkProps(item as Omit<NavigationMenuItem, 'type'>)" custom>
<component
:is="(orientation === 'horizontal' && (item.children?.length || !!slots[item.slot ? `${item.slot}-content` : 'item-content'])) ? NavigationMenuTrigger : NavigationMenuLink"
as-child
:active="active"
:disabled="item.disabled"
@select="item.onSelect"
>
<ULinkBase v-bind="slotProps" :class="ui.link({ class: [props.ui?.link, item.class], active: active || item.active, disabled: !!item.disabled, level: !(orientation === 'vertical' && item.children?.length) })">
<ReuseItemTemplate :item="(item as T)" :active="active || item.active" :index="index" />
</ULinkBase>
</component>
<ULinkBase v-bind="slotProps" :class="ui.link({ class: [props.ui?.link, item.class], active: active || item.active, disabled: !!item.disabled, level: !(orientation === 'vertical' && item.children?.length) })">
<ReuseLinkTemplate :item="(item as T)" :active="active || item.active" :index="index" />
</ULinkBase>
</component>
<NavigationMenuContent v-if="orientation === 'horizontal' && (item.children?.length || !!slots[item.slot ? `${item.slot}-content` : 'item-content'])" v-bind="contentProps" :class="ui.content({ class: props.ui?.content })">
<slot :name="item.slot ? `${item.slot}-content` : 'item-content'" :item="(item as T)" :active="active" :index="index">
<ul :class="ui.childList({ class: props.ui?.childList })">
<li v-for="(childItem, childIndex) in item.children" :key="childIndex" :class="ui.childItem({ class: props.ui?.childItem })">
<ULink v-slot="{ active: childActive, ...childSlotProps }" v-bind="pickLinkProps(childItem)" custom>
<NavigationMenuLink as-child :active="childActive" @select="childItem.onSelect">
<ULinkBase v-bind="childSlotProps" :class="ui.childLink({ class: [props.ui?.childLink, childItem.class], active: childActive })">
<UIcon v-if="childItem.icon" :name="childItem.icon" :class="ui.childLinkIcon({ class: props.ui?.childLinkIcon, active: childActive })" />
<div :class="ui.childLinkWrapper({ class: props.ui?.childLinkWrapper })">
<p :class="ui.childLinkLabel({ class: props.ui?.childLinkLabel, active: childActive })">
{{ get(childItem, props.labelKey as string) }}
<UIcon v-if="childItem.target === '_blank'" :name="appConfig.ui.icons.external" :class="ui.childLinkLabelExternalIcon({ class: props.ui?.childLinkLabelExternalIcon, active: childActive })" />
</p>
<p v-if="childItem.description" :class="ui.childLinkDescription({ class: props.ui?.childLinkDescription, active: childActive })">
{{ childItem.description }}
</p>
</div>
</ULinkBase>
</NavigationMenuLink>
</ULink>
</li>
</ul>
</slot>
</NavigationMenuContent>
</ULink>
<template v-if="orientation === 'vertical' && item.children?.length" #content>
<NavigationMenuContent v-if="orientation === 'horizontal' && (item.children?.length || !!slots[item.slot ? `${item.slot}-content` : 'item-content'])" v-bind="contentProps" :class="ui.content({ class: props.ui?.content })">
<slot :name="item.slot ? `${item.slot}-content` : 'item-content'" :item="(item as T)" :active="active" :index="index">
<ul :class="ui.childList({ class: props.ui?.childList })">
<li v-for="(childItem, childIndex) in item.children" :key="childIndex" :class="ui.childItem({ class: props.ui?.childItem })">
<ULink v-slot="{ active: childActive, ...childSlotProps }" v-bind="pickLinkProps(childItem)" custom>
<NavigationMenuLink as-child :active="childActive" @select="childItem.onSelect">
<ULinkBase v-bind="childSlotProps" :class="ui.link({ class: [props.ui?.link, childItem.class], active: childActive, disabled: !!childItem.disabled, level: true })">
<ReuseItemTemplate :item="(childItem as T)" :active="childActive" :index="childIndex" />
<ULinkBase v-bind="childSlotProps" :class="ui.childLink({ class: [props.ui?.childLink, childItem.class], active: childActive })">
<UIcon v-if="childItem.icon" :name="childItem.icon" :class="ui.childLinkIcon({ class: props.ui?.childLinkIcon, active: childActive })" />
<div :class="ui.childLinkWrapper({ class: props.ui?.childLinkWrapper })">
<p :class="ui.childLinkLabel({ class: props.ui?.childLinkLabel, active: childActive })">
{{ get(childItem, props.labelKey as string) }}
<UIcon v-if="childItem.target === '_blank'" :name="appConfig.ui.icons.external" :class="ui.childLinkLabelExternalIcon({ class: props.ui?.childLinkLabelExternalIcon, active: childActive })" />
</p>
<p v-if="childItem.description" :class="ui.childLinkDescription({ class: props.ui?.childLinkDescription, active: childActive })">
{{ childItem.description }}
</p>
</div>
</ULinkBase>
</NavigationMenuLink>
</ULink>
</li>
</ul>
</template>
</component>
</slot>
</NavigationMenuContent>
</ULink>
<template v-if="orientation === 'vertical' && item.children?.length" #content>
<ul :class="ui.childList({ class: props.ui?.childList })">
<ReuseItemTemplate v-for="(childItem, childIndex) in item.children" :key="childIndex" :item="childItem" :index="childIndex" :class="ui.childItem({ class: props.ui?.childItem })" />
</ul>
</template>
</component>
</DefineItemTemplate>
<NavigationMenuRoot v-bind="rootProps" :data-collapsed="collapsed" :class="ui.root({ class: [props.class, props.ui?.root] })">
<template v-for="(list, listIndex) in lists" :key="`list-${listIndex}`">
<NavigationMenuList :class="ui.list({ class: props.ui?.list })">
<ReuseItemTemplate v-for="(item, index) in list" :key="`list-${listIndex}-${index}`" :item="item" :index="index" :class="ui.item({ class: props.ui?.item })" />
</NavigationMenuList>
<div v-if="orientation === 'vertical' && listIndex < lists.length - 1" :class="ui.separator({ class: props.ui?.separator })" />

View File

@@ -8,9 +8,9 @@ import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
import { tv } from '../utils/tv'
import type { ButtonProps } from '../types'
const appConfig = _appConfig as AppConfig & { ui: { pagination: Partial<typeof theme> } }
const appConfigPagination = _appConfig as AppConfig & { ui: { pagination: Partial<typeof theme> } }
const pagination = tv({ extend: tv(theme), ...(appConfig.ui?.pagination || {}) })
const pagination = tv({ extend: tv(theme), ...(appConfigPagination.ui?.pagination || {}) })
export interface PaginationProps extends Partial<Pick<PaginationRootProps, 'defaultPage' | 'disabled' | 'itemsPerPage' | 'page' | 'showEdges' | 'siblingCount' | 'total'>> {
/**

View File

@@ -7,9 +7,9 @@ import theme from '#build/ui/pin-input'
import { tv } from '../utils/tv'
import type { PartialString } from '../types/utils'
const appConfig = _appConfig as AppConfig & { ui: { pinInput: Partial<typeof theme> } }
const appConfigPinInput = _appConfig as AppConfig & { ui: { pinInput: Partial<typeof theme> } }
const pinInput = tv({ extend: tv(theme), ...(appConfig.ui?.pinInput || {}) })
const pinInput = tv({ extend: tv(theme), ...(appConfigPinInput.ui?.pinInput || {}) })
type PinInputVariants = VariantProps<typeof pinInput>
@@ -50,7 +50,7 @@ const props = withDefaults(defineProps<PinInputProps>(), {
const emits = defineEmits<PinInputEmits>()
const rootProps = useForwardPropsEmits(reactivePick(props, 'defaultValue', 'disabled', 'id', 'mask', 'modelValue', 'name', 'otp', 'placeholder', 'required', 'type'), emits)
const { emitFormInput, emitFormChange, emitFormBlur, size, color, id, name, highlight, disabled } = useFormField<PinInputProps>(props)
const { emitFormInput, emitFormFocus, emitFormChange, emitFormBlur, size, color, id, name, highlight, disabled, ariaAttrs } = useFormField<PinInputProps>(props)
const ui = computed(() => pinInput({
color: color.value,
@@ -77,7 +77,7 @@ function onBlur(event: FocusEvent) {
<template>
<PinInputRoot
v-bind="rootProps"
v-bind="{ ...rootProps, ...ariaAttrs }"
:id="id"
:name="name"
:class="ui.root({ class: [props.class, props.ui?.root] })"
@@ -92,6 +92,7 @@ function onBlur(event: FocusEvent) {
v-bind="$attrs"
:disabled="disabled"
@blur="onBlur"
@focus="emitFormFocus"
/>
</PinInputRoot>
</template>

View File

@@ -6,9 +6,9 @@ import theme from '#build/ui/popover'
import { tv } from '../utils/tv'
import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
const appConfig = _appConfig as AppConfig & { ui: { popover: Partial<typeof theme> } }
const appConfigPopover = _appConfig as AppConfig & { ui: { popover: Partial<typeof theme> } }
const popover = tv({ extend: tv(theme), ...(appConfig.ui?.popover || {}) })
const popover = tv({ extend: tv(theme), ...(appConfigPopover.ui?.popover || {}) })
export interface PopoverProps extends PopoverRootProps, Pick<HoverCardRootProps, 'openDelay' | 'closeDelay'> {
/**

View File

@@ -7,9 +7,9 @@ import _appConfig from '#build/app.config'
import theme from '#build/ui/progress'
import { tv } from '../utils/tv'
const appConfig = _appConfig as AppConfig & { ui: { progress: Partial<typeof theme> } }
const appConfigProgress = _appConfig as AppConfig & { ui: { progress: Partial<typeof theme> } }
const progress = tv({ extend: tv(theme), ...(appConfig.ui?.progress || {}) })
const progress = tv({ extend: tv(theme), ...(appConfigProgress.ui?.progress || {}) })
type ProgressVariants = VariantProps<typeof progress>

View File

@@ -7,9 +7,9 @@ import theme from '#build/ui/radio-group'
import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
import { tv } from '../utils/tv'
const appConfig = _appConfig as AppConfig & { ui: { radioGroup: Partial<typeof theme> } }
const appConfigRadioGroup = _appConfig as AppConfig & { ui: { radioGroup: Partial<typeof theme> } }
const radioGroup = tv({ extend: tv(theme), ...(appConfig.ui?.radioGroup || {}) })
const radioGroup = tv({ extend: tv(theme), ...(appConfigRadioGroup.ui?.radioGroup || {}) })
type RadioGroupVariants = VariantProps<typeof radioGroup>
@@ -87,7 +87,7 @@ const slots = defineSlots<RadioGroupSlots<T>>()
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'orientation', 'loop', 'required'), emits)
const { emitFormChange, emitFormInput, color, name, size, id: _id, disabled } = useFormField<RadioGroupProps<T>>(props, { bind: false })
const { emitFormChange, emitFormInput, color, name, size, id: _id, disabled, ariaAttrs } = useFormField<RadioGroupProps<T>>(props, { bind: false })
const id = _id.value ?? useId()
const ui = computed(() => radioGroup({
@@ -147,7 +147,7 @@ function onUpdate(value: any) {
:class="ui.root({ class: [props.class, props.ui?.root] })"
@update:model-value="onUpdate"
>
<fieldset :class="ui.fieldset({ class: props.ui?.fieldset })">
<fieldset :class="ui.fieldset({ class: props.ui?.fieldset })" v-bind="ariaAttrs">
<legend v-if="legend || !!slots.legend" :class="ui.legend({ class: props.ui?.legend })">
<slot name="legend">
{{ legend }}

View File

@@ -10,9 +10,9 @@ import { tv } from '../utils/tv'
import type { AvatarProps, ChipProps, InputProps } from '../types'
import type { PartialString, MaybeArrayOfArray, MaybeArrayOfArrayItem, SelectModelValue, SelectModelValueEmits, SelectItemKey } from '../types/utils'
const appConfig = _appConfig as AppConfig & { ui: { select: Partial<typeof theme> } }
const appConfigSelect = _appConfig as AppConfig & { ui: { select: Partial<typeof theme> } }
const select = tv({ extend: tv(theme), ...(appConfig.ui?.select || {}) })
const select = tv({ extend: tv(theme), ...(appConfigSelect.ui?.select || {}) })
export interface SelectItem {
label?: string
@@ -129,11 +129,11 @@ const emits = defineEmits<SelectEmits<T, V, M>>()
const slots = defineSlots<SelectSlots<T, M>>()
const appConfig = useAppConfig()
const rootProps = useForwardPropsEmits(reactivePick(props, 'modelValue', 'defaultValue', 'open', 'defaultOpen', 'disabled', 'autocomplete', 'required', 'multiple'), emits)
const rootProps = useForwardPropsEmits(reactivePick(props, 'open', 'defaultOpen', 'disabled', 'autocomplete', 'required', 'multiple'), emits)
const contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffset: 8, collisionPadding: 8, position: 'popper' }) as SelectContentProps)
const arrowProps = toRef(() => props.arrow as SelectArrowProps)
const { emitFormChange, emitFormInput, emitFormBlur, size: formGroupSize, color, id, name, highlight, disabled } = useFormField<InputProps>(props)
const { emitFormChange, emitFormInput, emitFormBlur, emitFormFocus, size: formGroupSize, color, id, name, highlight, disabled, ariaAttrs } = useFormField<InputProps>(props)
const { orientation, size: buttonGroupSize } = useButtonGroup<InputProps>(props)
const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(toRef(() => defu(props, { trailingIcon: appConfig.ui.icons.chevronDown })))
@@ -179,6 +179,7 @@ function onUpdateOpen(value: boolean) {
} else {
const event = new FocusEvent('focus')
emits('focus', event)
emitFormFocus()
}
}
</script>
@@ -186,16 +187,17 @@ function onUpdateOpen(value: boolean) {
<!-- eslint-disable vue/no-template-shadow -->
<template>
<SelectRoot
:id="id"
v-slot="{ modelValue, open }"
v-bind="rootProps"
:name="name"
v-bind="rootProps"
:autocomplete="autocomplete"
:disabled="disabled"
:default-value="(defaultValue as (AcceptableValue | AcceptableValue[] | undefined))"
:model-value="(modelValue as (AcceptableValue | AcceptableValue[] | undefined))"
@update:model-value="onUpdate"
@update:open="onUpdateOpen"
>
<SelectTrigger :class="ui.base({ class: [props.class, props.ui?.base] })">
<SelectTrigger :id="id" :class="ui.base({ class: [props.class, props.ui?.base] })" v-bind="ariaAttrs">
<span v-if="isLeading || !!avatar || !!slots.leading" :class="ui.leading({ class: props.ui?.leading })">
<slot name="leading" :model-value="(modelValue as M extends true ? AcceptableValue[] : AcceptableValue)" :open="open" :ui="ui">
<UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />

View File

@@ -10,9 +10,9 @@ import { tv } from '../utils/tv'
import type { AvatarProps, ChipProps, InputProps } from '../types'
import type { PartialString, MaybeArrayOfArray, MaybeArrayOfArrayItem, SelectModelValue, SelectModelValueEmits, SelectItemKey } from '../types/utils'
const appConfig = _appConfig as AppConfig & { ui: { selectMenu: Partial<typeof theme> } }
const appConfigSelectMenu = _appConfig as AppConfig & { ui: { selectMenu: Partial<typeof theme> } }
const selectMenu = tv({ extend: tv(theme), ...(appConfig.ui?.selectMenu || {}) })
const selectMenu = tv({ extend: tv(theme), ...(appConfigSelectMenu.ui?.selectMenu || {}) })
export interface SelectMenuItem {
label?: string
@@ -168,7 +168,7 @@ const contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffse
const arrowProps = toRef(() => props.arrow as ComboboxArrowProps)
const searchInputProps = toRef(() => defu(props.searchInput, { placeholder: t('selectMenu.search'), variant: 'none' }) as InputProps)
const { emitFormBlur, emitFormInput, emitFormChange, size: formGroupSize, color, id, name, highlight, disabled } = useFormField<InputProps>(props)
const { emitFormBlur, emitFormFocus, emitFormInput, emitFormChange, size: formGroupSize, color, id, name, highlight, disabled, ariaAttrs } = useFormField<InputProps>(props)
const { orientation, size: buttonGroupSize } = useButtonGroup<InputProps>(props)
const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(toRef(() => defu(props, { trailingIcon: appConfig.ui.icons.chevronDown })))
@@ -272,6 +272,7 @@ function onUpdateOpen(value: boolean) {
} else {
const event = new FocusEvent('focus')
emits('focus', event)
emitFormFocus()
clearTimeout(timeoutId)
}
}
@@ -298,7 +299,7 @@ function onUpdateOpen(value: boolean) {
<ComboboxRoot
:id="id"
v-slot="{ modelValue, open }"
v-bind="rootProps"
v-bind="{ ...rootProps, ...ariaAttrs }"
ignore-filter
as-child
:name="name"

View File

@@ -7,9 +7,9 @@ import theme from '#build/ui/separator'
import { tv } from '../utils/tv'
import type { AvatarProps } from '../types'
const appConfig = _appConfig as AppConfig & { ui: { separator: Partial<typeof theme> } }
const appConfigSeparator = _appConfig as AppConfig & { ui: { separator: Partial<typeof theme> } }
const separator = tv({ extend: tv(theme), ...(appConfig.ui?.separator || {}) })
const separator = tv({ extend: tv(theme), ...(appConfigSeparator.ui?.separator || {}) })
type SeparatorVariants = VariantProps<typeof separator>

View File

@@ -5,9 +5,9 @@ import theme from '#build/ui/skeleton'
import { tv } from '../utils/tv'
import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
const appConfig = _appConfig as AppConfig & { ui: { skeleton: Partial<typeof theme> } }
const appConfigSkeleton = _appConfig as AppConfig & { ui: { skeleton: Partial<typeof theme> } }
const skeleton = tv({ extend: tv(theme), ...(appConfig.ui?.skeleton || {}) })
const skeleton = tv({ extend: tv(theme), ...(appConfigSkeleton.ui?.skeleton || {}) })
export interface SkeletonProps {
/**

View File

@@ -8,9 +8,9 @@ import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
import { tv } from '../utils/tv'
import type { ButtonProps } from '../types'
const appConfig = _appConfig as AppConfig & { ui: { slideover: Partial<typeof theme> } }
const appConfigSlideover = _appConfig as AppConfig & { ui: { slideover: Partial<typeof theme> } }
const slideover = tv({ extend: tv(theme), ...(appConfig.ui?.slideover || {}) })
const slideover = tv({ extend: tv(theme), ...(appConfigSlideover.ui?.slideover || {}) })
type SlideoverVariants = VariantProps<typeof slideover>

View File

@@ -6,9 +6,9 @@ import _appConfig from '#build/app.config'
import theme from '#build/ui/slider'
import { tv } from '../utils/tv'
const appConfig = _appConfig as AppConfig & { ui: { slider: Partial<typeof theme> } }
const appConfigSlider = _appConfig as AppConfig & { ui: { slider: Partial<typeof theme> } }
const slider = tv({ extend: tv(theme), ...(appConfig.ui?.slider || {}) })
const slider = tv({ extend: tv(theme), ...(appConfigSlider.ui?.slider || {}) })
type SliderVariants = VariantProps<typeof slider>
@@ -55,7 +55,7 @@ const modelValue = defineModel<number | number[]>()
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'orientation', 'min', 'max', 'step', 'minStepsBetweenThumbs', 'inverted'), emits)
const { id, emitFormChange, emitFormInput, size, color, name, disabled } = useFormField<SliderProps>(props)
const { id, emitFormChange, emitFormInput, size, color, name, disabled, ariaAttrs } = useFormField<SliderProps>(props)
const defaultSliderValue = computed(() => {
if (typeof props.defaultValue === 'number') {
@@ -95,7 +95,7 @@ function onChange(value: any) {
<template>
<SliderRoot
v-bind="rootProps"
v-bind="{ ...rootProps, ...ariaAttrs }"
:id="id"
v-model="sliderValue"
:name="name"

View File

@@ -8,9 +8,9 @@ import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
import { tv } from '../utils/tv'
import type { DynamicSlots } from '../types/utils'
const appConfig = _appConfig as AppConfig & { ui: { stepper: Partial<typeof theme> } }
const appConfigStepper = _appConfig as AppConfig & { ui: { stepper: Partial<typeof theme> } }
const stepper = tv({ extend: tv(theme), ...(appConfig.ui?.stepper || {}) })
const stepper = tv({ extend: tv(theme), ...(appConfigStepper.ui?.stepper || {}) })
type StepperVariants = VariantProps<typeof stepper>

View File

@@ -8,9 +8,9 @@ import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
import { tv } from '../utils/tv'
import type { PartialString } from '../types/utils'
const appConfig = _appConfig as AppConfig & { ui: { switch: Partial<typeof theme> } }
const appConfigSwitch = _appConfig as AppConfig & { ui: { switch: Partial<typeof theme> } }
const switchTv = tv({ extend: tv(theme), ...(appConfig.ui?.switch || {}) })
const switchTv = tv({ extend: tv(theme), ...(appConfigSwitch.ui?.switch || {}) })
type SwitchVariants = VariantProps<typeof switchTv>
@@ -68,7 +68,7 @@ const modelValue = defineModel<boolean>({ default: undefined })
const appConfig = useAppConfig()
const rootProps = useForwardProps(reactivePick(props, 'required', 'value', 'defaultValue'))
const { id: _id, emitFormChange, emitFormInput, size, color, name, disabled } = useFormField<SwitchProps>(props)
const { id: _id, emitFormChange, emitFormInput, size, color, name, disabled, ariaAttrs } = useFormField<SwitchProps>(props)
const id = _id.value ?? useId()
const ui = computed(() => switchTv({
@@ -93,7 +93,7 @@ function onUpdate(value: any) {
<div :class="ui.container({ class: props.ui?.container })">
<SwitchRoot
:id="id"
v-bind="rootProps"
v-bind="{ ...rootProps, ...ariaAttrs }"
v-model="modelValue"
:name="name"
:disabled="disabled || loading"

View File

@@ -38,9 +38,9 @@ declare module '@tanstack/table-core' {
}
}
const appConfig = _appConfig as AppConfig & { ui: { table: Partial<typeof theme> } }
const appConfigTable = _appConfig as AppConfig & { ui: { table: Partial<typeof theme> } }
const table = tv({ extend: tv(theme), ...(appConfig.ui?.table || {}) })
const table = tv({ extend: tv(theme), ...(appConfigTable.ui?.table || {}) })
type TableVariants = VariantProps<typeof table>

View File

@@ -9,9 +9,9 @@ import { tv } from '../utils/tv'
import type { AvatarProps } from '../types'
import type { DynamicSlots, PartialString } from '../types/utils'
const appConfig = _appConfig as AppConfig & { ui: { tabs: Partial<typeof theme> } }
const appConfigTabs = _appConfig as AppConfig & { ui: { tabs: Partial<typeof theme> } }
const tabs = tv({ extend: tv(theme), ...(appConfig.ui?.tabs || {}) })
const tabs = tv({ extend: tv(theme), ...(appConfigTabs.ui?.tabs || {}) })
export interface TabsItem {
label?: string

View File

@@ -5,9 +5,9 @@ import _appConfig from '#build/app.config'
import theme from '#build/ui/textarea'
import { tv } from '../utils/tv'
const appConfig = _appConfig as AppConfig & { ui: { textarea: Partial<typeof theme> } }
const appConfigTextarea = _appConfig as AppConfig & { ui: { textarea: Partial<typeof theme> } }
const textarea = tv({ extend: tv(theme), ...(appConfig.ui?.textarea || {}) })
const textarea = tv({ extend: tv(theme), ...(appConfigTextarea.ui?.textarea || {}) })
type TextareaVariants = VariantProps<typeof textarea>
@@ -66,7 +66,7 @@ const emits = defineEmits<TextareaEmits>()
const [modelValue, modelModifiers] = defineModel<string | number>()
const { emitFormBlur, emitFormInput, emitFormChange, size, color, id, name, highlight, disabled } = useFormField<TextareaProps>(props, { deferInputValidation: true })
const { emitFormFocus, emitFormBlur, emitFormInput, emitFormChange, size, color, id, name, highlight, disabled, ariaAttrs } = useFormField<TextareaProps>(props, { deferInputValidation: true })
const ui = computed(() => textarea({
color: color.value,
@@ -185,10 +185,11 @@ onMounted(() => {
:class="ui.base({ class: props.ui?.base })"
:disabled="disabled"
:required="required"
v-bind="$attrs"
v-bind="{ ...$attrs, ...ariaAttrs }"
@input="onInput"
@blur="onBlur"
@change="onChange"
@focus="emitFormFocus"
/>
<slot />

View File

@@ -8,9 +8,9 @@ import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
import { tv } from '../utils/tv'
import type { AvatarProps, ButtonProps } from '../types'
const appConfig = _appConfig as AppConfig & { ui: { toast: Partial<typeof theme> } }
const appConfigToast = _appConfig as AppConfig & { ui: { toast: Partial<typeof theme> } }
const toast = tv({ extend: tv(theme), ...(appConfig.ui?.toast || {}) })
const toast = tv({ extend: tv(theme), ...(appConfigToast.ui?.toast || {}) })
type ToastVariants = VariantProps<typeof toast>

View File

@@ -7,9 +7,9 @@ import theme from '#build/ui/toaster'
import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
import { tv } from '../utils/tv'
const appConfig = _appConfig as AppConfig & { ui: { toaster: Partial<typeof theme> } }
const appConfigToaster = _appConfig as AppConfig & { ui: { toaster: Partial<typeof theme> } }
const toaster = tv({ extend: tv(theme), ...(appConfig.ui?.toaster || {}) })
const toaster = tv({ extend: tv(theme), ...(appConfigToaster.ui?.toaster || {}) })
type ToasterVariants = VariantProps<typeof toaster>

View File

@@ -7,9 +7,9 @@ import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
import { tv } from '../utils/tv'
import type { KbdProps } from '../types'
const appConfig = _appConfig as AppConfig & { ui: { tooltip: Partial<typeof theme> } }
const appConfigTooltip = _appConfig as AppConfig & { ui: { tooltip: Partial<typeof theme> } }
const tooltip = tv({ extend: tv(theme), ...(appConfig.ui?.tooltip || {}) })
const tooltip = tv({ extend: tv(theme), ...(appConfigTooltip.ui?.tooltip || {}) })
export interface TooltipProps extends TooltipRootProps {
/** The text content of the tooltip. */

View File

@@ -6,7 +6,7 @@ import type { GetObjectField } from '../types/utils'
export const buttonGroupInjectionKey: InjectionKey<ComputedRef<{
size: ButtonGroupProps['size']
orientation: ButtonGroupProps['orientation']
}> | undefined> = Symbol('nuxt-ui.button-group')
}>> = Symbol('nuxt-ui.button-group')
type Props<T> = {
size?: GetObjectField<T, 'size'>
@@ -14,7 +14,6 @@ type Props<T> = {
export function useButtonGroup<T>(props: Props<T>) {
const buttonGroup = inject(buttonGroupInjectionKey, undefined)
return {
orientation: computed(() => buttonGroup?.value.orientation),
size: computed(() => props?.size ?? buttonGroup?.value.size)

View File

@@ -1,4 +1,4 @@
import { inject, ref, computed, type InjectionKey, type Ref, type ComputedRef } from 'vue'
import { inject, computed, type InjectionKey, type Ref, type ComputedRef } from 'vue'
import { type UseEventBusReturn, useDebounceFn } from '@vueuse/core'
import type { FormFieldProps } from '../types'
import type { FormEvent, FormInputEvents, FormFieldInjectedOptions, FormInjectedOptions } from '../types/form'
@@ -9,13 +9,12 @@ type Props<T> = {
name?: string
size?: GetObjectField<T, 'size'>
color?: GetObjectField<T, 'color'>
legend?: string
highlight?: boolean
disabled?: boolean
}
export const formOptionsInjectionKey: InjectionKey<ComputedRef<FormInjectedOptions>> = Symbol('nuxt-ui.form-options')
export const formBusInjectionKey: InjectionKey<UseEventBusReturn<FormEvent, string>> = Symbol('nuxt-ui.form-events')
export const formBusInjectionKey: InjectionKey<UseEventBusReturn<FormEvent<any>, string>> = Symbol('nuxt-ui.form-events')
export const formFieldInjectionKey: InjectionKey<ComputedRef<FormFieldInjectedOptions<FormFieldProps>>> = Symbol('nuxt-ui.form-field')
export const inputIdInjectionKey: InjectionKey<Ref<string | undefined>> = Symbol('nuxt-ui.input-id')
export const formInputsInjectionKey: InjectionKey<Ref<Record<string, { id?: string, pattern?: RegExp }>>> = Symbol('nuxt-ui.form-inputs')
@@ -29,41 +28,40 @@ export function useFormField<T>(props?: Props<T>, opts?: { bind?: boolean, defer
const inputId = inject(inputIdInjectionKey, undefined)
if (formField && inputId) {
if (opts?.bind === false || props?.legend) {
if (opts?.bind === false) {
// Removes for="..." attribute on label for RadioGroup and alike.
inputId.value = undefined
} else if (props?.id) {
// Updates for="..." attribute on label if props.id is provided.
inputId.value = props?.id
}
if (formInputs && formField.value.name && inputId.value) {
formInputs.value[formField.value.name] = { id: inputId.value, pattern: formField.value.errorPattern }
}
}
const touched = ref(false)
function emitFormEvent(type: FormInputEvents, name?: string) {
function emitFormEvent(type: FormInputEvents, name?: string, eager?: boolean) {
if (formBus && formField && name) {
formBus.emit({ type, name })
formBus.emit({ type, name, eager })
}
}
function emitFormBlur() {
touched.value = true
emitFormEvent('blur', formField?.value.name)
}
function emitFormFocus() {
emitFormEvent('focus', formField?.value.name)
}
function emitFormChange() {
touched.value = true
emitFormEvent('change', formField?.value.name)
}
const emitFormInput = useDebounceFn(
() => {
if (!opts?.deferInputValidation || touched.value || formField?.value.eagerValidation) {
emitFormEvent('input', formField?.value.name)
}
emitFormEvent('input', formField?.value.name, !opts?.deferInputValidation || formField?.value.eagerValidation)
},
formField?.value.validateOnInputDelay ?? formOptions?.value.validateOnInputDelay ?? 0
)
@@ -77,6 +75,19 @@ export function useFormField<T>(props?: Props<T>, opts?: { bind?: boolean, defer
disabled: computed(() => formOptions?.value.disabled || props?.disabled),
emitFormBlur,
emitFormInput,
emitFormChange
emitFormChange,
emitFormFocus,
ariaAttrs: computed(() => {
if (!formField?.value) return
const descriptiveAttrs = ['error' as const, 'hint' as const, 'description' as const]
.filter(type => formField?.value?.[type])
.map(type => `${formField?.value.ariaId}-${type}`) || []
return {
'aria-describedby': descriptiveAttrs.join(' '),
'aria-invalid': !!formField?.value.error
}
})
}
}

View File

@@ -1,3 +1,4 @@
import { ref, nextTick } from 'vue'
import { useState } from '#imports'
import type { ToastProps } from '../types'
@@ -8,20 +9,40 @@ export interface Toast extends Omit<ToastProps, 'defaultOpen'> {
export function useToast() {
const toasts = useState<Toast[]>('toasts', () => [])
const maxToasts = 5
const running = ref(false)
const queue: Toast[] = []
function add(toast: Partial<Toast>): Toast {
const generateId = () => `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
async function processQueue() {
if (running.value || queue.length === 0) {
return
}
running.value = true
while (queue.length > 0) {
const toast = queue.shift()!
await nextTick()
toasts.value = [...toasts.value, toast].slice(-maxToasts)
}
running.value = false
}
async function add(toast: Partial<Toast>): Promise<Toast> {
const body = {
id: new Date().getTime().toString(),
id: generateId(),
open: true,
...toast
}
const index = toasts.value.findIndex((t: Toast) => t.id === body.id)
if (index === -1) {
toasts.value.push(body)
}
queue.push(body)
toasts.value = toasts.value.slice(-5)
await processQueue()
return body
}

54
src/runtime/locale/he.ts Normal file
View File

@@ -0,0 +1,54 @@
import { defineLocale } from '../composables/defineLocale'
export default defineLocale({
name: 'Hebrew',
code: 'he',
dir: 'rtl',
messages: {
inputMenu: {
noMatch: 'אין התאמה',
noData: 'אין נתונים',
create: 'צור "{label}"'
},
calendar: {
prevYear: 'שנה קודמת',
nextYear: 'שנה הבאה',
prevMonth: 'חודש קודם',
nextMonth: 'חודש הבא'
},
inputNumber: {
increment: 'הוסף',
decrement: 'הפחת'
},
commandPalette: {
placeholder: 'הקלד פקודה...',
noMatch: 'לא נמצאה התאמה',
noData: 'אין נתונים זמינים',
close: 'סגור'
},
selectMenu: {
noMatch: 'לא נמצאה התאמה',
noData: 'אין נתונים',
create: 'צור "{label}"',
search: 'חפש...'
},
toast: { close: 'סגור' },
carousel: {
prev: 'הקודם',
next: 'הבא',
goto: 'מעבר ל {slide}'
},
modal: {
close: 'סגור'
},
slideover: {
close: 'סגור'
},
alert: {
close: 'סגור'
},
table: {
noData: 'אין נתונים להצגה'
}
}
})

55
src/runtime/locale/hi.ts Normal file
View File

@@ -0,0 +1,55 @@
import { defineLocale } from '../composables/defineLocale'
export default defineLocale({
name: 'Hindi',
code: 'hi',
messages: {
inputMenu: {
noMatch: 'कोई मेल खाता डेटा नहीं',
noData: 'कोई डेटा नहीं',
create: '"{label}" बनाएँ'
},
calendar: {
prevYear: 'पिछला वर्ष',
nextYear: 'अगला वर्ष',
prevMonth: 'पिछला महीना',
nextMonth: 'अगला महीना'
},
inputNumber: {
increment: 'बढ़ाना',
decrement: 'घटाना'
},
commandPalette: {
placeholder: 'एक आदेश या खोज टाइप करें...',
noMatch: 'कोई मेल खाता डेटा नहीं',
noData: 'कोई डेटा नहीं',
close: 'बंद करें'
},
selectMenu: {
noMatch: 'कोई मेल खाता डेटा नहीं',
noData: 'कोई डेटा नहीं',
create: '"{label}" बनाएँ',
search: 'खोजें...'
},
toast: {
close: 'बंद करें'
},
carousel: {
prev: 'पिछला',
next: 'अगला',
goto: 'स्लाइड {slide} पर जाएँ'
},
modal: {
close: 'बंद करें'
},
slideover: {
close: 'बंद करें'
},
alert: {
close: 'बंद करें'
},
table: {
noData: 'कोई डेटा नहीं'
}
}
})

55
src/runtime/locale/hu.ts Normal file
View File

@@ -0,0 +1,55 @@
import { defineLocale } from '../composables/defineLocale'
export default defineLocale({
name: 'Magyar',
code: 'hu',
messages: {
inputMenu: {
noMatch: 'Nincs találat',
noData: 'Nincs adat',
create: '"{label}" létrehozása'
},
calendar: {
prevYear: 'Előző év',
nextYear: 'Következő év',
prevMonth: 'Előző hónap',
nextMonth: 'Következő hónap'
},
inputNumber: {
increment: 'Növel',
decrement: 'Csökkent'
},
commandPalette: {
placeholder: 'Írjon be egy parancsot vagy keressen...',
noMatch: 'Nincs találat',
noData: 'Nincs adat',
close: 'Bezárás'
},
selectMenu: {
noMatch: 'Nincs találat',
noData: 'Nincs adat',
create: '"{label}" létrehozása',
search: 'Keresés...'
},
toast: {
close: 'Bezárás'
},
carousel: {
prev: 'Előző',
next: 'Következő',
goto: 'Ugrás ide {slide}'
},
modal: {
close: 'Bezárás'
},
slideover: {
close: 'Bezárás'
},
alert: {
close: 'Bezárás'
},
table: {
noData: 'Nincs adat'
}
}
})

View File

@@ -9,6 +9,7 @@ export { default as es } from './es'
export { default as fa_ir } from './fa_ir'
export { default as fi } from './fi'
export { default as fr } from './fr'
export { default as hi } from './hi'
export { default as id } from './id'
export { default as it } from './it'
export { default as ja } from './ja'
@@ -25,5 +26,6 @@ export { default as th } from './th'
export { default as tr } from './tr'
export { default as uk } from './uk'
export { default as vi } from './vi'
export { default as zh_hans } from './zh_hans'
export { default as zh_hant } from './zh_hant'
export { default as zh_cn } from './zh_cn'
export { default as zh_tw } from './zh_tw'
export { default as he } from './he'

View File

@@ -2,7 +2,7 @@ import { defineLocale } from '../composables/defineLocale'
export default defineLocale({
name: '简体中文',
code: 'zh-Hans',
code: 'zh-CN',
messages: {
inputMenu: {
noMatch: '没有匹配的数据',

View File

@@ -2,7 +2,7 @@ import { defineLocale } from '../composables/defineLocale'
export default defineLocale({
name: '繁體中文',
code: 'zh-Hant',
code: 'zh-TW',
messages: {
inputMenu: {
noMatch: '沒有相符的資料',

View File

@@ -1,5 +1,5 @@
import type { StandardSchemaV1 } from '@standard-schema/spec'
import type { ComputedRef, Ref } from 'vue'
import type { ComputedRef, DeepReadonly, Ref } from 'vue'
import type { ZodSchema } from 'zod'
import type { Schema as JoiSchema } from 'joi'
import type { ObjectSchema as YupObjectSchema } from 'yup'
@@ -7,17 +7,22 @@ import type { GenericSchema as ValibotSchema, GenericSchemaAsync as ValibotSchem
import type { GetObjectField } from './utils'
import type { Struct as SuperstructSchema } from 'superstruct'
export interface Form<T> {
validate (opts?: { name?: string | string[], silent?: boolean, nested?: boolean, transform?: boolean }): Promise<T | false>
export interface Form<T extends object> {
validate (opts?: { name?: keyof T | (keyof T)[], silent?: boolean, nested?: boolean, transform?: boolean }): Promise<T | false>
clear (path?: string): void
errors: Ref<FormError[]>
setErrors (errs: FormError[], path?: string): void
getErrors (path?: string): FormError[]
setErrors (errs: FormError[], name?: keyof T): void
getErrors (name?: keyof T): FormError[]
submit (): Promise<void>
disabled: ComputedRef<boolean>
dirty: ComputedRef<boolean>
dirtyFields: DeepReadonly<Set<keyof T>>
touchedFields: DeepReadonly<Set<keyof T>>
blurredFields: DeepReadonly<Set<keyof T>>
}
export type FormSchema<T extends Record<string, any>> =
export type FormSchema<T extends object> =
| ZodSchema
| YupObjectSchema<T>
| ValibotSchema
@@ -28,7 +33,7 @@ export type FormSchema<T extends Record<string, any>> =
| SuperstructSchema<any, any>
| StandardSchemaV1
export type FormInputEvents = 'input' | 'blur' | 'change'
export type FormInputEvents = 'input' | 'blur' | 'change' | 'focus'
export interface FormError<P extends string = string> {
name: P
@@ -61,13 +66,14 @@ export type FormChildDetachEvent = {
formId: string | number
}
export type FormInputEvent = {
export type FormInputEvent<T extends object> = {
type: FormEventType
name?: string
name: keyof T
eager?: boolean
}
export type FormEvent =
| FormInputEvent
export type FormEvent<T extends object> =
| FormInputEvent<T>
| FormChildAttachEvent
| FormChildDetachEvent
@@ -83,6 +89,9 @@ export interface FormFieldInjectedOptions<T> {
eagerValidation?: boolean
validateOnInputDelay?: number
errorPattern?: RegExp
hint?: string
description?: string
ariaId: string
}
export interface ValidateReturnSchema<T> {

View File

@@ -47,9 +47,7 @@ export async function validateStandardSchema(
state: any,
schema: StandardSchemaV1
): Promise<ValidateReturnSchema<typeof state>> {
const result = await schema['~standard'].validate({
value: state
})
const result = await schema['~standard'].validate(state)
if (result.issues) {
return {
@@ -197,14 +195,14 @@ export function validateSchema<T extends object>(state: T, schema: FormSchema<T>
return validateZodSchema(state, schema)
} else if (isJoiSchema(schema)) {
return validateJoiSchema(state, schema)
} else if (isStandardSchema(schema)) {
return validateStandardSchema(state, schema)
} else if (isValibotSchema(schema)) {
return validateValibotSchema(state, schema)
} else if (isYupSchema(schema)) {
return validateYupSchema(state, schema)
} else if (isSuperStructSchema(schema)) {
return validateSuperstructSchema(state, schema)
} else if (isStandardSchema(schema)) {
return validateStandardSchema(state, schema)
} else {
throw new Error('Form validation failed: Unsupported form schema')
}

View File

@@ -2,6 +2,6 @@ import { createTV, type defaultConfig } from 'tailwind-variants'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
const appConfig = _appConfig as AppConfig & { ui: { tv: typeof defaultConfig } }
const appConfigTv = _appConfig as AppConfig & { ui: { tv: typeof defaultConfig } }
export const tv = createTV(appConfig.ui?.tv)
export const tv = createTV(appConfigTv.ui?.tv)

View File

@@ -52,9 +52,9 @@ interface NuxtLinkProps extends Omit<RouterLinkProps, 'to'> {
noPrefetch?: boolean
}
const appConfig = _appConfig as AppConfig & { ui: { link: Partial<typeof theme> } }
const appConfigLink = _appConfig as AppConfig & { ui: { link: Partial<typeof theme> } }
const link = tv({ extend: tv(theme), ...(appConfig.ui?.link || {}) })
const link = tv({ extend: tv(theme), ...(appConfigLink.ui?.link || {}) })
export interface LinkProps extends NuxtLinkProps {
/**

View File

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

View File

@@ -25,9 +25,9 @@ export default (options: Required<ModuleOptions>) => ({
childLinkLabelExternalIcon: 'inline-block size-3 align-top text-[var(--ui-text-dimmed)]',
childLinkDescription: 'text-sm text-[var(--ui-text-muted)]',
separator: 'px-2 h-px bg-[var(--ui-border)]',
viewportWrapper: 'absolute top-full left-0 flex w-full justify-center',
viewport: 'relative overflow-hidden bg-[var(--ui-bg)] shadow-lg rounded-[calc(var(--ui-radius)*1.5)] ring ring-[var(--ui-border)] h-[var(--reka-navigation-menu-viewport-height)] w-full transition-[width,height] origin-[top_center] data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in]',
content: 'absolute top-0 left-0 w-full data-[motion=from-start]:animate-[enter-from-left_200ms_ease] data-[motion=from-end]:animate-[enter-from-right_200ms_ease] data-[motion=to-start]:animate-[exit-to-left_200ms_ease] data-[motion=to-end]:animate-[exit-to-right_200ms_ease]',
viewportWrapper: 'absolute top-full left-0 flex w-full',
viewport: 'relative overflow-hidden bg-[var(--ui-bg)] shadow-lg rounded-[calc(var(--ui-radius)*1.5)] ring ring-[var(--ui-border)] h-[var(--reka-navigation-menu-viewport-height)] w-full transition-[width,height,left] duration-200 origin-[top_center] data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in]',
content: 'absolute top-0 left-0 w-full',
indicator: 'absolute data-[state=visible]:animate-[fade-in_100ms_ease-out] data-[state=hidden]:animate-[fade-out_100ms_ease-in] data-[state=hidden]:opacity-0 bottom-0 z-[1] w-[var(--reka-navigation-menu-indicator-size)] translate-x-[var(--reka-navigation-menu-indicator-position)] flex h-2.5 items-end justify-center overflow-hidden transition-[translate,width] duration-200',
arrow: 'relative top-[50%] size-2.5 rotate-45 border border-[var(--ui-border)] bg-[var(--ui-bg)] z-[1] rounded-[calc(var(--ui-radius)/2)]'
},
@@ -56,13 +56,24 @@ export default (options: Required<ModuleOptions>) => ({
list: 'flex items-center',
item: 'py-2',
link: 'px-2.5 py-1.5 before:inset-x-px before:inset-y-0',
childList: 'grid grid-cols-2 gap-2 p-2'
childList: 'grid p-2'
},
vertical: {
root: 'flex-col',
link: 'flex-row px-2.5 py-1.5 before:inset-y-px before:inset-x-0'
}
},
contentOrientation: {
horizontal: {
viewport: '',
viewportWrapper: 'justify-center',
content: 'data-[motion=from-start]:animate-[enter-from-left_200ms_ease] data-[motion=from-end]:animate-[enter-from-right_200ms_ease] data-[motion=to-start]:animate-[exit-to-left_200ms_ease] data-[motion=to-end]:animate-[exit-to-right_200ms_ease]'
},
vertical: {
viewport: 'sm:w-[var(--reka-navigation-menu-viewport-width)] left-[var(--reka-navigation-menu-viewport-left)]',
content: ''
}
},
active: {
true: {
childLink: 'bg-[var(--ui-bg-elevated)] text-[var(--ui-text-highlighted)]',
@@ -91,6 +102,19 @@ export default (options: Required<ModuleOptions>) => ({
}
},
compoundVariants: [{
orientation: 'horizontal',
contentOrientation: 'horizontal',
class: {
childList: 'grid-cols-2 gap-2'
}
}, {
orientation: 'horizontal',
contentOrientation: 'vertical',
class: {
childList: 'gap-1',
content: 'w-60'
}
}, {
orientation: 'horizontal',
highlight: true,
class: {

View File

@@ -278,6 +278,39 @@ describe('Form', () => {
{ id: 'passwordInput', name: 'password', message: 'Required' }
])
})
test('touchedFields works', async () => {
const emailInput = wrapper.find('#emailInput')
emailInput.trigger('focus')
await flushPromises()
expect(form.value.touchedFields.has('email')).toBe(true)
expect(form.value.touchedFields.has('password')).toBe(false)
})
test('touchedFields works', async () => {
const emailInput = wrapper.find('#emailInput')
emailInput.trigger('change')
await flushPromises()
expect(form.value.dirtyFields.has('email')).toBe(true)
expect(form.value.touchedFields.has('email')).toBe(true)
expect(form.value.dirtyFields.has('password')).toBe(false)
expect(form.value.touchedFields.has('password')).toBe(false)
})
test('blurredFields works', async () => {
const emailInput = wrapper.find('#emailInput')
emailInput.trigger('blur')
await flushPromises()
expect(form.value.blurredFields.has('email')).toBe(true)
expect(form.value.blurredFields.has('password')).toBe(false)
})
})
describe('nested', async () => {
@@ -444,6 +477,7 @@ describe('Form', () => {
}
)
})
test('form field errorPattern works', async () => {
const wrapper = await mountSuspended({
components: {

View File

@@ -1,16 +1,58 @@
import { defineComponent } from 'vue'
import { describe, it, expect } from 'vitest'
import FormField, { type FormFieldProps, type FormFieldSlots } from '../../src/runtime/components/FormField.vue'
import { describe, it, expect, test, vi } from 'vitest'
import type { FormFieldProps, FormFieldSlots } from '../../src/runtime/components/FormField.vue'
import ComponentRender from '../component-render'
import theme from '#build/ui/form-field'
import { mountSuspended } from '@nuxt/test-utils/runtime'
import {
UInput,
URadioGroup,
UTextarea,
UCheckbox,
USelect,
USelectMenu,
UInputMenu,
UInputNumber,
USwitch,
USlider,
UPinInput,
UFormField
} from '#components'
const inputComponents = [UInput, URadioGroup, UTextarea, UCheckbox, USelect, USelectMenu, UInputMenu, UInputNumber, USwitch, USlider, UPinInput]
async function renderFormField(options: {
props: Partial<FormFieldProps>
inputComponent: typeof inputComponents[number]
}) {
return await mountSuspended(UFormField, {
props: options.props,
slots: {
default: {
// @ts-expect-error - Object literal may only specify known properties, and setup does not exist in type
setup: () => ({ inputComponent: options.inputComponent }),
components: {
UFormField,
...inputComponents
},
template: `
<component :is="inputComponent" />
`
}
}
})
}
// A wrapper component is needed here because of a conflict with the error prop / expose.
// See: https://github.com/nuxt/test-utils/issues/684
const FormFieldWrapper = defineComponent({
components: {
UFormField: FormField
UFormField
},
template: `<UFormField>
template: `
<UFormField>
<template v-for="(_, name) in $slots" #[name]="slotData">
<slot :name="name" v-bind="slotData" />
</template>
@@ -42,4 +84,80 @@ describe('FormField', () => {
const html = await ComponentRender(nameOrHtml, options, FormFieldWrapper)
expect(html).toMatchSnapshot()
})
describe.each(inputComponents.map(inputComponent => [(inputComponent as any).__name, inputComponent]))('%s integration', async (name: string, inputComponent: any) => {
// Mock useId to force a consistent return value in Nuxt and Vue. This is required to test aria attributes.
vi.mock('vue', async () => {
const actual = await vi.importActual('vue')
return {
...actual,
useId: () => 'v-0-0' // Static value matching Nuxt's format
}
})
if (name === 'RadioGroup') {
test('unbinds label for', async () => {
const wrapper = await renderFormField({
props: { label: 'Label' },
inputComponent
})
const label = wrapper.find('label[for=v-0-0]')
expect(label.exists()).toBe(false)
})
} else {
test('binds label for', async () => {
const wrapper = await renderFormField({
props: { label: 'Label' },
inputComponent
})
const label = wrapper.find('label[for=v-0-0]')
expect(label.exists()).toBe(true)
const input = wrapper.find('[id=v-0-0]')
expect(input.exists()).toBe(true)
})
}
test('binds hints with aria-describedby', async () => {
const wrapper = await renderFormField({
props: { hint: 'somehint' },
inputComponent
})
const attr = wrapper.find('[aria-describedby=v-0-0-hint]')
expect(attr.exists()).toBe(true)
})
test('binds description with aria-describedby', async () => {
const wrapper = await renderFormField({
props: { description: 'somedescription' },
inputComponent
})
const attr = wrapper.find('[aria-describedby=v-0-0-description]')
expect(attr.exists()).toBe(true)
})
test('binds error with aria-describedby', async () => {
const wrapper = await renderFormField({
props: { error: 'someerror' },
inputComponent
})
const attr = wrapper.find('[aria-describedby=v-0-0-error]')
expect(attr.exists()).toBe(true)
})
test('binds aria-invalid on error', async () => {
const wrapper = await renderFormField({
props: { error: 'someerror' },
inputComponent
})
const attr = wrapper.find('[aria-invalid=true]')
expect(attr.exists()).toBe(true)
})
})
})

View File

@@ -84,9 +84,11 @@ describe('NavigationMenu', () => {
it.each([
// Props
['with items', { props }],
['with modelValue', { props: { ...props, modelValue: '0' } }],
['with labelKey', { props: { ...props, labelKey: 'icon' } }],
['with arrow', { props: { ...props, arrow: true } }],
['with orientation vertical', { props: { ...props, orientation: 'vertical' as const } }],
['with arrow', { props: { ...props, arrow: true, modelValue: '0' } }],
['with orientation vertical', { props: { ...props, orientation: 'vertical' as const, modelValue: '0' } }],
['with content orientation vertical', { props: { ...props, contentOrientation: 'vertical' as const, modelValue: '0' } }],
...variants.map((variant: string) => [`with primary variant ${variant}`, { props: { ...props, variant } }]),
...variants.map((variant: string) => [`with neutral variant ${variant}`, { props: { ...props, variant, color: 'neutral' } }]),
...variants.map((variant: string) => [`with primary variant ${variant} highlight`, { props: { ...props, variant, highlight: true } }]),

View File

@@ -13,7 +13,7 @@ exports[`Alert > renders with as correctly 1`] = `
`;
exports[`Alert > renders with avatar correctly 1`] = `
"<div class="relative overflow-hidden w-full rounded-[calc(var(--ui-radius)*2)] p-4 flex gap-2.5 items-center bg-[var(--ui-primary)] text-[var(--ui-bg)]"><span class="inline-flex items-center justify-center select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-11 text-[22px] shrink-0"><img role="img" src="https://github.com/benjamincanac.png" width="44" height="44" class="h-full w-full rounded-[inherit] object-cover" style="display: none;"><span class="font-medium leading-none text-[var(--ui-text-muted)] truncate"> </span></span>
"<div class="relative overflow-hidden w-full rounded-[calc(var(--ui-radius)*2)] p-4 flex gap-2.5 items-center bg-[var(--ui-primary)] text-[var(--ui-bg)]"><span class="inline-flex items-center justify-center select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-11 text-[22px] shrink-0"><img role="img" src="https://github.com/benjamincanac.png" width="44" height="44" class="h-full w-full rounded-[inherit] object-cover data-[error]:hidden" style="display: none;"><span class="font-medium leading-none text-[var(--ui-text-muted)] truncate"> </span></span>
<div class="min-w-0 flex-1 flex flex-col gap-1">
<div class="text-sm font-medium">Alert</div>
<!--v-if-->

View File

@@ -13,7 +13,7 @@ exports[`Alert > renders with as correctly 1`] = `
`;
exports[`Alert > renders with avatar correctly 1`] = `
"<div class="relative overflow-hidden w-full rounded-[calc(var(--ui-radius)*2)] p-4 flex gap-2.5 items-center bg-[var(--ui-primary)] text-[var(--ui-bg)]"><span class="inline-flex items-center justify-center select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-11 text-[22px] shrink-0"><img role="img" src="https://github.com/benjamincanac.png" width="44" height="44" class="h-full w-full rounded-[inherit] object-cover" style="display: none;"><span class="font-medium leading-none text-[var(--ui-text-muted)] truncate"> </span></span>
"<div class="relative overflow-hidden w-full rounded-[calc(var(--ui-radius)*2)] p-4 flex gap-2.5 items-center bg-[var(--ui-primary)] text-[var(--ui-bg)]"><span class="inline-flex items-center justify-center select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-11 text-[22px] shrink-0"><img role="img" src="https://github.com/benjamincanac.png" width="44" height="44" class="h-full w-full rounded-[inherit] object-cover data-[error]:hidden" style="display: none;"><span class="font-medium leading-none text-[var(--ui-text-muted)] truncate"> </span></span>
<div class="min-w-0 flex-1 flex flex-col gap-1">
<div class="text-sm font-medium">Alert</div>
<!--v-if-->

View File

@@ -14,25 +14,25 @@ exports[`Avatar > renders with default slot correctly 1`] = `"<span class="inlin
exports[`Avatar > renders with icon correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-8 text-base"><!--v-if--><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="text-[var(--ui-text-muted)] shrink-0" width="1em" height="1em" viewBox="0 0 16 16"></svg></span>"`;
exports[`Avatar > renders with size 2xl correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-11 text-[22px]"><img role="img" src="https://github.com/benjamincanac.png" width="44" height="44" class="h-full w-full rounded-[inherit] object-cover" style="display: none;"><span class="font-medium leading-none text-[var(--ui-text-muted)] truncate"> </span></span>"`;
exports[`Avatar > renders with size 2xl correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-11 text-[22px]"><img role="img" src="https://github.com/benjamincanac.png" width="44" height="44" class="h-full w-full rounded-[inherit] object-cover data-[error]:hidden" style="display: none;"><span class="font-medium leading-none text-[var(--ui-text-muted)] truncate"> </span></span>"`;
exports[`Avatar > renders with size 2xs correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-5 text-[10px]"><img role="img" src="https://github.com/benjamincanac.png" width="20" height="20" class="h-full w-full rounded-[inherit] object-cover" style="display: none;"><span class="font-medium leading-none text-[var(--ui-text-muted)] truncate"> </span></span>"`;
exports[`Avatar > renders with size 2xs correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-5 text-[10px]"><img role="img" src="https://github.com/benjamincanac.png" width="20" height="20" class="h-full w-full rounded-[inherit] object-cover data-[error]:hidden" style="display: none;"><span class="font-medium leading-none text-[var(--ui-text-muted)] truncate"> </span></span>"`;
exports[`Avatar > renders with size 3xl correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-12 text-2xl"><img role="img" src="https://github.com/benjamincanac.png" width="48" height="48" class="h-full w-full rounded-[inherit] object-cover" style="display: none;"><span class="font-medium leading-none text-[var(--ui-text-muted)] truncate"> </span></span>"`;
exports[`Avatar > renders with size 3xl correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-12 text-2xl"><img role="img" src="https://github.com/benjamincanac.png" width="48" height="48" class="h-full w-full rounded-[inherit] object-cover data-[error]:hidden" style="display: none;"><span class="font-medium leading-none text-[var(--ui-text-muted)] truncate"> </span></span>"`;
exports[`Avatar > renders with size 3xs correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-4 text-[8px]"><img role="img" src="https://github.com/benjamincanac.png" width="16" height="16" class="h-full w-full rounded-[inherit] object-cover" style="display: none;"><span class="font-medium leading-none text-[var(--ui-text-muted)] truncate"> </span></span>"`;
exports[`Avatar > renders with size 3xs correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-4 text-[8px]"><img role="img" src="https://github.com/benjamincanac.png" width="16" height="16" class="h-full w-full rounded-[inherit] object-cover data-[error]:hidden" style="display: none;"><span class="font-medium leading-none text-[var(--ui-text-muted)] truncate"> </span></span>"`;
exports[`Avatar > renders with size lg correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-9 text-lg"><img role="img" src="https://github.com/benjamincanac.png" width="36" height="36" class="h-full w-full rounded-[inherit] object-cover" style="display: none;"><span class="font-medium leading-none text-[var(--ui-text-muted)] truncate"> </span></span>"`;
exports[`Avatar > renders with size lg correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-9 text-lg"><img role="img" src="https://github.com/benjamincanac.png" width="36" height="36" class="h-full w-full rounded-[inherit] object-cover data-[error]:hidden" style="display: none;"><span class="font-medium leading-none text-[var(--ui-text-muted)] truncate"> </span></span>"`;
exports[`Avatar > renders with size md correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-8 text-base"><img role="img" src="https://github.com/benjamincanac.png" width="32" height="32" class="h-full w-full rounded-[inherit] object-cover" style="display: none;"><span class="font-medium leading-none text-[var(--ui-text-muted)] truncate"> </span></span>"`;
exports[`Avatar > renders with size md correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-8 text-base"><img role="img" src="https://github.com/benjamincanac.png" width="32" height="32" class="h-full w-full rounded-[inherit] object-cover data-[error]:hidden" style="display: none;"><span class="font-medium leading-none text-[var(--ui-text-muted)] truncate"> </span></span>"`;
exports[`Avatar > renders with size sm correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-7 text-sm"><img role="img" src="https://github.com/benjamincanac.png" width="28" height="28" class="h-full w-full rounded-[inherit] object-cover" style="display: none;"><span class="font-medium leading-none text-[var(--ui-text-muted)] truncate"> </span></span>"`;
exports[`Avatar > renders with size sm correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-7 text-sm"><img role="img" src="https://github.com/benjamincanac.png" width="28" height="28" class="h-full w-full rounded-[inherit] object-cover data-[error]:hidden" style="display: none;"><span class="font-medium leading-none text-[var(--ui-text-muted)] truncate"> </span></span>"`;
exports[`Avatar > renders with size xl correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-10 text-xl"><img role="img" src="https://github.com/benjamincanac.png" width="40" height="40" class="h-full w-full rounded-[inherit] object-cover" style="display: none;"><span class="font-medium leading-none text-[var(--ui-text-muted)] truncate"> </span></span>"`;
exports[`Avatar > renders with size xl correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-10 text-xl"><img role="img" src="https://github.com/benjamincanac.png" width="40" height="40" class="h-full w-full rounded-[inherit] object-cover data-[error]:hidden" style="display: none;"><span class="font-medium leading-none text-[var(--ui-text-muted)] truncate"> </span></span>"`;
exports[`Avatar > renders with size xs correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-6 text-xs"><img role="img" src="https://github.com/benjamincanac.png" width="24" height="24" class="h-full w-full rounded-[inherit] object-cover" style="display: none;"><span class="font-medium leading-none text-[var(--ui-text-muted)] truncate"> </span></span>"`;
exports[`Avatar > renders with size xs correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-6 text-xs"><img role="img" src="https://github.com/benjamincanac.png" width="24" height="24" class="h-full w-full rounded-[inherit] object-cover data-[error]:hidden" style="display: none;"><span class="font-medium leading-none text-[var(--ui-text-muted)] truncate"> </span></span>"`;
exports[`Avatar > renders with src correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-8 text-base"><img role="img" src="https://github.com/benjamincanac.png" width="32" height="32" class="h-full w-full rounded-[inherit] object-cover" style="display: none;"><span class="font-medium leading-none text-[var(--ui-text-muted)] truncate"> </span></span>"`;
exports[`Avatar > renders with src correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-8 text-base"><img role="img" src="https://github.com/benjamincanac.png" width="32" height="32" class="h-full w-full rounded-[inherit] object-cover data-[error]:hidden" style="display: none;"><span class="font-medium leading-none text-[var(--ui-text-muted)] truncate"> </span></span>"`;
exports[`Avatar > renders with text correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-8 text-base"><!--v-if--><span class="font-medium leading-none text-[var(--ui-text-muted)] truncate">+1</span></span>"`;

View File

@@ -14,25 +14,25 @@ exports[`Avatar > renders with default slot correctly 1`] = `"<span class="inlin
exports[`Avatar > renders with icon correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-8 text-base"><!--v-if--><span class="iconify i-lucide:image text-[var(--ui-text-muted)] shrink-0" aria-hidden="true"></span></span>"`;
exports[`Avatar > renders with size 2xl correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-11 text-[22px]"><img role="img" src="https://github.com/benjamincanac.png" width="44" height="44" class="h-full w-full rounded-[inherit] object-cover" style="display: none;"><span class="font-medium leading-none text-[var(--ui-text-muted)] truncate"> </span></span>"`;
exports[`Avatar > renders with size 2xl correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-11 text-[22px]"><img role="img" src="https://github.com/benjamincanac.png" width="44" height="44" class="h-full w-full rounded-[inherit] object-cover data-[error]:hidden" style="display: none;"><span class="font-medium leading-none text-[var(--ui-text-muted)] truncate"> </span></span>"`;
exports[`Avatar > renders with size 2xs correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-5 text-[10px]"><img role="img" src="https://github.com/benjamincanac.png" width="20" height="20" class="h-full w-full rounded-[inherit] object-cover" style="display: none;"><span class="font-medium leading-none text-[var(--ui-text-muted)] truncate"> </span></span>"`;
exports[`Avatar > renders with size 2xs correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-5 text-[10px]"><img role="img" src="https://github.com/benjamincanac.png" width="20" height="20" class="h-full w-full rounded-[inherit] object-cover data-[error]:hidden" style="display: none;"><span class="font-medium leading-none text-[var(--ui-text-muted)] truncate"> </span></span>"`;
exports[`Avatar > renders with size 3xl correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-12 text-2xl"><img role="img" src="https://github.com/benjamincanac.png" width="48" height="48" class="h-full w-full rounded-[inherit] object-cover" style="display: none;"><span class="font-medium leading-none text-[var(--ui-text-muted)] truncate"> </span></span>"`;
exports[`Avatar > renders with size 3xl correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-12 text-2xl"><img role="img" src="https://github.com/benjamincanac.png" width="48" height="48" class="h-full w-full rounded-[inherit] object-cover data-[error]:hidden" style="display: none;"><span class="font-medium leading-none text-[var(--ui-text-muted)] truncate"> </span></span>"`;
exports[`Avatar > renders with size 3xs correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-4 text-[8px]"><img role="img" src="https://github.com/benjamincanac.png" width="16" height="16" class="h-full w-full rounded-[inherit] object-cover" style="display: none;"><span class="font-medium leading-none text-[var(--ui-text-muted)] truncate"> </span></span>"`;
exports[`Avatar > renders with size 3xs correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-4 text-[8px]"><img role="img" src="https://github.com/benjamincanac.png" width="16" height="16" class="h-full w-full rounded-[inherit] object-cover data-[error]:hidden" style="display: none;"><span class="font-medium leading-none text-[var(--ui-text-muted)] truncate"> </span></span>"`;
exports[`Avatar > renders with size lg correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-9 text-lg"><img role="img" src="https://github.com/benjamincanac.png" width="36" height="36" class="h-full w-full rounded-[inherit] object-cover" style="display: none;"><span class="font-medium leading-none text-[var(--ui-text-muted)] truncate"> </span></span>"`;
exports[`Avatar > renders with size lg correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-9 text-lg"><img role="img" src="https://github.com/benjamincanac.png" width="36" height="36" class="h-full w-full rounded-[inherit] object-cover data-[error]:hidden" style="display: none;"><span class="font-medium leading-none text-[var(--ui-text-muted)] truncate"> </span></span>"`;
exports[`Avatar > renders with size md correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-8 text-base"><img role="img" src="https://github.com/benjamincanac.png" width="32" height="32" class="h-full w-full rounded-[inherit] object-cover" style="display: none;"><span class="font-medium leading-none text-[var(--ui-text-muted)] truncate"> </span></span>"`;
exports[`Avatar > renders with size md correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-8 text-base"><img role="img" src="https://github.com/benjamincanac.png" width="32" height="32" class="h-full w-full rounded-[inherit] object-cover data-[error]:hidden" style="display: none;"><span class="font-medium leading-none text-[var(--ui-text-muted)] truncate"> </span></span>"`;
exports[`Avatar > renders with size sm correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-7 text-sm"><img role="img" src="https://github.com/benjamincanac.png" width="28" height="28" class="h-full w-full rounded-[inherit] object-cover" style="display: none;"><span class="font-medium leading-none text-[var(--ui-text-muted)] truncate"> </span></span>"`;
exports[`Avatar > renders with size sm correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-7 text-sm"><img role="img" src="https://github.com/benjamincanac.png" width="28" height="28" class="h-full w-full rounded-[inherit] object-cover data-[error]:hidden" style="display: none;"><span class="font-medium leading-none text-[var(--ui-text-muted)] truncate"> </span></span>"`;
exports[`Avatar > renders with size xl correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-10 text-xl"><img role="img" src="https://github.com/benjamincanac.png" width="40" height="40" class="h-full w-full rounded-[inherit] object-cover" style="display: none;"><span class="font-medium leading-none text-[var(--ui-text-muted)] truncate"> </span></span>"`;
exports[`Avatar > renders with size xl correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-10 text-xl"><img role="img" src="https://github.com/benjamincanac.png" width="40" height="40" class="h-full w-full rounded-[inherit] object-cover data-[error]:hidden" style="display: none;"><span class="font-medium leading-none text-[var(--ui-text-muted)] truncate"> </span></span>"`;
exports[`Avatar > renders with size xs correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-6 text-xs"><img role="img" src="https://github.com/benjamincanac.png" width="24" height="24" class="h-full w-full rounded-[inherit] object-cover" style="display: none;"><span class="font-medium leading-none text-[var(--ui-text-muted)] truncate"> </span></span>"`;
exports[`Avatar > renders with size xs correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-6 text-xs"><img role="img" src="https://github.com/benjamincanac.png" width="24" height="24" class="h-full w-full rounded-[inherit] object-cover data-[error]:hidden" style="display: none;"><span class="font-medium leading-none text-[var(--ui-text-muted)] truncate"> </span></span>"`;
exports[`Avatar > renders with src correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-8 text-base"><img role="img" src="https://github.com/benjamincanac.png" width="32" height="32" class="h-full w-full rounded-[inherit] object-cover" style="display: none;"><span class="font-medium leading-none text-[var(--ui-text-muted)] truncate"> </span></span>"`;
exports[`Avatar > renders with src correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-8 text-base"><img role="img" src="https://github.com/benjamincanac.png" width="32" height="32" class="h-full w-full rounded-[inherit] object-cover data-[error]:hidden" style="display: none;"><span class="font-medium leading-none text-[var(--ui-text-muted)] truncate"> </span></span>"`;
exports[`Avatar > renders with text correctly 1`] = `"<span class="inline-flex items-center justify-center shrink-0 select-none overflow-hidden rounded-full align-middle bg-[var(--ui-bg-elevated)] size-8 text-base"><!--v-if--><span class="font-medium leading-none text-[var(--ui-text-muted)] truncate">+1</span></span>"`;

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