Compare commits
61 Commits
v3.0.2
...
fix-naviga
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b03c9be3fa | ||
|
|
3deed4c271 | ||
|
|
e6b1c238b9 | ||
|
|
626b023ddb | ||
|
|
4c667f75f4 | ||
|
|
e04dd53046 | ||
|
|
d25265c8b7 | ||
|
|
7d8353ffdc | ||
|
|
ba534f18b9 | ||
|
|
864083156a | ||
|
|
8435a0fe16 | ||
|
|
d339dcbfb8 | ||
|
|
cd7ab413f6 | ||
|
|
cea881abdc | ||
|
|
d227a105d8 | ||
|
|
f6ff157bc4 | ||
|
|
21fbd07639 | ||
|
|
de234e8aeb | ||
|
|
95a7707963 | ||
|
|
24b54f6d9a | ||
|
|
b3e37688d9 | ||
|
|
eeba3b4049 | ||
|
|
a0c9731f63 | ||
|
|
af1bf1bbde | ||
|
|
54a7d04217 | ||
|
|
dfa2113db4 | ||
|
|
abe0859691 | ||
|
|
cf91f3c4cf | ||
|
|
175fc73e63 | ||
|
|
1d459803dc | ||
|
|
60e2ee9a6c | ||
|
|
7f1c6caa6e | ||
|
|
7ac17ae7e8 | ||
|
|
52a97e2df7 | ||
|
|
041989549a | ||
|
|
31c37ce1a1 | ||
|
|
74cb2c3769 | ||
|
|
5025e15d14 | ||
|
|
36c24ffe5c | ||
|
|
b3a6b861cd | ||
|
|
c21eb32c70 | ||
|
|
44e6ba039d | ||
|
|
ef75610244 | ||
|
|
ffafd81e1e | ||
|
|
06414d344b | ||
|
|
cb193f1d25 | ||
|
|
4d8179ba08 | ||
|
|
ce767c8429 | ||
|
|
1ae5cc09cb | ||
|
|
9d2fed1250 | ||
|
|
924515ad07 | ||
|
|
4d138ad671 | ||
|
|
615fcfd73b | ||
|
|
f5e62849c9 | ||
|
|
f25fed58e9 | ||
|
|
ca15bc0c75 | ||
|
|
29f004db95 | ||
|
|
97274f15b8 | ||
|
|
8471fb9fa4 | ||
|
|
9ec159e207 | ||
|
|
0456670dac |
11
README.md
@@ -104,6 +104,17 @@ app.mount('#app')
|
||||
|
||||
Learn more in the [installation guide](https://ui.nuxt.com/getting-started/installation/vue).
|
||||
|
||||
## Contribution
|
||||
|
||||
Thank you for considering contributing to Nuxt UI. Here are a few ways you can get involved:
|
||||
|
||||
- Reporting Bugs: If you come across any bugs or issues, please check out the reporting bugs guide to learn how to submit a bug report.
|
||||
- Suggestions: Have any thoughts to enhance Nuxt UI? We'd love to hear them! Check out the [contribution guide](https://ui.nuxt.com/getting-started/contribution) to share your suggestions.
|
||||
|
||||
## Local Development
|
||||
|
||||
Follow the docs to [set up your local development environment](https://ui.nuxt.com/getting-started/contribution#local-development) and contribute.
|
||||
|
||||
## Credits
|
||||
|
||||
- [nuxt/nuxt](https://github.com/nuxt/nuxt)
|
||||
|
||||
@@ -6,9 +6,6 @@ export default defineBuildConfig({
|
||||
'./src/unplugin',
|
||||
'./src/vite'
|
||||
],
|
||||
rollup: {
|
||||
emitCJS: true
|
||||
},
|
||||
replace: {
|
||||
'process.env.DEV': 'false'
|
||||
},
|
||||
|
||||
@@ -33,7 +33,7 @@ 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 '../utils/tv'
|
||||
|
||||
const appConfig${camelName} = _appConfig as AppConfig & { ${key}: { ${prose ? 'prose: { ' : ''}${camelName}: Partial<typeof theme> } }${prose ? ' }' : ''}
|
||||
|
||||
@@ -76,7 +76,7 @@ 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 '../utils/tv'
|
||||
|
||||
const appConfig${camelName} = _appConfig as AppConfig & { ${key}: { ${prose ? 'prose: { ' : ''}${camelName}: Partial<typeof theme> } }${prose ? ' }' : ''}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ const { data: files } = useLazyAsyncData('search', () => queryCollectionSearchSe
|
||||
})
|
||||
|
||||
const links = useLinks()
|
||||
const searchLinks = useSearchLinks()
|
||||
const color = computed(() => colorMode.value === 'dark' ? (colors as any)[appConfig.ui.colors.neutral][900] : 'white')
|
||||
const radius = computed(() => `:root { --ui-radius: ${appConfig.theme.radius}rem; }`)
|
||||
const blackAsPrimary = computed(() => appConfig.theme.blackAsPrimary ? `:root { --ui-primary: black; } .dark { --ui-primary: white; }` : ':root {}')
|
||||
@@ -64,6 +65,7 @@ provide('navigation', mappedNavigation)
|
||||
|
||||
<ClientOnly>
|
||||
<LazyUContentSearch
|
||||
:links="searchLinks"
|
||||
:files="files"
|
||||
:groups="[{
|
||||
id: 'framework',
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
const route = useRoute()
|
||||
|
||||
const links = [{
|
||||
label: 'Figma',
|
||||
to: '/figma'
|
||||
label: 'Team',
|
||||
to: '/team'
|
||||
}, {
|
||||
label: 'Roadmap',
|
||||
to: '/roadmap'
|
||||
|
||||
@@ -22,8 +22,20 @@ onMounted(() => {
|
||||
|
||||
const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
|
||||
|
||||
const githubLink = computed(() => {
|
||||
return `https://github.com/nuxt/${value.value}`
|
||||
})
|
||||
|
||||
const desktopLinks = computed(() => props.links.map(({ icon, ...link }) => link))
|
||||
const mobileLinks = computed(() => props.links.map(link => ({ ...link, defaultOpen: link.children && route.path.startsWith(link.to as string) })))
|
||||
const mobileLinks = computed(() => [
|
||||
...props.links.map(link => ({ ...link, defaultOpen: link.children && route.path.startsWith(link.to as string) })),
|
||||
{
|
||||
label: 'Open on GitHub',
|
||||
to: githubLink.value,
|
||||
icon: 'i-simple-icons-github',
|
||||
target: '_blank'
|
||||
}
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -73,7 +85,7 @@ const mobileLinks = computed(() => props.links.map(link => ({ ...link, defaultOp
|
||||
:key="value"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
:to="`https://github.com/nuxt/${value}`"
|
||||
:to="githubLink"
|
||||
target="_blank"
|
||||
icon="i-simple-icons-github"
|
||||
aria-label="GitHub"
|
||||
|
||||
@@ -14,6 +14,7 @@ const props = withDefaults(defineProps<{
|
||||
color?: string
|
||||
size?: { min: number, max: number }
|
||||
speed?: 'slow' | 'normal' | 'fast'
|
||||
isIndex?: boolean
|
||||
}>(), {
|
||||
starCount: 50,
|
||||
color: 'var(--ui-primary)',
|
||||
@@ -21,7 +22,8 @@ const props = withDefaults(defineProps<{
|
||||
min: 1,
|
||||
max: 3
|
||||
}),
|
||||
speed: 'normal'
|
||||
speed: 'normal',
|
||||
isIndex: false
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
@@ -53,7 +55,7 @@ const twinkleDuration = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="absolute pointer-events-none z-[-1] inset-y-0 left-4 right-4 lg:right-[50%] overflow-hidden">
|
||||
<div class="absolute pointer-events-none z-[-1] overflow-hidden" :class="isIndex ? 'inset-y-0 left-4 right-4 lg:right-[50%]' : 'inset-0'">
|
||||
<div
|
||||
v-for="star in stars"
|
||||
:key="star.id"
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<script lang="ts" setup>
|
||||
import { createReusableTemplate, useMediaQuery } from '@vueuse/core'
|
||||
|
||||
const [DefineFormTemplate, ReuseFormTemplate] = createReusableTemplate()
|
||||
const isDesktop = useMediaQuery('(min-width: 768px)')
|
||||
|
||||
const open = ref(false)
|
||||
|
||||
const state = reactive({
|
||||
email: undefined
|
||||
})
|
||||
|
||||
const title = 'Edit profile'
|
||||
const description = 'Make changes to your profile here. Click save when you\'re done.'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DefineFormTemplate>
|
||||
<UForm :state="state" class="space-y-4">
|
||||
<UFormField label="Email" name="email" required>
|
||||
<UInput v-model="state.email" placeholder="shadcn@example.com" required />
|
||||
</UFormField>
|
||||
|
||||
<UButton label="Save changes" type="submit" />
|
||||
</UForm>
|
||||
</DefineFormTemplate>
|
||||
|
||||
<UModal v-if="isDesktop" v-model:open="open" :title="title" :description="description">
|
||||
<UButton label="Edit profile" color="neutral" variant="outline" />
|
||||
|
||||
<template #body>
|
||||
<ReuseFormTemplate />
|
||||
</template>
|
||||
</UModal>
|
||||
|
||||
<UDrawer v-else v-model:open="open" :title="title" :description="description">
|
||||
<UButton label="Edit profile" color="neutral" variant="outline" />
|
||||
|
||||
<template #body>
|
||||
<ReuseFormTemplate />
|
||||
</template>
|
||||
</UDrawer>
|
||||
</template>
|
||||
@@ -14,7 +14,7 @@ const validate = (state: any): FormError[] => {
|
||||
}
|
||||
|
||||
const toast = useToast()
|
||||
async function onSubmit(event: FormSubmitEvent<any>) {
|
||||
async function onSubmit(event: FormSubmitEvent<typeof state>) {
|
||||
toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' })
|
||||
console.log(event.data)
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ const items = [
|
||||
]
|
||||
|
||||
const toast = useToast()
|
||||
async function onSubmit(event: FormSubmitEvent<any>) {
|
||||
async function onSubmit(event: FormSubmitEvent<Schema>) {
|
||||
toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' })
|
||||
console.log(event.data)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ const state = reactive({
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
async function onSubmit(event: FormSubmitEvent<any>) {
|
||||
async function onSubmit(event: FormSubmitEvent<typeof schema>) {
|
||||
toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' })
|
||||
console.log(event.data)
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ type NestedSchema = z.output<typeof nestedSchema>
|
||||
const state = reactive<Partial<Schema & NestedSchema>>({ })
|
||||
|
||||
const toast = useToast()
|
||||
async function onSubmit(event: FormSubmitEvent<any>) {
|
||||
async function onSubmit(event: FormSubmitEvent<Schema>) {
|
||||
toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' })
|
||||
console.log(event.data)
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ function removeItem() {
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
async function onSubmit(event: FormSubmitEvent<any>) {
|
||||
async function onSubmit(event: FormSubmitEvent<Schema>) {
|
||||
toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' })
|
||||
console.log(event.data)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ const validate = (state: any): FormError[] => {
|
||||
}
|
||||
|
||||
const toast = useToast()
|
||||
async function onSubmit(event: FormSubmitEvent<any>) {
|
||||
async function onSubmit(event: FormSubmitEvent<typeof state>) {
|
||||
toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' })
|
||||
console.log(event.data)
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ const label = ref([])
|
||||
multiple
|
||||
placeholder="Search labels..."
|
||||
:groups="[{ id: 'labels', items }]"
|
||||
:ui="{ input: '[&>input]:h-8' }"
|
||||
:ui="{ input: '[&>input]:h-8 [&>input]:text-sm' }"
|
||||
/>
|
||||
</template>
|
||||
</UPopover>
|
||||
|
||||
@@ -111,7 +111,7 @@ function setBlackAsPrimary(value: boolean) {
|
||||
v-for="color in neutralColors"
|
||||
:key="color"
|
||||
:label="color"
|
||||
:chip="color"
|
||||
:chip="color === 'neutral' ? 'old-neutral' : color"
|
||||
:selected="neutral === color"
|
||||
@click="neutral = color"
|
||||
/>
|
||||
|
||||
@@ -84,10 +84,10 @@ export function useLinks() {
|
||||
label: 'Community',
|
||||
icon: 'i-lucide-users',
|
||||
children: [{
|
||||
label: 'Roadmap',
|
||||
description: 'Track our development progress in real-time.',
|
||||
icon: 'i-lucide-map',
|
||||
to: '/roadmap'
|
||||
icon: 'i-lucide-presentation',
|
||||
label: 'Showcase',
|
||||
description: 'Check out some amazing projects built with Nuxt UI.',
|
||||
to: '/showcase'
|
||||
}, {
|
||||
label: 'Devtools Integration',
|
||||
description: 'Integrate Nuxt UI with Nuxt Devtools with Compodium.',
|
||||
@@ -112,5 +112,5 @@ export function useLinks() {
|
||||
icon: 'i-lucide-rocket',
|
||||
to: 'https://github.com/nuxt/ui/releases',
|
||||
target: '_blank'
|
||||
}].filter(Boolean))
|
||||
}])
|
||||
}
|
||||
|
||||
66
docs/app/composables/useSearchLinks.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
export function useSearchLinks() {
|
||||
return [{
|
||||
label: 'Docs',
|
||||
icon: 'i-lucide-square-play',
|
||||
to: '/getting-started'
|
||||
}, {
|
||||
label: 'Components',
|
||||
icon: 'i-lucide-square-code',
|
||||
to: '/components'
|
||||
}, {
|
||||
icon: 'i-lucide-sparkles',
|
||||
label: 'Pro > Features',
|
||||
description: 'A collection of premium Vue components.',
|
||||
to: '/pro'
|
||||
}, {
|
||||
icon: 'i-lucide-credit-card',
|
||||
label: 'Pro > Pricing',
|
||||
description: 'Free in development, buy when ready to launch.',
|
||||
to: '/pro/pricing'
|
||||
}, {
|
||||
icon: 'i-lucide-panels-top-left',
|
||||
label: 'Pro > Templates',
|
||||
description: 'Official templates made with Nuxt UI Pro.',
|
||||
to: '/pro/templates'
|
||||
}, {
|
||||
icon: 'i-lucide-circle-check',
|
||||
label: 'Pro > Activate',
|
||||
description: 'Enable Nuxt UI Pro in your production projects.',
|
||||
to: '/pro/activate'
|
||||
}, {
|
||||
label: 'Figma',
|
||||
icon: 'i-simple-icons-figma',
|
||||
to: '/figma'
|
||||
}, {
|
||||
icon: 'i-lucide-presentation',
|
||||
label: 'Community > Showcase',
|
||||
description: 'Check out some of the amazing projects built with Nuxt UI.',
|
||||
to: '/showcase'
|
||||
}, {
|
||||
label: 'Community > Contribution',
|
||||
description: 'A comprehensive guide on contributing to Nuxt UI, including project structure, development workflow, and best practices.',
|
||||
icon: 'i-lucide-git-pull-request-arrow',
|
||||
to: '/getting-started/contribution'
|
||||
}, {
|
||||
label: 'Community > Roadmap',
|
||||
description: 'Track our development progress in real-time.',
|
||||
icon: 'i-lucide-map',
|
||||
to: '/roadmap'
|
||||
}, {
|
||||
label: 'Community > Devtools',
|
||||
description: 'Integrate Nuxt UI with Nuxt Devtools with Compodium.',
|
||||
icon: 'i-lucide-code',
|
||||
to: 'https://github.com/romhml/compodium',
|
||||
target: '_blank'
|
||||
}, {
|
||||
label: 'Community > Team',
|
||||
description: 'Meet the team behind Nuxt UI.',
|
||||
icon: 'i-lucide-users',
|
||||
to: '/team'
|
||||
}, {
|
||||
label: 'Releases',
|
||||
icon: 'i-lucide-rocket',
|
||||
to: 'https://github.com/nuxt/ui/releases',
|
||||
target: '_blank'
|
||||
}]
|
||||
}
|
||||
@@ -15,6 +15,7 @@ const { data: files } = useLazyAsyncData('search', () => queryCollectionSearchSe
|
||||
})
|
||||
|
||||
const links = useLinks()
|
||||
const searchLinks = useSearchLinks()
|
||||
const color = computed(() => colorMode.value === 'dark' ? (colors as any)[appConfig.ui.colors.neutral][900] : 'white')
|
||||
const radius = computed(() => `:root { --ui-radius: ${appConfig.theme.radius}rem; }`)
|
||||
const blackAsPrimary = computed(() => appConfig.theme.blackAsPrimary ? `:root { --ui-primary: black; } .dark { --ui-primary: white; }` : ':root {}')
|
||||
@@ -66,6 +67,7 @@ provide('navigation', mappedNavigation)
|
||||
|
||||
<ClientOnly>
|
||||
<LazyUContentSearch
|
||||
:links="searchLinks"
|
||||
:files="files"
|
||||
:groups="[{
|
||||
id: 'framework',
|
||||
|
||||
@@ -176,7 +176,11 @@ community:
|
||||
links:
|
||||
- label: Star on GitHub
|
||||
color: neutral
|
||||
variant: outline
|
||||
to: https://github.com/nuxt/ui
|
||||
target: _blank
|
||||
icon: i-lucide-star
|
||||
- label: Meet the team
|
||||
color: neutral
|
||||
variant: outline
|
||||
to: /team
|
||||
trailingIcon: i-lucide-arrow-right
|
||||
|
||||
@@ -67,9 +67,9 @@ if (!import.meta.prerender) {
|
||||
|
||||
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' : ''}`,
|
||||
titleTemplate: `%s ${type}- Nuxt UI ${page.value.module === 'ui-pro' ? 'Pro' : ''} ${page.value.framework === 'vue' ? ' for Vue' : ''}`,
|
||||
title: page.value.navigation?.title ? page.value.navigation.title : page.value.title,
|
||||
ogTitle: `${page.value.navigation?.title ? page.value.navigation.title : page.value.title} ${type}- Nuxt UI ${page.value.module === 'ui-pro' ? 'Pro' : ''} v3${page.value.framework === 'vue' ? ' for Vue' : ''}`,
|
||||
ogTitle: `${page.value.navigation?.title ? page.value.navigation.title : page.value.title} ${type}- Nuxt UI ${page.value.module === 'ui-pro' ? 'Pro' : ''} ${page.value.framework === 'vue' ? ' for Vue' : ''}`,
|
||||
description: page.value.description,
|
||||
ogDescription: page.value.description
|
||||
})
|
||||
@@ -112,7 +112,7 @@ const communityLinks = computed(() => [{
|
||||
to: 'https://nuxt.lemonsqueezy.com/affiliates',
|
||||
target: '_blank'
|
||||
}, {
|
||||
icon: 'i-lucide-life-buoy',
|
||||
icon: 'i-lucide-git-pull-request-arrow',
|
||||
label: 'Contribution',
|
||||
to: '/getting-started/contribution'
|
||||
}, {
|
||||
|
||||
@@ -7,7 +7,7 @@ const title = 'Vue Components'
|
||||
const description = 'Explore 99+ customizable UI components for Vue and Nuxt built with Tailwind CSS and Reka UI.'
|
||||
|
||||
useSeoMeta({
|
||||
titleTemplate: `%s - Nuxt UI`,
|
||||
titleTemplate: '%s - Nuxt UI',
|
||||
title,
|
||||
description,
|
||||
ogTitle: `${title} - Nuxt UI`,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { animate } from 'motion-v'
|
||||
import { joinURL } from 'ufo'
|
||||
|
||||
const { url } = useSiteConfig()
|
||||
|
||||
useSeoMeta({
|
||||
title: page.title,
|
||||
description: page.description,
|
||||
@@ -155,7 +156,7 @@ onMounted(async () => {
|
||||
:src="item.src"
|
||||
:alt="item.alt"
|
||||
class="w-full h-auto rounded-[calc(var(--ui-radius)*2)]"
|
||||
lazy
|
||||
loading="lazy"
|
||||
/>
|
||||
</template>
|
||||
</UTabs>
|
||||
@@ -165,7 +166,7 @@ onMounted(async () => {
|
||||
v-if="page.section2.image"
|
||||
v-bind="page.section2.image"
|
||||
class="w-full h-auto rounded-[calc(var(--ui-radius)*2)]"
|
||||
lazy
|
||||
loading="lazy"
|
||||
/>
|
||||
</UPageSection>
|
||||
<UPageSection v-bind="page.section3" orientation="horizontal" :ui="{ container: 'py-16 sm:pt-16 lg:pt-16' }">
|
||||
@@ -173,7 +174,7 @@ onMounted(async () => {
|
||||
v-if="page.section3.image"
|
||||
v-bind="page.section3.image"
|
||||
class="w-full h-auto rounded-[calc(var(--ui-radius)*2)]"
|
||||
lazy
|
||||
loading="lazy"
|
||||
/>
|
||||
</UPageSection>
|
||||
<USeparator />
|
||||
@@ -198,7 +199,7 @@ onMounted(async () => {
|
||||
v-if="step.image"
|
||||
v-bind="step.image"
|
||||
class="rounded-(--ui-radius)"
|
||||
lazy
|
||||
loading="lazy"
|
||||
/>
|
||||
<div>
|
||||
<h2 class="font-semibold inline-flex items-center gap-x-1">
|
||||
@@ -272,6 +273,7 @@ onMounted(async () => {
|
||||
:key="index"
|
||||
v-bind="logo"
|
||||
class="h-6 shrink-0 max-w-[140px] filter invert dark:invert-0"
|
||||
loading="lazy"
|
||||
>
|
||||
</UPageMarquee>
|
||||
</UPageCTA>
|
||||
|
||||
@@ -23,18 +23,7 @@ const { data: components } = await useAsyncData('ui-components', () => {
|
||||
.all()
|
||||
})
|
||||
|
||||
const { data: module } = await useFetch<{
|
||||
stats: {
|
||||
downloads: number
|
||||
stars: number
|
||||
}
|
||||
contributors: {
|
||||
username: string
|
||||
}[]
|
||||
}>('https://api.nuxt.com/modules/ui', {
|
||||
key: 'stats',
|
||||
transform: ({ stats, contributors }) => ({ stats, contributors })
|
||||
})
|
||||
const { data: module } = await useFetch('/api/module.json')
|
||||
|
||||
const { format } = Intl.NumberFormat('en', { notation: 'compact' })
|
||||
|
||||
@@ -85,7 +74,7 @@ useIntersectionObserver(contributorsRef, ([entry]) => {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<LazySkyBg />
|
||||
<LazySkyBg is-index />
|
||||
|
||||
<div class="h-[344px] lg:h-full lg:relative w-full lg:min-h-[calc(100vh-var(--ui-header-height)-1px)] overflow-hidden">
|
||||
<UPageMarquee
|
||||
@@ -176,12 +165,12 @@ useIntersectionObserver(contributorsRef, ([entry]) => {
|
||||
<circle cx="6.53711" cy="37.4551" r="1.5" fill="var(--ui-border-accented)" />
|
||||
<circle cx="38.5957" cy="37.4551" r="1.5" fill="var(--ui-border-accented)" />
|
||||
</svg>
|
||||
<UIcon :name="feature.icon" class="size-5 flex-shrink-0" />
|
||||
<UIcon :name="feature.icon" class="size-5 shrink-0" />
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<h2 class="font-medium text-(--ui-text-highlighted) inline-flex items-center gap-x-1">
|
||||
{{ feature.title }}
|
||||
<UIcon v-if="feature.to" name="i-lucide-arrow-right" class="size-4 flex-shrink-0 opacity-0 group-hover:opacity-100 transition-all duration-200 -translate-x-1 group-hover:translate-x-0" />
|
||||
<UIcon v-if="feature.to" name="i-lucide-arrow-right" class="size-4 shrink-0 opacity-0 group-hover:opacity-100 transition-all duration-200 -translate-x-1 group-hover:translate-x-0" />
|
||||
</h2>
|
||||
<p class="text-sm text-(--ui-text-muted)">
|
||||
{{ feature.description }}
|
||||
@@ -313,8 +302,8 @@ useIntersectionObserver(contributorsRef, ([entry]) => {
|
||||
:src="`/pro/blocks/image${i}.png`"
|
||||
width="460"
|
||||
height="258"
|
||||
:alt="`Nuxt UI Pro Screenshot ${i}`"
|
||||
loading="lazy"
|
||||
:alt="`Nuxt UI Pro Screenshot ${i}`"
|
||||
class="aspect-video border border-(--ui-border) rounded-[calc(var(--ui-radius)*2)] bg-white"
|
||||
>
|
||||
</UPageMarquee>
|
||||
|
||||
@@ -12,7 +12,7 @@ const { url } = useSiteConfig()
|
||||
useSeoMeta({
|
||||
title,
|
||||
description,
|
||||
ogTitle: `${title} - Nuxt UI Pro`,
|
||||
ogTitle: title,
|
||||
ogDescription: description,
|
||||
ogImage: joinURL(url, '/pro/og-image.png')
|
||||
})
|
||||
@@ -33,7 +33,7 @@ const activating = ref(false)
|
||||
const successMessage = ref()
|
||||
const errorMessage = ref('')
|
||||
|
||||
async function submit(event: FormSubmitEvent<any>) {
|
||||
async function submit(event: FormSubmitEvent<Schema>) {
|
||||
activating.value = true
|
||||
errorMessage.value = ''
|
||||
successMessage.value = ''
|
||||
|
||||
@@ -9,10 +9,10 @@ const { url } = useSiteConfig()
|
||||
|
||||
useSeoMeta({
|
||||
title: page.title,
|
||||
ogTitle: page.title,
|
||||
ogImage: joinURL(url, '/pro/og-image.png'),
|
||||
description: page.description,
|
||||
ogDescription: page.description
|
||||
ogTitle: page.title,
|
||||
ogDescription: page.description,
|
||||
ogImage: joinURL(url, '/pro/og-image.png')
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ const { url } = useSiteConfig()
|
||||
|
||||
useSeoMeta({
|
||||
title: page.title,
|
||||
ogTitle: page.title,
|
||||
description: page.description,
|
||||
ogTitle: page.title,
|
||||
ogDescription: page.description,
|
||||
ogImage: joinURL(url, '/pro/og-image.png')
|
||||
})
|
||||
@@ -81,6 +81,7 @@ useSeoMeta({
|
||||
:key="index"
|
||||
v-bind="logo"
|
||||
class="h-6 shrink-0 max-w-[140px] filter invert dark:invert-0"
|
||||
loading="lazy"
|
||||
>
|
||||
</UPageMarquee>
|
||||
<UContainer>
|
||||
|
||||
@@ -67,7 +67,7 @@ useSeoMeta({
|
||||
:items="(template.images as any[])"
|
||||
dots
|
||||
>
|
||||
<NuxtImg v-bind="item" class="w-full h-full object-cover" width="576" height="360" />
|
||||
<NuxtImg v-bind="item" class="w-full h-full object-cover" width="576" height="360" loading="lazy" />
|
||||
</UCarousel>
|
||||
<Placeholder v-else class="w-full h-full aspect-video" />
|
||||
</Motion>
|
||||
|
||||
@@ -13,7 +13,7 @@ const description = page.value.description
|
||||
useSeoMeta({
|
||||
title,
|
||||
description,
|
||||
ogTitle: `${title} - Nuxt UI Pro`,
|
||||
ogTitle: title,
|
||||
ogDescription: description,
|
||||
ogImage: joinURL(url, '/pro/og-image.png')
|
||||
})
|
||||
|
||||
@@ -5,8 +5,9 @@ const description = 'Discover our Volta board for @nuxt/ui development status.'
|
||||
useSeoMeta({
|
||||
titleTemplate: '%s - Nuxt UI',
|
||||
title,
|
||||
ogTitle: 'Nuxt UI Roadmap',
|
||||
description
|
||||
description,
|
||||
ogTitle: `${title} - Nuxt UI`,
|
||||
ogDescription: description
|
||||
})
|
||||
|
||||
defineOgImageComponent('Docs', {
|
||||
|
||||
83
docs/app/pages/showcase.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<script setup lang="ts">
|
||||
const { data: page } = await useAsyncData('showcase', () => queryCollection('showcase').first())
|
||||
if (!page.value) {
|
||||
throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
|
||||
}
|
||||
|
||||
useSeoMeta({
|
||||
titleTemplate: `%s - Nuxt UI`,
|
||||
title: page.value.title,
|
||||
description: page.value.description,
|
||||
ogTitle: `${page.value.title} - Nuxt UI`,
|
||||
ogDescription: page.value.description
|
||||
})
|
||||
|
||||
defineOgImageComponent('Docs', {
|
||||
headline: 'Community'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UMain v-if="page">
|
||||
<UPageHero
|
||||
:title="page.hero.title"
|
||||
:description="page.hero.description"
|
||||
:links="page.hero.links"
|
||||
:ui="{
|
||||
wrapper: 'lg:px-12',
|
||||
container: 'relative'
|
||||
}"
|
||||
>
|
||||
<template #top>
|
||||
<div class="absolute z-[-1] rounded-full bg-(--ui-primary) blur-[300px] size-60 sm:size-80 transform -translate-x-1/2 left-1/2 -translate-y-80" />
|
||||
</template>
|
||||
|
||||
<LazyStarsBg />
|
||||
|
||||
<div aria-hidden="true" class="hidden lg:block absolute z-[-1] border-x border-(--ui-border) inset-0 mx-4 sm:mx-6 lg:mx-8" />
|
||||
|
||||
<div class="border-l border-t border-(--ui-border)">
|
||||
<ul class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 items-start justify-center divide-y divide-x divide-(--ui-border)">
|
||||
<li
|
||||
v-for="item in page.items"
|
||||
:key="item.name"
|
||||
class="group relative flex items-center justify-center flex-1 size-full p-2 last:border-r last:border-b border-(--ui-border) overflow-hidden"
|
||||
>
|
||||
<NuxtLink class="inset-0 absolute" :to="item.url" target="_blank">
|
||||
<span class="sr-only">Go to {{ item.name }}</span>
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtImg
|
||||
:src="`/assets/showcase/${item.name.toLowerCase().replace(/\s/g, '-')}.png`"
|
||||
:alt="`Screenshot of ${item.name}`"
|
||||
width="327"
|
||||
height="184"
|
||||
:modifiers="{
|
||||
position: 'top'
|
||||
}"
|
||||
class="aspect-[16/9] size-full opacity-75 group-hover:opacity-100 group-hover:scale-110 duration-200 transition-[scale,opacity] pointer-events-none"
|
||||
/>
|
||||
|
||||
<div class="absolute flex items-center px-2.5 py-0.75 gap-1 opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none bg-black/90 rounded-full">
|
||||
<span class="text-sm text-white font-medium">
|
||||
{{ item.name }}
|
||||
</span>
|
||||
<UIcon name="i-lucide-arrow-up-right" class="size-4 shrink-0 text-white" />
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center -mb-[36px]">
|
||||
<UButton
|
||||
label="Submit your project"
|
||||
trailing-icon="i-lucide-plus"
|
||||
color="neutral"
|
||||
size="lg"
|
||||
to="https://github.com/nuxt/ui/edit/v3/docs/content/showcase.yml"
|
||||
target="_blank"
|
||||
/>
|
||||
</div>
|
||||
</UPageHero>
|
||||
</UMain>
|
||||
</template>
|
||||
156
docs/app/pages/team.vue
Normal file
@@ -0,0 +1,156 @@
|
||||
<script setup lang="ts">
|
||||
const title = 'Meet the Team'
|
||||
const description = 'The development of Nuxt UI is led by a community of developers from all over the world.'
|
||||
|
||||
useSeoMeta({
|
||||
titleTemplate: '%s - Nuxt UI',
|
||||
title,
|
||||
description,
|
||||
ogTitle: `${title} - Nuxt UI`,
|
||||
ogDescription: description
|
||||
})
|
||||
|
||||
defineOgImageComponent('Docs', {
|
||||
headline: 'Community'
|
||||
})
|
||||
|
||||
const { data: module } = await useFetch('/api/module.json')
|
||||
|
||||
const contributors = computed(() => module.value?.contributors?.filter(contributor => !module.value?.team?.find(user => user.login === contributor.username)))
|
||||
|
||||
const icons = {
|
||||
website: 'i-lucide-link',
|
||||
twitter: 'i-simple-icons-x',
|
||||
twitch: 'i-simple-icons-twitch',
|
||||
youtube: 'i-simple-icons-youtube',
|
||||
instagram: 'i-simple-icons-instagram',
|
||||
linkedin: 'i-simple-icons-linkedin',
|
||||
mastodon: 'i-simple-icons-mastodon',
|
||||
bluesky: 'i-simple-icons-bluesky',
|
||||
github: 'i-simple-icons-github'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UMain>
|
||||
<UPageHero
|
||||
:title="title"
|
||||
:description="description"
|
||||
class="relative"
|
||||
orientation="vertical"
|
||||
:ui="{ title: 'text-balance', container: 'relative' }"
|
||||
>
|
||||
<template #top>
|
||||
<div class="absolute z-[-1] rounded-full bg-(--ui-primary) blur-[300px] size-60 sm:size-80 transform -translate-x-1/2 left-1/2 -translate-y-80" />
|
||||
</template>
|
||||
|
||||
<LazyStarsBg />
|
||||
</UPageHero>
|
||||
|
||||
<UPageSection :ui="{ container: '!pt-0' }">
|
||||
<UPageGrid class="xl:grid-cols-4">
|
||||
<UPageCard
|
||||
v-for="(user, index) in module?.team"
|
||||
:key="index"
|
||||
:title="user.name"
|
||||
:description="[user.pronouns, user.location].filter(Boolean).join(' ・ ')"
|
||||
:ui="{
|
||||
container: 'gap-y-4 lg:p-8',
|
||||
leading: 'flex justify-center',
|
||||
title: 'text-center',
|
||||
description: 'text-center text-(--ui-text-muted)'
|
||||
}"
|
||||
variant="subtle"
|
||||
>
|
||||
<template #leading>
|
||||
<UAvatar
|
||||
:src="`https://ipx.nuxt.com/f_auto,s_80x80/gh_avatar/${user.login}`"
|
||||
:srcset="`https://ipx.nuxt.com/f_auto,s_160x160/gh_avatar/${user.login} 2x`"
|
||||
size="3xl"
|
||||
class="mx-auto"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<UButton
|
||||
v-for="(link, key) in user.socialAccounts"
|
||||
:key="key"
|
||||
color="neutral"
|
||||
variant="link"
|
||||
:to="link.url"
|
||||
:icon="icons[key as keyof typeof icons] || icons.website"
|
||||
:alt="`Link to ${user.name}'s ${key} profile`"
|
||||
target="_blank"
|
||||
size="sm"
|
||||
/>
|
||||
<UButton
|
||||
:to="`https://github.com/${user.login}`"
|
||||
color="neutral"
|
||||
variant="link"
|
||||
:alt="`Link to ${user.name}'s GitHub profile`"
|
||||
:icon="icons.github"
|
||||
target="_blank"
|
||||
/>
|
||||
<UButton
|
||||
v-if="user.websiteUrl"
|
||||
:to="user.websiteUrl"
|
||||
color="neutral"
|
||||
variant="link"
|
||||
:alt="`Link to ${user.name}'s personal website`"
|
||||
:icon="icons.website"
|
||||
target="_blank"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="user.sponsorsListing" class="flex items-center justify-center">
|
||||
<UButton
|
||||
:to="user.sponsorsListing"
|
||||
target="_blank"
|
||||
color="neutral"
|
||||
variant="subtle"
|
||||
icon="i-lucide-heart"
|
||||
label="Sponsor"
|
||||
:ui="{ leadingIcon: 'text-pink-500 dark:text-pink-400' }"
|
||||
/>
|
||||
</div>
|
||||
</UPageCard>
|
||||
</UPageGrid>
|
||||
|
||||
<ProseHr />
|
||||
|
||||
<UPageGrid class="xl:grid-cols-6">
|
||||
<UPageCard
|
||||
v-for="contributor in contributors"
|
||||
:key="contributor.username"
|
||||
:title="contributor.username"
|
||||
:ui="{
|
||||
container: 'gap-y-2',
|
||||
leading: 'flex justify-center',
|
||||
title: 'text-center',
|
||||
description: 'text-center text-(--ui-text-muted)'
|
||||
}"
|
||||
>
|
||||
<template #leading>
|
||||
<UAvatar
|
||||
:src="`https://ipx.nuxt.com/f_auto,s_80x80/gh_avatar/${contributor.username}`"
|
||||
:srcset="`https://ipx.nuxt.com/f_auto,s_160x160/gh_avatar/${contributor.username} 2x`"
|
||||
size="3xl"
|
||||
class="mx-auto"
|
||||
loading="lazy"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<UButton
|
||||
:to="`https://github.com/${contributor.username}`"
|
||||
color="neutral"
|
||||
variant="link"
|
||||
:alt="`Link to ${contributor.username}'s GitHub profile`"
|
||||
:icon="icons.github"
|
||||
target="_blank"
|
||||
/>
|
||||
</div>
|
||||
</UPageCard>
|
||||
</UPageGrid>
|
||||
</UPageSection>
|
||||
</UMain>
|
||||
</template>
|
||||
@@ -1,6 +1,18 @@
|
||||
import { defineCollection, z } from '@nuxt/content'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
const Button = z.object({
|
||||
label: z.string(),
|
||||
icon: z.string().optional(),
|
||||
trailingIcon: z.string().optional(),
|
||||
to: z.string().optional(),
|
||||
color: z.enum(['primary', 'neutral', 'success', 'warning', 'error', 'info']).optional(),
|
||||
size: z.enum(['xs', 'sm', 'md', 'lg', 'xl']).optional(),
|
||||
variant: z.enum(['solid', 'outline', 'subtle', 'soft', 'ghost', 'link']).optional(),
|
||||
id: z.string().optional(),
|
||||
target: z.enum(['_blank', '_self']).optional()
|
||||
})
|
||||
|
||||
const schema = z.object({
|
||||
category: z.enum(['layout', 'form', 'element', 'navigation', 'data', 'overlay']).optional(),
|
||||
framework: z.string().optional(),
|
||||
@@ -42,5 +54,26 @@ export const collections = {
|
||||
include: '**/*'
|
||||
}, pro!].filter(Boolean),
|
||||
schema
|
||||
}),
|
||||
showcase: defineCollection({
|
||||
type: 'page',
|
||||
source: 'showcase.yml',
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
hero: z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
links: z.array(Button)
|
||||
}),
|
||||
items: z.array(z.object({
|
||||
name: z.string(),
|
||||
url: z.string(),
|
||||
screenshotUrl: z.string().optional(),
|
||||
screenshotOptions: z.object({
|
||||
delay: z.number()
|
||||
})
|
||||
}))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ In Nuxt UI v2, we had a mix between a design system with `primary`, `gray`, `err
|
||||
|
||||
This change introduces several breaking changes that you need to be aware of:
|
||||
|
||||
1. The `gray` color has been renamed to `neutral`
|
||||
- The `gray` color has been renamed to `neutral`
|
||||
|
||||
```diff
|
||||
<template>
|
||||
@@ -203,7 +203,7 @@ You can also use the new [design tokens](/getting-started/theme#neutral-palette)
|
||||
```
|
||||
::
|
||||
|
||||
2. The `DEFAULT` shade that let you write `text-primary` no longer exists, you can use [color shades](/getting-started/theme#color-shades) instead:
|
||||
- The `DEFAULT` shade that let you write `text-primary` no longer exists, you can use [color shades](/getting-started/theme#color-shades) instead:
|
||||
|
||||
```diff
|
||||
<template>
|
||||
@@ -212,7 +212,7 @@ You can also use the new [design tokens](/getting-started/theme#neutral-palette)
|
||||
</template>
|
||||
```
|
||||
|
||||
3. The `gray`, `black` and `white` in the `color` props have been removed in favor of `neutral`:
|
||||
- The `gray`, `black` and `white` in the `color` props have been removed in favor of `neutral`:
|
||||
|
||||
```diff
|
||||
- <UButton color="black" />
|
||||
@@ -225,7 +225,7 @@ You can also use the new [design tokens](/getting-started/theme#neutral-palette)
|
||||
+ <UButton color="neutral" variant="outline" />
|
||||
```
|
||||
|
||||
4. You can no longer use Tailwind CSS colors in the `color` props, use the new aliases instead:
|
||||
- You can no longer use Tailwind CSS colors in the `color` props, use the new aliases instead:
|
||||
|
||||
```diff
|
||||
- <UButton color="red" />
|
||||
@@ -236,7 +236,7 @@ You can also use the new [design tokens](/getting-started/theme#neutral-palette)
|
||||
Learn how to extend the design system to add new color aliases.
|
||||
::
|
||||
|
||||
5. The color configuration in `app.config.ts` has been moved into a `colors` object:
|
||||
- The color configuration in `app.config.ts` has been moved into a `colors` object:
|
||||
|
||||
```diff
|
||||
export default defineAppConfig({
|
||||
@@ -255,7 +255,7 @@ export default defineAppConfig({
|
||||
|
||||
Nuxt UI components are now styled using the [Tailwind Variants API](/getting-started/theme#components-theme), which makes all the overrides you made using the `app.config.ts` and the `ui` prop obsolete.
|
||||
|
||||
1. Update your [`app.config.ts`](/getting-started/theme#config) to override components with their new theme:
|
||||
- Update your [`app.config.ts`](/getting-started/theme#config) to override components with their new theme:
|
||||
|
||||
```diff
|
||||
export default defineAppConfig({
|
||||
@@ -278,7 +278,7 @@ export default defineAppConfig({
|
||||
})
|
||||
```
|
||||
|
||||
2. Update your [`ui` props](/getting-started/theme#props) to override each component's slots using their new theme:
|
||||
- Update your [`ui` props](/getting-started/theme#props) to override each component's slots using their new theme:
|
||||
|
||||
```diff
|
||||
<template>
|
||||
@@ -351,7 +351,7 @@ Here are the Nuxt UI Pro components that have been renamed or removed:
|
||||
|
||||
In addition to the renamed components, there are lots of changes to the components API. Let's detail the most important ones:
|
||||
|
||||
1. The `links` and `options` props have been renamed to `items` for consistency:
|
||||
- The `links` and `options` props have been renamed to `items` for consistency:
|
||||
|
||||
```diff
|
||||
<template>
|
||||
@@ -367,7 +367,25 @@ In addition to the renamed components, there are lots of changes to the componen
|
||||
This change affects the following components: `Breadcrumb`, `HorizontalNavigation`, `InputMenu`, `RadioGroup`, `Select`, `SelectMenu`, `VerticalNavigation`.
|
||||
::
|
||||
|
||||
2. The global `Modals`, `Slideovers` and `Notifications` components have been removed in favor the [App](/components/app) component:
|
||||
- The `click` field in different components has been removed in favor of the native Vue `onClick` event:
|
||||
|
||||
```diff
|
||||
<script setup lang="ts">
|
||||
const items = [{
|
||||
label: 'Edit',
|
||||
- click: () => {
|
||||
+ onClick: () => {
|
||||
console.log('Edit')
|
||||
}
|
||||
}]
|
||||
</script>
|
||||
```
|
||||
|
||||
::note
|
||||
This change affects the `Toast` component as well as all component that have `items` links like `NavigationMenu`, `DropdownMenu`, `CommandPalette`, etc.
|
||||
::
|
||||
|
||||
- The global `Modals`, `Slideovers` and `Notifications` components have been removed in favor the [App](/components/app) component:
|
||||
|
||||
```diff [app.vue]
|
||||
<template>
|
||||
@@ -380,7 +398,7 @@ This change affects the following components: `Breadcrumb`, `HorizontalNavigatio
|
||||
</template>
|
||||
```
|
||||
|
||||
3. The `v-model:open` directive and `default-open` prop are now used to control visibility:
|
||||
- The `v-model:open` directive and `default-open` prop are now used to control visibility:
|
||||
|
||||
```diff
|
||||
<template>
|
||||
@@ -393,7 +411,7 @@ This change affects the following components: `Breadcrumb`, `HorizontalNavigatio
|
||||
This change affects the following components: `ContextMenu`, `Modal` and `Slideover` and enables controlling visibility for `InputMenu`, `Select`, `SelectMenu` and `Tooltip`.
|
||||
::
|
||||
|
||||
4. The default slot is now used for the trigger and the content goes inside the `#content` slot (you don't need to use a `v-model:open` directive with this method):
|
||||
- The default slot is now used for the trigger and the content goes inside the `#content` slot (you don't need to use a `v-model:open` directive with this method):
|
||||
|
||||
```diff
|
||||
<script setup lang="ts">
|
||||
@@ -420,7 +438,7 @@ This change affects the following components: `ContextMenu`, `Modal` and `Slideo
|
||||
This change affects the following components: `Modal`, `Popover`, `Slideover`, `Tooltip`.
|
||||
::
|
||||
|
||||
5. A `#header`, `#body` and `#footer` slots have been added inside the `#content` slot like the `Card` component:
|
||||
- A `#header`, `#body` and `#footer` slots have been added inside the `#content` slot like the `Card` component:
|
||||
|
||||
```diff
|
||||
<template>
|
||||
@@ -439,10 +457,9 @@ This change affects the following components: `Modal`, `Popover`, `Slideover`, `
|
||||
This change affects the following components: `Modal`, `Slideover`.
|
||||
::
|
||||
|
||||
|
||||
### Changed composables
|
||||
|
||||
1. The `useToast()` composable `timeout` prop has been renamed to `duration`:
|
||||
- The `useToast()` composable `timeout` prop has been renamed to `duration`:
|
||||
|
||||
```diff
|
||||
<script setup lang="ts">
|
||||
@@ -453,7 +470,7 @@ const toast = useToast()
|
||||
</script>
|
||||
```
|
||||
|
||||
2. The `useModal` and `useSlideover` composables have been removed in favor of a more generic `useOverlay` composable:
|
||||
- The `useModal` and `useSlideover` composables have been removed in favor of a more generic `useOverlay` composable:
|
||||
|
||||
Some important differences:
|
||||
- The `useOverlay` composable is now used to create overlay instances
|
||||
|
||||
@@ -306,6 +306,17 @@ name: 'drawer-dismissible-example'
|
||||
In this example, the `header` slot is used to add a close button which is not done by default.
|
||||
::
|
||||
|
||||
### Responsive drawer
|
||||
|
||||
You can render a [Modal](/components/modal) component on desktop and a Drawer on mobile for example.
|
||||
|
||||
::component-example
|
||||
---
|
||||
prettier: true
|
||||
name: 'drawer-responsive-example'
|
||||
---
|
||||
::
|
||||
|
||||
### With footer slot
|
||||
|
||||
Use the `#footer` slot to add content after the Drawer's body.
|
||||
|
||||
@@ -177,6 +177,12 @@ props:
|
||||
|
||||
:component-emits
|
||||
|
||||
When accessing the component via a template ref, you can use the following:
|
||||
|
||||
| Name | Type |
|
||||
| ---- | ---- |
|
||||
| `inputsRef`{lang="ts-type"} | `Ref<ComponentPublicInstance[]>`{lang="ts-type"} |
|
||||
|
||||
## Theme
|
||||
|
||||
:component-theme
|
||||
|
||||
@@ -17,7 +17,7 @@ Use the `v-model` directive to control the value of the RadioGroup or the `defau
|
||||
|
||||
### Items
|
||||
|
||||
Use the `items` prop as an array of strings, numbers or booleans:
|
||||
Use the `items` prop as an array of strings or numbers:
|
||||
|
||||
::component-code
|
||||
---
|
||||
@@ -133,30 +133,6 @@ props:
|
||||
---
|
||||
::
|
||||
|
||||
### Orientation
|
||||
|
||||
Use the `orientation` prop to change the orientation of the RadioGroup. Defaults to `vertical`.
|
||||
|
||||
::component-code
|
||||
---
|
||||
prettier: true
|
||||
ignore:
|
||||
- defaultValue
|
||||
- items
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- RadioGroupItem[]
|
||||
props:
|
||||
orientation: 'horizontal'
|
||||
defaultValue: 'System'
|
||||
items:
|
||||
- 'System'
|
||||
- 'Light'
|
||||
- 'Dark'
|
||||
---
|
||||
::
|
||||
|
||||
### Color
|
||||
|
||||
Use the `color` prop to change the color of the RadioGroup.
|
||||
@@ -181,6 +157,35 @@ props:
|
||||
---
|
||||
::
|
||||
|
||||
### Variant :badge{label="Not released" class="align-text-top"}
|
||||
|
||||
Use the `variant` prop to change the variant of the RadioGroup.
|
||||
|
||||
::component-code
|
||||
---
|
||||
prettier: true
|
||||
ignore:
|
||||
- defaultValue
|
||||
- items
|
||||
external:
|
||||
- items
|
||||
props:
|
||||
color: 'primary'
|
||||
variant: 'table'
|
||||
defaultValue: 'pro'
|
||||
items:
|
||||
- label: 'Pro'
|
||||
value: 'pro'
|
||||
description: 'Tailored for indie hackers, freelancers and solo founders.'
|
||||
- label: 'Startup'
|
||||
value: 'startup'
|
||||
description: 'Best suited for small teams, startups and agencies.'
|
||||
- label: 'Enterprise'
|
||||
value: 'enterprise'
|
||||
description: 'Ideal for larger teams and organizations.'
|
||||
---
|
||||
::
|
||||
|
||||
### Size
|
||||
|
||||
Use the `size` prop to change the size of the RadioGroup.
|
||||
@@ -197,6 +202,57 @@ externalTypes:
|
||||
- RadioGroupItem[]
|
||||
props:
|
||||
size: 'xl'
|
||||
variant: 'list'
|
||||
defaultValue: 'System'
|
||||
items:
|
||||
- 'System'
|
||||
- 'Light'
|
||||
- 'Dark'
|
||||
---
|
||||
::
|
||||
|
||||
### Orientation
|
||||
|
||||
Use the `orientation` prop to change the orientation of the RadioGroup. Defaults to `vertical`.
|
||||
|
||||
::component-code
|
||||
---
|
||||
prettier: true
|
||||
ignore:
|
||||
- defaultValue
|
||||
- items
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- RadioGroupItem[]
|
||||
props:
|
||||
orientation: 'horizontal'
|
||||
variant: 'list'
|
||||
defaultValue: 'System'
|
||||
items:
|
||||
- 'System'
|
||||
- 'Light'
|
||||
- 'Dark'
|
||||
---
|
||||
::
|
||||
|
||||
### Indicator :badge{label="Not released" class="align-text-top"}
|
||||
|
||||
Use the `indicator` prop to change the position or hide the indicator. Defaults to `start`.
|
||||
|
||||
::component-code
|
||||
---
|
||||
prettier: true
|
||||
ignore:
|
||||
- defaultValue
|
||||
- items
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- RadioGroupItem[]
|
||||
props:
|
||||
indicator: 'end'
|
||||
variant: 'card'
|
||||
defaultValue: 'System'
|
||||
items:
|
||||
- 'System'
|
||||
|
||||
@@ -22,6 +22,17 @@ props:
|
||||
---
|
||||
::
|
||||
|
||||
### Rows
|
||||
|
||||
Use the `rows` prop to set the number of rows. Defaults to `3`.
|
||||
|
||||
::component-code
|
||||
---
|
||||
props:
|
||||
rows: 12
|
||||
---
|
||||
::
|
||||
|
||||
### Placeholder
|
||||
|
||||
Use the `placeholder` prop to set a placeholder text.
|
||||
@@ -33,6 +44,37 @@ props:
|
||||
---
|
||||
::
|
||||
|
||||
### Autoresize
|
||||
|
||||
Use the `autoresize` prop to enable autoresizing the height of the Textarea.
|
||||
|
||||
::component-code
|
||||
---
|
||||
ignore:
|
||||
- modelValue
|
||||
external:
|
||||
- modelValue
|
||||
props:
|
||||
modelValue: 'This is a long text that will autoresize the height of the Textarea.'
|
||||
autoresize: true
|
||||
---
|
||||
::
|
||||
|
||||
Use the `maxrows` prop to set the maximum number of rows when autoresizing. If set to `0`, the Textarea will grow indefinitely.
|
||||
|
||||
::component-code
|
||||
---
|
||||
ignore:
|
||||
- modelValue
|
||||
external:
|
||||
- modelValue
|
||||
props:
|
||||
modelValue: 'This is a long text that will autoresize the height of the Textarea with a maximum of 4 rows.'
|
||||
maxrows: 4
|
||||
autoresize: true
|
||||
---
|
||||
::
|
||||
|
||||
### Color
|
||||
|
||||
Use the `color` prop to change the ring color when the Textarea is focused.
|
||||
@@ -82,6 +124,102 @@ props:
|
||||
---
|
||||
::
|
||||
|
||||
### Icon :badge{label="Not released" class="align-text-top"}
|
||||
|
||||
Use the `icon` prop to show an [Icon](/components/icon) inside the Textarea.
|
||||
|
||||
::component-code
|
||||
---
|
||||
prettier: true
|
||||
ignore:
|
||||
- placeholder
|
||||
props:
|
||||
icon: 'i-lucide-search'
|
||||
size: md
|
||||
variant: outline
|
||||
placeholder: 'Search...'
|
||||
rows: 1
|
||||
---
|
||||
::
|
||||
|
||||
Use the `leading` and `trailing` props to set the icon position or the `leading-icon` and `trailing-icon` props to set a different icon for each position.
|
||||
|
||||
::component-code
|
||||
---
|
||||
prettier: true
|
||||
ignore:
|
||||
- placeholder
|
||||
props:
|
||||
trailingIcon: i-lucide-at-sign
|
||||
placeholder: 'Enter your email'
|
||||
size: md
|
||||
rows: 1
|
||||
---
|
||||
::
|
||||
|
||||
### Avatar :badge{label="Not released" class="align-text-top"}
|
||||
|
||||
Use the `avatar` prop to show an [Avatar](/components/avatar) inside the Textarea.
|
||||
|
||||
::component-code
|
||||
---
|
||||
prettier: true
|
||||
ignore:
|
||||
- placeholder
|
||||
props:
|
||||
avatar:
|
||||
src: 'https://github.com/nuxt.png'
|
||||
size: md
|
||||
variant: outline
|
||||
placeholder: 'Search...'
|
||||
rows: 1
|
||||
---
|
||||
::
|
||||
|
||||
### Loading :badge{label="Not released" class="align-text-top"}
|
||||
|
||||
Use the `loading` prop to show a loading icon on the Textarea.
|
||||
|
||||
::component-code
|
||||
---
|
||||
ignore:
|
||||
- placeholder
|
||||
props:
|
||||
loading: true
|
||||
trailing: false
|
||||
placeholder: 'Search...'
|
||||
rows: 1
|
||||
---
|
||||
::
|
||||
|
||||
### Loading Icon :badge{label="Not released" class="align-text-top"}
|
||||
|
||||
Use the `loading-icon` prop to customize the loading icon. Defaults to `i-lucide-refresh-cw`.
|
||||
|
||||
::component-code
|
||||
---
|
||||
ignore:
|
||||
- placeholder
|
||||
props:
|
||||
loading: true
|
||||
loadingIcon: 'i-lucide-repeat-2'
|
||||
placeholder: 'Search...'
|
||||
rows: 1
|
||||
---
|
||||
::
|
||||
|
||||
::framework-only
|
||||
#nuxt
|
||||
:::tip{to="/getting-started/icons/nuxt#theme"}
|
||||
You can customize this icon globally in your `app.config.ts` under `ui.icons.loading` key.
|
||||
:::
|
||||
|
||||
#vue
|
||||
:::tip{to="/getting-started/icons/vue#theme"}
|
||||
You can customize this icon globally in your `vite.config.ts` under `ui.icons.loading` key.
|
||||
:::
|
||||
::
|
||||
|
||||
### Disabled
|
||||
|
||||
Use the `disabled` prop to disable the Textarea.
|
||||
@@ -96,48 +234,6 @@ props:
|
||||
---
|
||||
::
|
||||
|
||||
### Rows
|
||||
|
||||
Use the `rows` prop to set the number of rows. Defaults to `3`.
|
||||
|
||||
::component-code
|
||||
---
|
||||
props:
|
||||
rows: 12
|
||||
---
|
||||
::
|
||||
|
||||
### Autoresize
|
||||
|
||||
Use the `autoresize` prop to enable autoresizing the height of the Textarea.
|
||||
|
||||
::component-code
|
||||
---
|
||||
ignore:
|
||||
- modelValue
|
||||
external:
|
||||
- modelValue
|
||||
props:
|
||||
modelValue: 'This is a long text that will autoresize the height of the Textarea.'
|
||||
autoresize: true
|
||||
---
|
||||
::
|
||||
|
||||
Use the `maxrows` prop to set the maximum number of rows when autoresizing. If set to `0`, the Textarea will grow indefinitely.
|
||||
|
||||
::component-code
|
||||
---
|
||||
ignore:
|
||||
- modelValue
|
||||
external:
|
||||
- modelValue
|
||||
props:
|
||||
modelValue: 'This is a long text that will autoresize the height of the Textarea with a maximum of 4 rows.'
|
||||
maxrows: 4
|
||||
autoresize: true
|
||||
---
|
||||
::
|
||||
|
||||
## API
|
||||
|
||||
### Props
|
||||
|
||||
53
docs/content/showcase.yml
Normal file
@@ -0,0 +1,53 @@
|
||||
title: Showcase
|
||||
description: Check out some of the amazing projects built with Nuxt UI.
|
||||
navigation: false
|
||||
hero:
|
||||
title: Showcase
|
||||
description: Discover our selection of projects built with Nuxt UI.
|
||||
items:
|
||||
- name: Details
|
||||
url: https://details.team/
|
||||
- name: Directus Docs
|
||||
url: https://docs.directus.io/
|
||||
- name: Espace Asso by Benevolt
|
||||
url: https://asso.benevolt.fr/
|
||||
- name: Juno.one
|
||||
url: https://www.juno.one/
|
||||
- name: Kassebil
|
||||
url: https://kassebil.dk/
|
||||
- name: LearnVue
|
||||
url: https://learnvue.co/
|
||||
- name: Mawrble
|
||||
url: https://mawrble.com/
|
||||
- name: Meet Sponsors
|
||||
url: https://meetsponsors.com/
|
||||
- name: Ovatu
|
||||
url: https://ovatu.com/
|
||||
- name: Pallyy
|
||||
url: https://pallyy.com/
|
||||
- name: Passionate People
|
||||
url: https://passionatepeople.io/
|
||||
- name: Postal
|
||||
url: https://postalserver.io/
|
||||
- name: Prismos
|
||||
url: https://prismos.co/
|
||||
- name: Readyy
|
||||
url: https://readyy.app/
|
||||
- name: Sagematt
|
||||
url: https://siedlung-sagematt.ch/
|
||||
- name: Shelve
|
||||
url: https://shelve.cloud/
|
||||
- name: Speaker Bot
|
||||
url: https://speaker.bot/
|
||||
- name: Super SaaS
|
||||
url: https://supersaas.dev/
|
||||
- name: The Companies API
|
||||
url: https://www.thecompaniesapi.com/
|
||||
- name: Thuprai
|
||||
url: https://thuprai.com/
|
||||
- name: Uneed
|
||||
url: https://uneed.best/
|
||||
- name: Wiredash
|
||||
url: https://wiredash.com/
|
||||
- name: Zielgestalt
|
||||
url: https://zielgestalt.de/
|
||||
56
docs/modules/showcase.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { defineNuxtModule } from '@nuxt/kit'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { join } from 'pathe'
|
||||
import captureWebsite from 'capture-website'
|
||||
|
||||
interface ContentFile {
|
||||
id?: string
|
||||
body?: {
|
||||
items: TemplateItem[]
|
||||
}
|
||||
}
|
||||
|
||||
interface TemplateItem {
|
||||
name: string
|
||||
url?: string
|
||||
screenshotUrl?: string
|
||||
screenshotOptions?: Record<string, any>
|
||||
}
|
||||
|
||||
export default defineNuxtModule((_, nuxt) => {
|
||||
nuxt.hook('content:file:afterParse', async ({ content: file }: { content: ContentFile }) => {
|
||||
if (!file.id?.includes('showcase')) {
|
||||
return
|
||||
}
|
||||
if (!file.body?.items?.length) {
|
||||
return
|
||||
}
|
||||
for (const template of file.body.items) {
|
||||
const url = template.screenshotUrl || template.url
|
||||
if (!url) {
|
||||
console.error(`Template ${template.name} has no "url" or "screenshotUrl" to take a screenshot from`)
|
||||
continue
|
||||
}
|
||||
|
||||
const name = template.name.toLowerCase().replace(/\s/g, '-')
|
||||
const filename = join(process.cwd(), 'docs/public/assets/showcase', `${name}.png`)
|
||||
|
||||
if (existsSync(filename)) {
|
||||
continue
|
||||
}
|
||||
|
||||
console.log(`Generating screenshot for Template ${template.name} hitting ${url}...`)
|
||||
|
||||
try {
|
||||
await captureWebsite.file(url, filename, {
|
||||
...(template.screenshotOptions || {}),
|
||||
launchOptions: { headless: true }
|
||||
})
|
||||
|
||||
console.log(`Screenshot for ${template.name} generated successfully`)
|
||||
} catch (error) {
|
||||
console.error(`Error generating screenshot for ${template.name}:`, error)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -4,35 +4,36 @@
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@iconify-json/logos": "^1.2.4",
|
||||
"@iconify-json/lucide": "^1.2.33",
|
||||
"@iconify-json/simple-icons": "^1.2.29",
|
||||
"@iconify-json/vscode-icons": "^1.2.17",
|
||||
"@iconify-json/lucide": "^1.2.35",
|
||||
"@iconify-json/simple-icons": "^1.2.31",
|
||||
"@iconify-json/vscode-icons": "^1.2.18",
|
||||
"@nuxt/content": "^3.4.0",
|
||||
"@nuxt/image": "^1.10.0",
|
||||
"@nuxt/ui": "latest",
|
||||
"@nuxt/ui-pro": "https://pkg.pr.new/@nuxt/ui-pro@63da8be",
|
||||
"@nuxthub/core": "^0.8.21",
|
||||
"@nuxthub/core": "^0.8.23",
|
||||
"@nuxtjs/plausible": "^1.2.0",
|
||||
"@octokit/rest": "^21.1.1",
|
||||
"@rollup/plugin-yaml": "^4.1.2",
|
||||
"@vueuse/nuxt": "^13.0.0",
|
||||
"@vueuse/integrations": "^13.0.0",
|
||||
"@vueuse/nuxt": "^13.1.0",
|
||||
"capture-website": "^4.2.0",
|
||||
"@vueuse/integrations": "^13.1.0",
|
||||
"sortablejs": "^1.15.6",
|
||||
"joi": "^17.13.3",
|
||||
"motion-v": "0.13.1",
|
||||
"nuxt": "^3.16.1",
|
||||
"nuxt": "^3.16.2",
|
||||
"nuxt-component-meta": "^0.10.1",
|
||||
"nuxt-llms": "^0.1.2",
|
||||
"nuxt-og-image": "^5.1.1",
|
||||
"prettier": "^3.5.3",
|
||||
"shiki-transformer-color-highlight": "^1.0.0",
|
||||
"superstruct": "^2.0.2",
|
||||
"ufo": "^1.5.4",
|
||||
"ufo": "^1.6.1",
|
||||
"valibot": "^1.0.0",
|
||||
"yup": "^1.6.1",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"wrangler": "^4.6.0"
|
||||
"wrangler": "^4.8.0"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
docs/public/assets/showcase/details.png
Normal file
|
After Width: | Height: | Size: 796 KiB |
BIN
docs/public/assets/showcase/directus-docs.png
Normal file
|
After Width: | Height: | Size: 211 KiB |
BIN
docs/public/assets/showcase/espace-asso-by-benevolt.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
docs/public/assets/showcase/juno.one.png
Normal file
|
After Width: | Height: | Size: 422 KiB |
BIN
docs/public/assets/showcase/kassebil.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
docs/public/assets/showcase/learnvue.png
Normal file
|
After Width: | Height: | Size: 558 KiB |
BIN
docs/public/assets/showcase/mawrble.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
docs/public/assets/showcase/meet-sponsors.png
Normal file
|
After Width: | Height: | Size: 367 KiB |
BIN
docs/public/assets/showcase/ovatu.png
Normal file
|
After Width: | Height: | Size: 5.4 MiB |
BIN
docs/public/assets/showcase/pallyy.png
Normal file
|
After Width: | Height: | Size: 518 KiB |
BIN
docs/public/assets/showcase/passionate-people.png
Normal file
|
After Width: | Height: | Size: 749 KiB |
BIN
docs/public/assets/showcase/postal.png
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
docs/public/assets/showcase/prismos.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
docs/public/assets/showcase/readyy.png
Normal file
|
After Width: | Height: | Size: 282 KiB |
BIN
docs/public/assets/showcase/sagematt.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
docs/public/assets/showcase/shelve.png
Normal file
|
After Width: | Height: | Size: 423 KiB |
BIN
docs/public/assets/showcase/speaker-bot.png
Normal file
|
After Width: | Height: | Size: 218 KiB |
BIN
docs/public/assets/showcase/super-saas.png
Normal file
|
After Width: | Height: | Size: 350 KiB |
BIN
docs/public/assets/showcase/the-companies-api.png
Normal file
|
After Width: | Height: | Size: 575 KiB |
BIN
docs/public/assets/showcase/thuprai.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
docs/public/assets/showcase/uneed.png
Normal file
|
After Width: | Height: | Size: 331 KiB |
BIN
docs/public/assets/showcase/wiredash.png
Normal file
|
After Width: | Height: | Size: 645 KiB |
BIN
docs/public/assets/showcase/zielgestalt.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
34
docs/server/api/module.json.get.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
interface TeamMember {
|
||||
name: string
|
||||
login: string
|
||||
avatarUrl: string
|
||||
pronouns?: string
|
||||
location?: string
|
||||
websiteUrl?: string
|
||||
sponsorsListing?: string
|
||||
socialAccounts: Record<string, { displayName: string, url: string }>
|
||||
}
|
||||
|
||||
interface Module {
|
||||
stats: {
|
||||
downloads: number
|
||||
stars: number
|
||||
}
|
||||
contributors: {
|
||||
username: string
|
||||
}[]
|
||||
}
|
||||
export default defineCachedEventHandler(async () => {
|
||||
const team = await $fetch<TeamMember[]>('https://api.nuxt.com/teams/ui')
|
||||
const { stats, contributors } = await $fetch<Module>('https://api.nuxt.com/modules/ui')
|
||||
|
||||
return {
|
||||
team,
|
||||
stats,
|
||||
contributors
|
||||
}
|
||||
}, {
|
||||
maxAge: 60 * 60, // 1 hour
|
||||
shouldBypassCache: () => !!import.meta.dev,
|
||||
getKey: () => 'module'
|
||||
})
|
||||
104
package.json
@@ -2,7 +2,7 @@
|
||||
"name": "@nuxt/ui",
|
||||
"description": "A UI Library for Modern Web Apps, powered by Vue & Tailwind CSS.",
|
||||
"version": "3.0.2",
|
||||
"packageManager": "pnpm@10.7.0",
|
||||
"packageManager": "pnpm@10.8.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/nuxt/ui.git"
|
||||
@@ -12,20 +12,17 @@
|
||||
"license": "MIT",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/module.d.ts",
|
||||
"types": "./dist/module.d.mts",
|
||||
"style": "./dist/runtime/index.css",
|
||||
"import": "./dist/module.mjs",
|
||||
"require": "./dist/module.cjs"
|
||||
"import": "./dist/module.mjs"
|
||||
},
|
||||
"./unplugin": {
|
||||
"types": "./dist/unplugin.d.ts",
|
||||
"import": "./dist/unplugin.mjs",
|
||||
"require": "./dist/unplugin.cjs"
|
||||
"types": "./dist/unplugin.d.mts",
|
||||
"import": "./dist/unplugin.mjs"
|
||||
},
|
||||
"./vite": {
|
||||
"types": "./dist/vite.d.ts",
|
||||
"import": "./dist/vite.mjs",
|
||||
"require": "./dist/vite.cjs"
|
||||
"types": "./dist/vite.d.mts",
|
||||
"import": "./dist/vite.mjs"
|
||||
},
|
||||
"./vue-plugin": {
|
||||
"types": "./vue-plugin.d.ts"
|
||||
@@ -33,6 +30,10 @@
|
||||
"./runtime/*": "./dist/runtime/*",
|
||||
"./components/*": "./dist/runtime/components/*",
|
||||
"./composables/*": "./dist/runtime/composables/*",
|
||||
"./utils": {
|
||||
"types": "./dist/runtime/utils/index.d.ts",
|
||||
"import": "./dist/runtime/utils/index.js"
|
||||
},
|
||||
"./utils/*": {
|
||||
"types": "./dist/runtime/utils/*.d.ts",
|
||||
"import": "./dist/runtime/utils/*.js"
|
||||
@@ -42,6 +43,40 @@
|
||||
"import": "./dist/runtime/locale/index.js"
|
||||
}
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
".": [
|
||||
"./dist/module.d.mts"
|
||||
],
|
||||
"unplugin": [
|
||||
"./dist/unplugin.d.mts"
|
||||
],
|
||||
"vite": [
|
||||
"./dist/vite.d.mts"
|
||||
],
|
||||
"vue-plugin": [
|
||||
"./vue-plugin.d.ts"
|
||||
],
|
||||
"runtime/*": [
|
||||
"./dist/runtime/*"
|
||||
],
|
||||
"components/*": [
|
||||
"./dist/runtime/components/*"
|
||||
],
|
||||
"composables/*": [
|
||||
"./dist/runtime/composables/*"
|
||||
],
|
||||
"utils": [
|
||||
"./dist/runtime/utils/index.d.ts"
|
||||
],
|
||||
"utils/*": [
|
||||
"./dist/runtime/utils/*.d.ts"
|
||||
],
|
||||
"locale": [
|
||||
"./dist/runtime/locale/index.d.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"imports": {
|
||||
"#build/ui/*": "./.nuxt/ui/*.ts",
|
||||
"#build/ui.css": "./.nuxt/ui.css"
|
||||
@@ -50,8 +85,7 @@
|
||||
"nuxt-ui": "./cli/index.mjs"
|
||||
},
|
||||
"style": "./dist/runtime/index.css",
|
||||
"main": "./dist/module.cjs",
|
||||
"types": "./dist/types.d.ts",
|
||||
"main": "./dist/module.mjs",
|
||||
"files": [
|
||||
".nuxt/ui",
|
||||
".nuxt/ui.css",
|
||||
@@ -81,28 +115,28 @@
|
||||
"@iconify/vue": "^4.3.0",
|
||||
"@internationalized/date": "^3.7.0",
|
||||
"@internationalized/number": "^3.6.0",
|
||||
"@nuxt/fonts": "^0.11.0",
|
||||
"@nuxt/fonts": "^0.11.1",
|
||||
"@nuxt/icon": "^1.11.0",
|
||||
"@nuxt/kit": "^3.16.1",
|
||||
"@nuxt/schema": "^3.16.1",
|
||||
"@nuxt/kit": "^3.16.2",
|
||||
"@nuxt/schema": "^3.16.2",
|
||||
"@nuxtjs/color-mode": "^3.5.2",
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
"@tailwindcss/postcss": "^4.0.17",
|
||||
"@tailwindcss/vite": "^4.0.17",
|
||||
"@tailwindcss/postcss": "^4.1.3",
|
||||
"@tailwindcss/vite": "^4.1.3",
|
||||
"@tanstack/vue-table": "^8.21.2",
|
||||
"@unhead/vue": "^2.0.2",
|
||||
"@vueuse/core": "^13.0.0",
|
||||
"@vueuse/integrations": "^13.0.0",
|
||||
"@unhead/vue": "^2.0.5",
|
||||
"@vueuse/core": "^13.1.0",
|
||||
"@vueuse/integrations": "^13.1.0",
|
||||
"colortranslator": "^4.1.0",
|
||||
"consola": "^3.4.2",
|
||||
"defu": "^6.1.4",
|
||||
"embla-carousel-auto-height": "^8.5.2",
|
||||
"embla-carousel-auto-scroll": "^8.5.2",
|
||||
"embla-carousel-autoplay": "^8.5.2",
|
||||
"embla-carousel-class-names": "^8.5.2",
|
||||
"embla-carousel-fade": "^8.5.2",
|
||||
"embla-carousel-vue": "^8.5.2",
|
||||
"embla-carousel-wheel-gestures": "^8.0.1",
|
||||
"embla-carousel-auto-height": "^8.6.0",
|
||||
"embla-carousel-auto-scroll": "^8.6.0",
|
||||
"embla-carousel-autoplay": "^8.6.0",
|
||||
"embla-carousel-class-names": "^8.6.0",
|
||||
"embla-carousel-fade": "^8.6.0",
|
||||
"embla-carousel-vue": "^8.6.0",
|
||||
"embla-carousel-wheel-gestures": "^8.0.2",
|
||||
"fuse.js": "^7.1.0",
|
||||
"hookable": "^5.5.3",
|
||||
"knitwork": "^1.2.0",
|
||||
@@ -110,10 +144,10 @@
|
||||
"mlly": "^1.7.4",
|
||||
"ohash": "^2.0.11",
|
||||
"pathe": "^2.0.3",
|
||||
"reka-ui": "^2.1.1",
|
||||
"reka-ui": "^2.2.0",
|
||||
"scule": "^1.3.0",
|
||||
"tailwind-variants": "^1.0.0",
|
||||
"tailwindcss": "^4.0.17",
|
||||
"tailwindcss": "^4.1.3",
|
||||
"tinyglobby": "^0.2.12",
|
||||
"unplugin": "^2.2.2",
|
||||
"unplugin-auto-import": "^19.1.2",
|
||||
@@ -123,17 +157,17 @@
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxt/eslint-config": "^1.2.0",
|
||||
"@nuxt/module-builder": "^0.8.4",
|
||||
"@nuxt/eslint-config": "^1.3.0",
|
||||
"@nuxt/module-builder": "^1.0.0",
|
||||
"@nuxt/test-utils": "^3.17.2",
|
||||
"@release-it/conventional-changelog": "^10.0.0",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"embla-carousel": "^8.5.2",
|
||||
"eslint": "^9.23.0",
|
||||
"embla-carousel": "^8.6.0",
|
||||
"eslint": "^9.24.0",
|
||||
"happy-dom": "^17.4.4",
|
||||
"nuxt": "^3.16.1",
|
||||
"nuxt": "^3.16.2",
|
||||
"release-it": "^18.1.2",
|
||||
"vitest": "^3.0.9",
|
||||
"vitest": "^3.1.1",
|
||||
"vitest-environment-nuxt": "^1.0.1",
|
||||
"vue-tsc": "^2.2.0"
|
||||
},
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"typescript": "^5.8.2",
|
||||
"vite": "^6.2.3",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.2.5",
|
||||
"vue-tsc": "^2.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ defineShortcuts({
|
||||
<UButton label="Select label (popover)" color="neutral" variant="outline" />
|
||||
|
||||
<template #content>
|
||||
<UCommandPalette v-model="label" placeholder="Search labels..." :groups="[{ id: 'labels', items: labels }]" :ui="{ input: '[&>input]:h-9' }" />
|
||||
<UCommandPalette v-model="label" placeholder="Search labels..." :groups="[{ id: 'labels', items: labels }]" :ui="{ input: '[&>input]:h-8 [&>input]:text-sm' }" />
|
||||
</template>
|
||||
</UPopover>
|
||||
</div>
|
||||
|
||||
@@ -38,7 +38,7 @@ const variants = Object.keys(theme.variants.variant) as Array<keyof typeof theme
|
||||
<div class="flex flex-col gap-4 w-48">
|
||||
<UInput placeholder="Disabled" disabled />
|
||||
<UInput placeholder="Required" required />
|
||||
<UInput file="i-lucide-calculator" type="number" :model-value="10" />
|
||||
<UInput icon="i-lucide-calculator" type="number" :model-value="10" />
|
||||
<UInput icon="i-lucide-folder" type="file" />
|
||||
<UInput icon="i-lucide-calendar" type="date" :model-value="new Date().toISOString().substring(0, 10)" />
|
||||
<UInput icon="i-lucide-lock" type="password" model-value="password" />
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import theme from '#build/ui/radio-group'
|
||||
|
||||
const sizes = Object.keys(theme.variants.size) as Array<keyof typeof theme.variants.size>
|
||||
const variants = Object.keys(theme.variants.variant)
|
||||
const variant = ref('list' as const)
|
||||
|
||||
const literalOptions = [
|
||||
'Option 1',
|
||||
@@ -23,27 +25,36 @@ const itemsWithDescription = [
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="flex flex-col gap-4 ms-[100px]">
|
||||
<URadioGroup :items="items" default-value="1" />
|
||||
<URadioGroup :items="items" color="neutral" default-value="1" />
|
||||
<URadioGroup :items="items" color="error" default-value="2" />
|
||||
<URadioGroup :items="literalOptions" />
|
||||
<URadioGroup :items="items" label="Disabled" disabled />
|
||||
<URadioGroup :items="items" orientation="horizontal" class="ms-[-91px]" />
|
||||
<USelect v-model="variant" :items="variants" />
|
||||
|
||||
<div class="flex flex-wrap gap-4 ms-[100px]">
|
||||
<URadioGroup :variant="variant" :items="items" default-value="1" />
|
||||
<URadioGroup :variant="variant" :items="items" color="neutral" default-value="1" />
|
||||
<URadioGroup :variant="variant" :items="items" color="error" default-value="2" />
|
||||
<URadioGroup :variant="variant" :items="literalOptions" />
|
||||
<URadioGroup :variant="variant" :items="items" disabled />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-4 ms-[100px]">
|
||||
<URadioGroup :variant="variant" :items="items" default-value="3" indicator="start" />
|
||||
<URadioGroup :variant="variant" :items="items" default-value="3" indicator="end" />
|
||||
<URadioGroup :variant="variant" :items="items" default-value="3" indicator="hidden" />
|
||||
</div>
|
||||
|
||||
<URadioGroup :variant="variant" :items="items" orientation="horizontal" class="ms-[95px]" />
|
||||
|
||||
<div class="flex items-center gap-4 ms-[34px]">
|
||||
<URadioGroup v-for="size in sizes" :key="size" :size="size" :items="items" />
|
||||
<URadioGroup v-for="size in sizes" :key="size" :size="size" :variant="variant" :items="items" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4 ms-[74px]">
|
||||
<URadioGroup v-for="size in sizes" :key="size" :size="size" :items="itemsWithDescription" />
|
||||
<URadioGroup v-for="size in sizes" :key="size" :size="size" :variant="variant" :items="itemsWithDescription" />
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4">
|
||||
<URadioGroup :items="items" legend="Legend" />
|
||||
<URadioGroup :items="items" legend="Legend" required />
|
||||
<URadioGroup :items="items">
|
||||
<URadioGroup :variant="variant" :items="items" legend="Legend" />
|
||||
<URadioGroup :variant="variant" :items="items" legend="Legend" required />
|
||||
<URadioGroup :variant="variant" :items="items">
|
||||
<template #legend>
|
||||
<span class="italic font-bold">
|
||||
With slots
|
||||
@@ -56,6 +67,6 @@ const itemsWithDescription = [
|
||||
</template>
|
||||
</URadioGroup>
|
||||
</div>
|
||||
<URadioGroup :items="items" legend="Legend" orientation="horizontal" required />
|
||||
<URadioGroup :variant="variant" :items="items" legend="Legend" orientation="horizontal" required />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -9,7 +9,7 @@ const variants = Object.keys(theme.variants.variant) as Array<keyof typeof theme
|
||||
<template>
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="flex flex-col gap-4 w-48">
|
||||
<UTextarea autofocus />
|
||||
<UTextarea autofocus placeholder="Search..." />
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<UTextarea v-for="variant in variants" :key="variant" :placeholder="upperFirst(variant)" :variant="variant" class="w-48" />
|
||||
@@ -36,11 +36,59 @@ const variants = Object.keys(theme.variants.variant) as Array<keyof typeof theme
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-4 w-48">
|
||||
<UTextarea placeholder="Search..." disabled />
|
||||
<UTextarea autoresize :maxrows="5" :rows="1" />
|
||||
<UTextarea placeholder="Disabled" disabled />
|
||||
<UTextarea placeholder="Required" required />
|
||||
<UTextarea icon="i-lucide-text-cursor" placeholder="Autoresize" autoresize :maxrows="5" :rows="1" />
|
||||
<UTextarea icon="i-lucide-search" placeholder="Search..." :rows="1" />
|
||||
<UTextarea loading placeholder="Search..." :rows="1" />
|
||||
<UTextarea loading trailing placeholder="Search..." :rows="1" />
|
||||
<UTextarea loading icon="i-lucide-search" trailing-icon="i-lucide-chevron-down" placeholder="Search..." :rows="1" />
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<UTextarea v-for="size in sizes" :key="size" placeholder="Search..." :size="size" class="w-48" />
|
||||
<UTextarea
|
||||
v-for="size in sizes"
|
||||
:key="size"
|
||||
placeholder="Search..."
|
||||
:size="size"
|
||||
:rows="1"
|
||||
class="w-48"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<UTextarea
|
||||
v-for="size in sizes"
|
||||
:key="size"
|
||||
icon="i-lucide-search"
|
||||
placeholder="Search..."
|
||||
:size="size"
|
||||
:rows="1"
|
||||
class="w-48"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<UTextarea
|
||||
v-for="size in sizes"
|
||||
:key="size"
|
||||
icon="i-lucide-search"
|
||||
trailing
|
||||
placeholder="Search..."
|
||||
:size="size"
|
||||
:rows="1"
|
||||
class="w-48"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<UTextarea
|
||||
v-for="size in sizes"
|
||||
:key="size"
|
||||
:avatar="{ src: 'https://github.com/benjamincanac.png' }"
|
||||
icon="i-lucide-search"
|
||||
trailing
|
||||
placeholder="Search..."
|
||||
:size="size"
|
||||
:rows="1"
|
||||
class="w-48"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
"generate": "nuxi generate"
|
||||
},
|
||||
"dependencies": {
|
||||
"@iconify-json/lucide": "^1.2.33",
|
||||
"@iconify-json/simple-icons": "^1.2.29",
|
||||
"@iconify-json/lucide": "^1.2.35",
|
||||
"@iconify-json/simple-icons": "^1.2.31",
|
||||
"@nuxt/ui": "latest",
|
||||
"@nuxthub/core": "^0.8.21",
|
||||
"nuxt": "^3.16.1",
|
||||
"@nuxthub/core": "^0.8.23",
|
||||
"nuxt": "^3.16.2",
|
||||
"zod": "^3.24.2"
|
||||
}
|
||||
}
|
||||
|
||||
3234
pnpm-lock.yaml
generated
@@ -2,6 +2,7 @@ import { defu } from 'defu'
|
||||
import { createResolver, defineNuxtModule, addComponentsDir, addImportsDir, addVitePlugin, addPlugin, installModule, hasNuxtModule } from '@nuxt/kit'
|
||||
import { addTemplates } from './templates'
|
||||
import { defaultOptions, getDefaultUiConfig, resolveColors } from './defaults'
|
||||
import { name, version } from '../package.json'
|
||||
|
||||
export type * from './runtime/types'
|
||||
|
||||
@@ -50,12 +51,13 @@ export interface ModuleOptions {
|
||||
|
||||
export default defineNuxtModule<ModuleOptions>({
|
||||
meta: {
|
||||
name: 'ui',
|
||||
name,
|
||||
version,
|
||||
docs: 'https://ui.nuxt.com/getting-started/installation/nuxt',
|
||||
configKey: 'ui',
|
||||
compatibility: {
|
||||
nuxt: '>=3.16.0'
|
||||
},
|
||||
docs: 'https://ui.nuxt.com/getting-started/installation/nuxt'
|
||||
}
|
||||
},
|
||||
defaults: defaultOptions,
|
||||
async setup(options, nuxt) {
|
||||
|
||||
@@ -258,7 +258,6 @@ const groups = computed(() => {
|
||||
:placeholder="placeholder || t('commandPalette.placeholder')"
|
||||
variant="none"
|
||||
:autofocus="autofocus"
|
||||
size="lg"
|
||||
v-bind="inputProps"
|
||||
:icon="icon || appConfig.ui.icons.search"
|
||||
:class="ui.input({ class: props.ui?.input })"
|
||||
|
||||
@@ -38,6 +38,7 @@ import { ContextMenu } from 'reka-ui/namespaced'
|
||||
import { useForwardPropsEmits } from 'reka-ui'
|
||||
import { reactiveOmit, createReusableTemplate } from '@vueuse/core'
|
||||
import { useAppConfig } from '#imports'
|
||||
import { useLocale } from '../composables/useLocale'
|
||||
import { omit, get, isArrayOfArray } from '../utils'
|
||||
import { pickLinkProps } from '../utils/link'
|
||||
import ULinkBase from './LinkBase.vue'
|
||||
@@ -53,11 +54,13 @@ const emits = defineEmits<ContextMenuContentEmits>()
|
||||
const slots = defineSlots<ContextMenuSlots<T>>()
|
||||
|
||||
const appConfig = useAppConfig()
|
||||
const { dir } = useLocale()
|
||||
const contentProps = useForwardPropsEmits(reactiveOmit(props, 'sub', 'items', 'portal', 'labelKey', 'checkedIcon', 'loadingIcon', 'externalIcon', 'class', 'ui', 'uiOverride'), emits)
|
||||
const proxySlots = omit(slots, ['default'])
|
||||
|
||||
const [DefineItemTemplate, ReuseItemTemplate] = createReusableTemplate<{ item: ContextMenuItem, active?: boolean, index: number }>()
|
||||
|
||||
const childrenIcon = computed(() => dir.value === 'rtl' ? appConfig.ui.icons.chevronLeft : appConfig.ui.icons.chevronRight)
|
||||
const groups = computed<ContextMenuItem[][]>(() =>
|
||||
props.items?.length
|
||||
? isArrayOfArray(props.items)
|
||||
@@ -86,7 +89,7 @@ const groups = computed<ContextMenuItem[][]>(() =>
|
||||
|
||||
<span :class="ui.itemTrailing({ class: uiOverride?.itemTrailing })">
|
||||
<slot :name="((item.slot ? `${item.slot}-trailing`: 'item-trailing') as keyof ContextMenuSlots<T>)" :item="item" :active="active" :index="index">
|
||||
<UIcon v-if="item.children?.length" :name="appConfig.ui.icons.chevronRight" :class="ui.itemTrailingIcon({ class: uiOverride?.itemTrailingIcon, color: item?.color, active })" />
|
||||
<UIcon v-if="item.children?.length" :name="childrenIcon" :class="ui.itemTrailingIcon({ class: uiOverride?.itemTrailingIcon, color: item?.color, active })" />
|
||||
<span v-else-if="item.kbds?.length" :class="ui.itemTrailingKbds({ class: uiOverride?.itemTrailingKbds })">
|
||||
<UKbd v-for="(kbd, kbdIndex) in item.kbds" :key="kbdIndex" :size="((props.uiOverride?.itemTrailingKbdsSize || ui.itemTrailingKbdsSize()) as KbdProps['size'])" v-bind="typeof kbd === 'string' ? { value: kbd } : kbd" />
|
||||
</span>
|
||||
@@ -132,7 +135,7 @@ const groups = computed<ContextMenuItem[][]>(() =>
|
||||
:external-icon="externalIcon"
|
||||
v-bind="item.content"
|
||||
>
|
||||
<template v-for="(_, name) in proxySlots" #[name]="slotData: any">
|
||||
<template v-for="(_, name) in proxySlots" #[name]="slotData">
|
||||
<slot :name="(name as keyof ContextMenuSlots<T>)" v-bind="slotData" />
|
||||
</template>
|
||||
</UContextMenuContent>
|
||||
|
||||
@@ -44,6 +44,7 @@ import { DropdownMenu } from 'reka-ui/namespaced'
|
||||
import { useForwardPropsEmits } from 'reka-ui'
|
||||
import { reactiveOmit, createReusableTemplate } from '@vueuse/core'
|
||||
import { useAppConfig } from '#imports'
|
||||
import { useLocale } from '../composables/useLocale'
|
||||
import { omit, get, isArrayOfArray } from '../utils'
|
||||
import { pickLinkProps } from '../utils/link'
|
||||
import ULinkBase from './LinkBase.vue'
|
||||
@@ -59,11 +60,13 @@ const emits = defineEmits<DropdownMenuContentEmits>()
|
||||
const slots = defineSlots<DropdownMenuContentSlots<T>>()
|
||||
|
||||
const appConfig = useAppConfig()
|
||||
const { dir } = useLocale()
|
||||
const contentProps = useForwardPropsEmits(reactiveOmit(props, 'sub', 'items', 'portal', 'labelKey', 'checkedIcon', 'loadingIcon', 'externalIcon', 'class', 'ui', 'uiOverride'), emits)
|
||||
const proxySlots = omit(slots, ['default'])
|
||||
|
||||
const [DefineItemTemplate, ReuseItemTemplate] = createReusableTemplate<{ item: DropdownMenuItem, active?: boolean, index: number }>()
|
||||
|
||||
const childrenIcon = computed(() => dir.value === 'rtl' ? appConfig.ui.icons.chevronLeft : appConfig.ui.icons.chevronRight)
|
||||
const groups = computed<DropdownMenuItem[][]>(() =>
|
||||
props.items?.length
|
||||
? isArrayOfArray(props.items)
|
||||
@@ -92,7 +95,7 @@ const groups = computed<DropdownMenuItem[][]>(() =>
|
||||
|
||||
<span :class="ui.itemTrailing({ class: uiOverride?.itemTrailing })">
|
||||
<slot :name="((item.slot ? `${item.slot}-trailing`: 'item-trailing') as keyof DropdownMenuContentSlots<T>)" :item="(item as Extract<NestedItem<T>, { slot: string; }>)" :active="active" :index="index">
|
||||
<UIcon v-if="item.children?.length" :name="appConfig.ui.icons.chevronRight" :class="ui.itemTrailingIcon({ class: uiOverride?.itemTrailingIcon, color: item?.color, active })" />
|
||||
<UIcon v-if="item.children?.length" :name="childrenIcon" :class="ui.itemTrailingIcon({ class: uiOverride?.itemTrailingIcon, color: item?.color, active })" />
|
||||
<span v-else-if="item.kbds?.length" :class="ui.itemTrailingKbds({ class: uiOverride?.itemTrailingKbds })">
|
||||
<UKbd v-for="(kbd, kbdIndex) in item.kbds" :key="kbdIndex" :size="((props.uiOverride?.itemTrailingKbdsSize || ui.itemTrailingKbdsSize()) as KbdProps['size'])" v-bind="typeof kbd === 'string' ? { value: kbd } : kbd" />
|
||||
</span>
|
||||
@@ -131,7 +134,6 @@ const groups = computed<DropdownMenuItem[][]>(() =>
|
||||
:ui-override="uiOverride"
|
||||
:portal="portal"
|
||||
:items="(item.children as T)"
|
||||
side="right"
|
||||
align="start"
|
||||
:align-offset="-4"
|
||||
:side-offset="3"
|
||||
@@ -141,7 +143,7 @@ const groups = computed<DropdownMenuItem[][]>(() =>
|
||||
:external-icon="externalIcon"
|
||||
v-bind="item.content"
|
||||
>
|
||||
<template v-for="(_, name) in proxySlots" #[name]="slotData: any">
|
||||
<template v-for="(_, name) in proxySlots" #[name]="slotData">
|
||||
<slot :name="(name as keyof DropdownMenuContentSlots<T>)" v-bind="slotData" />
|
||||
</template>
|
||||
</UDropdownMenuContent>
|
||||
|
||||
@@ -104,12 +104,6 @@ const ui = computed(() => input({
|
||||
|
||||
const inputRef = ref<HTMLInputElement | null>(null)
|
||||
|
||||
function autoFocus() {
|
||||
if (props.autofocus) {
|
||||
inputRef.value?.focus()
|
||||
}
|
||||
}
|
||||
|
||||
// Custom function to handle the v-model properties
|
||||
function updateInput(value: string | null) {
|
||||
if (modelModifiers.trim) {
|
||||
@@ -155,15 +149,21 @@ function onBlur(event: FocusEvent) {
|
||||
emits('blur', event)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
inputRef
|
||||
})
|
||||
function autoFocus() {
|
||||
if (props.autofocus) {
|
||||
inputRef.value?.focus()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
autoFocus()
|
||||
}, props.autofocusDelay)
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
inputRef
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -36,7 +36,6 @@ interface _InputMenuItem {
|
||||
* @defaultValue 'item'
|
||||
*/
|
||||
type?: 'label' | 'separator' | 'item'
|
||||
value?: string | number
|
||||
disabled?: boolean
|
||||
onSelect?(e?: Event): void
|
||||
[key: string]: any
|
||||
@@ -45,7 +44,7 @@ export type InputMenuItem = _InputMenuItem | AcceptableValue | boolean
|
||||
|
||||
type InputMenuVariants = VariantProps<typeof inputMenu>
|
||||
|
||||
export interface InputMenuProps<T extends ArrayOrNested<InputMenuItem> = ArrayOrNested<InputMenuItem>, VK extends GetItemKeys<T> | undefined = undefined, M extends boolean = false> extends Pick<ComboboxRootProps<T>, 'open' | 'defaultOpen' | 'disabled' | 'name' | 'resetSearchTermOnBlur' | 'highlightOnHover'>, UseComponentIconsProps {
|
||||
export interface InputMenuProps<T extends ArrayOrNested<InputMenuItem> = ArrayOrNested<InputMenuItem>, VK extends GetItemKeys<T> | undefined = undefined, M extends boolean = false> extends Pick<ComboboxRootProps<T>, 'open' | 'defaultOpen' | 'disabled' | 'name' | 'resetSearchTermOnBlur' | 'resetSearchTermOnSelect' | 'highlightOnHover'>, UseComponentIconsProps {
|
||||
/**
|
||||
* The element or component this component should render as.
|
||||
* @defaultValue 'div'
|
||||
@@ -176,7 +175,7 @@ export interface InputMenuSlots<
|
||||
</script>
|
||||
|
||||
<script setup lang="ts" generic="T extends ArrayOrNested<InputMenuItem>, VK extends GetItemKeys<T> | undefined = undefined, M extends boolean = false">
|
||||
import { computed, ref, toRef, onMounted, toRaw, nextTick } from 'vue'
|
||||
import { computed, ref, toRef, onMounted, toRaw } from 'vue'
|
||||
import { ComboboxRoot, ComboboxArrow, ComboboxAnchor, ComboboxInput, ComboboxTrigger, ComboboxPortal, ComboboxContent, ComboboxViewport, ComboboxEmpty, ComboboxGroup, ComboboxLabel, ComboboxSeparator, ComboboxItem, ComboboxItemIndicator, TagsInputRoot, TagsInputItem, TagsInputItemText, TagsInputItemDelete, TagsInputInput, useForwardPropsEmits, useFilter } from 'reka-ui'
|
||||
import { defu } from 'defu'
|
||||
import { isEqual } from 'ohash/utils'
|
||||
@@ -197,7 +196,9 @@ const props = withDefaults(defineProps<InputMenuProps<T, VK, M>>(), {
|
||||
type: 'text',
|
||||
autofocusDelay: 0,
|
||||
portal: true,
|
||||
labelKey: 'label' as never
|
||||
labelKey: 'label' as never,
|
||||
resetSearchTermOnBlur: true,
|
||||
resetSearchTermOnSelect: true
|
||||
})
|
||||
const emits = defineEmits<InputMenuEmits<T, VK, M>>()
|
||||
const slots = defineSlots<InputMenuSlots<T, VK, M>>()
|
||||
@@ -208,7 +209,7 @@ const { t } = useLocale()
|
||||
const appConfig = useAppConfig()
|
||||
const { contains } = useFilter({ sensitivity: 'base' })
|
||||
|
||||
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'open', 'defaultOpen', 'required', 'multiple', 'resetSearchTermOnBlur', 'highlightOnHover', 'ignoreFilter'), emits)
|
||||
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'open', 'defaultOpen', 'required', 'multiple', 'resetSearchTermOnBlur', 'resetSearchTermOnSelect', 'highlightOnHover', 'ignoreFilter'), emits)
|
||||
const contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffset: 8, collisionPadding: 8, position: 'popper' }) as ComboboxContentProps)
|
||||
const arrowProps = toRef(() => props.arrow as ComboboxArrowProps)
|
||||
|
||||
@@ -312,6 +313,10 @@ function onUpdate(value: any) {
|
||||
emits('change', event)
|
||||
emitFormChange()
|
||||
emitFormInput()
|
||||
|
||||
if (props.resetSearchTermOnSelect) {
|
||||
searchTerm.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
function onBlur(event: FocusEvent) {
|
||||
@@ -325,18 +330,29 @@ function onFocus(event: FocusEvent) {
|
||||
}
|
||||
|
||||
function onUpdateOpen(value: boolean) {
|
||||
let timeoutId
|
||||
|
||||
if (!value) {
|
||||
const event = new FocusEvent('blur')
|
||||
|
||||
emits('blur', event)
|
||||
emitFormBlur()
|
||||
|
||||
// Since we use `displayValue` prop inside ComboboxInput we should reset searchTerm manually
|
||||
// https://reka-ui.com/docs/components/combobox#api-reference
|
||||
if (props.resetSearchTermOnBlur) {
|
||||
const STATE_ANIMATION_DELAY_MS = 100
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
searchTerm.value = ''
|
||||
}, STATE_ANIMATION_DELAY_MS)
|
||||
}
|
||||
} else {
|
||||
const event = new FocusEvent('focus')
|
||||
emits('focus', event)
|
||||
emitFormFocus()
|
||||
clearTimeout(timeoutId)
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
searchTerm.value = ''
|
||||
})
|
||||
}
|
||||
|
||||
function onRemoveTag(event: any) {
|
||||
@@ -344,13 +360,26 @@ function onRemoveTag(event: any) {
|
||||
const modelValue = props.modelValue as GetModelValue<T, VK, true>
|
||||
const filteredValue = modelValue.filter(value => !isEqual(value, event))
|
||||
emits('update:modelValue', filteredValue as GetModelValue<T, VK, M>)
|
||||
onUpdate(filteredValue)
|
||||
}
|
||||
}
|
||||
|
||||
function onSelect(e: Event, item: InputMenuItem) {
|
||||
if (!isInputItem(item)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (item.disabled) {
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
item.onSelect?.(e)
|
||||
}
|
||||
|
||||
function isInputItem(item: InputMenuItem): item is _InputMenuItem {
|
||||
return typeof item === 'object' && item !== null
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
inputRef
|
||||
})
|
||||
@@ -414,7 +443,7 @@ defineExpose({
|
||||
</TagsInputItemDelete>
|
||||
</TagsInputItem>
|
||||
|
||||
<ComboboxInput as-child @update:model-value="searchTerm = $event">
|
||||
<ComboboxInput v-model="searchTerm" as-child>
|
||||
<TagsInputInput
|
||||
ref="inputRef"
|
||||
v-bind="{ ...$attrs, ...ariaAttrs }"
|
||||
@@ -476,7 +505,7 @@ defineExpose({
|
||||
:class="ui.item({ class: props.ui?.item })"
|
||||
:disabled="isInputItem(item) && item.disabled"
|
||||
:value="props.valueKey && isInputItem(item) ? get(item, String(props.valueKey)) : item"
|
||||
@select="isInputItem(item) && item.onSelect"
|
||||
@select="onSelect($event, item)"
|
||||
>
|
||||
<slot name="item" :item="(item as NestedItem<T>)" :index="index">
|
||||
<slot name="item-leading" :item="(item as NestedItem<T>)" :index="index">
|
||||
|
||||
@@ -14,7 +14,7 @@ const inputNumber = tv({ extend: tv(theme), ...(appConfigInputNumber.ui?.inputNu
|
||||
|
||||
type InputNumberVariants = VariantProps<typeof inputNumber>
|
||||
|
||||
export interface InputNumberProps extends Pick<NumberFieldRootProps, 'modelValue' | 'defaultValue' | 'min' | 'max' | 'step' | 'disabled' | 'required' | 'id' | 'name' | 'formatOptions'> {
|
||||
export interface InputNumberProps extends Pick<NumberFieldRootProps, 'modelValue' | 'defaultValue' | 'min' | 'max' | 'step' | 'stepSnapping' | 'disabled' | 'required' | 'id' | 'name' | 'formatOptions' | 'disableWheelChange'> {
|
||||
/**
|
||||
* The element or component this component should render as.
|
||||
* @defaultValue 'div'
|
||||
@@ -94,7 +94,7 @@ const props = withDefaults(defineProps<InputNumberProps>(), {
|
||||
const emits = defineEmits<InputNumberEmits>()
|
||||
defineSlots<InputNumberSlots>()
|
||||
|
||||
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'min', 'max', 'step', 'formatOptions'), emits)
|
||||
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'min', 'max', 'step', 'stepSnapping', 'formatOptions', 'disableWheelChange'), emits)
|
||||
|
||||
const appConfig = useAppConfig()
|
||||
const { emitFormBlur, emitFormFocus, emitFormChange, emitFormInput, id, color, size, name, highlight, disabled, ariaAttrs } = useFormField<InputNumberProps>(props)
|
||||
@@ -115,18 +115,6 @@ const decrementIcon = computed(() => props.decrementIcon || (props.orientation =
|
||||
|
||||
const inputRef = ref<InstanceType<typeof NumberFieldInput> | null>(null)
|
||||
|
||||
function autoFocus() {
|
||||
if (props.autofocus) {
|
||||
inputRef.value?.$el?.focus()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
autoFocus()
|
||||
}, props.autofocusDelay)
|
||||
})
|
||||
|
||||
function onUpdate(value: number) {
|
||||
// @ts-expect-error - 'target' does not exist in type 'EventInit'
|
||||
const event = new Event('change', { target: { value } })
|
||||
@@ -141,6 +129,18 @@ function onBlur(event: FocusEvent) {
|
||||
emits('blur', event)
|
||||
}
|
||||
|
||||
function autoFocus() {
|
||||
if (props.autofocus) {
|
||||
inputRef.value?.$el?.focus()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
autoFocus()
|
||||
}, props.autofocusDelay)
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
inputRef
|
||||
})
|
||||
|
||||
@@ -182,20 +182,9 @@ const rootProps = useForwardPropsEmits(computed(() => ({
|
||||
const contentProps = toRef(() => props.content)
|
||||
|
||||
const appConfig = useAppConfig()
|
||||
const [DefineLinkTemplate, ReuseLinkTemplate] = createReusableTemplate<
|
||||
{ item: NavigationMenuItem, index: number, active?: boolean },
|
||||
NavigationMenuSlots<T>
|
||||
>()
|
||||
const [DefineItemTemplate, ReuseItemTemplate] = createReusableTemplate<
|
||||
{ item: NavigationMenuItem, index: number, level?: number },
|
||||
NavigationMenuSlots<T>
|
||||
>({
|
||||
props: {
|
||||
item: Object,
|
||||
index: Number,
|
||||
level: Number
|
||||
}
|
||||
})
|
||||
|
||||
const [DefineLinkTemplate, ReuseLinkTemplate] = createReusableTemplate<{ item: NavigationMenuItem, index: number, active?: boolean }>()
|
||||
const [DefineItemTemplate, ReuseItemTemplate] = createReusableTemplate<{ item: NavigationMenuItem, index: number, level?: number }>()
|
||||
|
||||
const ui = computed(() => navigationMenu({
|
||||
orientation: props.orientation,
|
||||
|
||||
@@ -37,6 +37,8 @@ export interface PinInputProps extends Pick<PinInputRootProps, 'defaultValue' |
|
||||
* @defaultValue 5
|
||||
*/
|
||||
length?: number | string
|
||||
autofocus?: boolean
|
||||
autofocusDelay?: number
|
||||
highlight?: boolean
|
||||
class?: any
|
||||
ui?: PartialString<typeof pinInput.slots>
|
||||
@@ -50,7 +52,8 @@ export type PinInputEmits = PinInputRootEmits & {
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import type { ComponentPublicInstance } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { PinInputInput, PinInputRoot, useForwardPropsEmits } from 'reka-ui'
|
||||
import { reactivePick } from '@vueuse/core'
|
||||
import { useFormField } from '../composables/useFormField'
|
||||
@@ -58,7 +61,8 @@ import { looseToNumber } from '../utils'
|
||||
|
||||
const props = withDefaults(defineProps<PinInputProps>(), {
|
||||
type: 'text',
|
||||
length: 5
|
||||
length: 5,
|
||||
autofocusDelay: 0
|
||||
})
|
||||
const emits = defineEmits<PinInputEmits>()
|
||||
|
||||
@@ -72,6 +76,8 @@ const ui = computed(() => pinInput({
|
||||
highlight: highlight.value
|
||||
}))
|
||||
|
||||
const inputsRef = ref<ComponentPublicInstance[]>([])
|
||||
|
||||
const completed = ref(false)
|
||||
function onComplete(value: string[]) {
|
||||
// @ts-expect-error - 'target' does not exist in type 'EventInit'
|
||||
@@ -86,6 +92,22 @@ function onBlur(event: FocusEvent) {
|
||||
emitFormBlur()
|
||||
}
|
||||
}
|
||||
|
||||
function autoFocus() {
|
||||
if (props.autofocus) {
|
||||
inputsRef.value[0]?.$el?.focus()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
autoFocus()
|
||||
}, props.autofocusDelay)
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
inputsRef
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -100,6 +122,7 @@ function onBlur(event: FocusEvent) {
|
||||
<PinInputInput
|
||||
v-for="(ids, index) in looseToNumber(props.length)"
|
||||
:key="ids"
|
||||
:ref="el => (inputsRef[index] = el as ComponentPublicInstance)"
|
||||
:index="index"
|
||||
:class="ui.base({ class: props.ui?.base })"
|
||||
:disabled="disabled"
|
||||
|
||||
@@ -49,6 +49,10 @@ export interface RadioGroupProps<T extends RadioGroupItem = RadioGroupItem> exte
|
||||
* @defaultValue 'md'
|
||||
*/
|
||||
size?: RadioGroupVariants['size']
|
||||
/**
|
||||
* @defaultValue 'list'
|
||||
*/
|
||||
variant?: RadioGroupVariants['variant']
|
||||
/**
|
||||
* @defaultValue 'primary'
|
||||
*/
|
||||
@@ -58,6 +62,11 @@ export interface RadioGroupProps<T extends RadioGroupItem = RadioGroupItem> exte
|
||||
* @defaultValue 'vertical'
|
||||
*/
|
||||
orientation?: RadioGroupRootProps['orientation']
|
||||
/**
|
||||
* Position of the indicator.
|
||||
* @defaultValue 'start'
|
||||
*/
|
||||
indicator?: RadioGroupVariants['indicator']
|
||||
class?: any
|
||||
ui?: Partial<typeof radioGroup.slots>
|
||||
}
|
||||
@@ -101,7 +110,9 @@ const ui = computed(() => radioGroup({
|
||||
color: color.value,
|
||||
disabled: disabled.value,
|
||||
required: props.required,
|
||||
orientation: props.orientation
|
||||
orientation: props.orientation,
|
||||
variant: props.variant,
|
||||
indicator: props.indicator
|
||||
}))
|
||||
|
||||
function normalizeItem(item: any) {
|
||||
@@ -167,7 +178,7 @@ function onUpdate(value: any) {
|
||||
{{ legend }}
|
||||
</slot>
|
||||
</legend>
|
||||
<div v-for="item in normalizedItems" :key="item.value" :class="ui.item({ class: props.ui?.item })">
|
||||
<component :is="variant === 'list' ? 'div' : Label" v-for="item in normalizedItems" :key="item.value" :class="ui.item({ class: props.ui?.item })">
|
||||
<div :class="ui.container({ class: props.ui?.container })">
|
||||
<RadioGroupItem
|
||||
:id="item.id"
|
||||
@@ -180,16 +191,18 @@ function onUpdate(value: any) {
|
||||
</div>
|
||||
|
||||
<div :class="ui.wrapper({ class: props.ui?.wrapper })">
|
||||
<Label :class="ui.label({ class: props.ui?.label })" :for="item.id">
|
||||
<slot name="label" :item="item" :model-value="(modelValue as RadioGroupValue)">{{ item.label }}</slot>
|
||||
</Label>
|
||||
<component :is="variant === 'list' ? Label : 'p'" :class="ui.label({ class: props.ui?.label })" :for="item.id">
|
||||
<slot name="label" :item="item" :model-value="(modelValue as RadioGroupValue)">
|
||||
{{ item.label }}
|
||||
</slot>
|
||||
</component>
|
||||
<p v-if="item.description || !!slots.description" :class="ui.description({ class: props.ui?.description })">
|
||||
<slot name="description" :item="item" :model-value="(modelValue as RadioGroupValue)">
|
||||
{{ item.description }}
|
||||
</slot>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</component>
|
||||
</fieldset>
|
||||
</RadioGroupRoot>
|
||||
</template>
|
||||
|
||||
@@ -38,6 +38,7 @@ interface SelectItemBase {
|
||||
type?: 'label' | 'separator' | 'item'
|
||||
value?: string | number
|
||||
disabled?: boolean
|
||||
onSelect?(e?: Event): void
|
||||
[key: string]: any
|
||||
}
|
||||
export type SelectItem = SelectItemBase | AcceptableValue | boolean
|
||||
@@ -279,6 +280,7 @@ function isSelectItem(item: SelectItem): item is SelectItemBase {
|
||||
:class="ui.item({ class: props.ui?.item })"
|
||||
:disabled="isSelectItem(item) && item.disabled"
|
||||
:value="isSelectItem(item) ? get(item, props.valueKey as string) : item"
|
||||
@select="isSelectItem(item) && item.onSelect?.($event)"
|
||||
>
|
||||
<slot name="item" :item="(item as NestedItem<T>)" :index="index">
|
||||
<slot name="item-leading" :item="(item as NestedItem<T>)" :index="index">
|
||||
|
||||
@@ -36,7 +36,6 @@ interface _SelectMenuItem {
|
||||
* @defaultValue 'item'
|
||||
*/
|
||||
type?: 'label' | 'separator' | 'item'
|
||||
value?: string | number
|
||||
disabled?: boolean
|
||||
onSelect?(e?: Event): void
|
||||
[key: string]: any
|
||||
@@ -45,7 +44,7 @@ export type SelectMenuItem = _SelectMenuItem | AcceptableValue | boolean
|
||||
|
||||
type SelectMenuVariants = VariantProps<typeof selectMenu>
|
||||
|
||||
export interface SelectMenuProps<T extends ArrayOrNested<SelectMenuItem> = ArrayOrNested<SelectMenuItem>, VK extends GetItemKeys<T> | undefined = undefined, M extends boolean = false> extends Pick<ComboboxRootProps<T>, 'open' | 'defaultOpen' | 'disabled' | 'name' | 'resetSearchTermOnBlur' | 'highlightOnHover'>, UseComponentIconsProps {
|
||||
export interface SelectMenuProps<T extends ArrayOrNested<SelectMenuItem> = ArrayOrNested<SelectMenuItem>, VK extends GetItemKeys<T> | undefined = undefined, M extends boolean = false> extends Pick<ComboboxRootProps<T>, 'open' | 'defaultOpen' | 'disabled' | 'name' | 'resetSearchTermOnBlur' | 'resetSearchTermOnSelect' | 'highlightOnHover'>, UseComponentIconsProps {
|
||||
id?: string
|
||||
/** The placeholder text when the select is empty. */
|
||||
placeholder?: string
|
||||
@@ -189,7 +188,8 @@ const props = withDefaults(defineProps<SelectMenuProps<T, VK, M>>(), {
|
||||
portal: true,
|
||||
searchInput: true,
|
||||
labelKey: 'label' as never,
|
||||
resetSearchTermOnBlur: true
|
||||
resetSearchTermOnBlur: true,
|
||||
resetSearchTermOnSelect: true
|
||||
})
|
||||
const emits = defineEmits<SelectMenuEmits<T, VK, M>>()
|
||||
const slots = defineSlots<SelectMenuSlots<T, VK, M>>()
|
||||
@@ -200,7 +200,7 @@ const { t } = useLocale()
|
||||
const appConfig = useAppConfig()
|
||||
const { contains } = useFilter({ sensitivity: 'base' })
|
||||
|
||||
const rootProps = useForwardPropsEmits(reactivePick(props, 'modelValue', 'defaultValue', 'open', 'defaultOpen', 'required', 'multiple', 'resetSearchTermOnBlur', 'highlightOnHover'), emits)
|
||||
const rootProps = useForwardPropsEmits(reactivePick(props, 'modelValue', 'defaultValue', 'open', 'defaultOpen', 'required', 'multiple', 'resetSearchTermOnBlur', 'resetSearchTermOnSelect', 'highlightOnHover'), emits)
|
||||
const contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffset: 8, collisionPadding: 8, position: 'popper' }) as ComboboxContentProps)
|
||||
const arrowProps = toRef(() => props.arrow as ComboboxArrowProps)
|
||||
const searchInputProps = toRef(() => defu(props.searchInput, { placeholder: t('selectMenu.search'), variant: 'none' }) as InputProps)
|
||||
@@ -294,6 +294,10 @@ function onUpdate(value: any) {
|
||||
emits('change', event)
|
||||
emitFormChange()
|
||||
emitFormInput()
|
||||
|
||||
if (props.resetSearchTermOnSelect) {
|
||||
searchTerm.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
function onUpdateOpen(value: boolean) {
|
||||
@@ -322,6 +326,19 @@ function onUpdateOpen(value: boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
function onSelect(e: Event, item: SelectMenuItem) {
|
||||
if (!isSelectItem(item)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (item.disabled) {
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
item.onSelect?.(e)
|
||||
}
|
||||
|
||||
function isSelectItem(item: SelectMenuItem): item is _SelectMenuItem {
|
||||
return typeof item === 'object' && item !== null
|
||||
}
|
||||
@@ -413,7 +430,7 @@ function isSelectItem(item: SelectMenuItem): item is _SelectMenuItem {
|
||||
:class="ui.item({ class: props.ui?.item })"
|
||||
:disabled="isSelectItem(item) && item.disabled"
|
||||
:value="props.valueKey && isSelectItem(item) ? get(item, props.valueKey as string) : item"
|
||||
@select="isSelectItem(item) && item.onSelect"
|
||||
@select="onSelect($event, item)"
|
||||
>
|
||||
<slot name="item" :item="(item as NestedItem<T>)" :index="index">
|
||||
<slot name="item-leading" :item="(item as NestedItem<T>)" :index="index">
|
||||
|
||||
@@ -145,7 +145,7 @@ defineExpose({
|
||||
<StepperTrigger :class="ui.trigger({ class: props.ui?.trigger })">
|
||||
<StepperIndicator :class="ui.indicator({ class: props.ui?.indicator })">
|
||||
<slot name="indicator" :item="item">
|
||||
<UIcon v-if="item.icon" :name="item.icon" :class="ui.icon({ class: props.ui?.indicator })" />
|
||||
<UIcon v-if="item.icon" :name="item.icon" :class="ui.icon({ class: props.ui?.icon })" />
|
||||
<template v-else>
|
||||
{{ count + 1 }}
|
||||
</template>
|
||||
@@ -174,7 +174,7 @@ defineExpose({
|
||||
</StepperItem>
|
||||
</div>
|
||||
|
||||
<div v-if="currentStep?.content || !!slots.content || currentStep?.slot" :class="ui.content({ class: props.ui?.description })">
|
||||
<div v-if="currentStep?.content || !!slots.content || currentStep?.slot" :class="ui.content({ class: props.ui?.content })">
|
||||
<slot
|
||||
:name="((currentStep?.slot || 'content') as keyof StepperSlots<T>)"
|
||||
:item="(currentStep as Extract<T, { slot: string }>)"
|
||||
|
||||
@@ -3,7 +3,9 @@ import type { VariantProps } from 'tailwind-variants'
|
||||
import type { AppConfig } from '@nuxt/schema'
|
||||
import _appConfig from '#build/app.config'
|
||||
import theme from '#build/ui/textarea'
|
||||
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
|
||||
import { tv } from '../utils/tv'
|
||||
import type { AvatarProps } from '../types'
|
||||
import type { PartialString } from '../types/utils'
|
||||
|
||||
const appConfigTextarea = _appConfig as AppConfig & { ui: { textarea: Partial<typeof theme> } }
|
||||
@@ -12,7 +14,7 @@ const textarea = tv({ extend: tv(theme), ...(appConfigTextarea.ui?.textarea || {
|
||||
|
||||
type TextareaVariants = VariantProps<typeof textarea>
|
||||
|
||||
export interface TextareaProps {
|
||||
export interface TextareaProps extends UseComponentIconsProps {
|
||||
/**
|
||||
* The element or component this component should render as.
|
||||
* @defaultValue 'div'
|
||||
@@ -37,11 +39,12 @@ export interface TextareaProps {
|
||||
required?: boolean
|
||||
autofocus?: boolean
|
||||
autofocusDelay?: number
|
||||
autoresize?: boolean
|
||||
autoresizeDelay?: number
|
||||
disabled?: boolean
|
||||
class?: any
|
||||
rows?: number
|
||||
maxrows?: number
|
||||
autoresize?: boolean
|
||||
/** Highlight the ring color like a focus state. */
|
||||
highlight?: boolean
|
||||
ui?: PartialString<typeof textarea.slots>
|
||||
@@ -54,13 +57,16 @@ export interface TextareaEmits {
|
||||
}
|
||||
|
||||
export interface TextareaSlots {
|
||||
leading(props?: {}): any
|
||||
default(props?: {}): any
|
||||
trailing(props?: {}): any
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, nextTick, watch } from 'vue'
|
||||
import { Primitive } from 'reka-ui'
|
||||
import { useComponentIcons } from '../composables/useComponentIcons'
|
||||
import { useFormField } from '../composables/useFormField'
|
||||
import { looseToNumber } from '../utils'
|
||||
|
||||
@@ -69,30 +75,30 @@ defineOptions({ inheritAttrs: false })
|
||||
const props = withDefaults(defineProps<TextareaProps>(), {
|
||||
rows: 3,
|
||||
maxrows: 0,
|
||||
autofocusDelay: 0
|
||||
autofocusDelay: 0,
|
||||
autoresizeDelay: 0
|
||||
})
|
||||
defineSlots<TextareaSlots>()
|
||||
const slots = defineSlots<TextareaSlots>()
|
||||
const emits = defineEmits<TextareaEmits>()
|
||||
|
||||
const [modelValue, modelModifiers] = defineModel<string | number | null>()
|
||||
|
||||
const { emitFormFocus, emitFormBlur, emitFormInput, emitFormChange, size, color, id, name, highlight, disabled, ariaAttrs } = useFormField<TextareaProps>(props, { deferInputValidation: true })
|
||||
const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(props)
|
||||
|
||||
const ui = computed(() => textarea({
|
||||
color: color.value,
|
||||
variant: props.variant,
|
||||
size: size?.value,
|
||||
highlight: highlight.value
|
||||
loading: props.loading,
|
||||
highlight: highlight.value,
|
||||
autoresize: props.autoresize,
|
||||
leading: isLeading.value || !!props.avatar || !!slots.leading,
|
||||
trailing: isTrailing.value || !!slots.trailing
|
||||
}))
|
||||
|
||||
const textareaRef = ref<HTMLTextAreaElement | null>(null)
|
||||
|
||||
function autoFocus() {
|
||||
if (props.autofocus) {
|
||||
textareaRef.value?.focus()
|
||||
}
|
||||
}
|
||||
|
||||
// Custom function to handle the v-model properties
|
||||
function updateInput(value: string | null) {
|
||||
if (modelModifiers.trim) {
|
||||
@@ -140,18 +146,14 @@ function onBlur(event: FocusEvent) {
|
||||
emits('blur', event)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
autoFocus()
|
||||
}, props.autofocusDelay)
|
||||
})
|
||||
function autoFocus() {
|
||||
if (props.autofocus) {
|
||||
textareaRef.value?.focus()
|
||||
}
|
||||
}
|
||||
|
||||
function autoResize() {
|
||||
if (props.autoresize) {
|
||||
if (!textareaRef.value) {
|
||||
return
|
||||
}
|
||||
|
||||
if (props.autoresize && textareaRef.value) {
|
||||
textareaRef.value.rows = props.rows
|
||||
const overflow = textareaRef.value.style.overflow
|
||||
textareaRef.value.style.overflow = 'hidden'
|
||||
@@ -176,14 +178,18 @@ watch(modelValue, () => {
|
||||
nextTick(autoResize)
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
textareaRef
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
autoFocus()
|
||||
}, props.autofocusDelay)
|
||||
|
||||
setTimeout(() => {
|
||||
autoResize()
|
||||
}, 100)
|
||||
}, props.autoresizeDelay)
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
textareaRef
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -207,5 +213,18 @@ onMounted(() => {
|
||||
/>
|
||||
|
||||
<slot />
|
||||
|
||||
<span v-if="isLeading || !!avatar || !!slots.leading" :class="ui.leading({ class: props.ui?.leading })">
|
||||
<slot name="leading">
|
||||
<UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
|
||||
<UAvatar v-else-if="!!avatar" :size="((props.ui?.leadingAvatarSize || ui.leadingAvatarSize()) as AvatarProps['size'])" v-bind="avatar" :class="ui.leadingAvatar({ class: props.ui?.leadingAvatar })" />
|
||||
</slot>
|
||||
</span>
|
||||
|
||||
<span v-if="isTrailing || !!slots.trailing" :class="ui.trailing({ class: props.ui?.trailing })">
|
||||
<slot name="trailing">
|
||||
<UIcon v-if="trailingIconName" :name="trailingIconName" :class="ui.trailingIcon({ class: props.ui?.trailingIcon })" />
|
||||
</slot>
|
||||
</span>
|
||||
</Primitive>
|
||||
</template>
|
||||
|
||||
@@ -112,7 +112,6 @@ export type TreeSlots<
|
||||
|
||||
<script setup lang="ts" generic="T extends TreeItem[], VK extends GetItemKeys<T> = 'value', M extends boolean = false">
|
||||
import { computed } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { TreeRoot, TreeItem, useForwardPropsEmits } from 'reka-ui'
|
||||
import { reactivePick, createReusableTemplate } from '@vueuse/core'
|
||||
import { get } from '../utils'
|
||||
@@ -127,22 +126,14 @@ const slots = defineSlots<TreeSlots<T>>()
|
||||
|
||||
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'items', 'multiple', 'expanded', 'disabled', 'propagateSelect'), emits)
|
||||
|
||||
const [DefineTreeTemplate, ReuseTreeTemplate] = createReusableTemplate<
|
||||
{ items?: NestedItem<T>[], level: number },
|
||||
TreeSlots<T>
|
||||
>({
|
||||
props: {
|
||||
items: Array as PropType<NestedItem<T>[]>,
|
||||
level: Number
|
||||
}
|
||||
})
|
||||
const [DefineTreeTemplate, ReuseTreeTemplate] = createReusableTemplate<{ items?: TreeItem[], level: number }, TreeSlots<T>>()
|
||||
|
||||
const ui = computed(() => tree({
|
||||
color: props.color,
|
||||
size: props.size
|
||||
}))
|
||||
|
||||
function getItemLabel(item: NestedItem<T>): string {
|
||||
function getItemLabel<Item extends TreeItem = NestedItem<T>>(item: Item): string {
|
||||
return get(item, props.labelKey as string)
|
||||
}
|
||||
|
||||
@@ -209,7 +200,7 @@ const defaultExpanded = computed(() =>
|
||||
</button>
|
||||
|
||||
<ul v-if="item.children?.length && isExpanded" :class="ui.listWithChildren({ class: props.ui?.listWithChildren })">
|
||||
<ReuseTreeTemplate :items="(item.children as NestedItem<T>[])" :level="level + 1" />
|
||||
<ReuseTreeTemplate :items="item.children" :level="level + 1" />
|
||||
</ul>
|
||||
</TreeItem>
|
||||
</li>
|
||||
@@ -222,6 +213,6 @@ const defaultExpanded = computed(() =>
|
||||
:default-expanded="defaultExpanded"
|
||||
:selection-behavior="selectionBehavior"
|
||||
>
|
||||
<ReuseTreeTemplate :items="(items as NestedItem<T>[] | undefined)" :level="0" />
|
||||
<ReuseTreeTemplate :items="items" :level="0" />
|
||||
</TreeRoot>
|
||||
</template>
|
||||
|
||||
56
src/runtime/locale/bg.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import type { Messages } from '../types'
|
||||
import { defineLocale } from '../composables/defineLocale'
|
||||
|
||||
export default defineLocale<Messages>({
|
||||
name: 'Български',
|
||||
code: 'bg',
|
||||
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: 'Няма данни'
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,5 +1,6 @@
|
||||
export { default as ar } from './ar'
|
||||
export { default as az } from './az'
|
||||
export { default as bg } from './bg'
|
||||
export { default as bn } from './bn'
|
||||
export { default as ca } from './ca'
|
||||
export { default as cs } from './cs'
|
||||
|
||||
@@ -2,8 +2,6 @@ import { computed } from 'vue'
|
||||
import colors from 'tailwindcss/colors'
|
||||
import type { UseHeadInput } from '@unhead/vue/types'
|
||||
import { defineNuxtPlugin, useAppConfig, useNuxtApp, useHead } from '#imports'
|
||||
// FIXME: https://github.com/nuxt/module-builder/issues/141#issuecomment-2078248248
|
||||
import type {} from '#app'
|
||||
|
||||
const shades = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950] as const
|
||||
|
||||
|
||||
@@ -18,14 +18,14 @@ export default (options: Required<ModuleOptions>) => ({
|
||||
vertical: {
|
||||
container: 'flex-col -mt-4',
|
||||
item: 'pt-4',
|
||||
prev: '-top-12 left-1/2 -translate-x-1/2 rotate-90 rtl:-rotate-90',
|
||||
next: '-bottom-12 left-1/2 -translate-x-1/2 rotate-90 rtl:-rotate-90'
|
||||
prev: 'top-4 sm:-top-12 left-1/2 -translate-x-1/2 rotate-90 rtl:-rotate-90',
|
||||
next: 'bottom-4 sm:-bottom-12 left-1/2 -translate-x-1/2 rotate-90 rtl:-rotate-90'
|
||||
},
|
||||
horizontal: {
|
||||
container: 'flex-row -ms-4',
|
||||
item: 'ps-4',
|
||||
prev: '-start-12 top-1/2 -translate-y-1/2',
|
||||
next: '-end-12 top-1/2 -translate-y-1/2'
|
||||
prev: 'start-4 sm:-start-12 top-1/2 -translate-y-1/2',
|
||||
next: 'end-4 sm:-end-12 top-1/2 -translate-y-1/2'
|
||||
}
|
||||
},
|
||||
active: {
|
||||
|
||||
@@ -9,8 +9,8 @@ export default (options: Required<ModuleOptions>) => ({
|
||||
viewport: 'relative divide-y divide-(--ui-border) scroll-py-1 overflow-y-auto flex-1 focus:outline-none',
|
||||
group: 'p-1 isolate',
|
||||
empty: 'py-6 text-center text-sm text-(--ui-text-muted)',
|
||||
label: 'px-2 py-1.5 text-xs font-semibold text-(--ui-text-highlighted)',
|
||||
item: 'group relative w-full flex items-center gap-2 px-2 py-1.5 text-sm select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-[calc(var(--ui-radius)*1.5)] data-disabled:cursor-not-allowed data-disabled:opacity-75',
|
||||
label: 'p-1.5 text-xs font-semibold text-(--ui-text-highlighted)',
|
||||
item: 'group relative w-full flex items-center gap-1.5 p-1.5 text-sm select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-[calc(var(--ui-radius)*1.5)] data-disabled:cursor-not-allowed data-disabled:opacity-75',
|
||||
itemLeadingIcon: 'shrink-0 size-5',
|
||||
itemLeadingAvatar: 'shrink-0',
|
||||
itemLeadingAvatarSize: '2xs',
|
||||
@@ -33,8 +33,8 @@ export default (options: Required<ModuleOptions>) => ({
|
||||
itemLeadingIcon: 'text-(--ui-text)'
|
||||
},
|
||||
false: {
|
||||
item: ['text-(--ui-text) data-highlighted:text-(--ui-text-highlighted) data-highlighted:before:bg-(--ui-bg-elevated)/50', options.theme.transitions && 'transition-colors before:transition-colors'],
|
||||
itemLeadingIcon: ['text-(--ui-text-dimmed) group-data-highlighted:text-(--ui-text)', options.theme.transitions && 'transition-colors']
|
||||
item: ['text-(--ui-text) data-highlighted:not-data-disabled:text-(--ui-text-highlighted) data-highlighted:not-data-disabled:before:bg-(--ui-bg-elevated)/50', options.theme.transitions && 'transition-colors before:transition-colors'],
|
||||
itemLeadingIcon: ['text-(--ui-text-dimmed) group-data-highlighted:not-group-data-disabled:text-(--ui-text)', options.theme.transitions && 'transition-colors']
|
||||
}
|
||||
},
|
||||
loading: {
|
||||
|
||||
@@ -14,8 +14,8 @@ export default (options: Required<ModuleOptions>) => {
|
||||
empty: 'py-2 text-center text-sm text-(--ui-text-muted)',
|
||||
label: 'font-semibold text-(--ui-text-highlighted)',
|
||||
separator: '-mx-1 my-1 h-px bg-(--ui-border)',
|
||||
item: ['group relative w-full flex items-center gap-1.5 p-1.5 text-sm select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-[calc(var(--ui-radius)*1.5)] data-disabled:cursor-not-allowed data-disabled:opacity-75 text-(--ui-text) data-highlighted:text-(--ui-text-highlighted) data-highlighted:before:bg-(--ui-bg-elevated)/50', options.theme.transitions && 'transition-colors before:transition-colors'],
|
||||
itemLeadingIcon: ['shrink-0 text-(--ui-text-dimmed) group-data-highlighted:text-(--ui-text)', options.theme.transitions && 'transition-colors'],
|
||||
item: ['group relative w-full flex items-center gap-1.5 p-1.5 text-sm select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-[calc(var(--ui-radius)*1.5)] data-disabled:cursor-not-allowed data-disabled:opacity-75 text-(--ui-text) data-highlighted:not-data-disabled:text-(--ui-text-highlighted) data-highlighted:not-data-disabled:before:bg-(--ui-bg-elevated)/50', options.theme.transitions && 'transition-colors before:transition-colors'],
|
||||
itemLeadingIcon: ['shrink-0 text-(--ui-text-dimmed) group-data-highlighted:not-group-data-disabled:text-(--ui-text)', options.theme.transitions && 'transition-colors'],
|
||||
itemLeadingAvatar: 'shrink-0',
|
||||
itemLeadingAvatarSize: '',
|
||||
itemLeadingChip: 'shrink-0',
|
||||
|
||||
@@ -27,7 +27,7 @@ export default (options: Required<ModuleOptions>) => ({
|
||||
separator: 'px-2 h-px bg-(--ui-border)',
|
||||
viewportWrapper: 'absolute top-full left-0 flex w-full',
|
||||
viewport: 'relative overflow-hidden bg-(--ui-bg) shadow-lg rounded-[calc(var(--ui-radius)*1.5)] ring ring-(--ui-border) h-(--reka-navigation-menu-viewport-height) w-full transition-[width,height,left] duration-200 origin-[top_center] data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in] z-[1]',
|
||||
content: 'absolute top-0 left-0 w-full',
|
||||
content: 'absolute top-0 left-0 w-full sm:w-auto',
|
||||
indicator: 'absolute data-[state=visible]:animate-[fade-in_100ms_ease-out] data-[state=hidden]:animate-[fade-out_100ms_ease-in] data-[state=hidden]:opacity-0 bottom-0 z-[2] w-(--reka-navigation-menu-indicator-size) translate-x-(--reka-navigation-menu-indicator-position) flex h-2.5 items-end justify-center overflow-hidden transition-[translate,width] duration-200',
|
||||
arrow: 'relative top-[50%] size-2.5 rotate-45 border border-(--ui-border) bg-(--ui-bg) z-[1] rounded-[calc(var(--ui-radius)/2)]'
|
||||
},
|
||||
@@ -65,13 +65,11 @@ export default (options: Required<ModuleOptions>) => ({
|
||||
},
|
||||
contentOrientation: {
|
||||
horizontal: {
|
||||
viewport: '',
|
||||
viewportWrapper: 'justify-center',
|
||||
content: 'data-[motion=from-start]:animate-[enter-from-left_200ms_ease] data-[motion=from-end]:animate-[enter-from-right_200ms_ease] data-[motion=to-start]:animate-[exit-to-left_200ms_ease] data-[motion=to-end]:animate-[exit-to-right_200ms_ease]'
|
||||
},
|
||||
vertical: {
|
||||
viewport: 'sm:w-(--reka-navigation-menu-viewport-width) left-(--reka-navigation-menu-viewport-left)',
|
||||
content: ''
|
||||
viewport: 'sm:w-(--reka-navigation-menu-viewport-width) left-(--reka-navigation-menu-viewport-left)'
|
||||
}
|
||||
},
|
||||
active: {
|
||||
|
||||
@@ -9,7 +9,7 @@ export default (options: Required<ModuleOptions>) => ({
|
||||
base: 'rounded-full ring ring-inset ring-(--ui-border-accented) focus-visible:outline-2 focus-visible:outline-offset-2',
|
||||
indicator: 'flex items-center justify-center size-full rounded-full after:bg-(--ui-bg) after:rounded-full',
|
||||
container: 'flex items-center',
|
||||
wrapper: 'ms-2',
|
||||
wrapper: 'w-full',
|
||||
label: 'block font-medium text-(--ui-text)',
|
||||
description: 'text-(--ui-text-muted)'
|
||||
},
|
||||
@@ -24,6 +24,16 @@ export default (options: Required<ModuleOptions>) => ({
|
||||
indicator: 'bg-(--ui-bg-inverted)'
|
||||
}
|
||||
},
|
||||
variant: {
|
||||
list: {
|
||||
},
|
||||
card: {
|
||||
item: 'items-center border border-(--ui-border-muted) rounded-lg'
|
||||
},
|
||||
table: {
|
||||
item: 'border border-(--ui-border-muted)'
|
||||
}
|
||||
},
|
||||
orientation: {
|
||||
horizontal: {
|
||||
fieldset: 'flex-row',
|
||||
@@ -33,6 +43,20 @@ export default (options: Required<ModuleOptions>) => ({
|
||||
fieldset: 'flex-col'
|
||||
}
|
||||
},
|
||||
indicator: {
|
||||
start: {
|
||||
item: 'flex-row',
|
||||
base: 'me-2'
|
||||
},
|
||||
end: {
|
||||
item: 'flex-row-reverse',
|
||||
base: 'ms-2'
|
||||
},
|
||||
hidden: {
|
||||
base: 'sr-only',
|
||||
wrapper: 'text-center'
|
||||
}
|
||||
},
|
||||
size: {
|
||||
xs: {
|
||||
fieldset: 'gap-0.5',
|
||||
@@ -87,8 +111,62 @@ export default (options: Required<ModuleOptions>) => ({
|
||||
}
|
||||
}
|
||||
},
|
||||
compoundVariants: [
|
||||
{ size: 'xs', variant: ['card', 'table'], class: { item: 'p-2.5' } },
|
||||
{ size: 'sm', variant: ['card', 'table'], class: { item: 'p-3' } },
|
||||
{ size: 'md', variant: ['card', 'table'], class: { item: 'p-3.5' } },
|
||||
{ size: 'lg', variant: ['card', 'table'], class: { item: 'p-4' } },
|
||||
{ size: 'xl', variant: ['card', 'table'], class: { item: 'p-4.5' } },
|
||||
{
|
||||
orientation: 'horizontal',
|
||||
variant: 'table',
|
||||
class: {
|
||||
item: 'first-of-type:rounded-l-lg last-of-type:rounded-r-lg',
|
||||
fieldset: 'gap-0 -space-x-px'
|
||||
}
|
||||
},
|
||||
{
|
||||
orientation: 'vertical',
|
||||
variant: 'table',
|
||||
class: {
|
||||
item: 'first-of-type:rounded-t-lg last-of-type:rounded-b-lg',
|
||||
fieldset: 'gap-0 -space-y-px'
|
||||
}
|
||||
},
|
||||
...(options.theme.colors || []).map((color: string) => ({
|
||||
color,
|
||||
variant: 'card',
|
||||
class: {
|
||||
item: `has-data-[state=checked]:border-(--ui-${color})`
|
||||
}
|
||||
})),
|
||||
{
|
||||
color: 'neutral',
|
||||
variant: 'card',
|
||||
class: {
|
||||
item: 'has-data-[state=checked]:border-(--ui-border-elevated)'
|
||||
}
|
||||
},
|
||||
...(options.theme.colors || []).map((color: string) => ({
|
||||
color,
|
||||
variant: 'table',
|
||||
class: {
|
||||
item: `has-data-[state=checked]:bg-(--ui-${color})/10 has-data-[state=checked]:border-(--ui-${color})/50 has-data-[state=checked]:z-[1]`
|
||||
}
|
||||
})),
|
||||
{
|
||||
color: 'neutral',
|
||||
variant: 'table',
|
||||
class: {
|
||||
item: 'has-data-[state=checked]:bg-(--ui-bg-elevated) has-data-[state=checked]:border-(--ui-border-inverted)/25 has-data-[state=checked]:z-[1]'
|
||||
}
|
||||
}
|
||||
],
|
||||
defaultVariants: {
|
||||
size: 'md',
|
||||
color: 'primary'
|
||||
color: 'primary',
|
||||
variant: 'list',
|
||||
orientation: 'vertical',
|
||||
indicator: 'start'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -17,8 +17,8 @@ export default (options: Required<ModuleOptions>) => {
|
||||
empty: 'py-2 text-center text-sm text-(--ui-text-muted)',
|
||||
label: 'font-semibold text-(--ui-text-highlighted)',
|
||||
separator: '-mx-1 my-1 h-px bg-(--ui-border)',
|
||||
item: ['group relative w-full flex items-center select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-[calc(var(--ui-radius)*1.5)] data-disabled:cursor-not-allowed data-disabled:opacity-75 text-(--ui-text) data-highlighted:text-(--ui-text-highlighted) data-highlighted:before:bg-(--ui-bg-elevated)/50', options.theme.transitions && 'transition-colors before:transition-colors'],
|
||||
itemLeadingIcon: ['shrink-0 text-(--ui-text-dimmed) group-data-highlighted:text-(--ui-text)', options.theme.transitions && 'transition-colors'],
|
||||
item: ['group relative w-full flex items-center select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-[calc(var(--ui-radius)*1.5)] data-disabled:cursor-not-allowed data-disabled:opacity-75 text-(--ui-text) data-highlighted:not-data-disabled:text-(--ui-text-highlighted) data-highlighted:not-data-disabled:before:bg-(--ui-bg-elevated)/50', options.theme.transitions && 'transition-colors before:transition-colors'],
|
||||
itemLeadingIcon: ['shrink-0 text-(--ui-text-dimmed) group-data-highlighted:not-group-data-disabled:text-(--ui-text)', options.theme.transitions && 'transition-colors'],
|
||||
itemLeadingAvatar: 'shrink-0',
|
||||
itemLeadingAvatarSize: '',
|
||||
itemLeadingChip: 'shrink-0',
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import { defu } from 'defu'
|
||||
import type { ModuleOptions } from '../module'
|
||||
import input from './input'
|
||||
|
||||
export default (options: Required<ModuleOptions>) => {
|
||||
return input(options)
|
||||
return defu({
|
||||
variants: {
|
||||
autoresize: {
|
||||
true: {
|
||||
base: 'resize-none'
|
||||
}
|
||||
}
|
||||
}
|
||||
}, input(options))
|
||||
}
|
||||
|
||||
@@ -186,6 +186,12 @@ describe('InputMenu', () => {
|
||||
valueKey: 'value'
|
||||
})).toEqualTypeOf<[number[]]>()
|
||||
|
||||
// with object item and object valueKey
|
||||
expectEmitPayloadType('update:modelValue', () => InputMenu({
|
||||
items: [{ label: 'foo', value: { id: 1, name: 'bar' } }],
|
||||
valueKey: 'value'
|
||||
})).toEqualTypeOf<[{ id: number, name: string }]>()
|
||||
|
||||
// with string item
|
||||
expectEmitPayloadType('update:modelValue', () => InputMenu({
|
||||
items: ['foo']
|
||||
|
||||
@@ -8,6 +8,8 @@ import type { FormInputEvents } from '~/src/module'
|
||||
|
||||
describe('RadioGroup', () => {
|
||||
const sizes = Object.keys(theme.variants.size) as any
|
||||
const variants = Object.keys(theme.variants.variant) as any
|
||||
const indicators = Object.keys(theme.variants.indicator) as any
|
||||
|
||||
const items = [
|
||||
{ value: '1', label: 'Option 1' },
|
||||
@@ -28,8 +30,10 @@ describe('RadioGroup', () => {
|
||||
['with description', { props: { items: items.map((opt, count) => ({ ...opt, description: `Description ${count}` })) } }],
|
||||
['with required', { props: { ...props, legend: 'Legend', required: true } }],
|
||||
...sizes.map((size: string) => [`with size ${size}`, { props: { ...props, size } }]),
|
||||
['with color neutral', { props: { color: 'neutral', defaultValue: '1' } }],
|
||||
['with orientation', { props: { ...props, orientation: 'horizontal' } }],
|
||||
...variants.map((variant: string) => [`with primary variant ${variant}`, { props: { ...props, variant, defaultValue: '1' } }]),
|
||||
...variants.map((variant: string) => [`with neutral variant ${variant}`, { props: { ...props, variant, color: 'neutral', defaultValue: '1' } }]),
|
||||
...variants.map((variant: string) => [`with horizontal variant ${variant}`, { props: { ...props, variant, orientation: 'horizontal', defaultValue: '1' } }]),
|
||||
...indicators.map((indicator: string) => [`with indicator ${indicator}`, { props: { ...props, indicator } }]),
|
||||
['with ariaLabel', { props, attrs: { 'aria-label': 'Aria label' } }],
|
||||
['with as', { props: { ...props, as: 'section' } }],
|
||||
['with class', { props: { ...props, class: 'absolute' } }],
|
||||
|
||||