mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-15 12:39:35 +01:00
Compare commits
72 Commits
v3.0.0-alp
...
v3.0.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc10a1cabe | ||
|
|
dcd86144a2 | ||
|
|
761680b5cb | ||
|
|
3183e4afe3 | ||
|
|
cd16b95c98 | ||
|
|
d27be06164 | ||
|
|
f3958773d6 | ||
|
|
2006ec0646 | ||
|
|
3320e0473c | ||
|
|
c0b485d563 | ||
|
|
891ba1fec6 | ||
|
|
1a95104631 | ||
|
|
ac86ee01b9 | ||
|
|
8f7f579da0 | ||
|
|
8e96daa5cc | ||
|
|
36d7402be1 | ||
|
|
63b7de4159 | ||
|
|
f8b4de587e | ||
|
|
890c3d0840 | ||
|
|
7441b6451d | ||
|
|
2b7ff3edf6 | ||
|
|
9b5a957cdd | ||
|
|
00c5f26111 | ||
|
|
aafddd8eed | ||
|
|
8d941e1360 | ||
|
|
ba3d5e2c7d | ||
|
|
1b989c419d | ||
|
|
b8b7a8366d | ||
|
|
fb94ee379c | ||
|
|
a5ed62f83a | ||
|
|
12b6c78a17 | ||
|
|
9cafd1295e | ||
|
|
545c3917a1 | ||
|
|
3e2e5a075d | ||
|
|
629dcfab16 | ||
|
|
53d636aa9b | ||
|
|
eb068b2f90 | ||
|
|
75a470d588 | ||
|
|
90dc03cd03 | ||
|
|
088dc9bf38 | ||
|
|
b95b91391a | ||
|
|
b8d99726ef | ||
|
|
b88f67ccfe | ||
|
|
55b233dc3d | ||
|
|
f0297e02d0 | ||
|
|
64421a190f | ||
|
|
3af77ccca1 | ||
|
|
a3a562b699 | ||
|
|
3159a89436 | ||
|
|
ba1dd13173 | ||
|
|
3fc2210e03 | ||
|
|
931211a634 | ||
|
|
27fdc8e260 | ||
|
|
1e88512bef | ||
|
|
533ccec110 | ||
|
|
86e1888474 | ||
|
|
12a1ab00df | ||
|
|
b64b24f65a | ||
|
|
b8276020b3 | ||
|
|
75f7064b40 | ||
|
|
51e5e65be7 | ||
|
|
6df9a1a44b | ||
|
|
ec5d5c98a2 | ||
|
|
de9ecb1d76 | ||
|
|
865a47f125 | ||
|
|
9ccfe8fbb3 | ||
|
|
1bf370e6fd | ||
|
|
3309ef60b2 | ||
|
|
a6cc7bf53b | ||
|
|
e2cee110b4 | ||
|
|
01b7547ccc | ||
|
|
e7c10bcb0d |
42
CHANGELOG.md
42
CHANGELOG.md
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}>
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
5
docs/app/assets/icons/fuse-js.svg
Normal file
5
docs/app/assets/icons/fuse-js.svg
Normal 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 |
@@ -6,6 +6,10 @@ const value = ref<string | undefined>(undefined)
|
||||
onMounted(() => {
|
||||
value.value = framework.value
|
||||
})
|
||||
|
||||
watch(framework, () => {
|
||||
value.value = framework.value
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -6,6 +6,10 @@ const value = ref<string | undefined>(undefined)
|
||||
onMounted(() => {
|
||||
value.value = module.value
|
||||
})
|
||||
|
||||
watch(module, () => {
|
||||
value.value = module.value
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -51,7 +51,8 @@ const items = [
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'GitHub'
|
||||
label: 'GitHub',
|
||||
icon: 'i-simple-icons-github'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -113,7 +113,7 @@ provide('navigation', mappedNavigation)
|
||||
items: modules
|
||||
}]"
|
||||
:navigation="filteredNavigation"
|
||||
:fuse="{ resultLimit: 42 }"
|
||||
:fuse="{ resultLimit: 100 }"
|
||||
/>
|
||||
</ClientOnly>
|
||||
</UApp>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
253
docs/content/1.getting-started/contribution.md
Normal file
253
docs/content/1.getting-started/contribution.md
Normal 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! ❤️
|
||||
@@ -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
|
||||
|
||||
@@ -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%)'
|
||||
---
|
||||
::
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
45
package.json
45
package.json
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -13,7 +13,7 @@ export default defineNuxtConfig({
|
||||
},
|
||||
|
||||
ui: {
|
||||
fonts: false
|
||||
fonts: !process.env.DEVTOOLS
|
||||
},
|
||||
|
||||
future: {
|
||||
|
||||
@@ -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
4440
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -26,5 +26,6 @@
|
||||
}, {
|
||||
"matchDepTypes": ["resolutions"],
|
||||
"enabled": false
|
||||
}]
|
||||
}],
|
||||
"postUpdateOptions": ["pnpmDedupe"]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 || ' ' }}</span>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
/**
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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'> {
|
||||
/**
|
||||
|
||||
@@ -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}%`
|
||||
}))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
/**
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 }>()
|
||||
|
||||
@@ -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'> {
|
||||
/**
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 }>()
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 })">
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 {
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 })" />
|
||||
|
||||
@@ -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'>> {
|
||||
/**
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'> {
|
||||
/**
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 })" />
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 {
|
||||
/**
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
54
src/runtime/locale/he.ts
Normal 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
55
src/runtime/locale/hi.ts
Normal 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
55
src/runtime/locale/hu.ts
Normal 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'
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -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
Reference in New Issue
Block a user