Merge branch 'v3' into chore/content-3.3

This commit is contained in:
Farnabaz
2025-03-19 14:49:23 +01:00
175 changed files with 4300 additions and 3121 deletions

View File

@@ -5,7 +5,7 @@ body:
- type: markdown
attributes:
value: |
Before reporting a bug, please make sure that you have read through our [v3 documentation](https://ui3.nuxt.dev/) and existing [issues](https://github.com/nuxt/ui/issues?q=is%3Aissue%20is%3Aopen%20sort%3Aupdated-desc%20label%3Av3).
Before reporting a bug, please make sure that you have read through our [v3 documentation](https://ui.nuxt.com/) and existing [issues](https://github.com/nuxt/ui/issues?q=is%3Aissue%20is%3Aopen%20sort%3Aupdated-desc%20label%3Av3).
- type: textarea
id: env
attributes:
@@ -37,7 +37,7 @@ body:
id: version
attributes:
label: Version
placeholder: v3.0.0-alpha.x
placeholder: v3.0.0
validations:
required: true
- type: textarea

View File

@@ -5,7 +5,7 @@ body:
- type: markdown
attributes:
value: |
Before reporting a bug, please make sure that you have read through our [documentation](https://ui.nuxt.com) and existing [issues](https://github.com/nuxt/ui/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc).
Before reporting a bug, please make sure that you have read through our [documentation](https://ui2.nuxt.com) and existing [issues](https://github.com/nuxt/ui/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc).
- type: textarea
id: env
attributes:

View File

@@ -5,7 +5,7 @@ body:
- type: markdown
attributes:
value: |
Before requesting a feature, please make sure that you have read through our [v3 documentation](https://ui3.nuxt.dev/) and existing [issues](https://github.com/nuxt/ui/issues?q=is%3Aissue%20is%3Aopen%20sort%3Aupdated-desc%20label%3Av3).
Before requesting a feature, please make sure that you have read through our [v3 documentation](https://ui.nuxt.com/) and existing [issues](https://github.com/nuxt/ui/issues?q=is%3Aissue%20is%3Aopen%20sort%3Aupdated-desc%20label%3Av3).
- type: textarea
id: description
attributes:

View File

@@ -5,7 +5,7 @@ body:
- type: markdown
attributes:
value: |
Before requesting a feature, please make sure that you have read through our [documentation](https://ui.nuxt.com) and existing [issues](https://github.com/nuxt/ui/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc).
Before requesting a feature, please make sure that you have read through our [documentation](https://ui2.nuxt.com) and existing [issues](https://github.com/nuxt/ui/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc).
- type: textarea
id: description
attributes:

View File

@@ -5,7 +5,7 @@ body:
- type: markdown
attributes:
value: |
Before asking a question, please make sure that you have read through our [v3 documentation](https://ui3.nuxt.dev/) and existing [issues](https://github.com/nuxt/ui/issues?q=is%3Aissue%20is%3Aopen%20sort%3Aupdated-desc%20label%3Av3).
Before asking a question, please make sure that you have read through our [v3 documentation](https://ui.nuxt.com/) and existing [issues](https://github.com/nuxt/ui/issues?q=is%3Aissue%20is%3Aopen%20sort%3Aupdated-desc%20label%3Av3).
- type: textarea
id: description
attributes:

View File

@@ -5,7 +5,7 @@ body:
- type: markdown
attributes:
value: |
Before asking a question, please make sure that you have read through our [documentation](https://ui.nuxt.com) and existing [issues](https://github.com/nuxt/ui/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc).
Before asking a question, please make sure that you have read through our [documentation](https://ui2.nuxt.com) and existing [issues](https://github.com/nuxt/ui/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc).
- type: textarea
id: description
attributes:

86
.github/workflows/integration.yml vendored Normal file
View File

@@ -0,0 +1,86 @@
name: integration
on:
workflow_run:
workflows: ["module"]
types:
- completed
jobs:
nuxt:
runs-on: ${{ matrix.os }}
permissions:
contents: read
pull-requests: read
strategy:
matrix:
os: [ubuntu-latest] # macos-latest, windows-latest
node: [22]
steps:
- name: Checkout
uses: actions/checkout@v4
with:
repository: benjamincanac/app-ui3
- name: Set short SHA
run: echo "COMMIT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Install node
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: pnpm
- name: Install dependencies
run: pnpm install https://pkg.pr.new/@nuxt/ui@${{ env.COMMIT }}
- name: Typecheck
run: pnpm run typecheck
- name: Build
run: pnpm run build
vue:
runs-on: ${{ matrix.os }}
permissions:
contents: read
pull-requests: read
strategy:
matrix:
os: [ubuntu-latest] # macos-latest, windows-latest
node: [22]
steps:
- name: Checkout
uses: actions/checkout@v4
with:
repository: benjamincanac/app-ui3-vue
- name: Set short SHA
run: echo "COMMIT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Install node
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: pnpm
- name: Install dependencies
run: pnpm install https://pkg.pr.new/@nuxt/ui@${{ env.COMMIT }}
# - name: Typecheck
# run: pnpm run typecheck
- name: Build
run: pnpm run build

View File

@@ -1,5 +1,36 @@
# Changelog
## [3.0.0](https://github.com/nuxt/ui/compare/v3.0.0-beta.4...v3.0.0) (2025-03-12)
## [3.0.0-beta.4](https://github.com/nuxt/ui/compare/v3.0.0-beta.3...v3.0.0-beta.4) (2025-03-12)
### Features
* **Form:** global errors ([#3482](https://github.com/nuxt/ui/issues/3482)) ([6e03d9c](https://github.com/nuxt/ui/commit/6e03d9c6efc8f4cfc306813e733d7d3e03706323))
* **Input/Textarea:** allow `null` value in model ([#3415](https://github.com/nuxt/ui/issues/3415)) ([cfe9b2e](https://github.com/nuxt/ui/commit/cfe9b2ecf34827bc11a5281a069988ab96030047))
* **useLocale:** handle generic messages ([#3100](https://github.com/nuxt/ui/issues/3100)) ([a9c8eb3](https://github.com/nuxt/ui/commit/a9c8eb3f60a10d1a71632991c9db594716b0fba1))
### Bug Fixes
* **Button:** missing import ([21dbf01](https://github.com/nuxt/ui/commit/21dbf01888a161a9d8ac6eb0d957c1342f6cc30d)), closes [nuxt/ui#3417](https://github.com/nuxt/ui/issues/3417)
* **Form:** input blur validation on submit ([#3504](https://github.com/nuxt/ui/issues/3504)) ([97c8098](https://github.com/nuxt/ui/commit/97c8098d4a35c392719ae179d36aa008d6f8f78a))
* **vue:** prevent calling `useHead` in colors ([5ecd227](https://github.com/nuxt/ui/commit/5ecd2271ca86087cb805548397d75c38763ad412))
## [3.0.0-beta.3](https://github.com/nuxt/ui/compare/v3.0.0-beta.2...v3.0.0-beta.3) (2025-03-07)
### Features
* **Button:** handle `active` state ([bd2d484](https://github.com/nuxt/ui/commit/bd2d4848d246a3d5930f8059913f5a1a0abe29fd)), closes [#3417](https://github.com/nuxt/ui/issues/3417)
* **Table:** add `loading` slot ([99e531d](https://github.com/nuxt/ui/commit/99e531d8dfb7954322b7ab7feda3d8814c6d8d02)), closes [#3444](https://github.com/nuxt/ui/issues/3444)
### Bug Fixes
* **InputMenu/SelectMenu:** proxy `required` in root props ([60b7e2d](https://github.com/nuxt/ui/commit/60b7e2d69e80afa7e221855dcec46479d0ca5c6c))
* **InputMenu:** wrong `required` in multiple mode ([01fa230](https://github.com/nuxt/ui/commit/01fa230eae4b6623c5fd71cc218d114d9f6f0f25)), closes [#2741](https://github.com/nuxt/ui/issues/2741)
* **Pagination:** add missing slots ([a47c5ff](https://github.com/nuxt/ui/commit/a47c5ff46616eafee3158cb9801183965f5f9874)), closes [#3441](https://github.com/nuxt/ui/issues/3441)
* **Pagination:** wrong next link ([e823022](https://github.com/nuxt/ui/commit/e823022b19bb172d2e5fabb9144b4a4286a25a5f)), closes [#3008](https://github.com/nuxt/ui/issues/3008)
* **templates:** prevent overriding existing colors ([ccbd89c](https://github.com/nuxt/ui/commit/ccbd89c908fe8af54c7d723dd12da5b7f3906c8f)), closes [#3426](https://github.com/nuxt/ui/issues/3426)
## [3.0.0-beta.2](https://github.com/nuxt/ui/compare/v3.0.0-beta.1...v3.0.0-beta.2) (2025-02-28)
### Bug Fixes

View File

@@ -11,31 +11,31 @@
[![License][license-src]][license-href]
[![Nuxt][nuxt-src]][nuxt-href]
We're thrilled to introduce Nuxt UI v3, a significant upgrade to our UI library that delivers extensive improvements and robust new capabilities. This major update harnesses the combined strengths of [Reka UI](https://reka-ui.com/), [Tailwind CSS v4](https://tailwindcss.com/), and [Tailwind Variants](https://www.tailwind-variants.org/) to offer developers an unparalleled set of tools for creating sophisticated, accessible, and highly performant user interfaces.
Nuxt UI harnesses the combined strengths of [Reka UI](https://reka-ui.com/), [Tailwind CSS](https://tailwindcss.com/), and [Tailwind Variants](https://www.tailwind-variants.org/) to offer developers an unparalleled set of tools for creating sophisticated, accessible, and highly performant user interfaces.
> [!NOTE]
> You are on the `v3` development branch, check out the [dev branch](https://github.com/nuxt/ui/tree/dev) for Nuxt UI v2.
> You are on the `v3` development branch, check out the [v2 branch](https://github.com/nuxt/ui/tree/v2) for Nuxt UI v2.
## Documentation
Visit https://ui3.nuxt.dev to explore the documentation.
Visit https://ui.nuxt.com to explore the documentation.
## Installation
```bash [pnpm]
pnpm add @nuxt/ui@next
pnpm add @nuxt/ui
```
```bash [yarn]
yarn add @nuxt/ui@next
yarn add @nuxt/ui
```
```bash [npm]
npm install @nuxt/ui@next
npm install @nuxt/ui
```
```bash [bun]
bun add @nuxt/ui@next
bun add @nuxt/ui
```
### Nuxt
@@ -55,7 +55,7 @@ export default defineNuxtConfig({
@import "@nuxt/ui";
```
Learn more in the [installation guide](https://ui3.nuxt.dev/getting-started/installation/nuxt).
Learn more in the [installation guide](https://ui.nuxt.com/getting-started/installation/nuxt).
### Vue
@@ -102,7 +102,7 @@ app.mount('#app')
@import "@nuxt/ui";
```
Learn more in the [installation guide](https://ui3.nuxt.dev/getting-started/installation/vue).
Learn more in the [installation guide](https://ui.nuxt.com/getting-started/installation/vue).
## Credits
@@ -119,7 +119,7 @@ Learn more in the [installation guide](https://ui3.nuxt.dev/getting-started/inst
Licensed under the [MIT license](https://github.com/nuxt/ui/blob/v3/LICENSE.md).
<!-- Badges -->
[npm-version-src]: https://img.shields.io/npm/v/@nuxt/ui/next.svg?style=flat&colorA=18181B&colorB=28CF8D
[npm-version-src]: https://img.shields.io/npm/v/@nuxt/ui/latest.svg?style=flat&colorA=18181B&colorB=28CF8D
[npm-version-href]: https://npmjs.com/package/@nuxt/ui
[npm-downloads-src]: https://img.shields.io/npm/dm/@nuxt/ui.svg?style=flat&colorA=18181B&colorB=28CF8D

View File

@@ -1,7 +1,6 @@
<script setup lang="ts">
// import { withoutTrailingSlash } from 'ufo'
import { withoutTrailingSlash } from 'ufo'
import colors from 'tailwindcss/colors'
// import { debounce } from 'perfect-debounce'
const route = useRoute()
const appConfig = useAppConfig()
@@ -12,16 +11,6 @@ const { data: files } = useLazyAsyncData('search', () => queryCollectionSearchSe
server: false
})
const searchTerm = ref('')
// watch(searchTerm, debounce((query: string) => {
// if (!query) {
// return
// }
// useTrackEvent('Search', { props: { query: `${query} - ${searchTerm.value?.commandPaletteRef.results.length} results` } })
// }, 500))
const links = useLinks()
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; }`)
@@ -33,8 +22,8 @@ useHead({
{ key: 'theme-color', name: 'theme-color', content: color }
],
link: [
{ rel: 'icon', type: 'image/svg+xml', href: '/icon.svg' }
// { rel: 'canonical', href: `https://ui.nuxt.com${withoutTrailingSlash(route.path)}` }
{ rel: 'icon', type: 'image/svg+xml', href: '/icon.svg' },
{ rel: 'canonical', href: `https://ui.nuxt.com${withoutTrailingSlash(route.path)}` }
],
style: [
{ innerHTML: radius, id: 'nuxt-ui-radius', tagPriority: -2 },
@@ -61,7 +50,7 @@ provide('navigation', mappedNavigation)
<NuxtLoadingIndicator color="var(--ui-primary)" :height="2" />
<template v-if="!route.path.startsWith('/examples')">
<!-- <Banner /> -->
<Banner />
<Header :links="links" />
</template>
@@ -75,7 +64,6 @@ provide('navigation', mappedNavigation)
<ClientOnly>
<LazyUContentSearch
v-model:search-term="searchTerm"
:files="files"
:groups="[{
id: 'framework',
@@ -95,5 +83,5 @@ provide('navigation', mappedNavigation)
</template>
<style>
/* Safelist (do not remove): [&>div]:*:my-0 [&>div]:*:w-full h-64 !px-0 !py-0 !pt-0 !pb-0 !p-0 !justify-start !min-h-96 h-136 */
/* Safelist (do not remove): [&>div]:*:my-0 [&>div]:*:w-full h-64 !px-0 !py-0 !pt-0 !pb-0 !p-0 !justify-start !justify-end !min-h-96 h-136 */
</style>

View File

@@ -0,0 +1,50 @@
<script setup lang="ts">
const el = ref<HTMLDivElement | null>(null)
onMounted(() => {
if (!el.value) {
return
}
const script = document.createElement('script')
script.setAttribute('type', 'text/javascript')
script.setAttribute('src', 'https://cdn.carbonads.com/carbon.js?serve=CWYIVK3E&placement=uinuxtcom')
script.setAttribute('id', '_carbonads_js')
el.value?.appendChild(script)
})
</script>
<template>
<div ref="el" class="carbon" />
</template>
<style scoped>
@reference "../assets/css/main.css";
.carbon :deep(#carbonads) {
@apply relative border border-(--ui-border) rounded-[calc(var(--ui-radius)*1.5)] hover:bg-(--ui-bg-elevated)/50 w-full transition-colors min-h-[220px] p-2;
.carbon-img {
@apply flex justify-center w-full;
& > img {
@apply !max-w-full w-full rounded-(--ui-radius);
}
}
.carbon-text {
@apply text-sm text-(--ui-text-muted) transition-colors text-center text-pretty flex pt-2;
}
.carbon-poweredby {
@apply block text-[10px] text-center text-(--ui-text-dimmed) pt-2;
}
&:hover {
.carbon-text {
@apply text-(--ui-text);
}
}
}
</style>

View File

@@ -1,7 +1,18 @@
<template>
<UBanner icon="i-lucide-construction" :actions="[{ label: 'Go to Nuxt UI v2', to: 'https://ui.nuxt.com', trailingIcon: 'i-lucide-arrow-right' }]" :close="false">
<UBanner
id="ui3-launch"
icon="i-lucide-rocket"
:actions="[
{
label: 'Discover Nuxt UI Pro',
to: '/pro/pricing',
trailingIcon: 'i-lucide-arrow-right'
}
]"
close
>
<template #title>
You're looking at the documentation for <span class="font-semibold">Nuxt UI v3</span>!
<span class="font-semibold">Nuxt UI v3</span> is officially released.
</template>
</UBanner>
</template>

View File

@@ -0,0 +1,51 @@
<script setup lang="ts">
const endDate = new Date('2025-03-14T23:59:59Z')
const second = 1000
const minute = second * 60
const hour = minute * 60
const day = hour * 24
function getCountdown() {
const distance = Math.floor((endDate.getTime() - Date.now()))
return {
day: Math.floor(distance / day),
hour: Math.floor((distance % (day)) / (hour)),
minute: Math.floor((distance % (hour)) / (minute)),
second: Math.floor((distance % (minute)) / (second)),
distance
}
}
const countdown = ref(getCountdown())
let interval: any
if (countdown.value.distance > 0) {
onMounted(() => {
interval = setInterval(() => {
countdown.value = getCountdown()
if (countdown.value.distance <= 0) {
clearInterval(interval)
}
}, 1000)
})
}
const plural = (value: number) => (value === 1 ? '' : 's')
const double = (value: number) => (value < 10 ? `0${value}` : value)
</script>
<template>
<div>
<p class="font-semibold text-gray-900 dark:text-white text-sm mb-3">
Nuxt UI v3 launch offer ends in:
</p>
<div class="flex items-center justify-center gap-2 text-center">
<template v-for="(value, key) in countdown" :key="key">
<div v-if="key !== 'distance'" class="flex flex-col items-center gap-2">
<UBadge color="primary" class="w-14 h-14 font-bold text-2xl flex items-center justify-center tabular-nums" variant="subtle">
{{ double(value) }}
</UBadge>
<span class="text-[10px] font-semibold text-gray-900 dark:text-white tracking-wide tabular-nums uppercase">{{ key }}{{ plural(value) }}</span>
</div>
</template>
</div>
</div>
</template>

View File

@@ -1,4 +1,6 @@
<script setup lang="ts">
const route = useRoute()
const links = [{
label: 'Figma',
to: '/figma'
@@ -16,7 +18,7 @@ const links = [{
</script>
<template>
<USeparator icon="i-simple-icons-nuxtdotjs" class="h-px" />
<USeparator :icon="route.path === '/' ? undefined : 'i-simple-icons-nuxtdotjs'" class="h-px" />
<UFooter>
<template #left>

View File

@@ -30,14 +30,18 @@ const mobileLinks = computed(() => props.links.map(link => ({ ...link, defaultOp
<UHeader :ui="{ left: 'min-w-0' }" :menu="{ shouldScaleBackground: true }">
<template #left>
<NuxtLink to="/" class="flex items-end gap-2 font-bold text-xl text-(--ui-text-highlighted) min-w-0 focus-visible:outline-(--ui-primary) shrink-0" aria-label="Nuxt UI">
<LogoPro class="w-auto h-6 shrink-0 ui-pro-only" />
<Logo class="w-auto h-6 shrink-0 ui-only" />
<Logo v-if="route.path === '/'" class="w-auto h-6 shrink-0" />
<LogoPro v-else-if="route.path.startsWith('/pro')" class="w-auto h-6 shrink-0" />
<template v-else>
<LogoPro class="w-auto h-6 shrink-0 ui-pro-only" />
<Logo class="w-auto h-6 shrink-0 ui-only" />
</template>
</NuxtLink>
<UDropdownMenu
v-slot="{ open }"
:modal="false"
:items="[{ label: `v${config.version}`, active: true, color: 'primary', checked: true, type: 'checkbox' }, { label: module === 'ui-pro' ? 'v1.5' : 'v2.19', to: module === 'ui-pro' ? 'https://ui.nuxt.com/pro' : 'https://ui.nuxt.com' }]"
:items="[{ label: `v${config.version}`, active: true, color: 'primary', checked: true, type: 'checkbox' }, { label: module === 'ui-pro' ? 'v1.7.1' : 'v2.21.1', to: module === 'ui-pro' ? 'https://ui2.nuxt.com/pro' : 'https://ui2.nuxt.com' }]"
:ui="{ content: 'w-(--reka-dropdown-menu-trigger-width) min-w-0' }"
size="xs"
>

View File

@@ -0,0 +1,91 @@
<script setup lang="ts">
interface Star {
x: number
y: number
size: number
twinkleDelay: number
id: string
}
const props = withDefaults(defineProps<{
starCount?: number
color?: string
size?: { min: number, max: number }
speed?: 'slow' | 'normal' | 'fast'
}>(), {
starCount: 50,
color: 'var(--ui-primary)',
size: () => ({
min: 1,
max: 3
}),
speed: 'normal'
})
// Generate random stars
const generateStars = (count: number): Star[] => {
return Array.from({ length: count }, () => {
const x = Math.floor(Math.random() * 100)
const y = Math.floor(Math.random() * 100)
const size = Math.random() * (props.size.max - props.size.min) + props.size.min
const twinkleDelay = Math.random() * 5
return { x, y, size, twinkleDelay, id: Math.random().toString(36).substring(2, 9) }
})
}
// Generate all stars
const stars = ref<Star[]>(generateStars(props.starCount))
// Compute twinkle animation duration based on speed
const twinkleDuration = computed(() => {
const speedMap: Record<string, string> = {
slow: '4s',
normal: '2s',
fast: '1s'
}
return speedMap[props.speed]
})
</script>
<template>
<div class="absolute pointer-events-none z-[-1] inset-y-0 left-4 right-4 lg:right-[50%] overflow-hidden">
<ClientOnly>
<div
v-for="star in stars"
:key="star.id"
class="star absolute"
:style="{
'left': `${star.x}%`,
'top': `${star.y}%`,
'transform': 'translate(-50%, -50%)',
'--star-size': `${star.size}px`,
'--star-color': color,
'--twinkle-delay': `${star.twinkleDelay}s`,
'--twinkle-duration': twinkleDuration
}"
/>
</ClientOnly>
</div>
</template>
<style scoped>
.star {
width: var(--star-size);
height: var(--star-size);
background-color: var(--star-color);
border-radius: 50%;
animation: twinkle var(--twinkle-duration) ease-in-out infinite;
animation-delay: var(--twinkle-delay);
will-change: opacity;
}
@keyframes twinkle {
0%, 100% {
opacity: 0.2;
}
50% {
opacity: 1;
}
}
</style>

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { upperFirst, camelCase } from 'scule'
import { upperFirst, camelCase, kebabCase } from 'scule'
import type { ComponentMeta } from 'vue-component-meta'
import * as theme from '#build/ui'
import * as themePro from '#build/ui-pro'
@@ -112,7 +112,7 @@ const metaProps: ComputedRef<ComponentMeta['props']> = computed(() => {
<ProseTd>
<HighlightInlineType v-if="prop.type" :type="prop.type" />
<MDC v-if="prop.description" :value="prop.description" class="text-(--ui-text-toned) mt-1" />
<MDC v-if="prop.description" :value="prop.description" class="text-(--ui-text-toned) mt-1" :cache-key="`${kebabCase(route.path)}-${prop.name}-description`" />
<ComponentPropsLinks v-if="prop.tags?.length" :prop="prop" />
<ComponentPropsSchema v-if="prop.schema" :prop="prop" :ignore="ignore" />

View File

@@ -1,17 +1,20 @@
<script setup lang="ts">
import { kebabCase } from 'scule'
import type { PropertyMeta } from 'vue-component-meta'
const props = defineProps<{
prop: PropertyMeta
}>()
const route = useRoute()
const links = computed(() => props.prop.tags?.filter((tag: any) => tag.name === 'link'))
</script>
<template>
<ProseUl v-if="links?.length">
<ProseLi v-for="link in links" :key="link.name">
<MDC :value="link.text ?? ''" class="my-1" />
<ProseLi v-for="(link, index) in links" :key="index">
<MDC :value="link.text ?? ''" class="my-1" :cache-key="`${kebabCase(route.path)}-${prop.name}-link-${index}`" />
</ProseLi>
</ProseUl>
</template>

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import { kebabCase } from 'scule'
import type { PropertyMeta } from 'vue-component-meta'
const props = defineProps<{
@@ -6,6 +7,8 @@ const props = defineProps<{
ignore?: string[]
}>()
const route = useRoute()
function getSchemaProps(schema: PropertyMeta['schema']): any {
if (!schema || typeof schema === 'string' || !schema.schema) {
return []
@@ -40,7 +43,7 @@ const schemaProps = computed(() => {
<ProseLi v-for="schemaProp in schemaProps" :key="schemaProp.name">
<HighlightInlineType :type="`${schemaProp.name}${schemaProp.required === false ? '?' : ''}: ${schemaProp.type}`" />
<MDC v-if="schemaProp.description" :value="schemaProp.description" class="text-(--ui-text-muted) my-1" />
<MDC v-if="schemaProp.description" :value="schemaProp.description" class="text-(--ui-text-muted) my-1" :cache-key="`${kebabCase(route.path)}-${prop.name}-${schemaProp.name}-description`" />
</ProseLi>
</ProseUl>
</ProseCollapsible>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { upperFirst, camelCase } from 'scule'
import { upperFirst, camelCase, kebabCase } from 'scule'
const props = defineProps<{
prose?: boolean
@@ -36,7 +36,7 @@ const meta = await fetchComponentMeta(name as any)
<ProseTd>
<HighlightInlineType v-if="slot.type" :type="slot.type" />
<MDC v-if="slot.description" :value="slot.description" class="text-(--ui-text-toned) mt-1" />
<MDC v-if="slot.description" :value="slot.description" class="text-(--ui-text-toned) mt-1" :cache-key="`${kebabCase(route.path)}-${slot.name}-description`" />
</ProseTd>
</ProseTr>
</ProseTbody>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { murmurHash } from 'ohash'
import { hash } from 'ohash'
const props = defineProps<{
type: string
@@ -23,7 +23,7 @@ const type = computed(() => {
return type
})
const { data: ast } = await useAsyncData(`hightlight-inline-code-${murmurHash(type.value)}`, () => parseMarkdown(`\`${type.value}\`{lang="ts-type"}`))
const { data: ast } = await useAsyncData(`hightlight-inline-code-${hash(type.value).slice(0, 10)}`, () => parseMarkdown(`\`${type.value}\`{lang="ts-type"}`))
</script>
<template>

View File

@@ -9,21 +9,22 @@ const props = withDefaults(defineProps<{
function getEmojiFlag(locale: string): string {
const languageToCountry: Record<string, string> = {
ar: 'sa',
bn: 'bd',
cs: 'cz',
da: 'dk',
el: 'gr',
et: 'ee',
en: 'gb',
hi: 'in',
ja: 'jp',
km: 'kh',
ko: 'kr',
nb: 'no',
sv: 'se',
uk: 'ua',
vi: 'vn'
ar: 'sa', // Arabic -> Saudi Arabia
bn: 'bd', // Bengali -> Bangladesh
cs: 'cz', // Czech -> Czech Republic (note: modern country code is actually 'cz')
da: 'dk', // Danish -> Denmark
el: 'gr', // Greek -> Greece
et: 'ee', // Estonian -> Estonia
en: 'gb', // English -> Great Britain
he: 'il', // Hebrew -> Israel
hi: 'in', // Hindi -> India
ja: 'jp', // Japanese -> Japan
km: 'kh', // Khmer -> Cambodia
ko: 'kr', // Korean -> South Korea
nb: 'no', // Norwegian Bokmål -> Norway
sv: 'se', // Swedish -> Sweden
uk: 'ua', // Ukrainian -> Ukraine
vi: 'vn' // Vietnamese -> Vietnam
}
const baseLanguage = locale.split('-')[0]?.toLowerCase() || locale

View File

@@ -2,6 +2,7 @@
const searchTerm = ref('')
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
key: 'command-palette-users',
transform: (data: { id: number, name: string, email: string }[]) => {
return data?.map(user => ({ id: user.id, label: user.name, suffix: user.email, avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` } })) || []
},

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
const { data: users } = await useFetch('https://jsonplaceholder.typicode.com/users', {
key: 'command-palette-users',
transform: (data: { id: number, name: string, email: string }[]) => {
return data?.map(user => ({ id: user.id, label: user.name, suffix: user.email, avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` } })) || []
},

View File

@@ -3,6 +3,7 @@ const searchTerm = ref('')
const searchTermDebounced = refDebounced(searchTerm, 200)
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
key: 'command-palette-users',
params: { q: searchTermDebounced },
transform: (data: { id: number, name: string, email: string }[]) => {
return data?.map(user => ({ id: user.id, label: user.name, suffix: user.email, avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` } })) || []

View File

@@ -2,6 +2,7 @@
const searchTerm = ref('')
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
key: 'command-palette-users',
params: { q: searchTerm },
transform: (data: { id: number, name: string, email: string }[]) => {
return data?.map(user => ({ id: user.id, label: user.name, suffix: user.email, avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` } })) || []

View File

@@ -22,7 +22,7 @@ async function onSubmit(event: FormSubmitEvent<Schema>) {
</script>
<template>
<UForm :schema="v.safeParser(schema)" :state="state" class="space-y-4" @submit="onSubmit">
<UForm :schema="schema" :state="state" class="space-y-4" @submit="onSubmit">
<UFormField label="Email" name="email">
<UInput v-model="state.email" />
</UFormField>

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
key: 'typicode-users',
transform: (data: { id: number, name: string }[]) => {
return data?.map(user => ({
label: user.name,

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
const { data: users, status } = 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,

View File

@@ -3,6 +3,7 @@ const searchTerm = ref('')
const searchTermDebounced = refDebounced(searchTerm, 200)
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
key: 'typicode-users',
params: { q: searchTermDebounced },
transform: (data: { id: number, name: string }[]) => {
return data?.map(user => ({

View File

@@ -2,6 +2,7 @@
const searchTerm = ref('')
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
key: 'command-palette-users',
params: { q: searchTerm },
transform: (data: { id: number, name: string, email: string }[]) => {
return data?.map(user => ({ id: user.id, label: user.name, suffix: user.email, avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` } })) || []

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
key: 'typicode-users',
transform: (data: { id: number, name: string }[]) => {
return data?.map(user => ({
label: user.name,

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
const { data: users, status } = 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,

View File

@@ -3,6 +3,7 @@ const searchTerm = ref('')
const searchTermDebounced = refDebounced(searchTerm, 200)
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
key: 'typicode-users',
params: { q: searchTermDebounced },
transform: (data: { id: number, name: string }[]) => {
return data?.map(user => ({

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
key: 'typicode-users',
transform: (data: { id: number, name: string }[]) => {
return data?.map(user => ({
label: user.name,

View File

@@ -13,6 +13,7 @@ type User = {
}
const { data, status } = await useFetch<User[]>('https://jsonplaceholder.typicode.com/users', {
key: 'table-users',
transform: (data) => {
return data?.map(user => ({
...user,

View File

@@ -0,0 +1,139 @@
<script setup lang="ts">
const props = withDefaults(defineProps<{
contributors?: {
username: string
}[]
level?: number
max?: number
paused?: boolean
}>(), {
level: 0,
max: 4,
paused: false
})
const contributors = computed(() => props.contributors?.slice(0, 5) ?? [])
const el = ref(null)
const { width } = useElementSize(el)
</script>
<template>
<div
class="isolate rounded-full relative circle w-full aspect-[1/1] p-8 sm:p-12 md:p-14 lg:p-10 xl:p-16 before:absolute before:inset-px before:bg-(--ui-bg) before:rounded-full z-(--level)"
:class="{ 'animation-paused': paused }"
:style="{
'--duration': `${((level + 1) * 8)}s`,
'--level': level + 1
}"
>
<HomeContributors
v-if="(level + 1) < max"
:max="max"
:level="level + 1"
:contributors="props.contributors?.slice(5) ?? []"
:paused="paused"
/>
<div
ref="el"
class="avatars absolute inset-0 grid"
:style="{
'--total': contributors.length,
'--offset': `${width / 2}px`
}"
>
<UTooltip
v-for="(contributor, index) in contributors"
:key="contributor.username"
:text="contributor.username"
:delay-duration="0"
>
<NuxtLink
:to="`https://github.com/${contributor.username}`"
:aria-label="contributor.username"
target="_blank"
class="avatar flex absolute top-1/2 left-1/2"
tabindex="-1"
:style="{
'--index': index + 1
}"
>
<img
width="56"
height="56"
:src="`https://ipx.nuxt.com/s_56x56/gh_avatar/${contributor.username}`"
:srcset="`https://ipx.nuxt.com/s_112x112/gh_avatar/${contributor.username} 2x`"
:alt="contributor.username"
class="ring-2 ring-(--ui-border) lg:hover:ring-(--ui-border-inverted) transition rounded-full size-7"
loading="lazy"
>
</NuxtLink>
</UTooltip>
</div>
</div>
</template>
<style scoped>
.circle:after {
--start: 0deg;
--end: 360deg;
--border-color: var(--ui-border);
--highlight-color: var(--ui-color-neutral-400);
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: -1px;
opacity: 1;
border-radius: 9999px;
z-index: -1;
background: var(--border-color);
@supports (background: paint(houdini)) {
background: linear-gradient(var(--angle), var(--border-color), var(--border-color), var(--border-color), var(--border-color), var(--highlight-color));
animation: var(--duration) rotate linear infinite;
}
}
.dark .circle:after {
--highlight-color: var(--color-white);
}
.animation-paused.circle:after,
.animation-paused .avatars {
animation-play-state: paused;
}
.avatars {
--start: calc(var(--level) * 36deg);
--end: calc(360deg + (var(--level) * 36deg));
transform: rotate(var(--angle));
animation: calc(var(--duration) + 60s) rotate linear infinite;
}
.avatar {
--deg: calc(var(--index) * (360deg / var(--total)));
--transformX: calc(cos(var(--deg)) * var(--offset));
--transformY: calc(sin(var(--deg)) * var(--offset));
transform: translate(calc(-50% + var(--transformX)), calc(-50% + var(--transformY))) rotate(calc(360deg - var(--angle)));
}
@keyframes rotate {
from {
--angle: var(--start);
}
to {
--angle: var(--end);
}
}
@property --angle {
syntax: '<angle>';
initial-value: 0deg;
inherits: true;
}
</style>

View File

@@ -3,7 +3,7 @@ withDefaults(defineProps<{
title: string
description: string
component: string
module: string
module?: string
}>(), {
module: ''
})

View File

@@ -3,8 +3,8 @@ withDefaults(defineProps<{
title: string
description: string
headline: string
framework: string
module: string
framework?: string
module?: string
}>(), {
framework: 'nuxt',
module: ''

View File

@@ -37,6 +37,10 @@ export const useContentNavigation = (navigation: Ref<ContentNavigationItem[] | u
return {
...item,
children: item.children?.filter((child: any) => {
if (child.path.startsWith('/components')) {
return true
}
if (child.framework && child.framework !== framework.value) {
return false
}

View File

@@ -12,23 +12,17 @@ export function useLinks() {
to: '/components',
active: route.path === '/components',
children: [{
label: 'Layout',
to: '/components#layout',
description: 'Container, grid, divider and responsive layout.',
icon: 'i-lucide-layout',
active: route.fullPath === '/components#layout'
label: 'Element',
to: '/components#element',
description: 'Button, badge, icon, alert, and small UI elements.',
icon: 'i-lucide-mouse-pointer',
active: route.fullPath === '/components#element'
}, {
label: 'Form',
to: '/components#form',
description: 'Input, select, checkbox, radio and form validation.',
icon: 'i-lucide-text-cursor-input',
active: route.fullPath === '/components#form'
}, {
label: 'Element',
to: '/components#element',
description: 'Button, badge, icon, alert, and small UI elements.',
icon: 'i-lucide-mouse-pointer',
active: route.fullPath === '/components#element'
}, {
label: 'Data',
to: '/components#data',
@@ -47,6 +41,12 @@ export function useLinks() {
description: 'Modal, tooltip, dialog and popover.',
icon: 'i-lucide-layers',
active: route.fullPath === '/components#overlay'
}, {
label: 'Layout',
to: '/components#layout',
description: 'Container, grid, divider and responsive layout.',
icon: 'i-lucide-layout',
active: route.fullPath === '/components#layout'
}]
}, {
label: 'Pro',

View File

@@ -1,6 +1,5 @@
<script setup lang="ts">
import colors from 'tailwindcss/colors'
// import { debounce } from 'perfect-debounce'
import type { NuxtError } from '#app'
const props = defineProps<{
@@ -15,16 +14,6 @@ const { data: files } = useLazyAsyncData('search', () => queryCollectionSearchSe
server: false
})
const searchTerm = ref('')
// watch(searchTerm, debounce((query: string) => {
// if (!query) {
// return
// }
// useTrackEvent('Search', { props: { query: `${query} - ${searchTerm.value?.commandPaletteRef.results.length} results` } })
// }, 500))
const links = useLinks()
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; }`)
@@ -48,7 +37,7 @@ useHead({
})
useSeoMeta({
titleTemplate: '%s - Nuxt UI v3',
titleTemplate: '%s - Nuxt UI',
title: String(props.error.statusCode)
})
@@ -67,17 +56,16 @@ provide('navigation', mappedNavigation)
<UApp>
<NuxtLoadingIndicator color="#FFF" />
<!-- <Banner /> -->
<Banner />
<Header :links="links" />
<UError :error="error" />
<!-- <Footer /> -->
<Footer />
<ClientOnly>
<LazyUContentSearch
v-model:search-term="searchTerm"
:files="files"
:groups="[{
id: 'framework',

View File

@@ -1,5 +1,3 @@
<template>
<div>
<slot />
</div>
<slot />
</template>

182
docs/app/pages/.index.yml Normal file
View File

@@ -0,0 +1,182 @@
title: The Intuitive Vue UI Library
description: Create beautiful, responsive & accessible web apps quickly with Vue or Nuxt. Nuxt UI is an open-source UI library of 50+ customizable components built with Tailwind CSS and Reka UI.
hero:
title: The Intuitive Vue UI Library
description: Create beautiful, responsive & accessible web apps quickly with Vue or Nuxt. Nuxt UI is an open-source UI library of 50+ customizable components built with Tailwind CSS and Reka UI.
links:
- label: Get started
to: /getting-started/installation/nuxt
class: 'ui-only nuxt-only'
- label: Get started
to: /getting-started/installation/vue
class: 'ui-only vue-only'
- label: Get started
to: /getting-started/installation/pro/nuxt
class: 'ui-pro-only nuxt-only'
- label: Get started
to: /getting-started/installation/pro/vue
class: 'ui-pro-only vue-only'
- label: Explore components
to: /components
variant: outline
color: neutral
trailingIcon: i-lucide-arrow-right
features:
- icon: i-logos-tailwindcss-icon
title: Styled with Tailwind CSS v4
description: Beautifully styled by default, overwrite any style you want.
- icon: i-custom-reka-ui
title: Accessible with Reka UI
description: Robust accessibility out of the box.
- icon: i-logos-typescript-icon
title: Type-safe with TypeScript
description: Auto-complete and type safety for all components.
features:
- title: Build for the modern web
description: Powered by Tailwind CSS v4 and Reka UI for top performance and accessibility.
icon: i-lucide-rocket
to: /getting-started
- title: Flexible design system
description: Beautiful by default and easily customizable with design tokens to your brand.
icon: i-lucide-swatch-book
to: /getting-started/theme#design-system
- title: Internationalization (i18n)
description: Nuxt UI is translated into 30+ languages, works well with i18n and multi-directional support (LTR/RTL).
icon: i-lucide-globe
to: /getting-started/i18n/nuxt
- title: Easy font customization
description: Performance-optimized fonts with first-class @nuxt/fonts integration.
icon: i-lucide-a-large-small
to: /getting-started/fonts
- title: Large icons sets
description: Access to over 200,000 customizable icons from Iconify, seamlessly integrated with Iconify.
icon: i-lucide-smile
to: /getting-started/icons
- title: Light & Dark
description: Dark mode-ready components, seamless integration with @nuxtjs/color-mode.
icon: i-lucide-sun-moon
to: /getting-started/color-mode/nuxt
design_system:
title: Flexible design system
description: Build your next project faster with Nuxt UI's comprehensive design system. Featuring semantic color aliases, comprehensive design tokens, and automatic light/dark mode support for accessible components out of the box.
links:
- label: Customize design system
to: /getting-started/theme#design-system
variant: outline
color: neutral
trailingIcon: i-lucide-arrow-right
features:
- title: Color aliases via AppConfig
description: Configure 7 semantic color aliases (primary, secondary, success, info, warning, error, neutral) at runtime through AppConfig without rebuilding your application
icon: i-lucide-palette
- title: Comprehensive design tokens
description: Extensive set of neutral palette tokens for text, backgrounds, and borders with automatic light/dark mode support via CSS variables like --ui-text, --ui-bg, --ui-border
icon: i-lucide-component
- title: Global style variables
description: Customize global styling with --ui-radius for consistent border rounding and --ui-container for layout widths across your entire application
icon: i-lucide-ruler
code: |
::code-group
```ts [app.config.ts]
export default defineAppConfig({
ui: {
colors: {
primary: 'indigo',
secondary: 'pink',
success: 'green',
info: 'blue',
warning: 'orange',
error: 'red',
neutral: 'zinc'
}
}
})
```
```css [main.css]
@import "tailwindcss" theme(static);
@import "@nuxt/ui";
:root {
--ui-radius: var(--radius-sm);
--ui-container: 90rem;
--ui-bg: var(--ui-color-neutral-50);
--ui-text: var(--ui-color-neutral-900);
}
.dark {
--ui-bg: var(--ui-color-neutral-950);
--ui-border: var(--ui-color-neutral-900);
}
```
::
component_customization:
title: Powerful component customization
description: Nuxt UI leverages [Tailwind Variants](https://www.tailwind-variants.org/) to provide a powerful, maintainable system for managing component styles and intelligently merging Tailwind CSS classes without conflicts.
links:
- label: Customize components
to: /getting-started/theme#customize-theme
variant: outline
color: neutral
trailingIcon: i-lucide-arrow-right
features:
- title: Powerful slot and variant system
description: Customize component parts with slots and apply different styles based on props, creating consistent UI patterns with granular control over styling
icon: i-lucide-layout-grid
- title: Global theme with AppConfig
description: Configure component styles project-wide with a centralized AppConfig that maintains consistency across your application without rebuilding
icon: i-lucide-settings-2
- title: Per-component customization
description: Fine-tune individual components with the ui prop for slot-specific styling and class prop for root element overrides, providing maximum flexibility
icon: i-lucide-component
code: |
::code-group
```ts [app.config.ts]
export default defineAppConfig({
ui: {
button: {
slots: {
base: 'group font-bold',
trailingIcon: 'group-data-[state=open]:rotate-180 transition-transform duration-200'
},
defaultVariants: {
color: 'neutral',
variant: 'subtle'
}
}
}
})
```
```vue [Collapsible.vue]
<template>
<UCollapsible>
<UButton
label="Open"
color="neutral"
variant="subtle"
trailing-icon="i-lucide-chevron-down"
:ui="{
trailingIcon: 'group-data-[state=open]:rotate-180 transition-transform duration-200'
}"
class="group font-bold"
/>
</UCollapsible>
</template>
```
::
community:
title: Nuxt UI open-source community
description: Join our thriving community to contribute code, report issues, suggest features, or help with documentation. Every contribution makes Nuxt UI better for everyone.
links:
- label: Star on GitHub
color: neutral
variant: outline
to: https://github.com/nuxt/ui
target: _blank
icon: i-lucide-star

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import { kebabCase } from 'scule'
import type { ContentNavigationItem } from '@nuxt/content'
import { findPageBreadcrumb, mapContentNavigation } from '#ui-pro/utils/content'
@@ -9,7 +10,7 @@ definePageMeta({
layout: 'docs'
})
const { data: page } = await useAsyncData(route.path, () => queryCollection('content').path(route.path).first())
const { data: page } = await useAsyncData(kebabCase(route.path), () => queryCollection('content').path(route.path).first())
if (!page.value) {
throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
}
@@ -24,7 +25,7 @@ watch(page, () => {
}
}, { immediate: true })
const { data: surround } = await useAsyncData(`${route.path}-surround`, () => {
const { data: surround } = await useAsyncData(`${kebabCase(route.path)}-surround`, () => {
return queryCollectionItemSurroundings('content', route.path, {
fields: ['description']
}).orWhere(group => group.where('framework', '=', framework.value).where('framework', 'IS NULL'))
@@ -81,6 +82,8 @@ if (route.path.startsWith('/components')) {
})
} else {
defineOgImageComponent('Docs', {
title: page.value.title,
description: page.value.description,
headline: breadcrumb.value?.[breadcrumb.value.length - 1]?.label || 'Nuxt UI',
framework: page.value?.framework,
module: page.value.module
@@ -106,23 +109,6 @@ const communityLinks = computed(() => [{
icon: 'i-lucide-map',
to: '/roadmap'
}])
// const resourcesLinks = [{
// icon: 'i-simple-icons-figma',
// label: 'Figma Kit',
// to: 'https://www.figma.com/community/file/1288455405058138934',
// target: '_blank'
// }, {
// label: 'Playground',
// icon: 'i-simple-icons-stackblitz',
// to: 'https://stackblitz.com/edit/nuxt-ui',
// target: '_blank'
// }, {
// icon: 'i-simple-icons-nuxtdotjs',
// label: 'Nuxt docs',
// to: 'https://nuxt.com',
// target: '_blank'
// }]
</script>
<template>
@@ -137,7 +123,7 @@ const communityLinks = computed(() => [{
</template>
<template #description>
<MDC v-if="page.description" :value="page.description" unwrap="p" />
<MDC v-if="page.description" :value="page.description" unwrap="p" :cache-key="`${kebabCase(route.path)}-description`" />
</template>
<template v-if="page.links?.length" #links>
@@ -171,14 +157,9 @@ const communityLinks = computed(() => [{
<UPageLinks title="Community" :links="communityLinks" />
<!-- <USeparator type="dashed" />
<UPageLinks title="Resources" :links="resourcesLinks" />
<USeparator type="dashed" />
<AdsPro />
<AdsCarbon /> -->
<AdsCarbon />
</template>
</UContentToc>
</template>

View File

@@ -15,7 +15,7 @@ useSeoMeta({
ogImage: joinURL(url, '/og-image.png')
})
const { data: components } = await useAsyncData('components', () => {
const { data: components } = await useAsyncData('all-components', () => {
return queryCollection('content')
.where('path', 'LIKE', '/components/%')
.where('extension', '=', 'md')
@@ -31,17 +31,13 @@ const componentsPerCategory = computed(() => {
})
const categories = [{
id: 'layout',
title: 'Layout',
description: 'Structural components for organizing content, including containers, grids, dividers, and responsive layout systems.'
id: 'element',
title: 'Element',
description: 'Core UI building blocks like buttons, badges, icons, avatars, and other fundamental interface elements.'
}, {
id: 'form',
title: 'Form',
description: 'Interactive form elements including inputs, selects, checkboxes, radio buttons, and advanced form validation components.'
}, {
id: 'element',
title: 'Element',
description: 'Core UI building blocks like buttons, badges, icons, avatars, and other fundamental interface elements.'
}, {
id: 'data',
title: 'Data',
@@ -54,6 +50,10 @@ const categories = [{
id: 'overlay',
title: 'Overlay',
description: 'Floating UI elements like modals, dialogs, tooltips, popovers, and other components that overlay the main content.'
}, {
id: 'layout',
title: 'Layout',
description: 'Structural components for organizing content, including containers, grids, dividers, and responsive layout systems.'
}]
const { y } = useWindowScroll()
@@ -81,7 +81,10 @@ onMounted(() => {
orientation="vertical"
:ui="{ title: 'text-balance', container: 'relative' }"
>
<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" />
<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>
<template #headline>
<UButton
to="https://tailwindcss.com"
@@ -96,6 +99,7 @@ onMounted(() => {
<template #title>
Build beautiful UI with <span class="text-(--ui-primary)">{{ components!.length }}+</span> powerful components
</template>
<template #links>
<UButton
to="/getting-started/installation/vue"
@@ -114,10 +118,9 @@ onMounted(() => {
size="xl"
/>
</template>
<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" />
<StarsBg />
</template>
<StarsBg />
<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" />
</UPageHero>
<div v-for="category in categories" :key="category.id">
@@ -148,6 +151,7 @@ onMounted(() => {
:description="component.description"
:to="component.path"
:ui="{ wrapper: 'order-last', container: 'lg:flex' }"
class="group"
>
<template #title>
<div class="flex items-center gap-0.5">
@@ -156,7 +160,7 @@ onMounted(() => {
</div>
</template>
<div class="group rounded-[calc(var(--ui-radius)*1.5)] border border-(--ui-border-muted) overflow-hidden aspect-[16/9]">
<div class="rounded-[calc(var(--ui-radius)*1.5)] border border-(--ui-border-muted) overflow-hidden aspect-[16/9]">
<UColorModeImage
:light="`${component.path.replace('/components/', '/components/light/')}.png`"
:dark="`${component.path.replace('/components/', '/components/dark/')}.png`"

View File

@@ -8,7 +8,6 @@ hero:
links:
- label: Purchase Pro Kit
to: 'https://nuxt.lemonsqueezy.com/buy/17213c49-621b-4c2c-9478-3a50a099003d'
trailing-icon: i-lucide-arrow-right
target: _blank
- label: Free Figma Kit
to: 'https://go.nuxt.com/figma'
@@ -121,7 +120,6 @@ section4:
links:
- label: Get access now
to: 'https://nuxt.lemonsqueezy.com/buy/17213c49-621b-4c2c-9478-3a50a099003d'
trailing-icon: i-lucide-arrow-right
- label: Preview UI Pro Design Kit
to: 'https://go.nuxt.com/figma-pro'
icon: i-logos-figma
@@ -180,6 +178,7 @@ pricing:
- title: Solo License
description: Design faster with all Nuxt UI Pro components.
price: $149
# discount: $119
billing_period: one-time payment
billing_cycle: plus local taxes
class: bg-(--ui-bg-elevated)/50
@@ -201,6 +200,7 @@ pricing:
- title: Team License
description: Everything you need to deliver faster as a team.
price: $349
# discount: $279
billing_period: one-time payment
billing_cycle: plus local taxes
class: bg-(--ui-bg-elevated)/50
@@ -277,16 +277,14 @@ faq:
content: As the Figma Pro Kit is a digital product packaged as a zip file, we cannot offer refunds once the purchase is made.
- label: Do you have a Figma to Code plugin?
content: >
We recommend the open source [TeamPad Dev](https://github.com/ecomfe/tempad-dev) inspect panel with the [TeamPad Dev Nuxt UI Plugin](https://github.com/Justineo/tempad-dev-plugin-nuxt-ui):
We recommend the open source [TemPad Dev](https://github.com/ecomfe/tempad-dev) inspect panel with the [TemPad Dev Nuxt UI Plugin](https://github.com/Justineo/tempad-dev-plugin-nuxt-ui):
1. Install the [TeamPad Dev Chrome Extension](https://chromewebstore.google.com/detail/tempad-dev/lgoeakbaikpkihoiphamaeopmliaimpc)
1. Install the [TemPad Dev Chrome Extension](https://chromewebstore.google.com/detail/tempad-dev/lgoeakbaikpkihoiphamaeopmliaimpc)
2. Open your Figma file with Nuxt UI components (reload the page if you don't see the TeamPad Dev panel)
2. Open your Figma file with Nuxt UI components (reload the page if you don't see the TemPad Dev panel)
3. Install the `@nuxt` in TeamPad Dev's plugins section
3. Install the `@nuxt` (or `@nuxt/pro` for Nuxt UI Pro) in TemPad Dev's plugins section
4. Select any Nuxt UI component and inspect the code it generates
![TeamPad Dev Nuxt UI Plugin](/pro/figma/teampad-dev-nuxt-ui-plugin.gif){.w-full .rounded .mb-2 .max-w-[636px]}
*Right now, only Nuxt UI components are supported, but the code of the plugin is [open source](https://github.com/Justineo/tempad-dev-plugin-nuxt-ui) and anyone can contribute to it.*
![TemPad Dev Nuxt UI Plugin](/pro/figma/teampad-dev-nuxt-ui-plugin.gif){.w-full .rounded .mb-2 .max-w-[636px]}

View File

@@ -93,10 +93,10 @@ onMounted(async () => {
}"
>
<template #title>
<MDC :value="page.hero.title" unwrap="p" />
<MDC :value="page.hero.title" unwrap="p" cache-key="figma-hero-title" />
</template>
<template #description>
<MDC :value="page.hero.description" unwrap="p" />
<MDC :value="page.hero.description" unwrap="p" cache-key="figma-hero-description" />
</template>
<!-- <img src="/pro/figma/nuxt-ui-figma.png" alt="Screnshot of the Nuxt UI Figma design kit" class="w-full h-auto border border-(--ui-border) border-b-0"> -->
<div class="relative">
@@ -140,10 +140,10 @@ onMounted(async () => {
class="rounded-none bg-gradient-to-b from-(--ui-bg-muted) to-(--ui-bg)"
>
<template #title>
<MDC :value="page.cta1.title" unwrap="p" />
<MDC :value="page.cta1.title" unwrap="p" cache-key="figma-cta-1-title" />
</template>
<template #description>
<MDC :value="page.cta1.description" unwrap="p" />
<MDC :value="page.cta1.description" unwrap="p" cache-key="figma-cta-1-description" />
</template>
</UPageCTA>
<UPageSection v-bind="page.section1" orientation="horizontal" :ui="{ container: 'py-16 sm:py-16 lg:py-16' }">
@@ -189,7 +189,7 @@ onMounted(async () => {
}"
>
<template #description>
<MDC :value="page.section4.description" unwrap="p" />
<MDC :value="page.section4.description" unwrap="p" cache-key="figma-section-4-description" />
</template>
<div aria-hidden="true" class="absolute z-[-1] border-x border-(--ui-border) inset-0 mx-4 sm:mx-6 lg:mx-8" />
<ul class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 items-start justify-center border border-(--ui-border) border-b-0 sm:divide-x divide-y lg:divide-y-0 divide-(--ui-border)">
@@ -233,6 +233,7 @@ onMounted(async () => {
:title="plan.title"
:description="plan.description"
:price="plan.price"
:discount="plan.discount"
:billing-period="plan.billing_period"
:billing-cycle="plan.billing_cycle"
:highlight="plan.highlight"
@@ -245,7 +246,7 @@ onMounted(async () => {
<template #features>
<li v-for="(feature, i) in plan.features" :key="i" class="flex items-center gap-2 min-w-0">
<UIcon name="i-lucide-circle-check" class="size-5 shrink-0 text-(--ui-primary)" />
<MDC :value="feature" unwrap="p" tag="span" class="text-sm truncate text-(--ui-text-accented)" />
<MDC :value="feature" unwrap="p" tag="span" class="text-sm truncate text-(--ui-text-accented)" :cache-key="`figma-pricing-plan-${index}-feature-${i}`" />
</li>
</template>
<template #button>
@@ -281,8 +282,8 @@ onMounted(async () => {
:items="(page.faq.items as any[])"
class="max-w-4xl mx-auto"
>
<template #body="{ item }">
<MDC :value="item.content" unwrap="p" />
<template #body="{ item, index }">
<MDC :value="item.content" unwrap="p" :cache-key="`figma-faq-${index}-content`" />
</template>
</UPageAccordion>
</UPageSection>

307
docs/app/pages/index.vue Normal file
View File

@@ -0,0 +1,307 @@
<script setup lang="ts">
import { joinURL } from 'ufo'
// @ts-expect-error yaml is not typed
import page from '.index.yml'
const { url } = useSiteConfig()
useSeoMeta({
titleTemplate: `%s - Nuxt UI`,
title: page.title,
description: page.description,
ogTitle: `${page.title} - Nuxt UI`,
ogDescription: page.description,
ogImage: joinURL(url, '/og-image.png')
})
const { data: components } = await useAsyncData('ui-components', () => {
return queryCollection('content')
.where('path', 'LIKE', '/components/%')
.where('extension', '=', 'md')
.where('module', 'IS NULL')
.select('path', 'title', 'description', 'category', 'module')
.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 { format } = Intl.NumberFormat('en', { notation: 'compact' })
const contributorsRef = ref(null)
const isContributorsInView = ref(false)
const isContributorsHovered = useElementHover(contributorsRef)
useIntersectionObserver(contributorsRef, ([entry]) => {
isContributorsInView.value = entry?.isIntersecting || false
})
</script>
<template>
<UMain>
<UPageHero
orientation="horizontal"
:ui="{
container: 'pb-0 sm:pb-0 lg:py-0',
title: 'lg:mt-16',
links: 'lg:mb-16',
description: 'text-balance'
}"
>
<template #title>
The Intuitive <br> <span class="text-(--ui-primary)">Vue UI Library</span>
</template>
<template #description>
{{ page.hero.description }}
</template>
<template #links>
<UButton v-for="link of page.hero.links" :key="link.label" v-bind="link" size="xl" />
<div class="w-full my-6">
<USeparator class="w-1/2" type="dashed" />
</div>
<div class="flex flex-col gap-4">
<Motion
v-for="(feature, index) in page.hero.features"
:key="feature.title"
as-child
:initial="{ opacity: 0, transform: 'translateX(-10px)' }"
:while-in-view="{ opacity: 1, transform: 'translateX(0)' }"
:transition="{ delay: 0.2 + 0.4 * index }"
:in-view-options="{ once: true }"
>
<UPageFeature v-bind="feature" class="opacity-0" />
</Motion>
</div>
</template>
<SkyBg />
<div class="h-[344px] lg:h-full lg:relative w-full lg:min-h-[calc(100vh-var(--ui-header-height)-1px)] overflow-hidden">
<UPageMarquee
pause-on-hover
:overlay="false"
:ui="{
root: '[--gap:--spacing(4)] [--duration:40s] border-(--ui-border) absolute w-full left-0 border-y lg:border-x lg:border-y-0 lg:w-[calc(50%-6px)] 2xl:w-[320px] lg:flex-col',
content: 'lg:w-auto lg:h-full lg:flex-col lg:animate-[marquee-vertical_var(--duration)_linear_infinite] lg:rtl:animate-[marquee-vertical-rtl_var(--duration)_linear_infinite] lg:h-[fit-content]'
}"
>
<ULink
v-for="component of components?.slice(0, 10)"
:key="component.path"
class="relative group/link aspect-video border-(--ui-border) w-[290px] xl:w-[330px] 2xl:w-[320px] 2xl:p-2 2xl:border-y"
:to="component.path"
>
<UColorModeImage
:light="`${component.path.replace('/components/', '/components/light/')}.png`"
:dark="`${component.path.replace('/components/', '/components/dark/')}.png`"
class="hover:scale-105 lg:hover:scale-110 transition-transform aspect-video w-full border-x lg:border-x-0 lg:border-y border-(--ui-border) 2xl:border-y-0"
/>
<UBadge color="neutral" variant="outline" size="md" :label="component.title" class="hidden lg:block absolute mx-auto top-4 left-6 xl:left-4 group-hover/link:opacity-100 opacity-0 transition-all duration-300 pointer-events-none -translate-y-2 group-hover/link:translate-y-0" />
</ULink>
</UPageMarquee>
<UPageMarquee
pause-on-hover
reverse
:overlay="false"
:ui="{
root: '[--gap:--spacing(4)] [--duration:40s] border-(--ui-border) absolute w-full mt-[180px] left-0 border-y lg:mt-auto lg:left-auto lg:border-y-0 lg:border-x lg:w-[calc(50%-6px)] 2xl:w-[320px] lg:right-0 lg:flex-col',
content: 'lg:w-auto lg:h-full lg:flex-col lg:animate-[marquee-vertical_var(--duration)_linear_infinite] lg:rtl:animate-[marquee-vertical-rtl_var(--duration)_linear_infinite] lg:h-[fit-content] lg:[animation-direction:reverse]'
}"
>
<ULink
v-for="component of components?.slice(10, 20)"
:key="component.path"
class="relative group/link aspect-video border-(--ui-border) w-[290px] xl:w-[330px] 2xl:w-[320px] 2xl:p-2 2xl:border-y"
:to="component.path"
>
<UColorModeImage
:light="`${component.path.replace('/components/', '/components/light/')}.png`"
:dark="`${component.path.replace('/components/', '/components/dark/')}.png`"
class="hover:scale-105 lg:hover:scale-110 transition-transform aspect-video w-full border-x lg:border-x-0 lg:border-y border-(--ui-border) 2xl:border-y-0"
/>
<UBadge color="neutral" variant="outline" size="md" :label="component.title" class="hidden lg:block absolute mx-auto top-4 left-6 xl:left-4 group-hover/link:opacity-100 opacity-0 transition-all duration-300 pointer-events-none -translate-y-2 group-hover/link:translate-y-0" />
</ULink>
</UPageMarquee>
</div>
</UPageHero>
<USeparator />
<UPageSection :ui="{ container: 'lg:py-16' }">
<ul class="grid grid-cols-1 gap-x-6 sm:grid-cols-2 lg:grid-cols-3 gap-y-6 lg:gap-x-8 lg:gap-y-8 xl:gap-y-10">
<Motion
v-for="(feature, index) in page?.features"
:key="feature.title"
as="li"
:initial="{ opacity: 0, transform: 'translateY(10px)' }"
:while-in-view="{ opacity: 1, transform: 'translateY(0)' }"
:transition="{ delay: 0.1 * index }"
:in-view-options="{ once: true }"
class="flex items-start gap-x-3 relative group"
>
<NuxtLink v-if="feature.to" :to="feature.to" class="absolute inset-0 z-10" />
<div class="relative p-3">
<svg class="absolute inset-0" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
<line x1="6.5" x2="6.5" y2="44" stroke="var(--ui-border)" />
<line x1="38.5" x2="38.5" y2="44" stroke="var(--ui-border)" />
<line y1="5.5" x2="44" y2="5.5" stroke="var(--ui-border)" />
<line y1="37.5" x2="44" y2="37.5" stroke="var(--ui-border)" />
<circle cx="6.53613" cy="5.45508" r="1.5" fill="var(--ui-border-accented)" />
<circle cx="38.5957" cy="5.45508" r="1.5" fill="var(--ui-border-accented)" />
<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" />
</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" />
</h2>
<p class="text-sm text-(--ui-text-muted)">
{{ feature.description }}
</p>
</div>
</Motion>
</ul>
</UPageSection>
<USeparator />
<UPageSection
:title="page.design_system.title"
:description="page.design_system.description"
:features="page.design_system.features"
:links="page.design_system.links"
orientation="horizontal"
>
<MDC :value="page.design_system.code" cache-key="index-design-system-code" />
</UPageSection>
<USeparator />
<UPageSection
:title="page.component_customization.title"
:features="page.component_customization.features"
:links="page.component_customization.links"
orientation="horizontal"
>
<template #description>
<MDC :value="page.component_customization.description" cache-key="index-component-customization-description" />
</template>
<MDC :value="page.component_customization.code" cache-key="index-component-customization-code" />
</UPageSection>
<USeparator />
<UPageSection
:title="page.community.title"
:description="page.community.description"
:links="page.community.links"
orientation="horizontal"
:ui="{ features: 'flex items-center gap-4 lg:gap-8' }"
class="border-b border-(--ui-border)"
>
<template #features>
<NuxtLink to="https://npm.chart.dev/@nuxt/ui" target="_blank" class="min-w-0">
<p class="text-4xl font-semibold text-(--ui-text-highlighted) truncate">
{{ format(module?.stats?.downloads ?? 0) }}+
</p>
<p class="text-(--ui-text-muted) text-sm truncate">monthly downloads</p>
</NuxtLink>
<NuxtLink to="https://github.com/nuxt/ui" target="_blank" class="min-w-0">
<p class="text-4xl font-semibold text-(--ui-text-highlighted) truncate">
{{ format(module?.stats?.stars ?? 0) }}+
</p>
<p class="text-(--ui-text-muted) text-sm truncate">GitHub stars</p>
</NuxtLink>
<NuxtLink to="https://github.com/nuxt/ui/graphs/contributors" target="_blank" class="min-w-0">
<p class="text-4xl font-semibold text-(--ui-text-highlighted) truncate">
175+
</p>
<p class="text-(--ui-text-muted) text-sm truncate">Contributors</p>
</NuxtLink>
</template>
<div ref="contributorsRef" class="p-4 sm:px-6 md:px-8 lg:px-12 xl:px-14 overflow-hidden flex relative">
<LazyHomeContributors :contributors="module?.contributors" :paused="!isContributorsInView || isContributorsHovered" />
</div>
</UPageSection>
<UPageSection :ui="{ container: 'relative !pb-0 overflow-hidden' }">
<template #title>
Build faster with Nuxt UI <span class="text-(--ui-primary)">Pro</span>.
</template>
<template #description>
A collection of premium Vue components, composables and utils built on top of Nuxt UI. <br> Focused on structure and layout, these <span class="text-(--ui-text)">responsive components</span> are designed to be the perfect <span class="text-(--ui-text)">building blocks for your next idea</span>.
</template>
<template #links>
<UButton to="/pro" size="lg">
Discover Nuxt UI Pro
</UButton>
<UButton to="/pro/templates" size="lg" variant="outline" trailing-icon="i-lucide-arrow-right" color="neutral">
Explore templates
</UButton>
</template>
<StarsBg />
<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="relative h-[400px] border border-(--ui-border) bg-(--ui-bg-muted) overflow-hidden border-x-0 -mx-4 sm:-mx-6 lg:mx-0 lg:border-x w-screen lg:w-full">
<UPageMarquee reverse orientation="vertical" :overlay="false" :ui="{ root: '[--duration:40s] absolute w-[460px] -left-[100px] -top-[300px] h-[940px] transform-3d rotate-x-55 rotate-y-0 rotate-z-30' }">
<img
v-for="i in 4"
:key="i"
:src="`/pro/blocks/image${i}.png`"
width="460"
height="258"
loading="lazy"
:alt="`Nuxt UI Pro Screenshot ${i}`"
class="aspect-video border border-(--ui-border) rounded-[calc(var(--ui-radius)*2)] bg-white"
>
</UPageMarquee>
<UPageMarquee orientation="vertical" :overlay="false" :ui="{ root: '[--duration:40s] absolute w-[460px] -top-[400px] left-[480px] h-[1160px] transform-3d rotate-x-55 rotate-y-0 rotate-z-30' }">
<img
v-for="i in [5, 6, 7, 8]"
:key="i"
:src="`/pro/blocks/image${i}.png`"
width="460"
height="258"
loading="lazy"
:alt="`Nuxt UI Pro Screenshot ${i}`"
class="aspect-video border border-(--ui-border) rounded-[calc(var(--ui-radius)*2)] bg-white"
>
</UPageMarquee>
<UPageMarquee reverse orientation="vertical" :overlay="false" :ui="{ root: 'hidden md:flex [--duration:40s] absolute w-[460px] -top-[300px] left-[1020px] h-[1060px] transform-3d rotate-x-55 rotate-y-0 rotate-z-30' }">
<img
v-for="i in [9, 10, 11, 12]"
:key="i"
:src="`/pro/blocks/image${i}.png`"
width="460"
height="258"
:alt="`Nuxt UI Pro Screenshot ${i}`"
loading="lazy"
class="aspect-video border border-(--ui-border) rounded-[calc(var(--ui-radius)*2)] bg-white"
>
</UPageMarquee>
</div>
</UPageSection>
</UMain>
</template>

View File

@@ -17,7 +17,10 @@ pricing:
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.
orientation: horizontal
price: $149 - $349
price: $149
# discount: $119
billing_period: one-time payment
billing_cycle: plus local taxes
terms: Solo & Team licenses available.
features:
- 1700+ components & variants from Nuxt UI & UI Pro
@@ -29,13 +32,14 @@ pricing:
- Free [preview available](https://www.figma.com/design/mxXR9binOSLU3rYKZZRPXs/PREVIEW---NuxtUIPro-V3-BETA?m=auto&t=c4598Wr0rZwKPs5M-1)
- Includes Nuxt UI [Figma Kit](https://www.figma.com/community/file/1288455405058138934)
button:
label: Explore Figma Kit Pricing
label: Buy Figma Kit
to: 'https://nuxt.lemonsqueezy.com/buy/17213c49-621b-4c2c-9478-3a50a099003d'
color: 'neutral'
plans:
- title: Solo
description: Tailored for indie hackers, freelancers and solo founders.
price: $249
# discount: $199
billing_period: one-time payment
billing_cycle: plus local taxes
features:
@@ -50,6 +54,7 @@ pricing:
- title: Startup
description: Best suited for small teams, startups and agencies.
price: $499
# discount: $399
billing_period: one-time payment
billing_cycle: plus local taxes
features:
@@ -65,6 +70,7 @@ pricing:
- title: Organization
description: Ideal for larger teams and organizations.
price: $999
# discount: $799
billing_period: one-time payment
billing_cycle: plus local taxes
features:
@@ -174,7 +180,7 @@ testimonials:
- quote: "Nuxt UI Pro is my preferred choice for everything, from a POC to a web platform. It's ready to use out-of-the-box and assists me in crafting pixel-perfect UIs. It saves me a significant amount of time while remaining highly customizable. Give it a try, and you won't be let down."
user:
name: 'Estéban Soubiran'
description: 'Web developer and UnJS member'
description: 'Software engineer'
to: 'https://x.com/soubiran_'
target: _blank
avatar:

View File

@@ -7,13 +7,13 @@ hero:
- label: Buy a license
size: xl
to: /pro/pricing
trailing-icon: i-lucide-arrow-right
- label: Get started
- label: Try for free
trailing: true
color: neutral
variant: ghost
variant: outline
to: /getting-started/installation/pro/nuxt
size: xl
trailingIcon: i-lucide-arrow-right
features:
title: Create stunning Vue applications faster.
description: Nuxt UI Pro comes packed with powerful features to help you build modern, performant, accessible and responsive Nuxt applications at record speed. From pre-built UI sections to Figma design kits, every detail is crafted to speed up your development and deliver a polished user experience.
@@ -157,8 +157,8 @@ cta:
links:
- label: Buy a license
to: '/pro/pricing'
trailing-icon: i-lucide-arrow-right
- label: Get started
to: '/getting-started/license'
variant: ghost
- label: Try for free
to: /getting-started/installation/pro/nuxt
variant: outline
color: neutral
trailingIcon: i-lucide-arrow-right

View File

@@ -5,16 +5,16 @@ hero:
description: 'Ready to use templates powered by our premium Vue components and Nuxt Content.<br class="hidden lg:block"> The templates are responsive, accessible and easy to customize so you can get started in no time.'
navigation: false
links:
- label: Get started
to: /getting-started/installation/pro/nuxt#use-an-official-template
color: neutral
- label: Buy a license
color: primary
size: xl
trailingIcon: i-lucide-arrow-right
- label: Purchase a license
to: /pro/pricing
- label: Try for free
to: /getting-started/installation/pro/nuxt#use-an-official-template
size: xl
color: neutral
variant: outline
to: /pro/pricing
trailingIcon: i-lucide-arrow-right
templates:
- title: 'Dashboard'
description: "A template to illustrate how to build your own dashboard with the 15+ latest Nuxt UI Pro components, designed specifically to create a consistent look and feel."
@@ -30,7 +30,7 @@ templates:
- title: Resizable multi-column layout
icon: i-lucide-columns-3
links:
- label: Live Preview
- label: Preview
to: https://dashboard-template.nuxt.dev
target: _blank
leadingIcon: i-logos-nuxt-icon
@@ -42,7 +42,7 @@ templates:
icon: i-simple-icons-github
color: neutral
variant: outline
- label: Live Preview
- label: Preview
to: https://vue-dashboard-template.nuxt.dev
target: _blank
leadingIcon: i-logos-vue
@@ -68,7 +68,7 @@ templates:
- title: Authentication pages (login, register)
icon: i-lucide-user-round-check
links:
- label: Live Preview
- label: Preview
to: https://saas-template.nuxt.dev
target: _blank
leadingIcon: i-logos-nuxt-icon
@@ -94,7 +94,7 @@ templates:
- title: Write content in YAML
icon: i-simple-icons-yaml
links:
- label: Live Preview
- label: Preview
to: https://landing-template.nuxt.dev
target: _blank
leadingIcon: i-logos-nuxt-icon
@@ -120,7 +120,7 @@ templates:
- title: Full-text search out of the box
icon: i-lucide-search
links:
- label: Live Preview
- label: Preview
to: https://docs-template.nuxt.dev
target: _blank
leadingIcon: i-logos-nuxt-icon
@@ -144,7 +144,7 @@ templates:
- title: Nuxt 4 Compatibility Enabled
icon: i-simple-icons-nuxtdotjs
links:
- label: Live Preview
- label: Preview
to: https://ui-pro-starter.nuxt.dev
target: _blank
leadingIcon: i-logos-nuxt-icon
@@ -156,7 +156,7 @@ templates:
variant: outline
icon: i-simple-icons-github
color: neutral
- label: Live Preview
- label: Preview
to: https://ui-pro-starter-vue.nuxt.dev
target: _blank
leadingIcon: i-logos-vue

View File

@@ -70,14 +70,12 @@ onMounted(() => {
<template>
<UMain>
<UPageHero headline="License Activation" :title="title" :description="description" :ui="{ container: 'relative' }">
<template #top>
<StarsBg />
</template>
<UPageHero headline="License Activation" :title="title" :description="description" :ui="{ container: 'relative overflow-hidden', wrapper: 'lg:px-12', description: 'text-pretty' }">
<StarsBg />
<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="lg:border-y border-(--ui-border)">
<UCard class="lg:w-1/2 m-auto lg:rounded-none overflow-hidden" variant="outline" :ui="{ footer: 'bg-(--ui-bg-muted)' }">
<div class="px-4 py-10 lg:border border-(--ui-border) bg-(--ui-bg)">
<div class="max-w-xl mx-auto">
<UForm
:schema="schema"
:validate-on="['blur']"
@@ -107,12 +105,13 @@ onMounted(() => {
</UAlert>
<UAlert v-else-if="errorMessage" color="error" variant="subtle" :title="errorMessage" />
</UForm>
<template #footer>
<p class="text-sm text-center text-neutral-500 dark:text-neutral-400">
If you purchased a license with multiple seats, activate the license key for each member of your team.
</p>
</template>
</UCard>
<ProseHr />
<ProseNote>
If you purchased a license with multiple seats, activate the license key for each member of your team.
</ProseNote>
</div>
</div>
</UPageHero>
</UMain>

View File

@@ -26,15 +26,14 @@ useSeoMeta({
}"
>
<template #title>
<MDC :value="page.hero.title" unwrap="p" />
<MDC :value="page.hero.title" tag="span" unwrap="p" cache-key="pro-hero-title" />
</template>
<template #description>
<MDC :value="page.hero.description" unwrap="p" />
</template>
<template #top>
<StarsBg />
<MDC :value="page.hero.description" tag="span" unwrap="p" cache-key="pro-hero-description" />
</template>
<StarsBg />
<Motion as-child :initial="{ height: 0 }" :animate="{ height: 'auto' }" :transition="{ delay: 0.2, duration: 1 }">
<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" />
</Motion>
@@ -83,11 +82,11 @@ useSeoMeta({
}"
>
<template #description>
<Motion :initial="{ opacity: 0, transform: 'translateY(10px)' }" :in-view="{ opacity: 1, transform: 'translateY(0)' }" :in-view-options="{ once: true }" :transition="{ delay: 0.2 }">
<MDC :value="page.testimonial.quote" unwrap="p" class="before:content-[open-quote] after:content-[close-quote] " />
<Motion :initial="{ opacity: 0, transform: 'translateY(10px)' }" :while-in-view="{ opacity: 1, transform: 'translateY(0)' }" :in-view-options="{ once: true }" :transition="{ delay: 0.2 }">
<MDC :value="page.testimonial.quote" tag="span" unwrap="p" class="before:content-[open-quote] after:content-[close-quote]" cache-key="pro-testimonial-quote" />
</Motion>
</template>
<Motion :initial="{ opacity: 0, transform: 'translateY(10px)' }" :in-view="{ opacity: 1, transform: 'translateY(0)' }" :in-view-options="{ once: true }" :transition="{ delay: 0.3 }">
<Motion :initial="{ opacity: 0, transform: 'translateY(10px)' }" :while-in-view="{ opacity: 1, transform: 'translateY(0)' }" :in-view-options="{ once: true }" :transition="{ delay: 0.3 }">
<UUser
v-bind="page.testimonial.user"
class="justify-center"
@@ -104,7 +103,7 @@ useSeoMeta({
}"
class="border-t border-(--ui-border)"
>
<Motion as-child :initial="{ height: 0 }" :in-view="{ height: 'auto' }" :transition="{ delay: 0.4, duration: 1 }">
<Motion as-child :initial="{ height: 0 }" :while-in-view="{ height: 'auto' }" :transition="{ delay: 0.4, duration: 1 }">
<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" />
</Motion>
</UPageSection>
@@ -117,13 +116,13 @@ useSeoMeta({
wrapper: 'grid grid-cols-1 lg:grid-cols-2',
description: 'lg:mt-0' }"
orientation="horizontal"
class="rounded-none border-t border-(--ui-border) bg-gradient-to-b from-(--ui-bg-muted) to-(--ui-bg)"
class="rounded-none border-t border-(--ui-border) bg-gradient-to-b from-(--ui-bg-elevated)/50 to-(--ui-bg)"
>
<template #title>
<MDC :value="page.mainSection.title" unwrap="p" />
<MDC :value="page.mainSection.title" tag="span" unwrap="p" cache-key="pro-main-section-title" />
</template>
<template #description>
<MDC :value="page.mainSection.description" unwrap="p" />
<MDC :value="page.mainSection.description" tag="span" unwrap="p" cache-key="pro-main-section-description" />
</template>
</UPageCTA>
<UPageSection
@@ -140,7 +139,7 @@ useSeoMeta({
container: index === 0 ? 'pb-0 sm:pb-0 lg:pb-0 py-16 sm:py-16 lg:py-16' : ''
}"
>
<MDC :value="section.code" />
<MDC :value="section.code" :cache-key="`pro-section-${index}-code`" />
</UPageSection>
<UPageSection
@@ -198,6 +197,7 @@ useSeoMeta({
orientation="horizontal"
>
<StarsBg />
<video
class="rounded-[var(--ui-radius)] z-10"
preload="none"

View File

@@ -24,11 +24,10 @@ useSeoMeta({
}"
>
<template #title>
<MDC :value="page.pricing.title" unwrap="p" />
</template>
<template #top>
<StarsBg />
<MDC :value="page.pricing.title" unwrap="p" cache-key="pro-pricing-title" />
</template>
<StarsBg />
<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="flex flex-col bg-(--ui-bg) gap-8 lg:gap-0">
<UPricingPlan
@@ -43,6 +42,7 @@ useSeoMeta({
:title="plan.title"
:description="plan.description"
:price="plan.price"
:discount="plan.discount"
:billing-period="plan.billing_period"
:billing-cycle="plan.billing_cycle"
:variant="plan.highlight ? 'soft' : 'outline'"
@@ -54,12 +54,14 @@ useSeoMeta({
<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-(--ui-border)"
>
<template #features>
<li v-for="(feature, index) in page.pricing.figma.features" :key="index" class="flex items-center gap-2 min-w-0">
<UIcon name="i-lucide-circle-check" class="size-5 text-(--ui-primary) shrink-0" />
<MDC :value="feature" unwrap="p" class="text-sm truncate text-(--ui-text-toned)" />
<MDC :value="feature" unwrap="p" class="text-sm truncate text-(--ui-text-toned)" :cache-key="`pro-pricing-figma-feature-${index}`" />
</li>
</template>
</UPricingPlan>
@@ -111,8 +113,8 @@ useSeoMeta({
:items="(page.faq.items as any[])"
class="max-w-4xl mx-auto"
>
<template #body="{ item }">
<MDC :value="item.content" unwrap="p" />
<template #body="{ item, index }">
<MDC :value="item.content" unwrap="p" :cache-key="`pro-pricing-faq-${index}-content`" />
</template>
</UPageAccordion>
</UPageSection>

View File

@@ -16,59 +16,60 @@ useSeoMeta({
<!-- eslint-disable vue/no-v-html -->
<template>
<UPageHero :links="page.links" :ui="{ container: 'relative' }">
<template #top>
<div class="relative">
<UPageHero :links="page.links" :ui="{ container: 'relative' }">
<StarsBg />
</template>
<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 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" />
<template #title>
<MDC :value="page.hero.title" unwrap="p" />
</template>
<template #title>
<MDC :value="page.hero.title" unwrap="p" cache-key="pro-templates-hero-title" />
</template>
<template #description>
<MDC :value="page.hero.description" unwrap="p" />
</template>
</UPageHero>
<template #description>
<MDC :value="page.hero.description" unwrap="p" cache-key="pro-templates-hero-description" />
</template>
</UPageHero>
<UPageSection
v-for="(template, index) in page.templates"
:key="index"
:title="template.title"
:links="template.links"
:features="template.features"
orientation="horizontal"
class="lg:border-t border-(--ui-border)"
:ui="{
title: 'lg:text-4xl',
wrapper: 'lg:py-16 lg:border-r border-(--ui-border) order-last lg:pr-16',
container: 'lg:py-0'
}"
>
<template #description>
<MDC :value="template.description" unwrap="p" />
</template>
<UPageSection
v-for="(template, index) in page.templates"
:key="index"
:title="template.title"
:links="template.links"
:features="template.features"
orientation="horizontal"
class="lg:border-t border-(--ui-border)"
:ui="{
title: 'lg:text-4xl',
wrapper: 'lg:py-16 lg:border-r border-(--ui-border) order-last lg:pr-16',
container: 'lg:py-0',
links: 'gap-x-3'
}"
>
<template #description>
<MDC :value="template.description" unwrap="p" :cache-key="`pro-templates-${index}-description`" />
</template>
<div class="lg:border-x border-(--ui-border) h-full flex items-center lg:bg-(--ui-bg-muted)/20">
<Motion class="flex-1" :initial="{ opacity: 0, transform: 'translateY(10px)' }" :in-view="{ opacity: 1, transform: 'translateY(0px)' }" :in-view-options="{ once: true }" :transition="{ duration: 0.5, delay: 0.2 }">
<UColorModeImage
v-if="template.thumbnail"
v-bind="template.thumbnail"
class="w-full h-auto border lg:border-y lg:border-x-0 border-(--ui-border) rounded-(--ui-radius) lg:rounded-none"
width="656"
height="369"
loading="lazy"
/>
<UCarousel
v-else-if="template.images"
v-slot="{ item }"
:items="(template.images as any[])"
dots
>
<NuxtImg v-bind="item" class="w-full h-full object-cover" width="576" height="360" />
</UCarousel>
<Placeholder v-else class="w-full h-full aspect-video" />
</Motion>
</div>
</UPageSection>
<div class="lg:border-x border-(--ui-border) h-full flex items-center lg:bg-(--ui-bg-muted)/20">
<Motion class="flex-1" :initial="{ opacity: 0, transform: 'translateY(10px)' }" :while-in-view="{ opacity: 1, transform: 'translateY(0px)' }" :in-view-options="{ once: true }" :transition="{ duration: 0.5, delay: 0.2 }">
<UColorModeImage
v-if="template.thumbnail"
v-bind="template.thumbnail"
class="w-full h-auto border lg:border-y lg:border-x-0 border-(--ui-border) rounded-(--ui-radius) lg:rounded-none"
width="656"
height="369"
loading="lazy"
/>
<UCarousel
v-else-if="template.images"
v-slot="{ item }"
:items="(template.images as any[])"
dots
>
<NuxtImg v-bind="item" class="w-full h-full object-cover" width="576" height="360" />
</UCarousel>
<Placeholder v-else class="w-full h-full aspect-video" />
</Motion>
</div>
</UPageSection>
</div>
</template>

View File

@@ -3,7 +3,7 @@ const title = 'Roadmap'
const description = 'Discover our Volta board for @nuxt/ui development status.'
useSeoMeta({
titleTemplate: '%s - Nuxt UI v3',
titleTemplate: '%s - Nuxt UI',
title,
ogTitle: 'Nuxt UI Roadmap',
description

View File

@@ -1,14 +1,9 @@
---
navigation.title: Introduction
title: Nuxt UI v3
description: 'A comprehensive, Nuxt-integrated UI library providing a rich set of fully-styled, accessible and highly customizable components for building modern web applications.'
title: Introduction
description: 'Nuxt UI harnesses the combined strengths of Reka UI, Tailwind CSS, and Tailwind Variants to offer developers an unparalleled set of tools for creating sophisticated, accessible, and highly performant user interfaces.'
navigation.icon: i-lucide-house
---
We're thrilled to introduce this major update to our UI library, bringing significant improvements and powerful new features. Nuxt UI v3 represents a leap forward in creating robust, accessible, and highly customizable user interfaces for Nuxt applications.
## What's New in v3?
<iframe width="100%" height="100%" src="https://www.youtube-nocookie.com/embed/_eQxomah-nA?si=pDSzchUBDKb2NQu7" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen style="aspect-ratio: 16/9;" class="rounded-[calc(var(--ui-radius)*1.5)]"></iframe>
### Reka UI
@@ -24,7 +19,7 @@ This transition empowers Nuxt UI to become a more comprehensive and flexible UI
### Tailwind CSS v4
Nuxt UI v3 integrates the latest Tailwind CSS v4, bringing significant improvements:
Nuxt UI integrates the latest Tailwind CSS v4, bringing significant improvements:
- **Built for performance**: Full builds in the new engine are up to 5x faster, and incremental builds are over 100x faster — and measured in microseconds.
- **Unified toolchain**: Built-in import handling, vendor prefixing, and syntax transforms, with no additional tooling required.
@@ -47,7 +42,7 @@ This integration unifies the styling of components, ensuring consistency and cod
### TypeScript Integration
Nuxt UI v3 offers significantly improved TypeScript integration, providing a superior developer experience:
Nuxt UI offers significantly improved TypeScript integration, providing a superior developer experience:
- **Enhanced Auto-completion**:
- Full auto-completion for component props based on your theme
@@ -112,11 +107,7 @@ Key points to consider:
## FAQ
::accordion
::accordion-item{label="What are the main considerations when upgrading to Nuxt UI v3?"}
The transition to v3 involves significant changes, including new component structures, updated theming approaches, and revised TypeScript definitions. We recommend a careful, incremental upgrade process, starting with thorough testing in a development environment.
::
::accordion-item{label="Is Nuxt UI v3 compatible with standalone Vue projects?"}
::accordion-item{label="Is Nuxt UI compatible with standalone Vue projects?"}
Nuxt UI is now compatible with Vue! You can follow the [installation guide](/getting-started/installation/vue) to get started.
::
@@ -124,23 +115,19 @@ Key points to consider:
We've also rebuilt Nuxt UI Pro from scratch as v3 to match Nuxt UI version. The license you bought or will buy is valid for both Nuxt UI Pro v1 and v3, this is a **free update**. You can follow the [installation guide](/getting-started/installation/pro/nuxt) to get started.
::
::accordion-item{label="Will Nuxt UI v3 work with other CSS frameworks like UnoCSS?"}
Nuxt UI v3 is currently designed to work exclusively with Tailwind CSS. While there's interest in UnoCSS support, implementing it would require significant changes to the theme structure due to differences in class naming conventions. As a result, we don't have plans to add UnoCSS support in v3.
::accordion-item{label="Will Nuxt UI work with other CSS frameworks like UnoCSS?"}
Nuxt UI is currently designed to work exclusively with Tailwind CSS. While there's interest in UnoCSS support, implementing it would require significant changes to the theme structure due to differences in class naming conventions. As a result, we don't have plans to add UnoCSS support.
::
::accordion-item{label="How does Nuxt UI v3 handle accessibility?"}
Nuxt UI v3 enhances accessibility through Reka UI integration. This provides automatic ARIA attributes, keyboard navigation support, intelligent focus management, and screen reader announcements. While offering a strong foundation, proper implementation and testing in your specific use case remains crucial for full accessibility compliance. For more detailed information, refer to [Reka UI's accessibility documentation](https://reka-ui.com/docs/overview/accessibility).
::accordion-item{label="How does Nuxt UI handle accessibility?"}
Nuxt UI enhances accessibility through Reka UI integration. This provides automatic ARIA attributes, keyboard navigation support, intelligent focus management, and screen reader announcements. While offering a strong foundation, proper implementation and testing in your specific use case remains crucial for full accessibility compliance. For more detailed information, refer to [Reka UI's accessibility documentation](https://reka-ui.com/docs/overview/accessibility).
::
::accordion-item{label="What is the testing approach for Nuxt UI v3?"}
Nuxt UI v3 ensures reliability with 1000+ Vitest tests, covering core functionality and accessibility. This robust testing suite supports the library's stability and serves as a reference for developers.
::
::accordion-item{label="Is this version stable and suitable for production use?"}
Nuxt UI v3 is now in beta and is stable enough to be used in production. We now recommend using v3 over v2. We welcome feedback from users to help improve the library further. Feel free to report any issues you encounter on our [GitHub repository](https://github.com/nuxt/ui/issues).
::accordion-item{label="What is the testing approach for Nuxt UI?"}
Nuxt UI ensures reliability with 1000+ Vitest tests, covering core functionality and accessibility. This robust testing suite supports the library's stability and serves as a reference for developers.
::
::
:hr
We're excited about the possibilities Nuxt UI v3 brings to your projects. Explore our documentation to learn more about new features, components, and best practices for building powerful, accessible user interfaces with Nuxt UI v3.
We're excited about the possibilities Nuxt UI v3 brings to your projects. Explore our documentation to learn more about new features, components, and best practices for building powerful, accessible user interfaces.

View File

@@ -20,24 +20,24 @@ Looking for the **Vue** version?
::steps{level="4"}
#### Install the Nuxt UI v3 beta package
#### Install the Nuxt UI package
::code-group{sync="pm"}
```bash [pnpm]
pnpm add @nuxt/ui@next
pnpm add @nuxt/ui
```
```bash [yarn]
yarn add @nuxt/ui@next
yarn add @nuxt/ui
```
```bash [npm]
npm install @nuxt/ui@next
npm install @nuxt/ui
```
```bash [bun]
bun add @nuxt/ui@next
bun add @nuxt/ui
```
::
@@ -85,7 +85,11 @@ It's recommended to install the [Tailwind CSS IntelliSense](https://marketplace.
},
"editor.quickSuggestions": {
"strings": "on"
}
},
"tailwindCSS.classAttributes": ["class", "ui"],
"tailwindCSS.experimental.classRegex": [
["ui:\\s*{([^)]*)\\s*}", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
]
```
::
@@ -108,12 +112,12 @@ The `App` component provides global configurations and is required for **Toast**
### Use our Nuxt Starter
Start your project using the [nuxt/starter#ui3](https://github.com/nuxt/starter/tree/ui3) template with Nuxt UI v3 pre-configured.
Start your project using the [nuxt/starter#ui](https://github.com/nuxt/starter/tree/ui) template with Nuxt UI pre-configured.
Create a new project locally by running the following command:
```bash [Terminal]
npx nuxi init -t ui3 <my-app>
npx nuxi init -t ui <my-app>
```
::note
@@ -225,30 +229,19 @@ This option adds the `transition-colors` class on components with hover or activ
## Continuous Releases
Nuxt UI v3 uses [pkg.pr.new](https://github.com/stackblitz-labs/pkg.pr.new) for continuous preview releases, providing developers with instant access to the latest features and bug fixes without waiting for official releases.
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.
Preview releases are automatically generated for every commit to the `v3` branch and pull requests targeting the `v3` branch. To use it into your project, use the installation command below by replacing `5385f84` with any commit hash or pull request number.
Automatic preview releases are created for all commits and PRs to the `v3` branch. Use them by replacing your package version with the specific commit hash or PR number.
::code-group{sync="pm"}
```bash [pnpm]
pnpm add https://pkg.pr.new/@nuxt/ui@5385f84
```diff [package.json]
{
"dependencies": {
- "@nuxt/ui": "^3.0.0",
+ "@nuxt/ui": "https://pkg.pr.new/@nuxt/ui@4c96909",
}
}
```
```bash [yarn]
yarn add https://pkg.pr.new/@nuxt/ui@5385f84
```
```bash [npm]
npm install https://pkg.pr.new/@nuxt/ui@5385f84
```
```bash [bun]
bun add https://pkg.pr.new/@nuxt/ui@5385f84
```
::
::note
**pkg.pr.new** will automatically comment on PRs with the installation URL, making it easy to test changes.
::

View File

@@ -20,24 +20,24 @@ Looking for the **Nuxt** version?
::steps{level="4"}
#### Install the Nuxt UI v3 beta package
#### Install the Nuxt UI package
::code-group{sync="pm"}
```bash [pnpm]
pnpm add @nuxt/ui@next
pnpm add @nuxt/ui
```
```bash [yarn]
yarn add @nuxt/ui@next
yarn add @nuxt/ui
```
```bash [npm]
npm install @nuxt/ui@next
npm install @nuxt/ui
```
```bash [bun]
bun add @nuxt/ui@next
bun add @nuxt/ui
```
::
@@ -145,7 +145,11 @@ It's recommended to install the [Tailwind CSS IntelliSense](https://marketplace.
},
"editor.quickSuggestions": {
"strings": "on"
}
},
"tailwindCSS.classAttributes": ["class", "ui"],
"tailwindCSS.experimental.classRegex": [
["ui:\\s*{([^)]*)\\s*}", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
]
```
::
@@ -168,7 +172,7 @@ The `App` component provides global configurations and is required for **Toast**
### Use our Vue starter
Start your project using the [nuxtlabs/nuxt-ui-vue-starter](https://github.com/nuxtlabs/nuxt-ui-vue-starter) template with Nuxt UI v3 pre-configured.
Start your project using the [nuxtlabs/nuxt-ui-vue-starter](https://github.com/nuxtlabs/nuxt-ui-vue-starter) template with Nuxt UI pre-configured.
Create a new project locally by running the following command:
@@ -313,30 +317,19 @@ This option adds the `transition-colors` class on components with hover or activ
## Continuous Releases
Nuxt UI v3 uses [pkg.pr.new](https://github.com/stackblitz-labs/pkg.pr.new) for continuous preview releases, providing developers with instant access to the latest features and bug fixes without waiting for official releases.
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.
Preview releases are automatically generated for every commit to the `v3` branch and pull requests targeting the `v3` branch. To use it into your project, use the installation command below by replacing `5385f84` with any commit hash or pull request number.
Automatic preview releases are created for all commits and PRs to the `v3` branch. Use them by replacing your package version with the specific commit hash or PR number.
::code-group{sync="pm"}
```bash [pnpm]
pnpm add https://pkg.pr.new/@nuxt/ui@5385f84
```diff [package.json]
{
"dependencies": {
- "@nuxt/ui": "^3.0.0",
+ "@nuxt/ui": "https://pkg.pr.new/@nuxt/ui@4c96909",
}
}
```
```bash [yarn]
yarn add https://pkg.pr.new/@nuxt/ui@5385f84
```
```bash [npm]
npm install https://pkg.pr.new/@nuxt/ui@5385f84
```
```bash [bun]
bun add https://pkg.pr.new/@nuxt/ui@5385f84
```
::
::note
**pkg.pr.new** will automatically comment on PRs with the installation URL, making it easy to test changes.
::

View File

@@ -56,19 +56,19 @@ npx @tailwindcss/upgrade
::::code-group{sync="pm"}
```bash [pnpm]
pnpm add @nuxt/ui@next
pnpm add @nuxt/ui
```
```bash [yarn]
yarn add @nuxt/ui@next
yarn add @nuxt/ui
```
```bash [npm]
npm install @nuxt/ui@next
npm install @nuxt/ui
```
```bash [bun]
bun add @nuxt/ui@next
bun add @nuxt/ui
```
::::
@@ -81,19 +81,19 @@ bun add @nuxt/ui@next
::::code-group{sync="pm"}
```bash [pnpm]
pnpm add @nuxt/ui-pro@next
pnpm add @nuxt/ui-pro
```
```bash [yarn]
yarn add @nuxt/ui-pro@next
yarn add @nuxt/ui-pro
```
```bash [npm]
npm install @nuxt/ui-pro@next
npm install @nuxt/ui-pro
```
```bash [bun]
bun add @nuxt/ui-pro@next
bun add @nuxt/ui-pro
```
::::
@@ -130,7 +130,7 @@ bun add @nuxt/ui-pro@next
#ui
:::div
5. Wrap you app with the [App](/components/app) component:
5. Wrap your app with the [App](/components/app) component:
:::
#ui-pro
@@ -145,7 +145,7 @@ export default defineNuxtConfig({
})
```
6. Wrap you app with the [App](/components/app) component:
6. Wrap your app with the [App](/components/app) component:
:::
::
@@ -359,7 +359,7 @@ In addition to the renamed components, there are lots of changes to the componen
+ <USelect :items="countries" />
- <UHorizontalNavigation :links="links" />
+ <UNavigationMenu :items="items" />
+ <UNavigationMenu :items="links" />
</template>
```
@@ -367,6 +367,165 @@ 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:
```diff [app.vue]
<template>
+ <UApp>
+ <NuxtPage />
+ </UApp>
- <UModals />
- <USlideovers />
- <UNotifications />
</template>
```
3. The `v-model:open` directive and `default-open` prop are now used to control visibility:
```diff
<template>
- <UModal v-model="open" />
+ <UModal v-model:open="open" />
</template>
```
::note
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):
```diff
<script setup lang="ts">
- const open = ref(false)
</script>
<template>
- <UButton label="Open" @click="open = true" />
- <UModal v-model="open">
+ <UModal>
+ <UButton label="Open" />
+ <template #content>
<div class="p-4">
<Placeholder class="h-48" />
</div>
+ </template>
</UModal>
</template>
```
::note
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:
```diff
<template>
- <UModal>
+ <UModal title="Title" description="Description">
- <div class="p-4">
+ <template #body>
<Placeholder class="h-48" />
+ </template>
- </div>
</UModal>
</template>
```
::note
This change affects the following components: `Modal`, `Slideover`.
::
### Changed composables
1. The `useToast()` composable `timeout` prop has been renamed to `duration`:
```diff
<script setup lang="ts">
const toast = useToast()
- toast.add({ title: 'Invitation sent', timeout: 0 })
+ toast.add({ title: 'Invitation sent', duration: 0 })
</script>
```
2. 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
- Overlays that are opened, can be awaited for their result
- Overlays can no longer be close using `modal.close()` or `slideover.close()`, rather, they close automatically: either when a `close` event is fired explicitly from the opened component OR when the overlay closes itself (clicking on backdrop, pressing the ESC key, etc)
- To capture the return value in the parent component you must explictly emit a `close` event with the desired value
```diff
<script setup lang="ts">
import { ModalExampleComponent } from '#components'
- const modal = useModal()
+ const overlay = useOverlay()
- modal.open(ModalExampleComponent)
+ const modal = overlay.create(ModalExampleComponent)
</script>
```
Props are now passed through a props attribute:
```diff
<script setup lang="ts">
import { ModalExampleComponent } from '#components'
- const modal = useModal()
+ const overlay = useOverlay()
const count = ref(0)
- modal.open(ModalExampleComponent, {
- count: count.value
- })
+ const modal = overlay.create(ModalExampleComponent, {
+ props: {
+ count: count.value
+ }
+ })
</script>
```
Closing a modal is now done through the `close` event. The `modal.open` method now returns a promise that resolves to the result of the modal whenever the modal is close:
```diff
<script setup lang="ts">
import { ModalExampleComponent } from '#components'
- const modal = useModal()
+ const overlay = useOverlay()
+ const modal = overlay.create(ModalExampleComponent)
- function openModal() {
- modal.open(ModalExampleComponent, {
- onSuccess() {
- toast.add({ title: 'Success!' })
- }
- })
- }
+ async function openModal() {
+ const result = await modal.open(ModalExampleComponent, {
+ count: count.value
+ })
+
+ if (result) {
+ toast.add({ title: 'Success!' })
+ }
+ }
</script>
```
---
::warning

View File

@@ -6,7 +6,7 @@ navigation.icon: i-lucide-swatch-book
## Tailwind CSS
Nuxt UI v3 uses Tailwind CSS v4, you can read the official [upgrade guide](https://tailwindcss.com/docs/upgrade-guide#changes-from-v3) to learn about all the breaking changes.
Nuxt UI uses Tailwind CSS v4, you can read the official [upgrade guide](https://tailwindcss.com/docs/upgrade-guide#changes-from-v3) to learn about all the breaking changes.
### `@theme`
@@ -142,7 +142,7 @@ Nuxt UI leverages Vite config to provide customizable color aliases based on [Ta
::framework-only
#nuxt
::div
:::div
You can configure these color aliases at runtime in your `app.config.ts` file under the `ui.colors` key, allowing for dynamic theme customization without requiring an application rebuild:
```ts [app.config.ts]
@@ -156,14 +156,19 @@ export default defineAppConfig({
})
```
::
:::
#vue
::module-only
#ui
:::div
You can configure these color aliases at runtime in your `vite.config.ts` file under the `ui.colors` key:
::::module-only
#ui
:::::div
```ts [vite.config.ts]
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
@@ -183,11 +188,12 @@ export default defineConfig({
]
})
```
:::
:::::
#ui-pro
:::div
You can configure these color aliases at runtime in your `vite.config.ts` file under the `uiPro.colors` key:
:::::div
```ts [vite.config.ts]
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
@@ -207,9 +213,12 @@ export default defineConfig({
]
})
```
:::
::
:::::
::::
:::
::
@@ -256,11 +265,17 @@ export default defineNuxtConfig({
:::
#vue
::module-only
#ui
:::tip
You can add you own dynamic color aliases in your `vite.config.ts`, you just have to make sure to also define them in the [`theme.colors`](/getting-started/installation/vue#themecolors) option of the `ui` plugin.
::::module-only
#ui
:::::div
```ts [vite.config.ts]
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
@@ -283,11 +298,11 @@ export default defineConfig({
})
```
:::
:::::
#ui-pro
:::tip
You can add you own dynamic color aliases in your `vite.config.ts`, you just have to make sure to also define them in the [`theme.colors`](/getting-started/installation/vue#themecolors) option of the `uiPro` plugin.
:::::div
```ts [vite.config.ts]
import { defineConfig } from 'vite'
@@ -311,9 +326,12 @@ export default defineConfig({
})
```
:::::
::::
:::
::
::
### Tokens

View File

@@ -17,6 +17,11 @@ Nuxt UI provides an **App** component that wraps your app to provide global conf
### Locale
::module-only
#ui
:::div
Use the `locale` prop with the locale you want to use from `@nuxt/ui/locale`:
```vue [app.vue]
@@ -31,13 +36,42 @@ import { fr } from '@nuxt/ui/locale'
</template>
```
:::
#ui-pro
:::div
Use the `locale` prop with the locale you want to use from `@nuxt/ui-pro/locale`:
```vue [app.vue]
<script setup lang="ts">
import { fr } from '@nuxt/ui-pro/locale'
</script>
<template>
<UApp :locale="fr">
<NuxtPage />
</UApp>
</template>
```
:::
::
### Custom locale
You also have the option to add your own locale using `defineLocale`:
::module-only
#ui
:::div
```vue [app.vue]
<script setup lang="ts">
const locale = defineLocale({
import type { Messages } from '@nuxt/ui'
const locale = defineLocale<Messages>({
name: 'My custom locale',
code: 'en',
dir: 'ltr',
@@ -54,6 +88,35 @@ const locale = defineLocale({
</template>
```
:::
#ui-pro
:::div
```vue [app.vue]
<script setup lang="ts">
import type { Messages } from '@nuxt/ui-pro'
const locale = defineLocale<Messages>({
name: 'My custom locale',
code: 'en',
dir: 'ltr',
messages: {
// implement pairs
}
})
</script>
<template>
<UApp :locale="locale">
<NuxtPage />
</UApp>
</template>
```
:::
::
::tip
Look at the `code` parameter, there you need to pass the iso code of the language. Example:
@@ -116,6 +179,11 @@ export default defineNuxtConfig({
#### Set the `locale` prop using `useI18n`
::module-only
#ui
:::div
```vue [app.vue]
<script setup lang="ts">
import * as locales from '@nuxt/ui/locale'
@@ -130,6 +198,28 @@ const { locale } = useI18n()
</template>
```
:::
#ui-pro
:::div
```vue [app.vue]
<script setup lang="ts">
import * as locales from '@nuxt/ui-pro/locale'
const { locale } = useI18n()
</script>
<template>
<UApp :locale="locales[locale]">
<NuxtPage />
</UApp>
</template>
```
:::
::
::
### Dynamic direction
@@ -138,6 +228,11 @@ Each locale has a `dir` property which will be used by the `App` component to se
In a multilingual application, you might want to set the `lang` and `dir` attributes on the `<html>` element dynamically based on the user's locale, which you can do with the [useHead](https://nuxt.com/docs/api/composables/use-head) composable:
::module-only
#ui
:::div
```vue [app.vue]
<script setup lang="ts">
import * as locales from '@nuxt/ui/locale'
@@ -162,6 +257,38 @@ useHead({
</template>
```
:::
#ui-pro
:::div
```vue [app.vue]
<script setup lang="ts">
import * as locales from '@nuxt/ui-pro/locale'
const { locale } = useI18n()
const lang = computed(() => locales[locale.value].code)
const dir = computed(() => locales[locale.value].dir)
useHead({
htmlAttrs: {
lang,
dir
}
})
</script>
<template>
<UApp :locale="locales[locale]">
<NuxtPage />
</UApp>
</template>
```
:::
::
## Supported languages
:supported-languages

View File

@@ -17,6 +17,11 @@ Nuxt UI provides an **App** component that wraps your app to provide global conf
### Locale
::module-only
#ui
:::div
Use the `locale` prop with the locale you want to use from `@nuxt/ui/locale`:
```vue [App.vue]
@@ -31,15 +36,43 @@ import { fr } from '@nuxt/ui/locale'
</template>
```
:::
#ui-pro
:::div
Use the `locale` prop with the locale you want to use from `@nuxt/ui-pro/locale`:
```vue [App.vue]
<script setup lang="ts">
import { fr } from '@nuxt/ui-pro/locale'
</script>
<template>
<UApp :locale="fr">
<RouterView />
</UApp>
</template>
```
:::
::
### Custom locale
You also have the option to add your locale using `defineLocale`:
::module-only
#ui
:::div
```vue [App.vue]
<script setup lang="ts">
import { defineLocale } from '@nuxt/ui/composables/defineLocale'
import type { Messages } from '@nuxt/ui'
import { defineLocale } from '@nuxt/ui/composables/defineLocale.js'
const locale = defineLocale({
const locale = defineLocale<Messages>({
name: 'My custom locale',
code: 'en',
dir: 'ltr',
@@ -56,6 +89,36 @@ const locale = defineLocale({
</template>
```
:::
#ui-pro
:::div
```vue [App.vue]
<script setup lang="ts">
import type { Messages } from '@nuxt/ui-pro'
import { defineLocale } from '@nuxt/ui/composables/defineLocale.js'
const locale = defineLocale<Messages>({
name: 'My custom locale',
code: 'en',
dir: 'ltr',
messages: {
// implement pairs
}
})
</script>
<template>
<UApp :locale="locale">
<RouterView />
</UApp>
</template>
```
:::
::
::tip
Look at the `code` parameter, there you need to pass the iso code of the language. Example:
@@ -131,6 +194,11 @@ app.mount('#app')
#### Set the `locale` prop using `useI18n`
::module-only
#ui
:::div
```vue [App.vue]
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
@@ -146,6 +214,29 @@ const { locale } = useI18n()
</template>
```
:::
#ui-pro
:::div
```vue [App.vue]
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import * as locales from '@nuxt/ui-pro/locale'
const { locale } = useI18n()
</script>
<template>
<UApp :locale="locales[locale]">
<RouterView />
</UApp>
</template>
```
:::
::
::
### Dynamic direction
@@ -154,6 +245,11 @@ Each locale has a `dir` property which will be used by the `App` component to se
In a multilingual application, you might want to set the `lang` and `dir` attributes on the `<html>` element dynamically based on the user's locale, which you can do with the [useHead](https://unhead.unjs.io/usage/composables/use-head) composable:
::module-only
#ui
:::div
```vue [App.vue]
<script setup lang="ts">
import { computed } from 'vue'
@@ -181,6 +277,41 @@ useHead({
</template>
```
:::
#ui-pro
:::div
```vue [App.vue]
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useHead } from '@unhead/vue'
import * as locales from '@nuxt/ui-pro/locale'
const { locale } = useI18n()
const lang = computed(() => locales[locale.value].code)
const dir = computed(() => locales[locale.value].dir)
useHead({
htmlAttrs: {
lang,
dir
}
})
</script>
<template>
<UApp :locale="locales[locale]">
<RouterView />
</UApp>
</template>
```
:::
::
## Supported languages
:supported-languages

View File

@@ -1,13 +1,13 @@
---
title: Contribution Guide
description: 'A comprehensive guide on contributing to Nuxt UI v3, including project structure, development workflow, and best practices.'
description: 'A comprehensive guide on contributing to Nuxt UI, including project structure, development workflow, and best practices.'
navigation: false
---
Nuxt UI thrives thanks to its incredible community ❤️. We welcome all contributions through bug reports, pull requests, and feedback to help make this library even better.
::caution
Before reporting a bug or requesting a feature, make sure that you have read through our [documentation](https://ui3.nuxt.dev/) and existing [issues](https://github.com/nuxt/ui/issues?q=is%3Aissue%20is%3Aopen%20sort%3Aupdated-desc%20label%3Av3).
Before reporting a bug or requesting a feature, make sure that you have read through our [documentation](https://ui.nuxt.com/) and existing [issues](https://github.com/nuxt/ui/issues?q=is%3Aissue%20is%3Aopen%20sort%3Aupdated-desc%20label%3Av3).
::
## Project Structure

View File

@@ -29,23 +29,6 @@ props:
---
::
### Link
You can pass any property from the [Link](/components/link#props) component such as `to`, `target`, etc.
::component-code
---
ignore:
- label
- target
props:
to: https://github.com/nuxt/ui
target: _blank
slots:
default: Button
---
::
### Color
Use the `color` prop to change the color of the Button.
@@ -160,6 +143,96 @@ props:
---
::
### Link
You can pass any property from the [Link](/components/link#props) component such as `to`, `target`, etc.
::component-code
---
ignore:
- target
props:
to: https://github.com/nuxt/ui
target: _blank
slots:
default: Button
---
::
When the Button is a link or when using the `active` prop, you can use the `active-color` and `active-variant` props to customize the active state.
::component-code
---
prettier: true
ignore:
- color
- variant
items:
activeColor:
- primary
- secondary
- success
- info
- warning
- error
- neutral
activeVariant:
- solid
- outline
- soft
- subtle
- ghost
- link
props:
active: true
color: neutral
variant: outline
activeColor: primary
activeVariant: solid
slots:
default: |
Button
---
Button
::
You can also use the `active-class` and `inactive-class` props to customize the active state.
::component-code
---
props:
active: true
activeClass: 'font-bold'
inactiveClass: 'font-light'
slots:
default: Button
---
Button
::
::tip
You can configure these styles globally in your `app.config.ts` file under the `ui.button.variants.active` key.
```ts
export default defineAppConfig({
ui: {
button: {
variants: {
active: {
true: {
base: 'font-bold'
}
}
}
}
}
})
```
::
### Loading
Use the `loading` prop to show a loading icon and disable the Button.

View File

@@ -9,7 +9,7 @@ links:
## Usage
Use the Form component to validate form data using schema libraries such as [Valibot](https://github.com/fabian-hiller/valibot), [Zod](https://github.com/colinhacks/zod), [Yup](https://github.com/jquense/yup), [Joi](https://github.com/hapijs/joi), [Superstruct](https://github.com/ianstormtaylor/superstruct) or your own validation logic.
Use the Form component to validate form data using validation libraries such as [Valibot](https://github.com/fabian-hiller/valibot), [Zod](https://github.com/colinhacks/zod), [Yup](https://github.com/jquense/yup), [Joi](https://github.com/hapijs/joi), [Superstruct](https://github.com/ianstormtaylor/superstruct) or your own validation logic.
It works with the [FormField](/components/form-field) component to display error messages around form elements automatically.
@@ -18,7 +18,7 @@ It works with the [FormField](/components/form-field) component to display error
It requires two props:
- `state` - a reactive object holding the form's state.
- `schema` - a schema object from a validation library like [Valibot](https://github.com/fabian-hiller/valibot), [Zod](https://github.com/colinhacks/zod), [Yup](https://github.com/jquense/yup), [Joi](https://github.com/hapijs/joi) or [Superstruct](https://github.com/ianstormtaylor/superstruct).
- `schema` - any [Standard Schema](https://standardschema.dev/) or a schema from [Yup](https://github.com/jquense/yup), [Joi](https://github.com/hapijs/joi) or [Superstruct](https://github.com/ianstormtaylor/superstruct).
::warning
**No validation library is included** by default, ensure you **install the one you need**.

View File

@@ -8,7 +8,6 @@ links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxt/ui/tree/v3/src/runtime/components/Tree.vue
navigation.badge: New
---
## Usage

View File

@@ -34,7 +34,7 @@ export default defineNuxtConfig({
},
$production: {
site: {
url: 'https://ui3.nuxt.dev'
url: 'https://ui.nuxt.com'
}
},
@@ -55,6 +55,7 @@ export default defineNuxtConfig({
}]
},
rootAttrs: {
// @ts-expect-error - vaul-drawer-wrapper is not typed
'vaul-drawer-wrapper': '',
'class': 'bg-(--ui-bg)'
}
@@ -85,8 +86,8 @@ export default defineNuxtConfig({
},
routeRules: {
'/': { redirect: '/getting-started', prerender: false },
'/getting-started/installation': { redirect: '/getting-started/installation/nuxt', prerender: false },
'/getting-started/installation/pro': { redirect: '/getting-started/installation/pro/nuxt', prerender: false },
'/getting-started/icons': { redirect: '/getting-started/icons/nuxt', prerender: false },
'/getting-started/color-mode': { redirect: '/getting-started/color-mode/nuxt', prerender: false },
'/getting-started/i18n': { redirect: '/getting-started/i18n/nuxt', prerender: false },
@@ -104,9 +105,10 @@ export default defineNuxtConfig({
routes: [
'/getting-started',
'/api/countries.json',
'/api/locales.json'
'/api/locales.json',
// '/api/releases.json',
// '/api/pulls.json'
'/404.html'
],
crawlLinks: true,
autoSubfolderIndex: false
@@ -127,7 +129,12 @@ export default defineNuxtConfig({
vite: {
plugins: [
yaml()
]
],
server: {
fs: {
allow: process.env.NUXT_UI_PRO_PATH ? [resolve(process.env.NUXT_UI_PRO_PATH)] : undefined
}
}
},
componentMeta: {
@@ -169,12 +176,12 @@ export default defineNuxtConfig({
},
llms: {
domain: 'https://ui3.nuxt.dev',
title: 'Nuxt UI v3',
domain: 'https://ui.nuxt.com',
title: 'Nuxt UI',
description: 'A comprehensive, Nuxt-integrated UI library providing a rich set of fully-styled, accessible and highly customizable components for building modern web applications.',
full: {
title: 'Nuxt UI v3 Full Documentation',
description: 'This is the full documentation for Nuxt UI v3. It includes all the Markdown files written with the MDC syntax.'
title: 'Nuxt UI Full Documentation',
description: 'This is the full documentation for Nuxt UI. It includes all the Markdown files written with the MDC syntax.'
},
sections: [
{

View File

@@ -4,34 +4,34 @@
"type": "module",
"dependencies": {
"@iconify-json/logos": "^1.2.4",
"@iconify-json/lucide": "^1.2.28",
"@iconify-json/simple-icons": "^1.2.27",
"@iconify-json/lucide": "^1.2.30",
"@iconify-json/simple-icons": "^1.2.28",
"@iconify-json/vscode-icons": "^1.2.16",
"@nuxt/content": "https://pkg.pr.new/@nuxt/content@819ab7f",
"@nuxt/content": "https://pkg.pr.new/@nuxt/content@be99f3a",
"@nuxt/image": "^1.9.0",
"@nuxt/ui": "latest",
"@nuxt/ui-pro": "https://pkg.pr.new/@nuxt/ui-pro@7cf4b69",
"@nuxthub/core": "^0.8.17",
"@nuxt/ui-pro": "https://pkg.pr.new/@nuxt/ui-pro@d96a086",
"@nuxthub/core": "^0.8.18",
"@nuxtjs/plausible": "^1.2.0",
"@octokit/rest": "^21.1.1",
"@rollup/plugin-yaml": "^4.1.2",
"@vueuse/nuxt": "^12.7.0",
"@vueuse/nuxt": "^13.0.0",
"joi": "^17.13.3",
"motion": "^12.4.7",
"motion-v": "0.11.0-beta.6",
"nuxt": "^3.15.4",
"motion": "^12.5.0",
"motion-v": "0.13.0",
"nuxt": "^3.16.0",
"nuxt-component-meta": "^0.10.0",
"nuxt-llms": "^0.1.0",
"nuxt-og-image": "^4.1.5",
"nuxt-og-image": "^5.0.4",
"prettier": "^3.5.3",
"shiki-transformer-color-highlight": "^0.2.0",
"shiki-transformer-color-highlight": "^1.0.0",
"superstruct": "^2.0.2",
"ufo": "^1.5.4",
"valibot": "^0.42.1",
"valibot": "^1.0.0",
"yup": "^1.6.1",
"zod": "^3.24.2"
},
"devDependencies": {
"wrangler": "^3.111.0"
"wrangler": "^3.114.0"
}
}

BIN
docs/public/pro/ad.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View File

@@ -1,13 +1,13 @@
{
"name": "@nuxt/ui",
"description": "A UI Library for Modern Web Apps, powered by Vue & Tailwind CSS.",
"version": "3.0.0-beta.2",
"packageManager": "pnpm@10.5.2",
"version": "3.0.0",
"packageManager": "pnpm@10.6.4",
"repository": {
"type": "git",
"url": "git+https://github.com/nuxt/ui.git"
},
"homepage": "https://ui3.nuxt.dev",
"homepage": "https://ui.nuxt.com",
"type": "module",
"license": "MIT",
"exports": {
@@ -67,7 +67,7 @@
"dev:build": "nuxi build playground",
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground && nuxi prepare docs && vite build playground-vue",
"docs": "DEV=true nuxi dev docs",
"docs:build": "nuxi build docs",
"docs:build": "NODE_OPTIONS='--max-old-space-size=8192' nuxi build docs",
"docs:prepare": "nuxt-component-meta docs",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
@@ -75,23 +75,23 @@
"test": "vitest",
"test:vue": "vitest -c vitest.vue.config.ts",
"test:vue:build": "vite build playground-vue",
"release": "release-it --preRelease=beta --npm.tag=next"
"release": "release-it"
},
"dependencies": {
"@iconify/vue": "^4.3.0",
"@internationalized/date": "^3.7.0",
"@internationalized/number": "^3.6.0",
"@nuxt/fonts": "^0.10.3",
"@nuxt/icon": "^1.10.3",
"@nuxt/kit": "^3.15.4",
"@nuxt/schema": "^3.15.4",
"@nuxt/fonts": "^0.11.0",
"@nuxt/icon": "^1.11.0",
"@nuxt/kit": "^3.16.0",
"@nuxt/schema": "^3.16.0",
"@nuxtjs/color-mode": "^3.5.2",
"@tailwindcss/postcss": "^4.0.9",
"@tailwindcss/vite": "^4.0.9",
"@tailwindcss/postcss": "^4.0.14",
"@tailwindcss/vite": "^4.0.14",
"@tanstack/vue-table": "^8.21.2",
"@unhead/vue": "^1.11.20",
"@vueuse/core": "^12.7.0",
"@vueuse/integrations": "^12.7.0",
"@unhead/vue": "^2.0.0-rc.13",
"@vueuse/core": "^13.0.0",
"@vueuse/integrations": "^13.0.0",
"colortranslator": "^4.1.0",
"consola": "^3.4.0",
"defu": "^6.1.4",
@@ -106,34 +106,36 @@
"knitwork": "^1.2.0",
"magic-string": "^0.30.17",
"mlly": "^1.7.4",
"ohash": "^1.1.5",
"ohash": "^2.0.11",
"pathe": "^2.0.3",
"reka-ui": "^2.0.2",
"reka-ui": "^2.1.0",
"scule": "^1.3.0",
"tailwind-variants": "^0.3.1",
"tailwindcss": "^4.0.9",
"tailwind-variants": "^1.0.0",
"tailwindcss": "^4.0.14",
"tinyglobby": "^0.2.12",
"unplugin": "^2.2.0",
"unplugin-auto-import": "^19.1.1",
"unplugin-vue-components": "^28.4.1",
"vaul-vue": "^0.3.0"
"vaul-vue": "^0.3.0",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@nuxt/eslint-config": "^1.1.0",
"@nuxt/eslint-config": "^1.2.0",
"@nuxt/module-builder": "^0.8.4",
"@nuxt/test-utils": "^3.17.1",
"@nuxt/test-utils": "^3.17.2",
"@release-it/conventional-changelog": "^10.0.0",
"@standard-schema/spec": "^1.0.0",
"@vue/test-utils": "^2.4.6",
"embla-carousel": "^8.5.2",
"eslint": "^9.21.0",
"happy-dom": "^17.1.2",
"eslint": "^9.22.0",
"happy-dom": "^17.4.4",
"joi": "^17.13.3",
"nuxt": "^3.15.4",
"nuxt": "^3.16.0",
"release-it": "^18.1.2",
"superstruct": "^2.0.2",
"valibot": "^0.42.1",
"vitest": "^3.0.7",
"valibot": "^1.0.0",
"vitest": "^3.0.9",
"vitest-environment-nuxt": "^1.0.1",
"vue-tsc": "^2.2.0",
"yup": "^1.6.1",
@@ -146,12 +148,9 @@
"@nuxt/ui": "workspace:*",
"chokidar": "3.6.0",
"debug": "4.3.7",
"happy-dom": "17.1.2",
"rollup": "4.32.1",
"rollup": "4.34.9",
"typescript": "5.6.3",
"unimport": "3.14.5",
"unplugin": "^2.2.0",
"vue": "3.5.13",
"vue-tsc": "2.2.0"
},
"pnpm": {

View File

@@ -2,8 +2,9 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Nuxt UI ❤️ Vue</title>
<title>Nuxt UI - Vue Playground</title>
</head>
<body>
<div id="app" class="isolate"></div>

View File

@@ -15,9 +15,9 @@
"vue-router": "^4.5.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
"@vitejs/plugin-vue": "^5.2.3",
"typescript": "^5.6.3",
"vite": "^6.2.0",
"vite": "^6.2.2",
"vue-tsc": "^2.2.0"
}
}

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<style>
.st0 { fill: #42B883; }
.st1 { fill: #35495E; }
</style>
<path class="st0" d="M78.8,10L64,35.4L49.2,10H0l64,110l64-110C128,10,78.8,10,78.8,10z" />
<path class="st1" d="M78.8,10L64,35.4L49.2,10H25.6L64,76l38.4-66H78.8z" />
</svg>

After

Width:  |  Height:  |  Size: 316 B

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { h, resolveComponent } from 'vue'
import { upperFirst } from 'scule'
import type { TableColumn } from '@nuxt/ui'
import type { TableColumn, TableRow } from '@nuxt/ui'
import { getPaginationRowModel } from '@tanstack/vue-table'
const UButton = resolveComponent('UButton')
@@ -279,6 +279,10 @@ function randomize() {
data.value = [...data.value].sort(() => Math.random() - 0.5)
}
function onSelect(row: TableRow<Payment>) {
console.log(row)
}
onMounted(() => {
setTimeout(() => {
loading.value = false
@@ -337,6 +341,7 @@ onMounted(() => {
}"
sticky
class="border border-(--ui-border-accented) rounded-(--ui-radius)"
@select="onSelect"
>
<template #expanded="{ row }">
<pre>{{ row.original }}</pre>

View File

@@ -8,10 +8,10 @@
"generate": "nuxi generate"
},
"dependencies": {
"@iconify-json/lucide": "^1.2.28",
"@iconify-json/simple-icons": "^1.2.27",
"@iconify-json/lucide": "^1.2.30",
"@iconify-json/simple-icons": "^1.2.28",
"@nuxt/ui": "latest",
"@nuxthub/core": "^0.8.17",
"nuxt": "^3.15.4"
"@nuxthub/core": "^0.8.18",
"nuxt": "^3.16.0"
}
}

4121
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,13 +6,11 @@
"enabled": true
},
"ignoreDeps": [
"happy-dom",
"valibot30",
"valibot31",
"typescript",
"vaul-vue",
"vue-tsc"
],
"baseBranches": ["dev", "v3"],
"baseBranches": ["v2", "v3"],
"packageRules": [{
"matchBaseBranches": ["v3"],
"labels": ["v3"]

View File

@@ -9,40 +9,40 @@ export interface ModuleOptions {
/**
* Prefix for components
* @defaultValue `U`
* @link https://ui3.nuxt.dev/getting-started/installation/nuxt#prefix
* @link https://ui.nuxt.com/getting-started/installation/nuxt#prefix
*/
prefix?: string
/**
* Enable or disable `@nuxt/fonts` module
* @defaultValue `true`
* @link https://ui3.nuxt.dev/getting-started/installation/nuxt#fonts
* @link https://ui.nuxt.com/getting-started/installation/nuxt#fonts
*/
fonts?: boolean
/**
* Enable or disable `@nuxtjs/color-mode` module
* @defaultValue `true`
* @link https://ui3.nuxt.dev/getting-started/installation/nuxt#colormode
* @link https://ui.nuxt.com/getting-started/installation/nuxt#colormode
*/
colorMode?: boolean
/**
* Customize how the theme is generated
* @link https://ui3.nuxt.dev/getting-started/theme
* @link https://ui.nuxt.com/getting-started/theme
*/
theme?: {
/**
* Define the color aliases available for components
* @defaultValue `['primary', 'secondary', 'success', 'info', 'warning', 'error']`
* @link https://ui3.nuxt.dev/getting-started/installation/nuxt#themecolors
* @link https://ui.nuxt.com/getting-started/installation/nuxt#themecolors
*/
colors?: string[]
/**
* Enable or disable transitions on components
* @defaultValue `true`
* @link https://ui3.nuxt.dev/getting-started/installation/nuxt#themetransitions
* @link https://ui.nuxt.com/getting-started/installation/nuxt#themetransitions
*/
transitions?: boolean
}
@@ -53,9 +53,9 @@ export default defineNuxtModule<ModuleOptions>({
name: 'ui',
configKey: 'ui',
compatibility: {
nuxt: '>=3.13.1'
nuxt: '>=3.16.0'
},
docs: 'https://ui3.nuxt.dev/getting-started/installation/nuxt'
docs: 'https://ui.nuxt.com/getting-started/installation/nuxt'
},
defaults: defaultOptions,
async setup(options, nuxt) {

View File

@@ -20,7 +20,11 @@ export default function ComponentImportPlugin(options: NuxtUIOptions & { prefix:
const pluginOptions = defu(options.components, <ComponentsOptions>{
dts: options.dts ?? true,
exclude: [/[\\/]node_modules[\\/](?!\.pnpm|@nuxt\/ui)/, /[\\/]\.git[\\/]/, /[\\/]\.nuxt[\\/]/],
exclude: [
/[\\/]node_modules[\\/](?!\.pnpm|@nuxt\/ui|@compodium\/examples)/,
/[\\/]\.git[\\/]/,
/[\\/]\.nuxt[\\/]/
],
resolvers: [
(componentName) => {
if (overrideNames.has(componentName))

View File

@@ -1,3 +1,4 @@
<!-- eslint-disable vue/block-tag-newline -->
<script lang="ts">
import type { AccordionRootProps, AccordionRootEmits } from 'reka-ui'
import type { AppConfig } from '@nuxt/schema'
@@ -60,6 +61,7 @@ export type AccordionSlots<T extends { slot?: string }> = {
content: SlotProps<T>
body: SlotProps<T>
} & DynamicSlots<T, SlotProps<T>>
</script>
<script setup lang="ts" generic="T extends AccordionItem">

View File

@@ -1,12 +1,11 @@
<script lang="ts">
import type { ConfigProviderProps, TooltipProviderProps } from 'reka-ui'
import { localeContextInjectionKey } from '../composables/useLocale'
import type { ToasterProps, Locale } from '../types'
import type { ToasterProps, Locale, Messages } from '../types'
export interface AppProps extends Omit<ConfigProviderProps, 'useId' | 'dir' | 'locale'> {
export interface AppProps<T extends Messages = Messages> extends Omit<ConfigProviderProps, 'useId' | 'dir' | 'locale'> {
tooltip?: TooltipProviderProps
toaster?: ToasterProps | null
locale?: Locale
locale?: Locale<T>
}
export interface AppSlots {
@@ -18,14 +17,15 @@ export default {
}
</script>
<script setup lang="ts">
<script setup lang="ts" generic="T extends Messages = Messages">
import { toRef, useId, provide } from 'vue'
import { ConfigProvider, TooltipProvider, useForwardProps } from 'reka-ui'
import { reactivePick } from '@vueuse/core'
import { localeContextInjectionKey } from '../composables/useLocale'
import UToaster from './Toaster.vue'
import UOverlayProvider from './OverlayProvider.vue'
const props = defineProps<AppProps>()
const props = defineProps<AppProps<T>>()
defineSlots<AppSlots>()
const configProviderProps = useForwardProps(reactivePick(props, 'scrollBody'))

View File

@@ -29,6 +29,7 @@ export interface AvatarProps {
*/
size?: AvatarVariants['size']
class?: any
style?: any
ui?: Partial<typeof avatar.slots>
}
@@ -83,7 +84,7 @@ function onError() {
</script>
<template>
<Primitive :as="as" :class="ui.root({ class: [props.class, props.ui?.root] })">
<Primitive :as="as" :class="ui.root({ class: [props.class, props.ui?.root] })" :style="props.style">
<component
:is="ImageComponent"
v-if="src && !error"

View File

@@ -1,3 +1,4 @@
<!-- eslint-disable vue/block-tag-newline -->
<script lang="ts">
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
@@ -51,6 +52,7 @@ export type BreadcrumbSlots<T extends { slot?: string }> = {
'item-trailing': SlotProps<T>
'separator'(props?: {}): any
} & DynamicSlots<T, SlotProps<T>>
</script>
<script setup lang="ts" generic="T extends BreadcrumbItem">

View File

@@ -21,10 +21,12 @@ export interface ButtonProps extends UseComponentIconsProps, Omit<LinkProps, 'ra
* @defaultValue 'primary'
*/
color?: ButtonVariants['color']
activeColor?: ButtonVariants['color']
/**
* @defaultValue 'solid'
*/
variant?: ButtonVariants['variant']
activeVariant?: ButtonVariants['variant']
/**
* @defaultValue 'md'
*/
@@ -58,8 +60,13 @@ import { pickLinkProps } from '../utils/link'
import UIcon from './Icon.vue'
import UAvatar from './Avatar.vue'
import ULink from './Link.vue'
import ULinkBase from './LinkBase.vue'
const props = defineProps<ButtonProps>()
const props = withDefaults(defineProps<ButtonProps>(), {
active: undefined,
activeClass: '',
inactiveClass: ''
})
const slots = defineSlots<ButtonSlots>()
const linkProps = useForwardProps(pickLinkProps(props))
@@ -87,7 +94,19 @@ const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponen
computed(() => ({ ...props, loading: isLoading.value }))
)
const ui = computed(() => button({
const ui = computed(() => tv({
extend: button,
variants: {
active: {
true: {
base: props.activeClass
},
false: {
base: props.inactiveClass
}
}
}
})({
color: props.color,
variant: props.variant,
size: buttonSize.value,
@@ -102,26 +121,37 @@ const ui = computed(() => button({
<template>
<ULink
v-slot="{ active, ...slotProps }"
:type="type"
:disabled="disabled || isLoading"
:class="ui.base({ class: [props.class, props.ui?.base] })"
v-bind="omit(linkProps, ['type', 'disabled'])"
raw
@click="onClickWrapper"
custom
>
<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>
<ULinkBase
v-bind="slotProps"
:class="ui.base({
class: [props.class, props.ui?.base],
active,
...(active && activeVariant ? { variant: activeVariant } : {}),
...(active && activeColor ? { color: activeColor } : {})
})"
@click="onClickWrapper"
>
<slot name="leading">
<UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="ui.leadingIcon({ class: props.ui?.leadingIcon, active })" />
<UAvatar v-else-if="!!avatar" :size="((props.ui?.leadingAvatarSize || ui.leadingAvatarSize()) as AvatarProps['size'])" v-bind="avatar" :class="ui.leadingAvatar({ class: props.ui?.leadingAvatar, active })" />
</slot>
<slot>
<span v-if="label" :class="ui.label({ class: props.ui?.label })">
{{ label }}
</span>
</slot>
<slot>
<span v-if="label" :class="ui.label({ class: props.ui?.label, active })">
{{ label }}
</span>
</slot>
<slot name="trailing">
<UIcon v-if="isTrailing && trailingIconName" :name="trailingIconName" :class="ui.trailingIcon({ class: props.ui?.trailingIcon })" />
</slot>
<slot name="trailing">
<UIcon v-if="isTrailing && trailingIconName" :name="trailingIconName" :class="ui.trailingIcon({ class: props.ui?.trailingIcon, active })" />
</slot>
</ULinkBase>
</ULink>
</template>

View File

@@ -1,3 +1,4 @@
<!-- eslint-disable vue/block-tag-newline -->
<script lang="ts">
import type { VariantProps } from 'tailwind-variants'
import type { MaybeRefOrGetter } from '@vueuse/shared'
@@ -70,6 +71,7 @@ export type ColorPickerProps = {
class?: any
ui?: Partial<typeof colorPicker.slots>
}
</script>
<script setup lang="ts">

View File

@@ -1,3 +1,4 @@
<!-- eslint-disable vue/block-tag-newline -->
<script lang="ts">
import type { ListboxRootProps, ListboxRootEmits } from 'reka-ui'
import type { FuseResult } from 'fuse.js'
@@ -130,6 +131,7 @@ export type CommandPaletteSlots<G extends { slot?: string }, T extends { slot?:
'item-label': SlotProps<T>
'item-trailing': SlotProps<T>
} & DynamicSlots<G, SlotProps<T>> & DynamicSlots<T, SlotProps<T>>
</script>
<script setup lang="ts" generic="G extends CommandPaletteGroup<T>, T extends CommandPaletteItem">

View File

@@ -1,3 +1,4 @@
<!-- eslint-disable vue/block-tag-newline -->
<script lang="ts">
import type { VariantProps } from 'tailwind-variants'
import type { ContextMenuRootProps, ContextMenuRootEmits, ContextMenuContentProps } from 'reka-ui'
@@ -93,6 +94,7 @@ export type ContextMenuSlots<T extends { slot?: string }> = {
'item-label': SlotProps<T>
'item-trailing': SlotProps<T>
} & DynamicSlots<T, SlotProps<T>>
</script>
<script setup lang="ts" generic="T extends ContextMenuItem">

View File

@@ -1,3 +1,4 @@
<!-- eslint-disable vue/block-tag-newline -->
<script lang="ts">
import type { VariantProps } from 'tailwind-variants'
import type { DropdownMenuRootProps, DropdownMenuRootEmits, DropdownMenuContentProps, DropdownMenuArrowProps } from 'reka-ui'
@@ -101,6 +102,7 @@ export type DropdownMenuSlots<T extends { slot?: string }> = {
'item-label': SlotProps<T>
'item-trailing': SlotProps<T>
} & DynamicSlots<T, SlotProps<T>>
</script>
<script setup lang="ts" generic="T extends DropdownMenuItem">

View File

@@ -12,14 +12,34 @@ const form = tv({ extend: tv(theme), ...(appConfigForm.ui?.form || {}) })
export interface FormProps<T extends object> {
id?: string | number
/** Schema to validate the form state. Supports Standard Schema objects, Yup, Joi, and Superstructs. */
schema?: FormSchema<T>
/** An object representing the current state of the form. */
state: Partial<T>
/**
* Custom validation function to validate the form state.
* @param state - The current state of the form.
* @returns A promise that resolves to an array of FormError objects, or an array of FormError objects directly.
*/
validate?: (state: Partial<T>) => Promise<FormError[]> | FormError[]
/**
* The list of input events that trigger the form validation.
* @defaultValue `['blur', 'change', 'input']`
*/
validateOn?: FormInputEvents[]
/** Disable all inputs inside the form. */
disabled?: boolean
/**
* Delay in milliseconds before validating the form on input events.
* @defaultValue `300`
*/
validateOnInputDelay?: number
class?: any
/**
* If true, schema transformations will be applied to the state on submit.
* @defaultValue `true`
*/
transform?: boolean
class?: any
onSubmit?: ((event: FormSubmitEvent<T>) => void | Promise<void>) | (() => void | Promise<void>)
}
@@ -29,7 +49,7 @@ export interface FormEmits<T extends object> {
}
export interface FormSlots {
default(props?: {}): any
default(props?: { errors: FormError[] }): any
}
</script>
@@ -69,7 +89,7 @@ onMounted(async () => {
nestedForms.value.set(event.formId, { validate: event.validate })
} else if (event.type === 'detach') {
nestedForms.value.delete(event.formId)
} else if (props.validateOn?.includes(event.type)) {
} else if (props.validateOn?.includes(event.type) && !loading.value) {
if (event.type !== 'input') {
await _validate({ name: event.name, silent: true, nested: false })
} else if (event.eager || blurredFields.has(event.name)) {
@@ -121,7 +141,7 @@ const blurredFields = new Set<keyof T>()
function resolveErrorIds(errs: FormError[]): FormErrorWithId[] {
return errs.map(err => ({
...err,
id: inputs.value[err.name]?.id
id: err?.name ? inputs.value[err.name]?.id : undefined
}))
}
@@ -159,12 +179,12 @@ async function _validate(opts: { name?: keyof T | (keyof T)[], silent?: boolean,
if (names) {
const otherErrors = errors.value.filter(error => !names.some((name) => {
const pattern = inputs.value?.[name]?.pattern
return name === error.name || (pattern && error.name.match(pattern))
return name === error.name || (pattern && error.name?.match(pattern))
}))
const pathErrors = (await getErrors()).filter(error => names.some((name) => {
const pattern = inputs.value?.[name]?.pattern
return name === error.name || (pattern && error.name.match(pattern))
return name === error.name || (pattern && error.name?.match(pattern))
}))
errors.value = otherErrors.concat(pathErrors)
@@ -269,6 +289,6 @@ defineExpose<Form<T>>({
:class="form({ class: props.class })"
@submit.prevent="onSubmitWrapper"
>
<slot />
<slot :errors="errors" />
</component>
</template>

View File

@@ -31,7 +31,12 @@ export interface FormFieldProps {
*/
size?: FormFieldVariants['size']
required?: boolean
/** If true, validation on input will be active immediately instead of waiting for a blur event. */
eagerValidation?: boolean
/**
* Delay in milliseconds before validating the form on input events.
* @defaultValue `300`
*/
validateOnInputDelay?: number
class?: any
ui?: Partial<typeof formField.slots>
@@ -63,7 +68,7 @@ const ui = computed(() => formField({
const formErrors = inject<Ref<FormError[]> | null>('form-errors', null)
const error = computed(() => props.error || formErrors?.value?.find(error => error.name === props.name || (props.errorPattern && error.name.match(props.errorPattern)))?.message)
const error = computed(() => props.error || formErrors?.value?.find(error => error.name && (error.name === props.name || (props.errorPattern && error.name.match(props.errorPattern))))?.message)
const id = ref(useId())
// Copies id's initial value to bind aria-attributes such as aria-describedby.

View File

@@ -82,7 +82,7 @@ const props = withDefaults(defineProps<InputProps>(), {
const emits = defineEmits<InputEmits>()
const slots = defineSlots<InputSlots>()
const [modelValue, modelModifiers] = defineModel<string | number>()
const [modelValue, modelModifiers] = defineModel<string | number | null>()
const { emitFormBlur, emitFormInput, emitFormChange, size: formGroupSize, color, id, name, highlight, disabled, emitFormFocus, ariaAttrs } = useFormField<InputProps>(props, { deferInputValidation: true })
const { orientation, size: buttonGroupSize } = useButtonGroup<InputProps>(props)
@@ -111,15 +111,19 @@ function autoFocus() {
}
// Custom function to handle the v-model properties
function updateInput(value: string) {
function updateInput(value: string | null) {
if (modelModifiers.trim) {
value = value.trim()
value = value?.trim() ?? null
}
if (modelModifiers.number || props.type === 'number') {
value = looseToNumber(value)
}
if (modelModifiers.nullify) {
value ||= null
}
modelValue.value = value
emitFormInput()
}

View File

@@ -157,7 +157,7 @@ export interface InputMenuSlots<T, M extends boolean> {
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'
import { isEqual } from 'ohash/utils'
import { reactivePick, createReusableTemplate } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { useButtonGroup } from '../composables/useButtonGroup'

View File

@@ -91,7 +91,7 @@ export interface LinkSlots {
<script setup lang="ts">
import { computed } from 'vue'
import { isEqual, diff } from 'ohash'
import { isEqual, diff } from 'ohash/utils'
import { useForwardProps } from 'reka-ui'
import { reactiveOmit } from '@vueuse/core'
import { useRoute } from '#imports'
@@ -124,11 +124,15 @@ const ui = computed(() => tv({
function isPartiallyEqual(item1: any, item2: any) {
const diffedKeys = diff(item1, item2).reduce((filtered, q) => {
if (q.type === 'added') {
filtered.push(q.key)
filtered.add(q.key)
}
return filtered
}, [] as string[])
return isEqual(item1, item2, { excludeKeys: key => diffedKeys.includes(key) })
}, new Set<string>())
const item1Filtered = Object.fromEntries(Object.entries(item1).filter(([key]) => !diffedKeys.has(key)))
const item2Filtered = Object.fromEntries(Object.entries(item2).filter(([key]) => !diffedKeys.has(key)))
return isEqual(item1Filtered, item2Filtered)
}
function isLinkActive({ route: linkRoute, isActive, isExactActive }: any) {

View File

@@ -1,4 +1,6 @@
<script lang="ts">
import type { LinkProps } from '../types'
export interface LinkBaseProps {
as?: string
type?: string
@@ -6,8 +8,8 @@ export interface LinkBaseProps {
onClick?: ((e: MouseEvent) => void | Promise<void>) | Array<((e: MouseEvent) => void | Promise<void>)>
href?: string
navigate?: (e: MouseEvent) => void
rel?: string
target?: string
target?: LinkProps['target']
rel?: LinkProps['rel']
isExternal?: boolean
}
</script>

View File

@@ -1,3 +1,4 @@
<!-- eslint-disable vue/block-tag-newline -->
<script lang="ts">
import type { VariantProps } from 'tailwind-variants'
import type { NavigationMenuRootProps, NavigationMenuRootEmits, NavigationMenuContentProps, CollapsibleRootProps } from 'reka-ui'
@@ -125,6 +126,7 @@ export type NavigationMenuSlots<T extends { slot?: string }> = {
'item-trailing': SlotProps<T>
'item-content': SlotProps<T>
} & DynamicSlots<T, SlotProps<T>>
</script>
<script setup lang="ts" generic="T extends MaybeArrayOfArrayItem<I>, I extends MaybeArrayOfArray<NavigationMenuItem>">

View File

@@ -1,7 +1,6 @@
<script lang="ts">
import type { PaginationRootProps, PaginationRootEmits } from 'reka-ui'
import type { AppConfig } from '@nuxt/schema'
import type { RouteLocationRaw } from '#vue-router'
import _appConfig from '#build/app.config'
import theme from '#build/ui/pagination'
import { tv } from '../utils/tv'
@@ -78,7 +77,7 @@ export interface PaginationProps extends Partial<Pick<PaginationRootProps, 'defa
* A function to render page controls as links.
* @param page The page number to navigate to.
*/
to?: (page: number) => RouteLocationRaw
to?: (page: number) => ButtonProps['to']
class?: any
ui?: Partial<typeof pagination.slots>
}

View File

@@ -1,3 +1,4 @@
<!-- eslint-disable vue/block-tag-newline -->
<script lang="ts">
import type { VariantProps } from 'tailwind-variants'
import type { PinInputRootEmits, PinInputRootProps } from 'reka-ui'
@@ -45,6 +46,7 @@ export type PinInputEmits = PinInputRootEmits & {
change: [payload: Event]
blur: [payload: Event]
}
</script>
<script setup lang="ts">

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