Compare commits

..

19 Commits

Author SHA1 Message Date
Benjamin Canac
afcf86ac63 up 2025-07-11 16:30:33 +02:00
Benjamin Canac
b13a4370da up 2025-07-11 16:23:20 +02:00
Benjamin Canac
5b0ffeac5e chore(deps): update nuxt to 4.0.0-rc. 2025-07-11 16:18:34 +02:00
Benjamin Canac
55e06e97e7 fix(Carousel): improve accessibility
Resolves #4494
2025-07-10 14:18:36 +02:00
Eugen Istoc
a813ea700e test(useOverlay): add composable tests (#4482)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-07-10 12:04:18 +02:00
Benjamin Canac
a4d0ca7396 fix(FormField): improve error type with boolean
Resolves #4496
2025-07-10 12:04:06 +02:00
Eugen Istoc
5ad7dabbdc fix(useOverlay): don't use patch when passing props to open (#4497) 2025-07-10 10:45:55 +02:00
Benjamin Canac
d8160ba6ef fix(vue): stub clearError 2025-07-09 17:48:30 +02:00
Alex
fc24e03cc4 fix(Carousel/Tree): add type to button elements for accessibility (#4493) 2025-07-09 17:43:49 +02:00
Benjamin Canac
1902492cf2 docs(pricing): add banner 2025-07-09 16:17:24 +02:00
Benjamin Canac
0c525638d7 chore(deps): update @nuxt/ui-pro 2025-07-09 16:17:09 +02:00
Benjamin Canac
35f90b9920 feat(module): add theme.defaultVariants option (#4400) 2025-07-09 14:37:49 +02:00
Benjamin Canac
836f74849b fix(NavigationMenu/Tabs): proxy fallthrough attributes 2025-07-09 12:58:35 +02:00
Benjamin Canac
78f92a24f8 fix(module): merge user's options when installing modules 2025-07-09 12:24:18 +02:00
Benjamin Canac
52908c19f1 docs(app): update banner 2025-07-08 15:38:00 +02:00
J-Michalek
513cca25f6 docs(input-menu/select-menu/select): add example with full item width (#4419)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-07-08 12:43:49 +02:00
Benjamin Canac
c1427a3264 docs(ComponentSlots): hide ui slot prop content 2025-07-08 11:48:05 +02:00
Eugen Istoc
6519a74de4 fix(useOverlay): improve props handling by merging existing and new (#4478) 2025-07-08 10:51:34 +02:00
Benjamin Canac
da05c37ffe docs(input): hide password reveal button in edge
Resolves #4484
2025-07-08 10:39:25 +02:00
118 changed files with 1464 additions and 1991 deletions

View File

@@ -53,7 +53,7 @@ provide('navigation', mappedNavigation)
<NuxtLoadingIndicator color="var(--ui-primary)" :height="2" />
<template v-if="!route.path.startsWith('/examples')">
<!-- <Banner /> -->
<Banner />
<Header :links="links" />
</template>

View File

@@ -1,15 +1,19 @@
<template>
<UBanner
id="ui3-launch"
title="Nuxt UI v3 is officially released!"
icon="i-lucide-rocket"
:actions="[
{
label: 'Discover Nuxt UI Pro',
to: '/pro/pricing',
trailingIcon: 'i-lucide-arrow-right'
}
]"
id="nuxtlabs-join-vercel"
title="NuxtLabs is joining Vercel"
icon="i-simple-icons-vercel"
to="https://nuxtlabs.com/?utm_source=nuxt-ui&utm_medium=banner&utm_campaign=nuxtlabs-vercel"
target="_blank"
close
:actions="[{
label: 'Read the announcement',
color: 'neutral',
variant: 'outline',
trailingIcon: 'i-lucide-arrow-right',
to: 'https://nuxtlabs.com/?utm_source=nuxt-ui&utm_medium=banner&utm_campaign=nuxtlabs-vercel',
target: '_blank',
class: 'ring-0'
}]"
/>
</template>

View File

@@ -34,7 +34,7 @@ const meta = await fetchComponentMeta(name as any)
</ProseCode>
</ProseTd>
<ProseTd>
<HighlightInlineType v-if="slot.type" :type="slot.type" />
<HighlightInlineType v-if="slot.type" :type="slot.type.replace(/ui:\s*\{[^}]*\}/g, 'ui: {}')" />
<MDC v-if="slot.description" :value="slot.description" class="text-toned mt-1" :cache-key="`${kebabCase(route.path)}-${slot.name}-description`" />
</ProseTd>

View File

@@ -14,8 +14,8 @@ const items = [
v-slot="{ item }"
orientation="vertical"
:items="items"
class="w-full max-w-xs mx-auto"
:ui="{ container: 'h-[336px]' }"
class="w-full max-w-xs mx-auto"
>
<img :src="item" width="320" height="320" class="rounded-lg">
</UCarousel>

View File

@@ -0,0 +1,31 @@
<script setup lang="ts">
const { data: users } = await useFetch('https://jsonplaceholder.typicode.com/users', {
key: 'typicode-users-email',
transform: (data: { id: number, name: string, email: string }[]) => {
return data?.map(user => ({
label: user.name,
value: String(user.id),
email: user.email,
avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` }
}))
},
lazy: true
})
</script>
<template>
<UInputMenu
:items="users"
icon="i-lucide-user"
placeholder="Select user"
:ui="{ content: 'min-w-fit' }"
>
<template #item-label="{ item }">
{{ item.label }}
<span class="text-muted">
{{ item.email }}
</span>
</template>
</UInputMenu>
</template>

View File

@@ -35,6 +35,7 @@ const items = ref([
}
}
] satisfies InputMenuItem[])
const value = ref(items.value[0])
</script>

View File

@@ -40,9 +40,9 @@ const text = computed(() => {
placeholder="Password"
:color="color"
:type="show ? 'text' : 'password'"
:ui="{ trailing: 'pe-1' }"
:aria-invalid="score < 4"
aria-describedby="password-strength"
:ui="{ trailing: 'pe-1' }"
class="w-full"
>
<template #trailing>

View File

@@ -24,3 +24,10 @@ const password = ref('')
</template>
</UInput>
</template>
<style>
/* Hide the password reveal button in Edge */
::-ms-reveal {
display: none;
}
</style>

View File

@@ -62,13 +62,13 @@ const items = [
<template>
<UNavigationMenu
:items="items"
class="w-full justify-center"
:ui="{
viewport: 'sm:w-(--reka-navigation-menu-viewport-width)',
content: 'sm:w-auto',
childList: 'sm:w-96',
childLinkDescription: 'text-balance line-clamp-2'
}"
class="w-full justify-center"
>
<template #docs-content="{ item }">
<ul class="grid gap-2 p-4 lg:w-[500px] lg:grid-cols-[minmax(0,.75fr)_minmax(0,1fr)]">

View File

@@ -0,0 +1,32 @@
<script setup lang="ts">
const { data: users } = await useFetch('https://jsonplaceholder.typicode.com/users', {
key: 'typicode-users-email',
transform: (data: { id: number, name: string, email: string }[]) => {
return data?.map(user => ({
label: user.name,
value: String(user.id),
email: user.email,
avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` }
}))
},
lazy: true
})
</script>
<template>
<USelectMenu
:items="users"
icon="i-lucide-user"
placeholder="Select user"
:ui="{ content: 'min-w-fit' }"
class="w-48"
>
<template #item-label="{ item }">
{{ item.label }}
<span class="text-muted">
{{ item.email }}
</span>
</template>
</USelectMenu>
</template>

View File

@@ -35,6 +35,7 @@ const items = ref([
}
}
] satisfies SelectMenuItem[])
const value = ref(items.value[0])
</script>

View File

@@ -24,6 +24,7 @@ const items = ref([
}
}
] satisfies SelectMenuItem[])
const value = ref(items.value[0])
</script>

View File

@@ -23,6 +23,7 @@ const items = ref([
icon: 'i-lucide-circle-check'
}
] satisfies SelectMenuItem[])
const value = ref(items.value[0])
</script>

View File

