Compare commits

...

72 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
Hugo Richard
f0297e02d0 docs(modal/slideover): improve programmatic examples (#3131) 2025-01-17 17:43:40 +01:00
Chandara H. Wei
64421a190f feat(locale): add Khmer language (#3119)
Co-authored-by: Hugo Richard <hugo.richard@epitech.eu>
2025-01-17 16:46:01 +01:00
Benjamin Canac
3af77ccca1 cli: fix component template import 2025-01-17 13:08:59 +01:00
Benjamin Canac
a3a562b699 docs(app): improve module and framework reactivity 2025-01-17 00:01:06 +01:00
Benjamin Canac
3159a89436 docs(deps): update @nuxt/content 2025-01-16 23:52:33 +01:00
Benjamin Canac
ba1dd13173 fix(Button): wrong avatar size with block prop 2025-01-16 16:49:43 +01:00
Benjamin Canac
3fc2210e03 feat(NavigationMenu): add collapsed prop 2025-01-16 16:34:36 +01:00
Benjamin Canac
931211a634 fix(NavigationMenu): highlight open items on horizontal orientation only 2025-01-16 16:09:08 +01:00
Benjamin Canac
27fdc8e260 feat(NavigationMenu): handle label type in items
Resolves #2993
2025-01-16 15:56:14 +01:00
Benjamin Canac
1e88512bef chore(ContextMenu/DropdownMenu): remove useless pointer-events-auto
Revert parts of #2881
2025-01-16 12:38:34 +01:00
Benjamin Canac
533ccec110 fix(colors): move css variables to base layer
Resolves #3075
2025-01-16 12:22:25 +01:00
Benjamin Canac
86e1888474 docs(deps): update 2025-01-15 18:26:59 +01:00
Farnabaz
12a1ab00df docs(deps): update @nuxt/content (#3108) 2025-01-15 17:55:18 +01:00
Benjamin Canac
b64b24f65a docs(deps): update @nuxt/ui-pro 2025-01-15 16:09:03 +01:00
Benjamin Canac
b8276020b3 docs(deps): update @nuxt/ui-pro 2025-01-15 14:12:19 +01:00
Benjamin Canac
75f7064b40 feat(css): add light variant to reverse colors 2025-01-15 14:00:37 +01:00
Alex
51e5e65be7 refactor(ColorPicker)!: migrate from color to colortranslator (#3097) 2025-01-14 15:01:47 +01:00
Hugo Richard
6df9a1a44b docs(contribution): add guide (#3076)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-01-14 14:21:38 +01:00
Alex
ec5d5c98a2 docs(SupportedLanguages): easti flag (#3099) 2025-01-14 11:31:38 +01:00
Romain Hamel
de9ecb1d76 fix(Form)!: include nested state in submit data (#3028) 2025-01-14 10:49:39 +01:00
Hugo Richard
865a47f125 docs(ComponentSlots): support custom slug (#3096) 2025-01-14 10:31:41 +01:00
Sigve Hansen
9ccfe8fbb3 feat(locale): add Norwegian Bokmål language (#3095) 2025-01-14 10:28:49 +01:00
Israel Ortuño
1bf370e6fd fix(locale): year translation missing ñ in es (#3090) 2025-01-13 22:49:58 +01:00
Sébastien Chopin
3309ef60b2 docs(getting-started): add nuxt devtools video & learnvue video 2025-01-13 18:56:48 +01:00
Daniele Nicosia
a6cc7bf53b docs(calendar/command-palette): fix external links (#3087) 2025-01-13 17:45:26 +01:00
Benjamin Canac
e2cee110b4 docs(deps): update @nuxt/ui-pro 2025-01-13 17:44:28 +01:00
Benjamin Canac
01b7547ccc docs(app): move watch before surround 2025-01-13 16:15:05 +01:00
Alex
e7c10bcb0d fix(Alert): allow actions wrap (#3083) 2025-01-13 14:46:45 +01:00
159 changed files with 4212 additions and 3890 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

@@ -33,11 +33,11 @@ const component = ({ name, primitive, pro, prose, content }) => {
import type { AppConfig } from '@nuxt/schema'
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'}
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 {
/**
@@ -76,11 +76,11 @@ import type { ${upperName}RootProps, ${upperName}RootEmits } from 'reka-ui'
import type { AppConfig } from '@nuxt/schema'
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'}
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

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 90 90">
<g transform="translate(0,90) scale(0.1,-0.1)" fill="#9b59b6" stroke="none">
<path d="M385 796 c-17 -12 -18 -19 -7 -109 20 -164 25 -158 -75 -77 -50 39 -97 70 -110 70 -26 0 -73 -72 -73 -112 0 -23 10 -30 103 -68 56 -24 106 -46 110 -50 4 -4 -32 -22 -80 -40 -146 -54 -155 -66 -108 -147 19 -31 32 -43 49 -43 13 0 61 29 109 65 99 75 94 80 75 -72 -11 -90 -10 -97 7 -109 24 -18 106 -18 130 0 17 12 18 19 7 109 -19 152 -24 147 75 72 47 -36 96 -65 109 -65 16 0 30 13 48 44 47 81 39 92 -107 147 -48 18 -84 36 -80 39 5 4 54 26 111 50 92 38 102 45 102 68 0 41 -47 112 -74 112 -13 0 -59 -29 -109 -70 -100 -81 -95 -86 -75 77 11 90 10 97 -7 109 -10 8 -40 14 -65 14 -25 0 -55 -6 -65 -14z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 756 B

View File

@@ -6,6 +6,10 @@ const value = ref<string | undefined>(undefined)
onMounted(() => {
value.value = framework.value
})
watch(framework, () => {
value.value = framework.value
})
</script>
<template>

View File

@@ -9,6 +9,16 @@ const props = defineProps<{
const config = useRuntimeConfig().public
const { module } = useSharedData()
const value = ref<string | undefined>(module.value)
watch(module, () => {
value.value = module.value
})
onMounted(() => {
value.value = module.value
})
const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
const items = computed(() => props.links.map(({ icon, ...link }) => link))
@@ -52,11 +62,11 @@ const items = computed(() => props.links.map(({ icon, ...link }) => link))
<UContentSearchButton />
</UTooltip>
<UTooltip text="Open on GitHub" :kbds="['meta', 'G']" class="hidden lg:flex">
<UTooltip text="Open on GitHub" class="hidden lg:flex">
<UButton
color="neutral"
variant="ghost"
:to="`https://github.com/nuxt/${module}`"
:to="`https://github.com/nuxt/${value}`"
target="_blank"
icon="i-simple-icons-github"
aria-label="GitHub"

View File

@@ -6,6 +6,10 @@ const value = ref<string | undefined>(undefined)
onMounted(() => {
value.value = module.value
})
watch(module, () => {
value.value = module.value
})
</script>
<template>

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

@@ -3,12 +3,13 @@ import { upperFirst, camelCase } from 'scule'
const props = defineProps<{
prose?: boolean
slug?: string
}>()
const route = useRoute()
const camelName = camelCase(route.params.slug?.[route.params.slug.length - 1] ?? '')
const name = props.prose ? `Prose${upperFirst(camelName)}` : `U${upperFirst(camelName)}`
const camelName = camelCase(props.slug ?? route.params.slug?.[route.params.slug.length - 1] ?? '')
const name = `${props.prose ? 'Prose' : 'U'}${upperFirst(camelName)}`
const meta = await fetchComponentMeta(name as any)
</script>

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

@@ -13,13 +13,16 @@ function getEmojiFlag(locale: string): string {
cs: 'cz',
da: 'dk',
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

@@ -4,12 +4,21 @@ const modal = useModal()
defineProps<{
count: number
}>()
const emit = defineEmits(['success'])
function onSuccess() {
emit('success')
}
</script>
<template>
<UModal :title="`This modal was opened programmatically ${count} times`">
<template #footer>
<UButton color="neutral" label="Close" @click="modal.close()" />
<div class="flex gap-2">
<UButton color="neutral" label="Close" @click="modal.close()" />
<UButton label="Success" @click="onSuccess" />
</div>
</template>
</UModal>
</template>

View File

@@ -3,6 +3,7 @@ import { LazyModalExample } from '#components'
const count = ref(0)
const toast = useToast()
const modal = useModal()
function open() {
@@ -10,7 +11,13 @@ function open() {
modal.open(LazyModalExample, {
description: 'And you can even provide a description!',
count: count.value
count: count.value,
onSuccess() {
toast.add({
title: 'Success !',
id: 'modal-success'
})
}
})
}
</script>

View File

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

View File

@@ -4,6 +4,12 @@ const slideover = useSlideover()
defineProps<{
count: number
}>()
const emit = defineEmits(['success'])
function onSuccess() {
emit('success')
}
</script>
<template>
@@ -13,7 +19,10 @@ defineProps<{
</template>
<template #footer>
<UButton color="neutral" label="Close" @click="slideover.close()" />
<div class="flex gap-2">
<UButton color="neutral" label="Close" @click="slideover.close()" />
<UButton label="Success" @click="onSuccess" />
</div>
</template>
</USlideover>
</template>

View File

@@ -3,6 +3,7 @@ import { LazySlideoverExample } from '#components'
const count = ref(0)
const toast = useToast()
const slideover = useSlideover()
function open() {
@@ -10,7 +11,13 @@ function open() {
slideover.open(LazySlideoverExample, {
title: 'Slideover',
count: count.value
count: count.value,
onSuccess() {
toast.add({
title: 'Success !',
id: 'modal-success'
})
}
})
}
</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

@@ -14,6 +14,16 @@ if (!page.value) {
throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
}
// Update the framework/module if the page has different ones
watch(page, () => {
if (page.value?.framework && page.value?.framework !== framework.value) {
framework.value = page.value?.framework as string
}
if (page.value?.module && page.value?.module !== module.value) {
module.value = page.value?.module as string
}
}, { immediate: true })
const { data: surround } = await useAsyncData(`${route.path}-surround`, () => {
return queryCollectionItemSurroundings('content', route.path, {
fields: ['description']
@@ -53,16 +63,6 @@ if (!import.meta.prerender) {
})
}
// Update the framework/module if the page has different ones
watch(page, () => {
if (page.value?.framework && page.value?.framework !== framework.value) {
framework.value = page.value?.framework as string
}
if (page.value?.module && page.value?.module !== module.value) {
module.value = page.value?.module as string
}
}, { immediate: true })
const type = page.value?.path.includes('components') ? 'Vue Component ' : page.value?.path.includes('composables') ? 'Vue Composable ' : ''
useSeoMeta({
titleTemplate: `%s ${type}- Nuxt UI ${page.value.module === 'ui-pro' ? 'Pro' : ''} v3${page.value.framework === 'vue' ? ' for Vue' : ''}`,
@@ -86,6 +86,14 @@ const communityLinks = computed(() => [{
label: 'Star on GitHub',
to: `https://github.com/nuxt/${page.value?.module === 'ui-pro' ? 'ui-pro' : 'ui'}`,
target: '_blank'
}, {
icon: 'i-lucide-life-buoy',
label: 'Contribution',
to: '/getting-started/contribution'
}, {
label: 'Roadmap',
icon: 'i-lucide-map',
to: '/roadmap'
}])
// const resourcesLinks = [{
@@ -136,7 +144,7 @@ const communityLinks = computed(() => [{
<UPageBody>
<ContentRenderer v-if="page.body" :value="page" />
<USeparator />
<USeparator v-if="surround?.filter(Boolean).length" />
<UContentSurround :surround="(surround as any)" />
</UPageBody>

View File

@@ -8,6 +8,8 @@ We're thrilled to introduce this major update to our UI library, bringing signif
## What's New in v3?
<iframe width="100%" height="100%" src="https://www.youtube-nocookie.com/embed/_eQxomah-nA?si=pDSzchUBDKb2NQu7" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen style="aspect-ratio: 16/9;"></iframe>
### Reka UI (Radix Vue)
We've transitioned from [Headless UI](https://headlessui.com/) to [Reka UI](https://reka-ui.com/) as our core component foundation. This shift brings several key advantages:
@@ -74,6 +76,20 @@ 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 UI is deeply integrated with Nuxt Devtools, providing a powerful development experience:
- **Component Inspector**: Inspect and analyze Nuxt UI components in real-time
- **Live Preview**: Modify component props and see changes instantly
- **Code Generation**: Get the corresponding code for your component configurations
::video{poster="https://res.cloudinary.com/nuxt/video/upload/so_0/v1736788078/nuxt-ui/nuxt-ui3-devtools_wbmgmc.jpg" controls class="w-full h-auto rounded"}
:source{src="https://res.cloudinary.com/nuxt/video/upload/v1736788078/nuxt-ui/nuxt-ui3-devtools_wbmgmc.webm" type="video/webm"}
:source{src="https://res.cloudinary.com/nuxt/video/upload/v1736788078/nuxt-ui/nuxt-ui3-devtools_wbmgmc.mp4" type="video/mp4"}
:source{src="https://res.cloudinary.com/nuxt/video/upload/v1736788078/nuxt-ui/nuxt-ui3-devtools_wbmgmc.ogg" type="video/ogg"}
::
## Migration
We want to be transparent: migrating from Nuxt UI v2 to v3 will require significant effort. While we've maintained core concepts and components, Nuxt UI v3 has been rebuilt from the ground up, resulting in a new library with enhanced capabilities.

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

@@ -0,0 +1,253 @@
---
title: Contribution Guide
description: 'A comprehensive guide on contributing to Nuxt UI v3, including project structure, development workflow, and best practices.'
navigation: false
---
Nuxt UI thrives thanks to its incredible community ❤️. We welcome all contributions through bug reports, pull requests, and feedback to help make this library even better.
::caution
Before reporting a bug or requesting a feature, make sure that you have read through our [documentation](https://ui3.nuxt.dev/) and existing [issues](https://github.com/nuxt/ui/issues?q=is%3Aissue%20is%3Aopen%20sort%3Aupdated-desc%20label%3Av3).
::
## Project Structure
Here's an overview of the key directories and files in the Nuxt UI project structure:
### Documentation
The documentation lives in the `docs` folder as a Nuxt app using `@nuxt/content` v3 to generate pages from Markdown files. See the [Content v3 Docs](https://content3.nuxt.dev/docs/getting-started) for details on how it works. Here's a breakdown of its structure:
```bash
├── app/
│ ├── assets/
│ ├── components/
│ │ └── content/
│ │ └── examples # Components used in documentation as examples
│ ├── composables/
│ └── ...
├── content/
│ ├── 1.getting-started
│ ├── 2.composables
│ └── 3.components # Components documentation
```
### Module
The module code resides in the `src` folder. Here's a breakdown of its structure:
```bash
├── devtools/
├── plugins/
├── runtime/
│ ├── components/ # Where all the components are located
│ │ ├── Accordion.vue
│ │ ├── Alert.vue
│ │ └── ...
│ ├── composables/
│ ├── locale/
│ ├── plugins/
│ ├── types/
│ ├── utils/
│ └── vue/
│ ├── components/
│ └── plugins/
├── theme/ # This where the theme for each component is located
│ ├── accordion.ts # Theme for Accordion component
│ ├── alert.ts
│ └── ...
└── module.ts
```
## CLI
To make development easier, we've created a CLI that you can use to generate components and locales. You can access it using the `nuxt-ui make` command.
First, you need to link the CLI to your global environment:
```sh
npm link
```
### Components
You can create new components using the following command:
```sh
nuxt-ui make component <name> [options]
```
Available options:
- `--primitive` Create a primitive component
- `--pro` Create a pro component
- `--prose` Create a prose component (requires `--pro`)
- `--content` Create a content component (requires `--pro`)
- `--template` Only generate specific template (available templates: `playground`, `docs`, `test`, `theme`, `component`)
Example:
```sh
# Create a basic component
nuxt-ui make component my-component
# Create a pro component
nuxt-ui make component page-section --pro
# Create a pro prose component
nuxt-ui make component heading --pro --prose
# Create a pro content component
nuxt-ui make component block --pro --content
# Generate only documentation template
nuxt-ui make component my-component --template=docs
```
::note
When creating a new component, the CLI will automatically generate all the necessary files like the component itself, theme, tests, and documentation.
::
### Locales
You can create new locales using the following command:
```sh
nuxt-ui make locale --code <code> --name <name>
```
::note{to="/getting-started/i18n/nuxt#supported-languages"}
Learn more about **i18n** in the documentation.
::
## Submit a Pull Request (PR)
Before you start, check if there's an existing issue describing the problem or feature request you're working on. If there is, please leave a comment on the issue to let us know you're working on it.
If there isn't, open a new issue to discuss the problem or feature.
### Local Development
To begin local development, follow these steps:
::steps{level="4"}
#### Clone the `nuxt/ui` repository to your local machine
```sh
git clone -b v3 https://github.com/nuxt/ui.git
```
#### Enable [Corepack](https://github.com/nodejs/corepack)
```sh
corepack enable
```
#### Install dependencies
```sh
pnpm install
```
#### Generate type stubs
```sh
pnpm run dev:prepare
```
#### Start development
- To work on the **documentation** located in the `docs` folder, run:
```sh
pnpm run docs
```
- To test the Nuxt components using the **playground**, run:
```sh
pnpm run dev
```
- To test the Vue components using the **playground**, run:
```sh
pnpm run dev:vue
```
::
::note{to="#cli"}
If you're working on implementing a new component, check the **CLI** section to kickstart the process.
::
### IDE Setup
We recommend using VSCode alongside the [ESLint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint). You can enable auto-fix and formatting when saving your code. Here's how:
```json
{
"editor.codeActionsOnSave": {
"source.fixAll": false,
"source.fixAll.eslint": true
}
}
```
::warning
Since ESLint is already configured to format the code, there's no need for duplicating functionality with **Prettier**. If you have it installed in your editor, we recommend disabling it to avoid conflicts.
::
### Linting
You can use the `lint` command to check for linting errors:
```sh
pnpm run lint # check for linting errors
pnpm run lint:fix # fix linting errors
```
### Type Checking
We use TypeScript for type checking. You can use the `typecheck` command to check for type errors:
```sh
pnpm run typecheck
```
### Testing
Before submitting a PR, ensure that you run the tests for both `nuxt` and `vue`:
```sh
pnpm run test # for Nuxt
pnpm run test:vue # for Vue
```
::tip
If you have to update the snapshots, press `u` when running the tests.
::
### Commit Conventions
We use [Conventional Commits](https://www.conventionalcommits.org/) for commit messages, which allows a changelog to be auto-generated based on the commits. Please read the [guide](https://www.conventionalcommits.org/en/v1.0.0/#summary) through if you aren't familiar with it already.
- Use `fix` and `feat` for code changes that affect functionality or logic
- Use `docs` for documentation changes and `chore` for maintenance tasks
### Making a Pull Request
- Follow along the [instructions](https://github.com/nuxt/ui/blob/v3/.github/PULL_REQUEST_TEMPLATE.md?plain=1) provided when creating a PR
- Ensure your PR's title adheres to the [Conventional Commits](https://www.conventionalcommits.org/) since it will be used once the code is merged.
- Multiple commits are fine; no need to rebase or force push. We'll use `Squash and Merge` when merging.
- Ensure `lint`, `typecheck` and `tests` work before submitting the PR. Avoid making unrelated changes.
We'll review it promptly. If assigned to a maintainer, they'll review it carefully. Ignore the red text; it's for tracking purposes.
## Thanks
Thank you again for being interested in this project! You are awesome! ❤️

View File

@@ -4,7 +4,7 @@ description: A calendar component for selecting single dates, multiple dates or
links:
- label: Calendar
icon: i-custom-reka-ui
to: https://www.reka-ui.com/components/calendar.html
to: https://reka-ui.com/docs/components/calendar
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxt/ui/tree/v3/src/runtime/components/Calendar.vue

View File

@@ -68,9 +68,9 @@ props:
---
::
### HWB Format
### CMYK Format
Use the `format` prop to set `hwb` value of the ColorPicker.
Use the `format` prop to set `cmyk` value of the ColorPicker.
::component-code
---
@@ -80,8 +80,25 @@ ignore:
external:
- modelValue
props:
format: hwb
modelValue: 'hwb(150, 0%, 24%)'
format: cmyk
modelValue: 'cmyk(100%, 0%, 45.08%, 24.31%)'
---
::
### CIELab Format
Use the `format` prop to set `lab` value of the ColorPicker.
::component-code
---
ignore:
- modelValue
- format
external:
- modelValue
props:
format: lab
modelValue: 'lab(68.88% -60.41% 32.55%)'
---
::

View File

@@ -3,6 +3,7 @@ title: CommandPalette
description: A command palette with full-text search powered by Fuse.js for efficient fuzzy matching.
links:
- label: Fuse.js
icon: i-custom-fuse-js
to: https://fusejs.io/
target: _blank
- label: Combobox

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(path?: string \| string[], opts: { silent?: 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

@@ -21,6 +21,7 @@ Use the `items` prop as an array of objects with the following properties:
- `avatar?: AvatarProps`{lang="ts-type"}
- `badge?: string | number | BadgeProps`{lang="ts-type"}
- `trailingIcon?: string`{lang="ts-type"}
- `type?: 'label' | 'link'`{lang="ts-type"}
- `value?: string`{lang="ts-type"}
- `disabled?: boolean`{lang="ts-type"}
- `class?: any`{lang="ts-type"}
@@ -138,7 +139,9 @@ Each item can take a `children` array of objects with the following properties t
Use the `orientation` prop to change the orientation of the NavigationMenu.
::note
When orientation is `vertical`, a [Collapsible](/components/collapsible) component is used to display children. You can control the open state of each item using the `open` and `defaultOpen` properties.
::
::component-code
---
@@ -151,7 +154,9 @@ external:
props:
orientation: 'vertical'
items:
- - label: Guide
- - label: Links
type: 'label'
- label: Guide
icon: i-lucide-book-open
children:
- label: Introduction
@@ -608,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": "https://pkg.pr.new/@nuxt/content@164ffb0",
"@nuxt/content": "^3.0.1",
"@nuxt/image": "^1.9.0",
"@nuxt/ui": "latest",
"@nuxt/ui-pro": "https://pkg.pr.new/@nuxt/ui-pro@7676093",
"@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,23 +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",
"@types/color": "^4.2.0",
"@unhead/vue": "^1.11.16",
"@vueuse/core": "^12.4.0",
"@vueuse/integrations": "^12.4.0",
"color": "^4.2.3",
"consola": "^3.3.3",
"@unhead/vue": "^1.11.18",
"@vueuse/core": "^12.5.0",
"@vueuse/integrations": "^12.5.0",
"colortranslator": "^4.1.0",
"consola": "^3.4.0",
"defu": "^6.1.4",
"embla-carousel-auto-height": "^8.5.2",
"embla-carousel-auto-scroll": "^8.5.2",
@@ -105,12 +104,12 @@
"magic-string": "^0.30.17",
"mlly": "^1.7.4",
"ohash": "^1.1.4",
"pathe": "^2.0.2",
"reka-ui": "1.0.0-alpha.8",
"pathe": "^2.0.1",
"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",
@@ -122,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

@@ -11,6 +11,46 @@ const actions = (color: string) => [{
}
}]
const multipleActions = (color: string) => [
{
label: 'Action',
color: color as any,
click() {
console.log('Action clicked')
}
},
{
label: 'Another action',
color: color as any,
click() {
console.log('Another action clicked')
}
},
{
label: 'One more action',
color: color as any,
click() {
console.log('One more action clicked')
}
},
{
label: 'And one more',
color: color as any,
icon: 'i-lucide-info',
click() {
console.log('And one more clicked')
}
},
{
label: 'Last one',
color: color as any,
icon: 'i-lucide-info',
click() {
console.log('Last one clicked')
}
}
]
const data = {
title: 'Heads up!',
description: 'You can change the primary color in your app config.',
@@ -28,6 +68,7 @@ const data = {
<UAlert :title="data.title" :icon="data.icon" :close="data.close" :actions="actions('neutral')" />
<UAlert :title="data.title" :icon="data.icon" :close="data.close" :description="data.description" />
<UAlert :title="data.title" :avatar="{ src: 'https://github.com/benjamincanac.png' }" :close="data.close" :description="data.description" />
<UAlert :title="data.title" :icon="data.icon" description="example with multiple actions." :actions="multipleActions('neutral')" />
</div>
<div class="flex items-center gap-2">

View File

@@ -4,15 +4,21 @@ 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)
const items = [
[{
label: 'Link',
type: 'label' as const
}, {
label: 'Documentation',
icon: 'i-lucide-book-open',
badge: 10,
@@ -40,33 +46,33 @@ const items = [
defaultOpen: true,
children: [{
label: 'Link',
icon: 'i-lucide-file',
icon: 'i-lucide-link',
description: 'Use NuxtLink with superpowers.',
to: '/components/link'
}, {
label: 'Modal',
icon: 'i-lucide-file',
description: 'Display a modal within your application.',
icon: 'i-lucide-square',
description: 'Display a modal dialog overlay for important content.',
to: '/components/modal'
}, {
label: 'NavigationMenu',
icon: 'i-lucide-file',
icon: 'i-lucide-list',
description: 'Display a list of links.',
to: '/components/navigation-menu',
trailingIcon: 'i-lucide-check'
}, {
label: 'Pagination',
icon: 'i-lucide-file',
icon: 'i-lucide-parking-meter',
description: 'Display a list of pages.',
to: '/components/pagination'
}, {
label: 'Popover',
icon: 'i-lucide-file',
icon: 'i-lucide-message-circle',
description: 'Display a non-modal dialog that floats around a trigger element.',
to: '/components/popover'
}, {
label: 'Progress',
icon: 'i-lucide-file',
icon: 'i-lucide-loader',
description: 'Show a horizontal bar to indicate task progression.',
to: '/components/progress'
}]
@@ -89,20 +95,24 @@ 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" />
</div>
<UNavigationMenu
arrow
:collapsed="collapsed"
:items="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)]'"
class="data-[orientation=vertical]:w-48"
class="data-[orientation=vertical]:data-[collapsed=false]:w-48"
/>
</div>
</template>

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"
}
}

4440
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

@@ -5,10 +5,11 @@ import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
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>
@@ -18,6 +19,27 @@ type HSVColor = {
v: number
}
function HSLtoHSV(hsl: HSLObject): HSVColor {
const x = hsl.S * (hsl.L < 50 ? hsl.L : 100 - hsl.L)
const v = hsl.L + (x / 100)
return {
h: hsl.H,
s: hsl.L === 0 ? hsl.S : 2 * x / v,
v
}
}
function HSVtoHSL(hsv: HSVColor): HSLObject {
const x = (200 - hsv.s) * hsv.v / 100
return {
H: hsv.h,
S: x === 0 || x === 200 ? 0 : Math.round(hsv.s * hsv.v / (x <= 100 ? x : 200 - x)),
L: Math.round(x / 2)
}
}
export type ColorPickerProps = {
/**
* The element or component this component should render as.
@@ -40,7 +62,7 @@ export type ColorPickerProps = {
* Format of the color
* @defaultValue 'hex'
*/
format?: 'hex' | 'rgb' | 'hsl' | 'hwb'
format?: 'hex' | 'rgb' | 'hsl' | 'cmyk' | 'lab'
size?: ColorPickerVariants['size']
class?: any
ui?: Partial<typeof colorPicker.slots>
@@ -52,7 +74,7 @@ import { ref, nextTick, computed, toValue } from 'vue'
import { Primitive } from 'reka-ui'
import { useEventListener, useElementBounding, watchThrottled, watchPausable } from '@vueuse/core'
import { isClient } from '@vueuse/shared'
import Color from 'color'
import { ColorTranslator } from 'colortranslator'
const props = withDefaults(defineProps<ColorPickerProps>(), {
format: 'hex',
@@ -64,28 +86,37 @@ const modelValue = defineModel<string>(undefined)
const pickedColor = computed<HSVColor>({
get() {
try {
const color = Color(modelValue.value || props.defaultValue)
return color.hsv().object() as HSVColor
const color = new ColorTranslator(modelValue.value || props.defaultValue)
return HSLtoHSV(color.HSLObject)
} catch (_) {
return { h: 0, s: 0, v: 100 }
}
},
set(value) {
const color = Color.hsv(value.h, value.s, value.v)
const color = new ColorTranslator(HSVtoHSL(value), {
decimals: 2,
labUnit: 'percent',
cmykUnit: 'percent',
cmykFunction: 'cmyk'
})
switch (props.format) {
case 'rgb':
modelValue.value = color.rgb().string()
modelValue.value = color.RGB
break
case 'hsl':
modelValue.value = color.hsl().string()
modelValue.value = color.HSL
break
case 'hwb':
modelValue.value = color.hwb().string()
case 'cmyk':
modelValue.value = color.CMYK
break
case 'lab':
modelValue.value = color.CIELab
break
case 'hex':
default:
modelValue.value = color.hex()
modelValue.value = color.HEX
}
}
})
@@ -202,18 +233,18 @@ watchThrottled([selectorThumbPosition, trackThumbPosition], () => {
nextTick(resumeWatchColor)
}, { throttle: () => props.throttle })
const trackThumbColor = computed(() => Color({
const trackThumbColor = computed(() => new ColorTranslator(HSVtoHSL({
h: normalizeHue(trackThumbPosition.value.y),
s: 100,
v: 100
}).hex())
})).HEX)
const selectorStyle = computed(() => ({
backgroundColor: trackThumbColor.value
}))
const selectorThumbStyle = computed(() => ({
backgroundColor: Color(modelValue.value || props.defaultValue).hex(),
backgroundColor: new ColorTranslator(modelValue.value || props.defaultValue).HEX,
left: `${selectorThumbPosition.value.x}%`,
top: `${selectorThumbPosition.value.y}%`
}))

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
@@ -60,7 +61,7 @@ const parentBus = inject(
provide(formBusInjectionKey, bus)
const nestedForms = ref<Map<string | number, { validate: () => any }>>(new Map())
const nestedForms = ref<Map<string | number, { validate: typeof _validate }>>(new Map())
onMounted(async () => {
bus.on(async (event) => {
@@ -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,12 +142,12 @@ async function getErrors(): Promise<FormErrorWithId[]> {
return resolveErrorIds(errs)
}
async function _validate(opts: { name?: string | string[], silent?: boolean, nested?: boolean } = { silent: false, nested: true }): Promise<T | false> {
const names = opts.name && !Array.isArray(opts.name) ? [opts.name] : opts.name as string[]
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(
({ validate }) => validate().then(() => undefined).catch((error: Error) => {
({ validate }) => validate(opts).then(() => undefined).catch((error: Error) => {
if (!(error instanceof FormValidationException)) {
throw error
}
@@ -151,13 +172,17 @@ async function _validate(opts: { name?: string | string[], silent?: boolean, nes
errors.value = await getErrors()
}
const childErrors = (await Promise.all(nestedValidatePromises)).filter(val => val)
const childErrors = (await Promise.all(nestedValidatePromises)).filter(val => val !== undefined)
if (errors.value.length + childErrors.length > 0) {
if (opts.silent) return false
throw new FormValidationException(formId, errors.value, childErrors)
}
if (opts.transform) {
Object.assign(props.state, transformedState.value)
}
return props.state as T
}
@@ -170,8 +195,7 @@ async function onSubmitWrapper(payload: Event) {
const event = payload as FormSubmitEvent<any>
try {
await _validate({ nested: true })
event.data = props.schema ? transformedState.value : props.state
event.data = await _validate({ nested: true, transform: true })
await props.onSubmit?.(event)
} catch (error) {
if (!(error instanceof FormValidationException)) {
@@ -200,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)
@@ -214,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)
}
@@ -229,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

View File

@@ -9,16 +9,16 @@ 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'> {
export interface NavigationMenuChildItem extends Omit<NavigationMenuItem, 'children' | 'type'> {
/** Description is only used when `orientation` is `horizontal`. */
description?: string
}
export interface NavigationMenuItem extends Omit<LinkProps, 'raw' | 'custom'>, Pick<CollapsibleRootProps, 'defaultOpen' | 'open'> {
export interface NavigationMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'custom'>, Pick<CollapsibleRootProps, 'defaultOpen' | 'open'> {
label?: string
icon?: string
avatar?: AvatarProps
@@ -28,6 +28,12 @@ export interface NavigationMenuItem extends Omit<LinkProps, 'raw' | 'custom'>, P
*/
badge?: string | number | BadgeProps
trailingIcon?: string
/**
* The type of the item.
* The `label` type only works on `vertical` orientation.
* @defaultValue 'link'
*/
type?: 'label' | 'link'
slot?: string
value?: string
children?: NavigationMenuChildItem[]
@@ -55,11 +61,23 @@ export interface NavigationMenuProps<T> extends Pick<NavigationMenuRootProps, 'm
* @defaultValue 'horizontal'
*/
orientation?: NavigationMenuRootProps['orientation']
/**
* Collapse the navigation menu to only show icons.
* Only works when `orientation` is `vertical`.
* @defaultValue false
*/
collapsed?: boolean
/** Display a line next to the active item. */
highlight?: boolean
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
@@ -130,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'
@@ -141,6 +160,7 @@ import UCollapsible from './Collapsible.vue'
const props = withDefaults(defineProps<NavigationMenuProps<I>>(), {
orientation: 'horizontal',
contentOrientation: 'horizontal',
delayDuration: 0,
unmountOnHide: true,
labelKey: 'label'
@@ -163,10 +183,14 @@ 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,
highlight: props.highlight,
@@ -177,14 +201,17 @@ 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 })" />
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.linkLeadingIcon({ class: props.ui?.linkLeadingIcon, active, disabled: !!item.disabled })" />
</slot>
<span v-if="get(item, props.labelKey as string) || !!slots[item.slot ? `${item.slot}-label` : 'item-label']" :class="ui.linkLabel({ class: props.ui?.linkLabel })">
<span
v-if="(!collapsed || orientation !== 'vertical') && (get(item, props.labelKey as string) || !!slots[item.slot ? `${item.slot}-label` : 'item-label'])"
:class="ui.linkLabel({ class: props.ui?.linkLabel })"
>
<slot :name="item.slot ? `${item.slot}-label` : 'item-label'" :item="(item as T)" :active="active" :index="index">
{{ get(item, props.labelKey as string) }}
</slot>
@@ -192,7 +219,7 @@ const lists = computed(() => props.items?.length ? (Array.isArray(props.items[0]
<UIcon v-if="item.target === '_blank'" :name="appConfig.ui.icons.external" :class="ui.linkLabelExternalIcon({ class: props.ui?.linkLabelExternalIcon, active })" />
</span>
<span v-if="item.badge || (orientation === 'horizontal' && (item.children?.length || !!slots[item.slot ? `${item.slot}-content` : 'item-content'])) || (orientation === 'vertical' && item.children?.length) || item.trailingIcon || !!slots[item.slot ? `${item.slot}-trailing` : 'item-trailing']" :class="ui.linkTrailing({ class: props.ui?.linkTrailing })">
<span v-if="(!collapsed || orientation !== 'vertical') && (item.badge || (orientation === 'horizontal' && (item.children?.length || !!slots[item.slot ? `${item.slot}-content` : 'item-content'])) || (orientation === 'vertical' && item.children?.length) || item.trailingIcon || !!slots[item.slot ? `${item.slot}-trailing` : 'item-trailing'])" :class="ui.linkTrailing({ class: props.ui?.linkTrailing })">
<slot :name="item.slot ? `${item.slot}-trailing` : 'item-trailing'" :item="(item as T)" :active="active" :index="index">
<UBadge
v-if="item.badge"
@@ -208,77 +235,73 @@ const lists = computed(() => props.items?.length ? (Array.isArray(props.items[0]
</slot>
</span>
</slot>
</DefineItemTemplate>
</DefineLinkTemplate>
<NavigationMenuRoot v-bind="rootProps" :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"
>
<ULink v-slot="{ active, ...slotProps }" v-bind="(orientation === 'vertical' && item.children?.length) ? {} : pickLinkProps(item)" 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

@@ -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
}

View File

@@ -1,5 +1,6 @@
@import './keyframes.css';
@variant light (&:where(.light, .light *));
@variant dark (&:where(.dark, .dark *));
@layer base {
@@ -7,7 +8,7 @@
@apply antialiased text-[var(--ui-text)] bg-[var(--ui-bg)] scheme-light dark:scheme-dark;
}
:root {
:root, .light {
--ui-text-dimmed: var(--ui-color-neutral-400);
--ui-text-muted: var(--ui-color-neutral-500);
--ui-text-toned: var(--ui-color-neutral-600);

View File

@@ -10,8 +10,8 @@ export default defineLocale({
create: 'Crear "{label}"'
},
calendar: {
prevYear: 'A o anterior',
nextYear: 'A o siguiente',
prevYear: 'Año anterior',
nextYear: 'Año siguiente',
prevMonth: 'Mes anterior',
nextMonth: 'Mes siguiente'
},

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,10 +9,12 @@ 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'
export { default as ko } from './ko'
export { default as nb_no } from './nb_no'
export { default as nl } from './nl'
export { default as pl } from './pl'
export { default as pt } from './pt'
@@ -24,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'

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