@@ -0,0 +1,35 @@
<script setup lang="ts">
const value = ref<string>()
const { data: users } = await useFetch('https://jsonplaceholder.typicode.com/users', {
key: 'typicode-users-email',
transform: (data: { id: number, name: string, email: string }[]) => {
return data?.map(user => ({
label: user.name,
email: user.email,
value: String(user.id),
avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` }
}))
},
lazy: true
})
</script>
<template>
<USelect
v-model="value"
:items="users"
placeholder="Select user"
value-key="value"
:ui="{ content: 'min-w-fit' }"
class="w-48"
>
<template #item-label="{ item }">
{{ item.label }}
<span class="text-muted">
{{ item.email }}
</span>
</template>
</USelect>
</template>

View File

@@ -24,8 +24,8 @@ function getUserAvatar(value: string) {
:loading="status === 'pending'"
icon="i-lucide-user"
placeholder="Select user"
class="w-48"
value-key="value"
class="w-48"
>
<template #leading="{ modelValue, ui }">
<UAvatar

View File

@@ -35,6 +35,7 @@ const items = ref([
}
}
] satisfies SelectItem[])
const value = ref(items.value[0]?.value)
const avatar = computed(() => items.value.find(item => item.value === value.value)?.avatar)

View File

@@ -23,6 +23,7 @@ const items = ref([
icon: 'i-lucide-circle-check'
}
] satisfies SelectItem[])
const value = ref(items.value[0]?.value)
const icon = computed(() => items.value.find(item => item.value === value.value)?.icon)

View File

@@ -26,7 +26,7 @@ const state = reactive({
</script>
<template>
<UTabs :items="items" variant="link" class="gap-4 w-full" :ui="{ trigger: 'grow' }">
<UTabs :items="items" variant="link" :ui="{ trigger: 'grow' }" class="gap-4 w-full">
<template #account="{ item }">
<p class="text-muted mb-4">
{{ item.description }}

View File

@@ -27,8 +27,8 @@ const items: TimelineItem[] = [{
<template>
<UTimeline
:items="items"
:ui="{ item: 'even:flex-row-reverse even:-translate-x-[calc(100%-2rem)] even:text-right' }"
:default-value="2"
:ui="{ item: 'even:flex-row-reverse even:-translate-x-[calc(100%-2rem)] even:text-right' }"
class="translate-x-[calc(50%-1rem)]"
/>
</template>

View File

@@ -42,11 +42,11 @@ const items = [{
<UTimeline
:items="items"
size="xs"
class="w-96"
:ui="{
date: 'float-end ms-1',
description: 'px-3 py-2 ring ring-default mt-2 rounded-md text-default'
}"
class="w-96"
>
<template #title="{ item }">
<span>{{ item.username }}</span>

View File

@@ -7,12 +7,12 @@ const appConfig = useAppConfig()
<UFormField
label="toaster.duration"
size="sm"
class="inline-flex ring ring-accented rounded-sm"
:ui="{
wrapper: 'bg-elevated/50 rounded-l-sm flex border-r border-accented',
label: 'text-muted px-2 py-1.5',
container: 'mt-0'
}"
class="inline-flex ring ring-accented rounded-sm"
>
<UInput
v-model="appConfig.toaster.duration"

View File

@@ -7,12 +7,12 @@ const appConfig = useAppConfig()
<UFormField
label="toaster.expand"
size="sm"
class="inline-flex ring ring-accented rounded-sm"
:ui="{
wrapper: 'bg-elevated/50 rounded-l-sm flex border-r border-accented',
label: 'text-muted px-2 py-1.5',
container: 'mt-0'
}"
class="inline-flex ring ring-accented rounded-sm"
>
<USelectMenu
v-model="appConfig.toaster.expand"

View File

@@ -10,12 +10,12 @@ const appConfig = useAppConfig()
<UFormField
label="toaster.position"
size="sm"
class="inline-flex ring ring-accented rounded-sm"
:ui="{
wrapper: 'bg-elevated/50 rounded-l-sm flex border-r border-accented',
label: 'text-muted px-2 py-1.5',
container: 'mt-0'
}"
class="inline-flex ring ring-accented rounded-sm"
>
<USelectMenu
v-model="appConfig.toaster.position"

View File

@@ -107,10 +107,6 @@ export function useLinks() {
to: 'https://github.com/Justineo/tempad-dev-plugin-nuxt-ui',
target: '_blank'
}]
}, {
label: 'Blog',
icon: 'i-lucide-file-text',
to: '/blog'
}, {
label: 'Releases',
icon: 'i-lucide-rocket',

View File

@@ -57,10 +57,6 @@ export function useSearchLinks() {
description: 'Meet the team behind Nuxt UI.',
icon: 'i-lucide-users',
to: '/team'
}, {
label: 'Blog',
icon: 'i-lucide-file-text',
to: '/blog'
}, {
label: 'Releases',
icon: 'i-lucide-rocket',

View File

@@ -59,7 +59,7 @@ provide('navigation', mappedNavigation)
<UApp>
<NuxtLoadingIndicator color="#FFF" />
<!-- <Banner /> -->
<Banner />
<Header :links="links" />

View File

@@ -1,7 +0,0 @@
seo:
title: Nuxt UI Blog
description: Read the latest news, tutorials, and updates about Nuxt UI.
title: Nuxt [UI]{.text-primary} Blog
navigation.title: Blog
description: Read the latest news, tutorials, and updates about Nuxt UI.
navigation.icon: i-lucide-newspaper

View File

@@ -1,183 +0,0 @@
<script setup lang="ts">
import { kebabCase } from 'scule'
const route = useRoute()
const [{ data: page }, { data: surround }] = await Promise.all([
useAsyncData(kebabCase(route.path), () => queryCollection('blog').path(route.path).first()),
useAsyncData(`${kebabCase(route.path)}-surround`, () => {
return queryCollectionItemSurroundings('blog', route.path, {
fields: ['description']
}).order('date', 'DESC')
})
])
if (!page.value) {
throw createError({ statusCode: 404, statusMessage: 'Article not found', fatal: true })
}
const title = page.value.seo?.title || page.value.title
const description = page.value.seo?.description || page.value.description
useSeoMeta({
title,
description,
ogDescription: description,
ogTitle: title
})
if (page.value.image) {
defineOgImage({ url: page.value.image })
} else {
defineOgImageComponent('Docs', {
headline: 'Blog',
title,
description
})
}
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
}).toUpperCase()
}
const getCategoryVariant = (category: string) => {
switch (category?.toLowerCase()) {
case 'release': return 'solid'
case 'tutorial': return 'soft'
case 'improvement': return 'soft'
default: return 'soft'
}
}
const getCategoryIcon = (category: string) => {
switch (category?.toLowerCase()) {
case 'release': return 'i-lucide-rocket'
case 'tutorial': return 'i-lucide-book-open'
case 'improvement': return 'i-lucide-trending-up'
default: return 'i-lucide-file-text'
}
}
</script>
<template>
<div v-if="page" class="min-h-screen">
<div class="border-b border-default">
<UContainer class="py-4">
<ULink to="/blog" class="flex items-center gap-2 text-sm">
<UIcon name="i-lucide-chevron-left" class="size-4" />
Back to Blog
</ULink>
</UContainer>
</div>
<div class="py-16 sm:pt-20 pb-10">
<UContainer class="max-w-4xl">
<div class="text-center space-y-6">
<div class="flex items-center justify-center gap-4 text-sm">
<UBadge
v-if="page.category"
:variant="getCategoryVariant(page.category)"
size="sm"
class="font-mono text-xs gap-2"
>
<UIcon :name="getCategoryIcon(page.category)" class="size-3" />
{{ page.category?.toUpperCase() }}
</UBadge>
<span class="text-muted font-mono text-xs">
{{ formatDate(page.date) }}
</span>
<span v-if="page.minRead" class="text-muted font-mono text-xs">
{{ page.minRead }} MIN READ
</span>
</div>
<Motion
:initial="{ opacity: 0, y: 20 }"
:animate="{ opacity: 1, y: 0 }"
:transition="{ duration: 0.6 }"
>
<h1 class="text-3xl sm:text-4xl lg:text-5xl font-bold text-highlighted leading-tight">
{{ page.title }}
</h1>
</Motion>
<Motion
:initial="{ opacity: 0, y: 20 }"
:animate="{ opacity: 1, y: 0 }"
:transition="{ delay: 0.1, duration: 0.6 }"
>
<p class="text-lg text-muted max-w-2xl mx-auto leading-relaxed">
{{ page.description }}
</p>
</Motion>
<Motion
v-if="page.authors?.length"
:initial="{ opacity: 0, y: 20 }"
:animate="{ opacity: 1, y: 0 }"
:transition="{ delay: 0.2, duration: 0.6 }"
class="flex justify-center"
>
<UAvatarGroup>
<ULink
v-for="(author, index) in page.authors"
:key="index"
:to="author.to"
raw
>
<UAvatar v-bind="author.avatar" />
</ULink>
</UAvatarGroup>
</Motion>
</div>
</UContainer>
</div>
<div v-if="page.image" class="py-4">
<UContainer class="max-w-6xl">
<Motion
:initial="{ opacity: 0, y: 30 }"
:animate="{ opacity: 1, y: 0 }"
:transition="{ delay: 0.3, duration: 0.8 }"
>
<NuxtImg
:src="page.image"
:alt="page.title"
class="w-full max-h-[400px] object-cover object-center max-w-5xl mx-auto"
/>
</Motion>
</UContainer>
</div>
<div class="py-12 sm:py-16">
<UContainer class="max-w-3xl">
<Motion
:initial="{ opacity: 0, y: 20 }"
:animate="{ opacity: 1, y: 0 }"
:transition="{ delay: 0.4, duration: 0.6 }"
>
<ContentRenderer
v-if="page.body"
:value="page"
/>
</Motion>
<div v-if="surround?.length" class="mt-16 pt-8 border-t border-default">
<Motion
:initial="{ opacity: 0, y: 20 }"
:animate="{ opacity: 1, y: 0 }"
:transition="{ delay: 0.6, duration: 0.6 }"
>
<UContentSurround :surround="surround" />
</Motion>
</div>
</UContainer>
</div>
</div>
</template>

View File

@@ -1,255 +0,0 @@
<script setup lang="ts">
// @ts-expect-error - yaml import not typed
import page from '.blog.yml'
const { data: posts } = await useAsyncData('blogs', () =>
queryCollection('blog').order('date', 'DESC').all()
)
const title = page.seo?.title || page.title
const description = page.seo?.description || page.description
useSeoMeta({
title,
description,
ogTitle: title,
ogDescription: description
})
const selectedFilter = ref('all')
const searchQuery = ref('')
const availableFilters = computed(() => {
if (!posts.value?.length) return [{ key: 'all', label: 'ALL', count: 0 }]
const postsData = posts.value
const categories = new Set(postsData.map(post => post.category?.toLowerCase()).filter(Boolean))
const filters = [
{ key: 'all', label: 'ALL', count: postsData.length }
]
categories.forEach((category) => {
const count = postsData.filter(p => p.category?.toLowerCase() === category).length
const label = category.replace(/\b\w/g, l => l.toUpperCase()).replace(/([a-z])([A-Z])/g, '$1 $2')
filters.push({
key: category,
label: label,
count
})
})
return filters.sort((a, b) => {
if (a.key === 'all') return -1
if (b.key === 'all') return 1
return b.count - a.count
})
})
const filteredPosts = computed(() => {
if (!posts.value) return []
let filtered = posts.value
if (selectedFilter.value !== 'all') {
filtered = filtered.filter(post => post.category?.toLowerCase() === selectedFilter.value)
}
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase()
filtered = filtered.filter(post =>
post.title?.toLowerCase().includes(query)
|| post.description?.toLowerCase().includes(query)
)
}
return filtered
})
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('en-US', {
month: 'short',
day: '2-digit'
}).toUpperCase()
}
const getCategoryVariant = (category: string) => {
switch (category?.toLowerCase()) {
case 'release': return 'solid'
case 'tutorial': return 'soft'
case 'improvement': return 'soft'
default: return 'soft'
}
}
const getCategoryIcon = (category: string) => {
switch (category?.toLowerCase()) {
case 'release': return 'i-lucide-rocket'
case 'tutorial': return 'i-lucide-book-open'
case 'improvement': return 'i-lucide-trending-up'
default: return 'i-lucide-file-text'
}
}
</script>
<template>
<div v-if="page" class="relative grid grid-rows-[auto_auto_1fr] min-h-[calc(100vh-150px)]">
<UPageHero :links="page.links" :ui="{ container: 'relative py-10 sm:py-16 lg:py-24' }">
<LazyStarsBg />
<div aria-hidden="true" class="absolute z-[-1] border-x border-default inset-0 mx-4 sm:mx-6 lg:mx-8" />
<template #title>
<MDC :value="page.title" unwrap="p" cache-key="pro-templates-hero-title" />
</template>
<template #description>
<MDC :value="page.description" unwrap="p" cache-key="pro-templates-hero-description" />
</template>
</UPageHero>
<UPageBody class="!my-0 !py-0 border-y border-default">
<UContainer>
<div class="border-x border-default px-4 sm:px-6 py-6 sm:py-8">
<div class="flex flex-col lg:flex-row lg:items-center justify-between gap-6 lg:gap-8 sm:mb-6">
<div class="flex flex-wrap items-center gap-2 sm:gap-3">
<Motion
v-for="(filter, index) in availableFilters"
:key="filter.key"
:initial="{ opacity: 0, y: 10 }"
:animate="{ opacity: 1, y: 0 }"
:transition="{ delay: index * 0.1 }"
>
<UButton
:variant="selectedFilter === filter.key ? 'solid' : 'ghost'"
:color="selectedFilter === filter.key ? 'primary' : 'neutral'"
size="sm"
class="font-medium transition-all duration-200 hover:scale-105 focus:scale-100 rounded-none text-xs sm:text-sm"
:leading-icon="selectedFilter === filter.key ? 'i-lucide-check' : 'i-lucide-circle'"
:label="filter.label"
@click="selectedFilter = filter.key"
>
<template #trailing>
<UBadge
:variant="selectedFilter === filter.key ? 'solid' : 'soft'"
size="xs"
>
{{ filter.count }}
</UBadge>
</template>
</UButton>
</Motion>
</div>
<div class="flex flex-col sm:flex-row items-stretch sm:items-center gap-2 sm:gap-3">
<div class="relative">
<UInput
v-model="searchQuery"
placeholder="Search posts..."
icon="i-lucide-search"
class="w-full sm:w-64"
:ui="{
base: 'rounded-none'
}"
/>
</div>
<UButton
variant="ghost"
class="rounded-none whitespace-nowrap"
icon="i-lucide-external-link"
label="Follow @nuxt_js on X"
to="https://x.com/nuxt_js"
target="_blank"
/>
</div>
</div>
</div>
<div class="border-x border-t border-default !gap-0">
<Motion
v-for="(post, index) in filteredPosts"
:key="post.path"
:initial="{ opacity: 0, x: -20 }"
:animate="{ opacity: 1, x: 0 }"
:transition="{ delay: index * 0.05, type: 'spring', stiffness: 300, damping: 30 }"
class="group border-b border-default last:border-b-0"
>
<ULink
:to="post.path"
class="flex flex-col sm:flex-row sm:items-center justify-between p-4 sm:p-6 hover:bg-muted/30 transition-all duration-200 gap-4 sm:gap-6"
>
<div class="flex flex-col sm:flex-row sm:items-center gap-3 sm:gap-6 flex-1 min-w-0">
<div class="text-xs text-muted font-mono shrink-0 sm:min-w-[60px]">
{{ formatDate(post.date) }}
</div>
<UBadge
:variant="getCategoryVariant(post.category)"
size="sm"
class="font-mono text-xs justify-center gap-2 shrink-0 self-start sm:self-center"
>
<UIcon :name="getCategoryIcon(post.category)" class="size-3" />
{{ post.category?.toUpperCase() || 'POST' }}
</UBadge>
<div class="flex-1 min-w-0">
<h3 class="font-medium text-highlighted group-hover:text-primary transition-colors duration-200 truncate sm:text-base">
{{ post.title }}
</h3>
<p class="text-sm text-muted mt-1 line-clamp-2 sm:line-clamp-1">
{{ post.description }}
</p>
</div>
</div>
<div class="flex items-center justify-between sm:justify-end gap-3 sm:gap-2 shrink-0">
<UAvatarGroup v-if="post.authors?.length" size="sm" class="sm:size-sm">
<UAvatar
v-for="author in post.authors.slice(0, 3)"
:key="author.name"
:src="author.avatar?.src"
:alt="author.name"
size="sm"
/>
</UAvatarGroup>
<UIcon
name="i-lucide-chevron-right"
class="size-4 text-muted group-hover:text-highlighted transition-colors duration-200 shrink-0"
/>
</div>
</ULink>
</Motion>
<Motion
v-if="filteredPosts.length === 0"
:initial="{ opacity: 0, y: 20 }"
:animate="{ opacity: 1, y: 0 }"
class="text-center py-12 sm:py-16 px-4 sm:px-6"
>
<UIcon name="i-lucide-search-x" class="size-10 sm:size-12 text-muted mx-auto mb-4" />
<h3 class="text-lg font-medium mb-2">
No posts found
</h3>
<p class="text-muted mb-4 text-sm sm:text-base">
{{ searchQuery ? `No posts match "${searchQuery}"` : 'No posts in this category yet' }}
</p>
<UButton
v-if="selectedFilter !== 'all' || searchQuery"
variant="outline"
label="Clear filters"
class="rounded-none"
@click="selectedFilter = 'all'; searchQuery = ''"
/>
</Motion>
</div>
</UContainer>
</UPageBody>
<UContainer class="relative min-h-24">
<div aria-hidden="true" class="absolute z-[-1] border-x border-default inset-0 mx-4 sm:mx-6 lg:mx-8" />
</UContainer>
</div>
</template>

View File

@@ -5,6 +5,17 @@ pricing:
title: Upgrade to Nuxt UI [Pro]{class="text-primary"}.
description: On top of 40+ open source components from Nuxt UI, Pro gives you access to 50+ premium Vue components to create beautiful & responsive Nuxt applications in minutes. It includes all primitives to build landing pages, documentations, blogs, dashboards or entire SaaS products.
freePlan:
description: "**NuxtLabs is joining Vercel** :tada: As part of this transition, Nuxt UI is becoming even more accessible.<br><br> **In September, we're launching Nuxt UI v4**: a free, open-source library that unifies Nuxt UI and Nuxt UI Pro, offering 100+ components and a complete free Figma Kit for everyone."
orientation: horizontal
button:
label: Read the announcement
to: 'https://nuxtlabs.com/?utm_source=nuxt-ui&utm_medium=banner&utm_campaign=nuxtlabs-vercel'
target: _blank
color: 'neutral'
trailingIcon: 'i-lucide-arrow-right'
ui:
trailingIcon: 'ms-0'
devPlan:
title: Free in development
description: Try Nuxt UI Pro for free in development, no credit card required. Upgrade when ready to deploy.
orientation: horizontal
@@ -13,6 +24,9 @@ pricing:
to: '/getting-started/installation/pro/nuxt'
color: 'neutral'
variant: 'subtle'
trailingIcon: 'i-lucide-arrow-right'
ui:
trailingIcon: 'ms-0'
figma:
title: Figma Kit Pro
description: Get all Nuxt UI Pro components in a Figma kit to design your next application before coding. Everything you need, from wire-framing to high-fidelity web integration.

View File

@@ -34,10 +34,19 @@ useSeoMeta({
<div class="flex flex-col bg-default gap-8 lg:gap-0">
<UPricingPlan
v-bind="page.pricing.freePlan"
variant="naked"
class="lg:rounded-none border-x border-default border-t border-b lg:border-b-0"
class="lg:rounded-none ring-primary/15 ring-inset -mb-px bg-primary/5 z-[1]"
:ui="{ description: 'mt-0 text-primary' }"
>
<template #description>
<MDC :value="page.pricing.freePlan.description" unwrap="p" />
</template>
</UPricingPlan>
<UPricingPlan
v-bind="page.pricing.devPlan"
class="lg:rounded-none ring-inset -mb-px"
/>
<UPricingPlans compact>
<UPricingPlans compact class="-space-x-px">
<UPricingPlan
v-for="(plan, index) in page.pricing.plans"
:key="index"
@@ -47,18 +56,17 @@ useSeoMeta({
:discount="plan.discount"
:billing-period="plan.billing_period"
:billing-cycle="plan.billing_cycle"
:variant="plan.highlight ? 'soft' : 'outline'"
:class="['lg:rounded-none', { 'border-2 lg:border lg:border-x-0 border-primary lg:border-default': plan.highlight }]"
:variant="plan.highlight ? 'subtle' : 'outline'"
class="lg:rounded-none ring-inset -mb-px"
:features="plan.features"
:button="plan.button"
/>
</UPricingPlans>
<UPricingPlan
v-bind="page.pricing.figma"
variant="naked"
:billing-period="page.pricing.figma.billing_period"
:billing-cycle="page.pricing.figma.billing_cycle"
class="lg:rounded-none border lg:border-y-0 border-default"
class="lg:rounded-none ring-inset -mb-px"
>
<template #features>
<li v-for="(feature, index) in page.pricing.figma.features" :key="index" class="flex items-center gap-2 min-w-0">

View File

@@ -13,22 +13,6 @@ const Button = z.object({
target: z.enum(['_blank', '_self']).optional()
})
const Image = z.object({
src: z.string(),
alt: z.string(),
width: z.number().optional(),
height: z.number().optional()
})
const Author = z.object({
name: z.string(),
description: z.string().optional(),
username: z.string().optional(),
twitter: z.string().optional(),
to: z.string().optional(),
avatar: Image.optional()
})
const schema = z.object({
category: z.enum(['layout', 'form', 'element', 'navigation', 'data', 'overlay']).optional(),
framework: z.string().optional(),
@@ -91,18 +75,5 @@ export const collections = {
})
}))
})
}),
blog: defineCollection({
type: 'page',
source: 'blog/*',
schema: z.object({
image: z.string().editor({ input: 'media' }),
authors: z.array(Author),
date: z.string().date(),
minRead: z.number(),
draft: z.boolean().optional(),
category: z.enum(['Release', 'Tutorial', 'Announcement', 'Article']),
tags: z.array(z.string())
})
})
}

View File

@@ -225,6 +225,27 @@ export default defineNuxtConfig({
This option adds the `transition-colors` class on components with hover or active states.
::
### `theme.defaultVariants` :badge{label="Soon" class="align-text-top"}
Use the `theme.defaultVariants` option to override the default `color` and `size` variants for components.
- Default: `{ color: 'primary', size: 'md' }`{lang="ts-type"}
```ts [nuxt.config.ts]
export default defineNuxtConfig({
modules: ['@nuxt/ui'],
css: ['~/assets/css/main.css'],
ui: {
theme: {
defaultVariants: {
color: 'neutral',
size: 'sm'
}
}
}
})
```
## Continuous Releases
Nuxt UI uses [pkg.pr.new](https://github.com/stackblitz-labs/pkg.pr.new) for continuous preview releases, providing developers with instant access to the latest features and bug fixes without waiting for official releases.

View File

@@ -333,6 +333,32 @@ export default defineConfig({
This option adds the `transition-colors` class on components with hover or active states.
::
### `theme.defaultVariants` :badge{label="Soon" class="align-text-top"}
Use the `theme.defaultVariants` option to override the default `color` and `size` variants for components.
- Default: `{ color: 'primary', size: 'md' }`{lang="ts-type"}
```ts [vite.config.ts]
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'
export default defineConfig({
plugins: [
vue(),
ui({
theme: {
defaultVariants: {
color: 'neutral',
size: 'sm'
}
}
})
]
})
```
### `inertia`
Use the `inertia` option to enable compatibility with [Inertia.js](https://inertiajs.com/).

View File

@@ -757,6 +757,33 @@ name: 'input-menu-filter-fields-example'
---
::
### With full content width
You can expand the content to the full width of its items by using the `ui.content` key.
::component-example
---
name: 'input-menu-content-width-example'
collapse: true
---
::
::tip
You can also change the content width globally in your `app.config.ts`:
```
export default defineAppConfig({
ui: {
inputMenu: {
slots: {
content: 'min-w-fit'
}
}
}
})
```
::
### As a CountryPicker
This example demonstrates using the InputMenu as a country picker with lazy loading - countries are only fetched when the menu is opened.

View File

@@ -790,6 +790,33 @@ name: 'select-menu-filter-fields-example'
---
::
### With full content width
You can expand the content to the full width of its items by using the `ui.content` key.
::component-example
---
name: 'select-menu-content-width-example'
collapse: true
---
::
::tip
You can also change the content width globally in your `app.config.ts`:
```
export default defineAppConfig({
ui: {
selectMenu: {
slots: {
content: 'min-w-fit'
}
}
}
})
```
::
### As a CountryPicker
This example demonstrates using the SelectMenu as a country picker with lazy loading - countries are only fetched when the menu is opened.
@@ -801,6 +828,8 @@ name: 'select-menu-countries-example'
---
::
## API
### Props

View File

@@ -695,6 +695,33 @@ collapse: true
---
::
### With full content width
You can expand the content to the full width of its items by using the `ui.content` key.
::component-example
---
name: 'select-content-width-example'
collapse: true
---
::
::tip
You can also change the content width globally in your `app.config.ts`:
```
export default defineAppConfig({
ui: {
select: {
slots: {
content: 'min-w-fit'
}
}
}
})
```
::
## API
### Props

View File

@@ -1,198 +0,0 @@
---
title: Nuxt UI v3
description: Nuxt UI v3 is out! After 1500+ commits, this major redesign brings
improved accessibility, Tailwind CSS v4 support, and full Vue compatibility
navigation: false
image: /assets/blog/nuxt-ui-v3.png
minRead: 7
authors:
- name: Benjamin Canac
avatar:
src: https://github.com/benjamincanac.png
to: https://x.com/benjamincanac
- name: Sébastien Chopin
avatar:
src: https://github.com/atinux.png
to: https://x.com/atinux
- name: Hugo Richard
avatar:
src: https://github.com/hugorcd.png
to: https://x.com/hugorcd__
date: 2025-03-12T10:00:00.000Z
category: Release
---
We are thrilled to announce the release of Nuxt UI v3, a complete redesign of our UI library that brings significant improvements in accessibility, performance, and developer experience. This major update represents over 1500 commits of hard work, collaboration, and innovation from our team and the community.
## 🚀 Reimagined from the Ground Up
Nuxt UI v3 represents a major leap forward in our journey to provide the most comprehensive UI solution for Vue and Nuxt developers. This version has been rebuilt from the ground up with modern technologies and best practices in mind.
### **From HeadlessUI to Reka UI**
With Reka UI at its core, Nuxt UI v3 delivers:
• Proper keyboard navigation across all interactive components
• ARIA attributes automatically handled for you
• Focus management that just works
• Screen reader friendly components out of the box
This means you can build applications that work for everyone without becoming an accessibility expert.
### **Tailwind CSS v4 Integration**
The integration with Tailwind CSS v4 brings huge performance improvements:
**5x faster runtime** with optimized component rendering
**100x faster build times** thanks to the new CSS-first engine
• Smaller bundle sizes with more efficient styling
Your applications will feel snappier, build quicker, and load faster for your users.
## 🎨 A Brand New Design System
```html
<!-- Before: Inconsistent color usage with duplicate dark mode classes -->
<div class="bg-gray-100 dark:bg-gray-800 p-4 rounded-lg">
<h2 class="text-gray-900 dark:text-white text-xl mb-2">User Profile</h2>
<p class="text-gray-600 dark:text-gray-300">Account settings and preferences</p>
<button class="bg-blue-500 text-white px-3 py-1 rounded mt-2">Edit Profile</button>
</div>
```
```html
<!-- After: Semantic design tokens with automatic dark mode support -->
<div class="bg-muted p-4 rounded-lg">
<h2 class="text-highlighted text-xl mb-2">User Profile</h2>
<p class="text-muted">Account settings and preferences</p>
<UButton color="primary" size="sm" class="mt-2">Edit Profile</UButton>
</div>
```
Our new color system includes 7 semantic color aliases:
| Color | Default | Description |
|-----------------------------------|----------|--------------------------------------------------|
| :code[primary]{.text-primary} | `blue` | Primary color to represent the brand.
| :code[secondary]{.text-secondary} | `blue` | Secondary color to complement the primary color.
| :code[success]{.text-success} | `green` | Used for success states.
| :code[info]{.text-info} | `blue` | Used for informational states.
| :code[warning]{.text-warning} | `yellow` | Used for warning states.
| :code[error]{.text-error} | `red` | Used for form error validation states. |
| `neutral` | `slate` | Neutral color for backgrounds, text, etc. |
This approach makes your codebase more maintainable and your UI more consistent—especially when working in teams. With these semantic tokens, light and dark mode transitions become effortless, as the system automatically handles the appropriate color values for each theme without requiring duplicate class definitions.
## 💚 Complete Vue Compatibility
We're really happy to expand the scope of Nuxt UI beyond the Nuxt framework. With v3, both Nuxt UI and Nuxt UI Pro now work seamlessly in any Vue project, this means you can:
• Use the same components across all your Vue projects
• Benefit from Nuxt UI's theming system in any Vue application
• Enjoy auto-imports and TypeScript support outside of Nuxt
• Leverage both basic components and advanced Pro components in any Vue project
```ts [vite.config.ts]
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'
export default defineConfig({
plugins: [
vue(),
ui()
]
})
```
## 📦 Components for Every Need
With 54 core components, 50 Pro components, and 42 Prose components, Nuxt UI v3 provides solutions for virtually any UI challenge:
• **Data Display**: Tables, charts, and visualizations that adapt to your data
• **Navigation**: Menus, tabs, and breadcrumbs that guide users intuitively
• **Feedback**: Toasts, alerts, and modals that communicate clearly
• **Forms**: Inputs, selectors, and validation that simplify data collection
• **Layout**: Grids, containers, and responsive systems that organize content beautifully
Each component is designed to be both beautiful out of the box and deeply customizable when needed.
## 🔷 Improved TypeScript Integration
We've completely revamped our TypeScript integration, with features that make you more productive:
- Complete type safety with helpful autocompletion
- Generic-based components for flexible APIs
- Type-safe theming through a clear, consistent API
```ts
export default defineAppConfig({
ui: {
button: {
// Your IDE will show all available options
slots: {
base: 'font-bold rounded-lg'
},
defaultVariants: {
size: 'md',
color: 'error'
}
}
}
})
```
## ⬆️ Upgrading to v3
We've prepared a comprehensive [migration](https://ui.nuxt.com/getting-started/migration) guide to help you upgrade from v2 to v3. While there are breaking changes due to our complete overhaul, we've worked hard to make the transition as smooth as possible.
## 🎯 Getting Started
Whether you're starting a new project or upgrading an existing one, getting started with Nuxt UI v3 is easy:
```bash
# Create a new Nuxt project with Nuxt UI
npx nuxi@latest init my-app -t ui
```
::code-group{sync="pm"}
```bash [pnpm]
pnpm add @nuxt/ui@latest
```
```bash [yarn]
yarn add @nuxt/ui@latest
```
```bash [npm]
npm install @nuxt/ui@latest
```
```bash [bun]
bun add @nuxt/ui@latest
```
::
::warning
If you're using **pnpm**, ensure that you either set [`shamefully-hoist=true`](https://pnpm.io/npmrc#shamefully-hoist) in your `.npmrc` file or install `tailwindcss` in your project's root directory.
::
Visit our [documentation](https://ui.nuxt.com/getting-started) to explore all the components and features available in Nuxt UI v3.
## 🙏 Thank You
This release represents thousands of hours of work from our team and the community. We'd like to thank everyone who contributed to making Nuxt UI v3 a reality.
We're excited to see what you'll build with Nuxt UI v3!

View File

@@ -1,198 +0,0 @@
---
title: Nuxt UI
description: Nuxt UI v3 is out! After 1500+ commits, this major redesign brings
improved accessibility, Tailwind CSS v4 support, and full Vue compatibility
navigation: false
image: /assets/blog/nuxt-ui-v3.png
minRead: 7
authors:
- name: Benjamin Canac
avatar:
src: https://github.com/benjamincanac.png
to: https://x.com/benjamincanac
- name: Sébastien Chopin
avatar:
src: https://github.com/atinux.png
to: https://x.com/atinux
- name: Hugo Richard
avatar:
src: https://github.com/hugorcd.png
to: https://x.com/hugorcd__
date: 2025-03-12T09:00:00.000Z
category: improvement
---
We are thrilled to announce the release of Nuxt UI v3, a complete redesign of our UI library that brings significant improvements in accessibility, performance, and developer experience. This major update represents over 1500 commits of hard work, collaboration, and innovation from our team and the community.
## 🚀 Reimagined from the Ground Up
Nuxt UI v3 represents a major leap forward in our journey to provide the most comprehensive UI solution for Vue and Nuxt developers. This version has been rebuilt from the ground up with modern technologies and best practices in mind.
### **From HeadlessUI to Reka UI**
With Reka UI at its core, Nuxt UI v3 delivers:
• Proper keyboard navigation across all interactive components
• ARIA attributes automatically handled for you
• Focus management that just works
• Screen reader friendly components out of the box
This means you can build applications that work for everyone without becoming an accessibility expert.
### **Tailwind CSS v4 Integration**
The integration with Tailwind CSS v4 brings huge performance improvements:
**5x faster runtime** with optimized component rendering
**100x faster build times** thanks to the new CSS-first engine
• Smaller bundle sizes with more efficient styling
Your applications will feel snappier, build quicker, and load faster for your users.
## 🎨 A Brand New Design System
```html
<!-- Before: Inconsistent color usage with duplicate dark mode classes -->
<div class="bg-gray-100 dark:bg-gray-800 p-4 rounded-lg">
<h2 class="text-gray-900 dark:text-white text-xl mb-2">User Profile</h2>
<p class="text-gray-600 dark:text-gray-300">Account settings and preferences</p>
<button class="bg-blue-500 text-white px-3 py-1 rounded mt-2">Edit Profile</button>
</div>
```
```html
<!-- After: Semantic design tokens with automatic dark mode support -->
<div class="bg-muted p-4 rounded-lg">
<h2 class="text-highlighted text-xl mb-2">User Profile</h2>
<p class="text-muted">Account settings and preferences</p>
<UButton color="primary" size="sm" class="mt-2">Edit Profile</UButton>
</div>
```
Our new color system includes 7 semantic color aliases:
| Color | Default | Description |
|-----------------------------------|----------|--------------------------------------------------|
| :code[primary]{.text-primary} | `blue` | Primary color to represent the brand.
| :code[secondary]{.text-secondary} | `blue` | Secondary color to complement the primary color.
| :code[success]{.text-success} | `green` | Used for success states.
| :code[info]{.text-info} | `blue` | Used for informational states.
| :code[warning]{.text-warning} | `yellow` | Used for warning states.
| :code[error]{.text-error} | `red` | Used for form error validation states. |
| `neutral` | `slate` | Neutral color for backgrounds, text, etc. |
This approach makes your codebase more maintainable and your UI more consistent—especially when working in teams. With these semantic tokens, light and dark mode transitions become effortless, as the system automatically handles the appropriate color values for each theme without requiring duplicate class definitions.
## 💚 Complete Vue Compatibility
We're really happy to expand the scope of Nuxt UI beyond the Nuxt framework. With v3, both Nuxt UI and Nuxt UI Pro now work seamlessly in any Vue project, this means you can:
• Use the same components across all your Vue projects
• Benefit from Nuxt UI's theming system in any Vue application
• Enjoy auto-imports and TypeScript support outside of Nuxt
• Leverage both basic components and advanced Pro components in any Vue project
```ts [vite.config.ts]
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'
export default defineConfig({
plugins: [
vue(),
ui()
]
})
```
## 📦 Components for Every Need
With 54 core components, 50 Pro components, and 42 Prose components, Nuxt UI v3 provides solutions for virtually any UI challenge:
• **Data Display**: Tables, charts, and visualizations that adapt to your data
• **Navigation**: Menus, tabs, and breadcrumbs that guide users intuitively
• **Feedback**: Toasts, alerts, and modals that communicate clearly
• **Forms**: Inputs, selectors, and validation that simplify data collection
• **Layout**: Grids, containers, and responsive systems that organize content beautifully
Each component is designed to be both beautiful out of the box and deeply customizable when needed.
## 🔷 Improved TypeScript Integration
We've completely revamped our TypeScript integration, with features that make you more productive:
- Complete type safety with helpful autocompletion
- Generic-based components for flexible APIs
- Type-safe theming through a clear, consistent API
```ts
export default defineAppConfig({
ui: {
button: {
// Your IDE will show all available options
slots: {
base: 'font-bold rounded-lg'
},
defaultVariants: {
size: 'md',
color: 'error'
}
}
}
})
```
## ⬆️ Upgrading to v3
We've prepared a comprehensive [migration](https://ui.nuxt.com/getting-started/migration) guide to help you upgrade from v2 to v3. While there are breaking changes due to our complete overhaul, we've worked hard to make the transition as smooth as possible.
## 🎯 Getting Started
Whether you're starting a new project or upgrading an existing one, getting started with Nuxt UI v3 is easy:
```bash
# Create a new Nuxt project with Nuxt UI
npx nuxi@latest init my-app -t ui
```
::code-group{sync="pm"}
```bash [pnpm]
pnpm add @nuxt/ui@latest
```
```bash [yarn]
yarn add @nuxt/ui@latest
```
```bash [npm]
npm install @nuxt/ui@latest
```
```bash [bun]
bun add @nuxt/ui@latest
```
::
::warning
If you're using **pnpm**, ensure that you either set [`shamefully-hoist=true`](https://pnpm.io/npmrc#shamefully-hoist) in your `.npmrc` file or install `tailwindcss` in your project's root directory.
::
Visit our [documentation](https://ui.nuxt.com/getting-started) to explore all the components and features available in Nuxt UI v3.
## 🙏 Thank You
This release represents thousands of hours of work from our team and the community. We'd like to thank everyone who contributed to making Nuxt UI v3 a reality.
We're excited to see what you'll build with Nuxt UI v3!

View File

@@ -17,7 +17,7 @@
"@nuxt/content": "^3.6.3",
"@nuxt/image": "^1.10.0",
"@nuxt/ui": "workspace:*",
"@nuxt/ui-pro": "https://pkg.pr.new/@nuxt/ui-pro@22fdc5e",
"@nuxt/ui-pro": "https://pkg.pr.new/@nuxt/ui-pro@17684e4",
"@nuxthub/core": "^0.9.0",
"@nuxtjs/plausible": "^1.2.0",
"@octokit/rest": "^22.0.0",
@@ -30,7 +30,7 @@
"joi": "^17.13.3",
"maska": "^3.2.0",
"motion-v": "^1.5.0",
"nuxt": "^3.17.6",
"nuxt": "4.0.0-rc.0",
"nuxt-component-meta": "^0.12.1",
"nuxt-llms": "^0.1.3",
"nuxt-og-image": "^5.1.9",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 515 KiB

View File

@@ -116,8 +116,8 @@
"@internationalized/number": "^3.6.3",
"@nuxt/fonts": "^0.11.4",
"@nuxt/icon": "^1.15.0",
"@nuxt/kit": "^3.17.6",
"@nuxt/schema": "^3.17.6",
"@nuxt/kit": "4.0.0-rc.0",
"@nuxt/schema": "4.0.0-rc.0",
"@nuxtjs/color-mode": "^3.5.2",
"@standard-schema/spec": "^1.0.0",
"@tailwindcss/postcss": "^4.1.11",
@@ -163,7 +163,7 @@
"embla-carousel": "^8.6.0",
"eslint": "^9.30.1",
"happy-dom": "^18.0.1",
"nuxt": "^3.17.6",
"nuxt": "4.0.0-rc.0",
"release-it": "^19.0.3",
"vitest": "^3.2.4",
"vitest-environment-nuxt": "^1.0.1",

View File

@@ -16,9 +16,9 @@
"zod": "^3.25.75"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.4",
"@vitejs/plugin-vue": "^6.0.0",
"typescript": "^5.8.3",
"vite": "^6.3.5",
"vite": "^7.0.4",
"vue-tsc": "^3.0.1"
}
}

View File

@@ -14,7 +14,7 @@
"@internationalized/date": "^3.8.2",
"@nuxt/ui": "workspace:*",
"@nuxthub/core": "^0.9.0",
"nuxt": "^3.17.6",
"nuxt": "4.0.0-rc.0",
"zod": "^3.25.75"
},
"devDependencies": {

1498
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,9 @@ import { name, version } from '../package.json'
export type * from './runtime/types'
type Color = 'primary' | 'secondary' | 'success' | 'info' | 'warning' | 'error' | (string & {})
type Size = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | (string & {})
export interface ModuleOptions {
/**
* Prefix for components
@@ -38,7 +41,7 @@ export interface ModuleOptions {
* @defaultValue `['primary', 'secondary', 'success', 'info', 'warning', 'error']`
* @link https://ui.nuxt.com/getting-started/installation/nuxt#themecolors
*/
colors?: string[]
colors?: Color[]
/**
* Enable or disable transitions on components
@@ -46,6 +49,20 @@ export interface ModuleOptions {
* @link https://ui.nuxt.com/getting-started/installation/nuxt#themetransitions
*/
transitions?: boolean
defaultVariants?: {
/**
* The default color variant to use for components
* @defaultValue `'primary'`
*/
color?: Color
/**
* The default size variant to use for components
* @defaultValue `'md'`
*/
size?: Size
}
}
}
@@ -85,7 +102,7 @@ export default defineNuxtModule<ModuleOptions>({
async function registerModule(name: string, key: string, options: Record<string, any>) {
if (!hasNuxtModule(name)) {
await installModule(name, options)
await installModule(name, defu((nuxt.options as any)[key], options))
} else {
(nuxt.options as any)[key] = defu((nuxt.options as any)[key], options)
}

View File

@@ -256,6 +256,7 @@ const scrollSnaps = ref<number[]>([])
function onInit(api: EmblaCarouselType) {
scrollSnaps.value = api?.scrollSnapList() || []
}
function onSelect(api: EmblaCarouselType) {
canScrollNext.value = api?.canScrollNext() || false
canScrollPrev.value = api?.canScrollPrev() || false
@@ -300,8 +301,7 @@ defineExpose({
<div
v-for="(item, index) in items"
:key="index"
role="group"
aria-roledescription="slide"
v-bind="dots ? { role: 'tabpanel' } : { 'role': 'group', 'aria-roledescription': 'slide' }"
:class="ui.item({ class: [props.ui?.item, isCarouselItem(item) && item.ui?.item, isCarouselItem(item) && item.class] })"
>
<slot :item="item" :index="index" />
@@ -333,13 +333,15 @@ defineExpose({
/>
</div>
<div v-if="dots" :class="ui.dots({ class: props.ui?.dots })">
<div v-if="dots" role="tablist" :aria-label="t('carousel.dots')" :class="ui.dots({ class: props.ui?.dots })">
<template v-for="(_, index) in scrollSnaps" :key="index">
<button
type="button"
role="tab"
:aria-label="t('carousel.goto', { slide: index + 1 })"
:aria-selected="selectedIndex === index"
:class="ui.dot({ class: props.ui?.dot, active: selectedIndex === index })"
:data-state="selectedIndex === index ? 'active' : undefined"
:aria-current="selectedIndex === index ? true : undefined"
@click="scrollTo(index)"
/>
</template>

View File

@@ -18,7 +18,7 @@ export interface FormFieldProps {
label?: string
description?: string
help?: string
error?: string | boolean
error?: boolean | string
hint?: string
/**
* @defaultValue 'md'
@@ -41,8 +41,8 @@ export interface FormFieldSlots {
hint(props: { hint?: string }): any
description(props: { description?: string }): any
help(props: { help?: string }): any
error(props: { error?: string | boolean }): any
default(props: { error?: string | boolean }): any
error(props: { error?: boolean | string }): any
default(props: { error?: boolean | string }): any
}
</script>

View File

@@ -177,6 +177,8 @@ import UBadge from './Badge.vue'
import UPopover from './Popover.vue'
import UTooltip from './Tooltip.vue'
defineOptions({ inheritAttrs: false })
const props = withDefaults(defineProps<NavigationMenuProps<T>>(), {
orientation: 'horizontal',
contentOrientation: 'horizontal',
@@ -392,7 +394,7 @@ function getAccordionDefaultValue(list: NavigationMenuItem[], level = 0) {
</component>
</DefineItemTemplate>
<NavigationMenuRoot v-bind="rootProps" :data-collapsed="collapsed" :class="ui.root({ class: [props.ui?.root, props.class] })">
<NavigationMenuRoot v-bind="{ ...rootProps, ...$attrs }" :data-collapsed="collapsed" :class="ui.root({ class: [props.ui?.root, props.class] })">
<slot name="list-leading" />
<template v-for="(list, listIndex) in lists" :key="`list-${listIndex}`">

View File

@@ -107,6 +107,8 @@ import { get } from '../utils'
import { tv } from '../utils/tv'
import UIcon from './Icon.vue'
defineOptions({ inheritAttrs: false })
const props = withDefaults(defineProps<TreeProps<T, VK, M>>(), {
labelKey: 'label' as never,
valueKey: 'value' as never
@@ -161,7 +163,7 @@ const defaultExpanded = computed(() =>
@toggle="item.onToggle"
@select="item.onSelect"
>
<button :disabled="item.disabled || disabled" :class="ui.link({ class: [props.ui?.link, item.ui?.link, item.class], selected: isSelected, disabled: item.disabled || disabled })">
<button type="button" :disabled="item.disabled || disabled" :class="ui.link({ class: [props.ui?.link, item.ui?.link, item.class], selected: isSelected, disabled: item.disabled || disabled })">
<slot :name="((item.slot || 'item') as keyof TreeSlots<T>)" v-bind="{ index, level, expanded: isExpanded, selected: isSelected }" :item="(item as Extract<NestedItem<T>, { slot: string; }>)">
<slot :name="((item.slot ? `${item.slot}-leading`: 'item-leading') as keyof TreeSlots<T>)" v-bind="{ index, level, expanded: isExpanded, selected: isSelected }" :item="(item as Extract<NestedItem<T>, { slot: string; }>)">
<UIcon
@@ -199,7 +201,7 @@ const defaultExpanded = computed(() =>
</DefineTreeTemplate>
<TreeRoot
v-bind="(rootProps as unknown as TreeRootProps<NestedItem<T>>)"
v-bind="{ ...(rootProps as unknown as TreeRootProps<NestedItem<T>>), ...$attrs }"
:class="ui.root({ class: [props.ui?.root, props.class] })"
:get-key="getItemValue"
:default-expanded="defaultExpanded"

View File

@@ -71,7 +71,7 @@ function _useOverlay() {
isMounted: !!defaultOpen,
destroyOnClose: !!destroyOnClose,
originalProps: props || {},
props: { ...(props || {}) }
props: { ...props }
})
overlays.push(options)
@@ -87,11 +87,11 @@ function _useOverlay() {
const open = <T extends Component>(id: symbol, props?: ComponentProps<T>): OpenedOverlay<T> => {
const overlay = getOverlay(id)
// If props are provided, update the overlay's props
// If props are provided, merge them with the original props, otherwise use the original props
if (props) {
patch(overlay.id, props)
overlay.props = { ...overlay.originalProps, ...props }
} else {
patch(overlay.id, overlay.originalProps)
overlay.props = { ...overlay.originalProps }
}
overlay.isOpen = true
@@ -135,7 +135,7 @@ function _useOverlay() {
const patch = <T extends Component>(id: symbol, props: Partial<ComponentProps<T>>): void => {
const overlay = getOverlay(id)
overlay.props = { ...props }
overlay.props = { ...overlay.props, ...props }
}
const getOverlay = (id: symbol): Overlay => {

View File

@@ -1,11 +1,10 @@
import { ref, onScopeDispose } from 'vue'
import type { Ref, Plugin as VuePlugin } from 'vue'
import { createHooks } from 'hookable'
import { usePage } from '@inertiajs/vue3'
import { useColorMode as useColorModeVueUse } from '@vueuse/core'
import appConfig from '#build/app.config'
import type { NuxtApp } from '#app'
import { useColorMode as useColorModeVueUse } from '@vueuse/core'
import { usePage } from '@inertiajs/vue3'
export { useHead } from '@unhead/vue'
@@ -16,6 +15,7 @@ export { useLocale } from '../composables/useLocale'
export const useRoute = () => {
const page = usePage()
return {
fullPath: page.url
}
@@ -25,6 +25,10 @@ export const useRouter = () => {
}
export const clearError = () => {
}
export const useColorMode = () => {
if (!appConfig.colorMode) {
return {

View File

@@ -40,7 +40,8 @@ export default defineLocale<Messages>({
carousel: {
prev: 'السابق',
next: 'التالي',
goto: 'الذهاب إلي شريحة {slide}'
dots: 'اختر الشريحة المراد عرضها',
goto: 'الذهاب إلى شريحة {slide}'
},
modal: {
close: 'إغلاق'

View File

@@ -39,6 +39,7 @@ export default defineLocale<Messages>({
carousel: {
prev: 'Əvvəlki',
next: 'Növbəti',
dots: 'Göstərmək üçün slayd seçin',
goto: 'Slayd {slide} keç'
},
modal: {

View File

@@ -39,6 +39,7 @@ export default defineLocale<Messages>({
carousel: {
prev: 'Назад',
next: 'Напред',
dots: 'Изберете слайд за показване',
goto: 'Отидете на слайд {slide}'
},
modal: {

View File

@@ -39,6 +39,7 @@ export default defineLocale<Messages>({
carousel: {
prev: 'পূর্ববর্তী',
next: 'পরবর্তী',
dots: 'প্রদর্শনের জন্য স্লাইড নির্বাচন করুন',
goto: 'স্লাইড {slide} এ যান'
},
modal: {

View File

@@ -39,6 +39,7 @@ export default defineLocale<Messages>({
carousel: {
prev: 'Anterior',
next: 'Següent',
dots: 'Tria la diapositiva a mostrar',
goto: 'Anar a la diapositiva {slide}'
},
modal: {

View File

@@ -38,8 +38,9 @@ export default defineLocale<Messages>({
close: 'داخستن'
},
carousel: {
prev: 'پێشوو',
prev: 'پێشووی',
next: 'داهاتوو',
dots: 'سلایدێک هەڵبژێرە بۆ پیشاندان',
goto: 'بڕۆ بۆ سلایدی {slide}'
},
modal: {

View File

@@ -39,6 +39,7 @@ export default defineLocale<Messages>({
carousel: {
prev: 'Předchozí',
next: 'Další',
dots: 'Vyberte snímek k zobrazení',
goto: 'Přejít na {slide}'
},
modal: {

View File

@@ -39,6 +39,7 @@ export default defineLocale<Messages>({
carousel: {
prev: 'Forrige',
next: 'Næste',
dots: 'Vælg dias til visning',
goto: 'Gå til slide {slide}'
},
modal: {

View File

@@ -39,6 +39,7 @@ export default defineLocale<Messages>({
carousel: {
prev: 'Zurück',
next: 'Weiter',
dots: 'Folie zur Anzeige auswählen',
goto: 'Gehe zu {slide}'
},
modal: {

View File

@@ -39,6 +39,7 @@ export default defineLocale<Messages>({
carousel: {
prev: 'Προηγούμενο',
next: 'Επόμενο',
dots: 'Επιλέξτε διαφάνεια για εμφάνιση',
goto: 'Μετάβαση στη διαφάνεια {slide}'
},
modal: {

View File

@@ -39,6 +39,7 @@ export default defineLocale<Messages>({
carousel: {
prev: 'Prev',
next: 'Next',
dots: 'Choose slide to display',
goto: 'Go to slide {slide}'
},
modal: {

View File

@@ -39,6 +39,7 @@ export default defineLocale<Messages>({
carousel: {
prev: 'Anterior',
next: 'Siguiente',
dots: 'Elegir diapositiva a mostrar',
goto: 'Ir a la diapositiva {slide}'
},
modal: {

View File

@@ -39,6 +39,7 @@ export default defineLocale<Messages>({
carousel: {
prev: 'Eel',
next: 'Järg',
dots: 'Valige kuvatav slaid',
goto: 'Mine slaidile {slide}'
},
modal: {

View File

@@ -40,6 +40,7 @@ export default defineLocale<Messages>({
carousel: {
prev: 'قبلی',
next: 'بعدی',
dots: 'اسلاید مورد نظر برای نمایش را انتخاب کنید',
goto: 'رفتن به اسلاید {slide}'
},
modal: {

View File

@@ -39,6 +39,7 @@ export default defineLocale<Messages>({
carousel: {
prev: 'Edellinen',
next: 'Seuraava',
dots: 'Valitse näytettävä dia',
goto: 'Siirry sivulle {slide}'
},
modal: {

View File

@@ -39,6 +39,7 @@ export default defineLocale<Messages>({
carousel: {
prev: 'Précédent',
next: 'Suivant',
dots: 'Choisir la diapositive à afficher',
goto: 'Aller à {slide}'
},
modal: {

View File

@@ -38,6 +38,7 @@ export default defineLocale<Messages>({
carousel: {
prev: 'הקודם',
next: 'הבא',
dots: 'בחר שקופית להצגה',
goto: 'מעבר ל {slide}'
},
modal: {

View File

@@ -39,7 +39,8 @@ export default defineLocale<Messages>({
carousel: {
prev: 'पिछला',
next: 'अगला',
goto: 'स्लाइड {slide} पर जाएँ'
dots: 'प्रदर्शित करने के लिए स्लाइड चुनें',
goto: 'स्लाइड {slide} पर जाएं'
},
modal: {
close: 'बंद करें'

View File

@@ -39,6 +39,7 @@ export default defineLocale<Messages>({
carousel: {
prev: 'Előző',
next: 'Következő',
dots: 'Válassza ki a megjelenítendő diát',
goto: 'Ugrás ide {slide}'
},
modal: {

View File

@@ -39,6 +39,7 @@ export default defineLocale<Messages>({
carousel: {
prev: 'Հետ',
next: 'Առաջ',
dots: 'Ընտրեք ցուցադրելու սլայդը',
goto: 'Անցնել {slide}-ին'
},
modal: {

View File

@@ -39,6 +39,7 @@ export default defineLocale<Messages>({
carousel: {
prev: 'Sebelumnya',
next: 'Berikutnya',
dots: 'Pilih slide untuk ditampilkan',
goto: 'Pergi ke slide {slide}'
},
modal: {

View File

@@ -39,6 +39,7 @@ export default defineLocale<Messages>({
carousel: {
prev: 'Precedente',
next: 'Successiva',
dots: 'Scegli diapositiva da visualizzare',
goto: 'Vai alla slide {slide}'
},
modal: {

View File

@@ -39,6 +39,7 @@ export default defineLocale<Messages>({
carousel: {
prev: '前へ',
next: '次へ',
dots: '表示するスライドを選択',
goto: 'スライド {slide} に移動'
},
modal: {

View File

@@ -39,6 +39,7 @@ export default defineLocale<Messages>({
carousel: {
prev: 'Алдыңғы',
next: 'Келесі',
dots: 'Көрсету үшін слайдты таңдаңыз',
goto: '{slide} слайдқа өту'
},
modal: {

View File

@@ -39,7 +39,8 @@ export default defineLocale<Messages>({
carousel: {
prev: 'មុន',
next: 'បន្ទាប់',
goto: 'លោតទៅកាន់ស្លាយ {slide}'
dots: 'ជ្រើសរើស​ស្លាយ​ដើម្បី​បង្ហាញ',
goto: 'ឡើងទៅស្លាយ {slide}'
},
modal: {
close: 'បិទ'

View File

@@ -39,6 +39,7 @@ export default defineLocale<Messages>({
carousel: {
prev: '이전',
next: '다음',
dots: '표시할 슬라이드 선택',
goto: '{slide} 페이지로 이동'
},
modal: {

View File

@@ -39,6 +39,7 @@ export default defineLocale<Messages>({
carousel: {
prev: 'Алдыңкы',
next: 'Кийинки',
dots: 'Көрсөтүү үчүн слайдды тандаңыз',
goto: '{slide} слайдга өтүү'
},
modal: {

View File

@@ -39,6 +39,7 @@ export default defineLocale<Messages>({
carousel: {
prev: 'Präz.',
next: 'Näch.',
dots: 'Wielt Dia fir ze weisen',
goto: 'Gitt op d\'Slide {Slide}'
},
modal: {

View File

@@ -39,6 +39,7 @@ export default defineLocale<Messages>({
carousel: {
prev: 'Atgal',
next: 'Pirmyn',
dots: 'Pasirinkite skaidrę rodymui',
goto: 'Eiti į skaidrę {slide}'
},
modal: {

View File

@@ -39,6 +39,7 @@ export default defineLocale<Messages>({
carousel: {
prev: 'Өмнөх',
next: 'Дараах',
dots: 'Харуулах слайдыг сонгоно уу',
goto: '{slide}-р хуудсанд шилжих'
},
modal: {

View File

@@ -39,6 +39,7 @@ export default defineLocale<Messages>({
carousel: {
prev: 'Sebelum',
next: 'Seterusnya',
dots: 'Pilih slaid untuk dipaparkan',
goto: 'Pergi ke slaid {slide}'
},
modal: {

View File

@@ -39,6 +39,7 @@ export default defineLocale<Messages>({
carousel: {
prev: 'Forrige',
next: 'Neste',
dots: 'Velg lysbilde som skal vises',
goto: 'Gå til lysbilde {slide}'
},
modal: {

View File

@@ -39,6 +39,7 @@ export default defineLocale<Messages>({
carousel: {
prev: 'Vorige',
next: 'Volgende',
dots: 'Kies dia om weer te geven',
goto: 'Ga naar dia {slide}'
},
modal: {

View File

@@ -39,6 +39,7 @@ export default defineLocale<Messages>({
carousel: {
prev: 'Poprzedni',
next: 'Następny',
dots: 'Wybierz slajd do wyświetlenia',
goto: 'Idź do {slide}'
},
modal: {

View File

@@ -39,6 +39,7 @@ export default defineLocale<Messages>({
carousel: {
prev: 'Anterior',
next: 'Próximo',
dots: 'Escolher slide para exibir',
goto: 'Ir ao diapositivo {slide}'
},
modal: {

View File

@@ -39,6 +39,7 @@ export default defineLocale<Messages>({
carousel: {
prev: 'Anterior',
next: 'Próximo',
dots: 'Escolher slide para exibir',
goto: 'Ir para a slide {slide}'
},
modal: {

View File

@@ -39,6 +39,7 @@ export default defineLocale<Messages>({
carousel: {
prev: 'Anterior',
next: 'Următor',
dots: 'Alegeți diapozitivul de afișat',
goto: 'Mergi la diapozitivul {slide}'
},
modal: {

View File

@@ -39,6 +39,7 @@ export default defineLocale<Messages>({
carousel: {
prev: 'Назад',
next: 'Далее',
dots: 'Выберите слайд для отображения',
goto: 'Перейти к {slide}'
},
modal: {

View File

@@ -39,6 +39,7 @@ export default defineLocale<Messages>({
carousel: {
prev: 'Predchádzajúci',
next: 'Nasledujúci',
dots: 'Vyberte snímku na zobrazenie',
goto: 'Prejsť na {slide}'
},
modal: {

View File

@@ -39,6 +39,7 @@ export default defineLocale<Messages>({
carousel: {
prev: 'Nazaj',
next: 'Naprej',
dots: 'Izberite diapozitiv za prikaz',
goto: 'Pojdi na {slide}'
},
modal: {

View File

@@ -39,6 +39,7 @@ export default defineLocale<Messages>({
carousel: {
prev: 'Föregående',
next: 'Nästa',
dots: 'Välj bild att visa',
goto: 'Gå till {slide}'
},
modal: {

View File

@@ -39,6 +39,7 @@ export default defineLocale<Messages>({
carousel: {
prev: 'ย้อนกลับ',
next: 'ถัดไป',
dots: 'เลือกสไลด์ที่จะแสดง',
goto: 'ไปที่ {slide}'
},
modal: {

View File

@@ -39,6 +39,7 @@ export default defineLocale<Messages>({
carousel: {
prev: 'Қаблӣ',
next: 'Баъдӣ',
dots: 'Слайдро барои намоиш интихоб кунед',
goto: 'Ба слайди {slide} гузаред'
},
modal: {

View File

@@ -39,6 +39,7 @@ export default defineLocale<Messages>({
carousel: {
prev: 'Önceki',
next: 'Sonraki',
dots: 'Görüntülenecek slaydı seçin',
goto: '{slide}. slayda git'
},
modal: {

View File

@@ -40,6 +40,7 @@ export default defineLocale<Messages>({
carousel: {
prev: 'ئالدىنقى بەت',
next: 'كېيىنكى بەت',
dots: 'كۆرسىتىدىغان سلايدنى تاللاڭ',
goto: '{slide}-بەتكە ئاتلاش'
},
modal: {

View File

@@ -39,6 +39,7 @@ export default defineLocale<Messages>({
carousel: {
prev: 'Назад',
next: 'Далі',
dots: 'Виберіть слайд для відображення',
goto: 'Перейти до {slide}'
},
modal: {

View File

@@ -40,6 +40,7 @@ export default defineLocale<Messages>({
carousel: {
prev: 'پچھلا',
next: 'اگلا',
dots: 'دکھانے کے لیے سلائیڈ منتخب کریں',
goto: 'سلائیڈ {slide} پر جائیں'
},
modal: {

View File

@@ -39,6 +39,7 @@ export default defineLocale<Messages>({
carousel: {
prev: 'Ortga',
next: 'Oldinga',
dots: 'Koʻrsatish uchun slaydni tanlang',
goto: '{slide}-slaydga otish'
},
modal: {

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