Compare commits

..

10 Commits

Author SHA1 Message Date
HugoRCD
d6fe4dc716 up 2025-07-08 10:29:05 +02:00
HugoRCD
203a36ae11 Merge remote-tracking branch 'origin/v3' into feat/3880 2025-07-08 10:24:23 +02:00
HugoRCD
c3ec805a9a up 2025-07-03 21:06:44 +02:00
HugoRCD
c025fbf81a up 2025-07-03 20:58:33 +02:00
HugoRCD
c82828d889 up 2025-07-03 17:07:16 +02:00
HugoRCD
7be03e0f51 Merge branch 'v3' into feat/3880 2025-07-03 16:41:03 +02:00
HugoRCD
48077c12e8 Merge branch 'v3' into feat/3880 2025-07-02 10:14:50 +02:00
HugoRCD
801375d4fb Merge remote-tracking branch 'origin/v3' into feat/3880 2025-07-01 10:34:31 +02:00
HugoRCD
b3e885211a up 2025-06-25 16:32:01 +02:00
HugoRCD
deae9eca84 feat(CommandPalette): add input-trailing-icon prop and slot 2025-06-25 16:26:41 +02:00
173 changed files with 2549 additions and 4320 deletions

View File

@@ -33,5 +33,5 @@ jobs:
Thank you for your understanding and support!
— Nuxt UI Team
exempt-issue-labels: 'feature,announcement,release,reka-ui,upstream'
operations-per-run: 300
exempt-issue-labels: 'feature,announcement'
operations-per-run: 300

View File

@@ -1 +0,0 @@
experimental.normalizeComponentNames=false

View File

@@ -35,7 +35,7 @@ ${pro ? `import type { ComponentConfig } from '@nuxt/ui'` : ''}
import theme from '#build/${path}/${prose ? 'prose/' : ''}${content ? 'content/' : ''}${kebabName}'
${!pro ? `import type { ComponentConfig } from '../types/utils'` : ''}
type ${upperName} = ComponentConfig<typeof theme, AppConfig, '${camelName}'${pro ? `, '${key}'` : ''}>
type ${upperName} = ComponentConfig<typeof theme, AppConfig, ${upperName}${pro ? `, '${key}'` : ''}>
export interface ${upperName}Props {
/**
@@ -80,7 +80,7 @@ ${pro ? `import type { ComponentConfig } from '@nuxt/ui'` : ''}
import theme from '#build/${path}/${prose ? 'prose/' : ''}${content ? 'content/' : ''}${kebabName}'
${!pro ? `import type { ComponentConfig } from '../types/utils'` : ''}
type ${upperName} = ComponentConfig<typeof theme, AppConfig, '${camelName}'${pro ? `, '${key}'` : ''}>
type ${upperName} = ComponentConfig<typeof theme, AppConfig, ${upperName}${pro ? `, '${key}'` : ''}>
export interface ${upperName}Props extends Pick<${upperName}RootProps> {
class?: any

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,15 +0,0 @@
<template>
<UDrawer :ui="{ content: 'h-full', overlay: 'bg-inverted/30' }">
<UButton label="Open" color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-up" />
<template #footer>
<UDrawer nested :ui="{ content: 'h-full', overlay: 'bg-inverted/30' }">
<UButton color="neutral" variant="outline" label="Open nested" />
<template #content>
<Placeholder class="flex-1 m-4" />
</template>
</UDrawer>
</template>
</UDrawer>
</template>

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,12 +6,14 @@ const count = ref(0)
const toast = useToast()
const overlay = useOverlay()
const modal = overlay.create(LazyModalExample)
const modal = overlay.create(LazyModalExample, {
props: {
count: count.value
}
})
async function open() {
const instance = modal.open({
count: count.value
})
const instance = modal.open()
const shouldIncrement = await instance.result

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,12 +6,14 @@ const count = ref(0)
const toast = useToast()
const overlay = useOverlay()
const slideover = overlay.create(LazySlideoverExample)
const slideover = overlay.create(LazySlideoverExample, {
props: {
count: count.value
}
})
async function open() {
const instance = slideover.open({
count: count.value
})
const instance = slideover.open()
const shouldIncrement = await instance.result

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import { onMounted, watch } from 'vue'
import FaviconSvg from '../../public/icon.svg?raw'
import FaviconSvg from 'public/icon.svg?raw'
export function useFaviconFromTheme() {
const colorMode = useColorMode()

View File

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

View File

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

View File

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

View File

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

View File

@@ -183,28 +183,7 @@ It's recommended to install the [Tailwind CSS IntelliSense](https://marketplace.
```
::note{to="/components/app"}
The `App` component sets up global config and is required for **Toast**, **Tooltip** and **programmatic overlays**.
::
#### Add the `isolate` class to your root container
```html [index.html]{9}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Nuxt UI</title>
</head>
<body>
<div id="app" class="isolate"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
```
::note
This ensures styles are scoped to your app and prevents issues with overlays and stacking contexts.
The `App` component provides global configurations and is required for **Toast**, **Tooltip** components to work as well as **Programmatic Overlays**.
::
::
@@ -354,32 +333,6 @@ export default defineConfig({
This option adds the `transition-colors` class on components with hover or active states.
::
### `theme.defaultVariants` :badge{label="Soon" class="align-text-top"}
Use the `theme.defaultVariants` option to override the default `color` and `size` variants for components.
- Default: `{ color: 'primary', size: 'md' }`{lang="ts-type"}
```ts [vite.config.ts]
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'
export default defineConfig({
plugins: [
vue(),
ui({
theme: {
defaultVariants: {
color: 'neutral',
size: 'sm'
}
}
})
]
})
```
### `inertia`
Use the `inertia` option to enable compatibility with [Inertia.js](https://inertiajs.com/).

View File

@@ -536,33 +536,6 @@ import { ModalExampleComponent } from '#components'
</script>
```
### Changed form validation
- The error object property for targeting form fields has been renamed from `path` to `name`:
```diff
<script setup lang="ts">
const validate = (state: any): FormError[] => {
const errors = []
if (!state.email) {
errors.push({
- path: 'email',
+ name: 'email',
message: 'Required'
})
}
if (!state.password) {
errors.push({
- path: 'password',
+ name: 'password',
message: 'Required'
})
}
return errors
}
</script>
```
---
::warning

View File

@@ -87,7 +87,7 @@ Read more about this in the `@nuxt/icon` documentation.
You can use local SVG files to create a custom Iconify collection.
For example, place your icons' SVG files under a folder of your choice, for example, `./app/assets/icons`:
For example, place your icons' SVG files under a folder of your choice, for example, `./assets/icons`:
```bash
assets/icons
@@ -104,7 +104,7 @@ export default defineNuxtConfig({
icon: {
customCollections: [{
prefix: 'custom',
dir: './app/assets/icons'
dir: './assets/icons'
}]
}
})

View File

@@ -328,17 +328,6 @@ name: 'drawer-responsive-example'
---
::
### Nested drawers :badge{label="Soon" class="align-text-top"}
You can nest drawers within each other by using the `nested` prop.
::component-example
---
prettier: true
name: 'drawer-nested-example'
---
::
### With footer slot
Use the `#footer` slot to add content after the Drawer's body.

View File

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

View File

@@ -62,19 +62,6 @@ items:
---
::
### Color :badge{label="Soon" class="align-text-top"}
Use the `color` prop to change the color of the Kbd.
::component-code
---
props:
color: neutral
slots:
default: K
---
::
### Variant
Use the `variant` prop to change the variant of the Kbd.
@@ -82,7 +69,6 @@ Use the `variant` prop to change the variant of the Kbd.
::component-code
---
props:
color: neutral
variant: solid
slots:
default: K

View File

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

View File

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

View File

@@ -83,9 +83,6 @@ Use the `columns` prop as an array of [ColumnDef](https://tanstack.com/table/lat
- `class`:
- `td`: [The classes to apply to the `td` element.]{class="text-muted"}
- `th`: [The classes to apply to the `th` element.]{class="text-muted"}
- `style`:
- `td`: [The style to apply to the `td` element.]{class="text-muted"}
- `th`: [The style to apply to the `th` element.]{class="text-muted"}
In order to render components or other HTML elements, you will need to use the Vue [`h` function](https://vuejs.org/api/render-function.html#h) inside the `header` and `cell` props. This is different from other components that use slots but allows for more flexibility.
@@ -115,8 +112,6 @@ Use the `meta` prop as an object ([TableMeta](https://tanstack.com/table/latest/
- `class`:
- `tr`: [The classes to apply to the `tr` element.]{class="text-muted"}
- `style`:
- `tr`: [The style to apply to the `tr` element.]{class="text-muted"}
### Loading

View File

@@ -19,13 +19,12 @@ Use the `items` prop as an array of objects with the following properties:
- `label?: string`{lang="ts-type"}
- `icon?: string`{lang="ts-type"}
- `avatar?: AvatarProps`{lang="ts-type"}
- `badge?: string | number | BadgeProps`{lang="ts-type"}
- `content?: string`{lang="ts-type"}
- `value?: string | number`{lang="ts-type"}
- `disabled?: boolean`{lang="ts-type"}
- [`slot?: string`{lang="ts-type"}](#with-custom-slot)
- `class?: any`{lang="ts-type"}
- `ui?: { trigger?: ClassNameValue, leadingIcon?: ClassNameValue, leadingAvatar?: ClassNameValue, leadingAvatarSize?: ClassNameValue, label?: ClassNameValue, trailingBadge?: ClassNameValue, trailingBadgeSize?: ClassNameValue, content?: ClassNameValue }`{lang="ts-type"}
- `ui?: { trigger?: ClassNameValue, leadingIcon?: ClassNameValue, leadingAvatar?: ClassNameValue, label?: ClassNameValue, content?: ClassNameValue }`{lang="ts-type"}
::component-code
---

View File

@@ -143,6 +143,10 @@ export default defineNuxtConfig({
'/releases': { redirect: 'https://github.com/nuxt/ui/releases', prerender: false }
},
future: {
compatibilityVersion: 4
},
compatibilityDate: '2024-07-09',
nitro: {

View File

@@ -11,26 +11,26 @@
"dependencies": {
"@ai-sdk/vue": "^1.2.12",
"@iconify-json/logos": "^1.2.4",
"@iconify-json/lucide": "^1.2.57",
"@iconify-json/simple-icons": "^1.2.44",
"@iconify-json/lucide": "^1.2.56",
"@iconify-json/simple-icons": "^1.2.42",
"@iconify-json/vscode-icons": "^1.2.23",
"@nuxt/content": "^3.6.3",
"@nuxt/image": "^1.10.0",
"@nuxt/ui": "workspace:*",
"@nuxt/ui-pro": "https://pkg.pr.new/@nuxt/ui-pro@17684e4",
"@nuxt/ui-pro": "https://pkg.pr.new/@nuxt/ui-pro@22fdc5e",
"@nuxthub/core": "^0.9.0",
"@nuxtjs/plausible": "^1.2.0",
"@octokit/rest": "^22.0.0",
"@rollup/plugin-yaml": "^4.1.2",
"@vueuse/integrations": "^13.5.0",
"@vueuse/nuxt": "^13.5.0",
"ai": "^4.3.19",
"ai": "^4.3.16",
"better-sqlite3": "^12.2.0",
"capture-website": "^4.2.0",
"joi": "^17.13.3",
"maska": "^3.2.0",
"motion-v": "^1.5.0",
"nuxt": "^4.0.1",
"nuxt": "^3.17.6",
"nuxt-component-meta": "^0.12.1",
"nuxt-llms": "^0.1.3",
"nuxt-og-image": "^5.1.9",
@@ -40,11 +40,11 @@
"superstruct": "^2.0.2",
"ufo": "^1.6.1",
"valibot": "^1.1.0",
"workers-ai-provider": "^0.7.2",
"workers-ai-provider": "^0.7.1",
"yup": "^1.6.1",
"zod": "^4.0.5"
"zod": "^3.25.75"
},
"devDependencies": {
"wrangler": "^4.25.0"
"wrangler": "^4.23.0"
}
}

View File

@@ -2,7 +2,7 @@
"name": "@nuxt/ui",
"description": "A UI Library for Modern Web Apps, powered by Vue & Tailwind CSS.",
"version": "3.2.0",
"packageManager": "pnpm@10.13.1",
"packageManager": "pnpm@10.12.4",
"repository": {
"type": "git",
"url": "git+https://github.com/nuxt/ui.git"
@@ -116,8 +116,8 @@
"@internationalized/number": "^3.6.3",
"@nuxt/fonts": "^0.11.4",
"@nuxt/icon": "^1.15.0",
"@nuxt/kit": "^4.0.1",
"@nuxt/schema": "^4.0.1",
"@nuxt/kit": "^3.17.6",
"@nuxt/schema": "^3.17.6",
"@nuxtjs/color-mode": "^3.5.2",
"@standard-schema/spec": "^1.0.0",
"@tailwindcss/postcss": "^4.1.11",
@@ -155,16 +155,16 @@
"vue-component-type-helpers": "^3.0.1"
},
"devDependencies": {
"@nuxt/eslint-config": "^1.6.0",
"@nuxt/eslint-config": "^1.5.2",
"@nuxt/module-builder": "^1.0.1",
"@nuxt/test-utils": "^3.19.2",
"@release-it/conventional-changelog": "^10.0.1",
"@vue/test-utils": "^2.4.6",
"embla-carousel": "^8.6.0",
"eslint": "^9.31.0",
"eslint": "^9.30.1",
"happy-dom": "^18.0.1",
"nuxt": "^4.0.1",
"release-it": "^19.0.4",
"nuxt": "^3.17.6",
"release-it": "^19.0.3",
"vitest": "^3.2.4",
"vitest-environment-nuxt": "^1.0.1",
"vue-tsc": "^3.0.1"
@@ -177,7 +177,7 @@
"valibot": "^1.0.0",
"vue-router": "^4.5.0",
"yup": "^1.6.0",
"zod": "^3.24.0 || ^4.0.0"
"zod": "^3.24.0"
},
"peerDependenciesMeta": {
"@inertiajs/vue3": {

View File

@@ -13,12 +13,12 @@
"@nuxt/ui": "workspace:*",
"vue": "^3.5.17",
"vue-router": "^4.5.1",
"zod": "^4.0.5"
"zod": "^3.25.75"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.0",
"@vitejs/plugin-vue": "^5.2.4",
"typescript": "^5.8.3",
"vite": "^7.0.5",
"vite": "^6.3.5",
"vue-tsc": "^3.0.1"
}
}

View File

@@ -1,16 +1,12 @@
<script setup lang="ts">
const colorHex = ref('#9C27B0')
function handleColorChange(event: Event) {
colorHex.value = (event.target as HTMLInputElement).value
}
</script>
<template>
<div class="flex flex-col gap-5">
<div class="flex items-center gap-2">
<span :style="{ backgroundColor: colorHex }" class="inline-flex w-5 h-5 rounded" />
<UInput :model-value="colorHex" @change="handleColorChange" />
<code class="font-mono">{{ colorHex }}</code>
</div>
<USeparator />
<div class="flex justify-between gap-2">
@@ -25,6 +21,6 @@ function handleColorChange(event: Event) {
</UButton>
</div>
<USeparator />
<UColorPicker v-model="colorHex" />
<UColorPicker v-model="colorHex" @update:model-value="() => console.log('model update')" />
</div>
</template>

View File

@@ -28,20 +28,6 @@ const inset = ref(false)
</template>
</UDrawer>
<UDrawer title="Drawer with nested" :inset="inset" :ui="{ content: 'h-full' }" should-scale-background>
<UButton color="neutral" variant="outline" label="Open nested" />
<template #footer>
<UDrawer :inset="inset" nested :ui="{ content: 'h-full' }">
<UButton color="neutral" variant="outline" label="Open nested" />
<template #content>
<Placeholder class="flex-1 m-4" />
</template>
</UDrawer>
</template>
</UDrawer>
<UDrawer title="Drawer with bottom direction" direction="bottom" :inset="inset">
<UButton color="neutral" variant="outline" label="Open on bottom" />

View File

@@ -3,16 +3,20 @@ import theme from '#build/ui/kbd'
import { kbdKeysMap } from '@nuxt/ui/composables/useKbd.js'
const sizes = Object.keys(theme.variants.size) as Array<keyof typeof theme.variants.size>
const variants = Object.keys(theme.variants.variant) as Array<keyof typeof theme.variants.variant>
const colors = Object.keys(theme.variants.color) as Array<keyof typeof theme.variants.color>
const kbdKeys = Object.keys(kbdKeysMap)
</script>
<template>
<div class="flex flex-col gap-2">
<div v-for="color in colors" :key="color" class="flex items-center gap-1 ms-[-22px]">
<UKbd v-for="variant in variants" :key="`${color}-${variant}`" value="meta" :variant="variant" :color="color" />
<div class="flex items-center gap-1">
<UKbd value="meta" />
</div>
<div class="flex items-center gap-1">
<UKbd value="meta" variant="subtle" />
</div>
<div class="flex items-center gap-1">
<UKbd value="meta" variant="solid" />
</div>
<div class="flex items-center gap-1 ms-[-220px]">
<UKbd v-for="(kdbKey, index) in kbdKeys" :key="index" :value="kdbKey" />

View File

@@ -25,8 +25,7 @@ const items = [{
label: 'Tab3',
icon: 'i-lucide-bell',
content: 'Finally, this is the content for Tab3',
slot: 'custom' as const,
badge: '300'
slot: 'custom' as const
}]
</script>

View File

@@ -10,6 +10,10 @@ export default defineNuxtConfig({
css: ['~/assets/css/main.css'],
future: {
compatibilityVersion: 4
},
compatibilityDate: '2024-07-09',
vite: {

View File

@@ -9,13 +9,13 @@
"typecheck": "nuxt typecheck"
},
"dependencies": {
"@iconify-json/lucide": "^1.2.57",
"@iconify-json/simple-icons": "^1.2.44",
"@iconify-json/lucide": "^1.2.56",
"@iconify-json/simple-icons": "^1.2.42",
"@internationalized/date": "^3.8.2",
"@nuxt/ui": "workspace:*",
"@nuxthub/core": "^0.9.0",
"nuxt": "^4.0.1",
"zod": "^4.0.5"
"nuxt": "^3.17.6",
"zod": "^3.25.75"
},
"devDependencies": {
"typescript": "^5.8.3",

3179
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -57,7 +57,7 @@ export interface AlertProps {
}
export interface AlertEmits {
'update:open': [value: boolean]
(e: 'update:open', value: boolean): void
}
export interface AlertSlots {

View File

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

View File

@@ -35,7 +35,7 @@ export interface ChipProps {
}
export interface ChipEmits {
'update:show': [payload: boolean]
(e: 'update:show', payload: boolean): void
}
export interface ChipSlots {

View File

@@ -31,7 +31,7 @@ function HSVtoHSL(hsv: HSVColor): HSLObject {
return {
H: hsv.h,
S: x === 0 || x === 200 ? 0 : Math.round(hsv.s * hsv.v / (x <= 100 ? x : 200 - x)),
L: x / 2
L: Math.round(x / 2)
}
}
@@ -102,6 +102,7 @@ const pickedColor = computed<HSVColor>({
},
set(value) {
const color = new ColorTranslator(HSVtoHSL(value), {
decimals: 2,
labUnit: 'percent',
cmykUnit: 'percent',
cmykFunction: 'cmyk'

View File

@@ -80,7 +80,7 @@ export interface CommandPaletteProps<G extends CommandPaletteGroup<T> = CommandP
* @defaultValue appConfig.ui.icons.chevronRight
* @IconifyIcon
*/
trailingIcon?: string
itemTrailingIcon?: string
/**
* The placeholder text for the input.
* @defaultValue t('commandPalette.placeholder')
@@ -91,6 +91,12 @@ export interface CommandPaletteProps<G extends CommandPaletteGroup<T> = CommandP
* @defaultValue true
*/
autofocus?: boolean
/**
* The icon displayed in the input.
* @defaultValue appConfig.ui.icons.search
* @IconifyIcon
*/
trailingIcon?: string
/**
* Display a close button in the input (useful when inside a Modal for example).
* `{ size: 'md', color: 'neutral', variant: 'ghost' }`{lang="ts-type"}
@@ -149,6 +155,7 @@ export type CommandPaletteSlots<G extends CommandPaletteGroup<T> = CommandPalett
'empty'(props: { searchTerm?: string }): any
'footer'(props: { ui: { [K in keyof Required<CommandPalette['slots']>]: (props?: Record<string, any>) => string } }): any
'back'(props: { ui: { [K in keyof Required<CommandPalette['slots']>]: (props?: Record<string, any>) => string } }): any
'actions'(props: { ui: { [K in keyof Required<CommandPalette['slots']>]: (props?: Record<string, any>) => string } }): any
'close'(props: { ui: { [K in keyof Required<CommandPalette['slots']>]: (props?: Record<string, any>) => string } }): any
'item': SlotProps<T>
'item-leading': SlotProps<T>
@@ -337,6 +344,7 @@ function onSelect(e: Event, item: T) {
:autofocus="autofocus"
v-bind="inputProps"
:icon="icon || appConfig.ui.icons.search"
:trailing-icon="trailingIcon"
:class="ui.input({ class: props.ui?.input })"
@keydown.backspace="onBackspace"
>
@@ -354,7 +362,9 @@ function onSelect(e: Event, item: T) {
</slot>
</template>
<template v-if="close || !!slots.close" #trailing>
<template v-if="trailingIcon || close || !!slots.close || !!slots.actions" #trailing>
<UIcon v-if="trailingIcon" :name="trailingIcon" :class="ui.trailingIcon({ class: props.ui?.trailingIcon })" />
<slot name="actions" :ui="ui" />
<slot name="close" :ui="ui">
<UButton
v-if="close"
@@ -417,7 +427,7 @@ function onSelect(e: Event, item: T) {
<slot :name="((item.slot ? `${item.slot}-trailing` : group.slot ? `${group.slot}-trailing` : `item-trailing`) as keyof CommandPaletteSlots<G, T>)" :item="(item as any)" :index="index">
<UIcon
v-if="item.children && item.children.length > 0"
:name="trailingIcon || appConfig.ui.icons.chevronRight"
:name="itemTrailingIcon || appConfig.ui.icons.chevronRight"
:class="ui.itemTrailingIcon({ class: [props.ui?.itemTrailingIcon, item.ui?.itemTrailingIcon] })"
/>

View File

@@ -37,11 +37,6 @@ export interface DrawerProps extends Pick<DrawerRootProps, 'activeSnapPoint' | '
* @defaultValue true
*/
portal?: boolean | string | HTMLElement
/**
* Whether the drawer is nested in another drawer.
* @defaultValue false
*/
nested?: boolean
class?: any
ui?: Drawer['slots']
}
@@ -62,7 +57,7 @@ export interface DrawerSlots {
<script setup lang="ts">
import { computed, toRef } from 'vue'
import { VisuallyHidden, useForwardPropsEmits } from 'reka-ui'
import { DrawerRoot, DrawerRootNested, DrawerTrigger, DrawerPortal, DrawerOverlay, DrawerContent, DrawerTitle, DrawerDescription, DrawerHandle } from 'vaul-vue'
import { DrawerRoot, DrawerTrigger, DrawerPortal, DrawerOverlay, DrawerContent, DrawerTitle, DrawerDescription, DrawerHandle } from 'vaul-vue'
import { reactivePick } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { usePortal } from '../composables/usePortal'
@@ -95,7 +90,7 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.drawer || {}
</script>
<template>
<component :is="nested ? DrawerRootNested : DrawerRoot" v-bind="rootProps">
<DrawerRoot v-bind="rootProps">
<DrawerTrigger v-if="!!slots.default" as-child :class="props.class">
<slot />
</DrawerTrigger>
@@ -149,5 +144,5 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.drawer || {}
</slot>
</DrawerContent>
</DrawerPortal>
</component>
</DrawerRoot>
</template>

View File

@@ -53,8 +53,8 @@ export interface FormProps<S extends FormSchema, T extends boolean = true> {
}
export interface FormEmits<S extends FormSchema, T extends boolean = true> {
submit: [payload: FormSubmitEvent<FormData<S, T>>]
error: [payload: FormErrorEvent]
(e: 'submit', payload: FormSubmitEvent<FormData<S, T>>): void
(e: 'error', payload: FormErrorEvent): void
}
export interface FormSlots {

View File

@@ -18,7 +18,7 @@ export interface FormFieldProps {
label?: string
description?: string
help?: string
error?: boolean | string
error?: string | boolean
hint?: string
/**
* @defaultValue 'md'
@@ -41,8 +41,8 @@ export interface FormFieldSlots {
hint(props: { hint?: string }): any
description(props: { description?: string }): any
help(props: { help?: string }): any
error(props: { error?: boolean | string }): any
default(props: { error?: boolean | string }): any
error(props: { error?: string | boolean }): any
default(props: { error?: string | boolean }): any
}
</script>
@@ -121,7 +121,7 @@ provide(formFieldInjectionKey, computed(() => ({
{{ error }}
</slot>
</div>
<div v-else-if="help || !!slots.help" :id="`${ariaId}-help`" :class="ui.help({ class: props.ui?.help })">
<div v-else-if="help || !!slots.help" :class="ui.help({ class: props.ui?.help })">
<slot name="help" :help="help">
{{ help }}
</slot>

View File

@@ -52,9 +52,9 @@ export interface InputProps<T extends AcceptableValue = AcceptableValue> extends
}
export interface InputEmits<T extends AcceptableValue = AcceptableValue> {
'update:modelValue': [payload: T]
'blur': [event: FocusEvent]
'change': [event: Event]
(e: 'update:modelValue', payload: T): void
(e: 'blur', event: FocusEvent): void
(e: 'change', event: Event): void
}
export interface InputSlots {

View File

@@ -128,16 +128,15 @@ export interface InputMenuProps<T extends ArrayOrNested<InputMenuItem> = ArrayOr
}
export type InputMenuEmits<A extends ArrayOrNested<InputMenuItem>, VK extends GetItemKeys<A> | undefined, M extends boolean> = Pick<ComboboxRootEmits, 'update:open'> & {
'change': [payload: Event]
'blur': [payload: FocusEvent]
'focus': [payload: FocusEvent]
'create': [item: string]
change: [payload: Event]
blur: [payload: FocusEvent]
focus: [payload: FocusEvent]
create: [item: string]
/** Event handler when highlighted element changes. */
'highlight': [payload: {
highlight: [payload: {
ref: HTMLElement
value: GetModelValue<A, VK, M>
} | undefined]
'remove-tag': [item: GetModelValue<A, VK, M>]
} & GetModelValueEmits<A, VK, M>
type SlotProps<T extends InputMenuItem> = (props: { item: T, index: number }) => any
@@ -172,7 +171,7 @@ export interface InputMenuSlots<
</script>
<script setup lang="ts" generic="T extends ArrayOrNested<InputMenuItem>, VK extends GetItemKeys<T> | undefined = undefined, M extends boolean = false">
import { computed, ref, toRef, onMounted, toRaw, nextTick } from 'vue'
import { computed, ref, toRef, onMounted, toRaw } from 'vue'
import { ComboboxRoot, ComboboxArrow, ComboboxAnchor, ComboboxInput, ComboboxTrigger, ComboboxPortal, ComboboxContent, ComboboxEmpty, ComboboxGroup, ComboboxLabel, ComboboxSeparator, ComboboxItem, ComboboxItemIndicator, TagsInputRoot, TagsInputItem, TagsInputItemText, TagsInputItemDelete, TagsInputInput, useForwardPropsEmits, useFilter } from 'reka-ui'
import { defu } from 'defu'
import { isEqual } from 'ohash/utils'
@@ -234,7 +233,11 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.inputMenu ||
}))
function displayValue(value: T): string {
const item = items.value.find(item => compare(typeof item === 'object' && props.valueKey ? get(item as Record<string, any>, props.valueKey as string) : item, value))
if (!props.valueKey) {
return value && (typeof value === 'object' ? get(value, props.labelKey as string) : value)
}
const item = items.value.find(item => compare(typeof item === 'object' ? get(item as Record<string, any>, props.valueKey as string) : item, value))
return item && (typeof item === 'object' ? get(item, props.labelKey as string) : item)
}
@@ -255,12 +258,8 @@ const filteredGroups = computed(() => {
const fields = Array.isArray(props.filterFields) ? props.filterFields : [props.labelKey] as string[]
return groups.value.map(items => items.filter((item) => {
if (item === undefined || item === null) {
return false
}
if (typeof item !== 'object') {
return groups.value.map(group => group.filter((item) => {
if (typeof item !== 'object' || item === null) {
return contains(String(item), searchTerm.value)
}
@@ -268,10 +267,7 @@ const filteredGroups = computed(() => {
return true
}
return fields.some((field) => {
const value = get(item, field)
return value !== undefined && value !== null && contains(String(value), searchTerm.value)
})
return fields.some(field => contains(get(item, field), searchTerm.value))
})).filter(group => group.filter(item =>
!isInputItem(item) || (!item.type || !['label', 'separator'].includes(item.type))
).length > 0)
@@ -302,10 +298,6 @@ function autoFocus() {
}
onMounted(() => {
nextTick(() => {
searchTerm.value = ''
})
setTimeout(() => {
autoFocus()
}, props.autofocusDelay)
@@ -367,7 +359,6 @@ function onRemoveTag(event: any) {
const modelValue = props.modelValue as GetModelValue<T, VK, true>
const filteredValue = modelValue.filter(value => !isEqual(value, event))
emits('update:modelValue', filteredValue as GetModelValue<T, VK, M>)
emits('remove-tag', event)
onUpdate(filteredValue)
}
}
@@ -441,7 +432,7 @@ defineExpose({
<TagsInputItem v-for="(item, index) in tags" :key="index" :value="item" :class="ui.tagsItem({ class: [props.ui?.tagsItem, isInputItem(item) && item.ui?.tagsItem] })">
<TagsInputItemText :class="ui.tagsItemText({ class: [props.ui?.tagsItemText, isInputItem(item) && item.ui?.tagsItemText] })">
<slot name="tags-item-text" :item="(item as NestedItem<T>)" :index="index">
{{ displayValue(item as T) ?? item }}
{{ displayValue(item as T) }}
</slot>
</TagsInputItemText>

View File

@@ -63,9 +63,9 @@ export interface InputNumberProps extends Pick<NumberFieldRootProps, 'modelValue
}
export interface InputNumberEmits {
'update:modelValue': [payload: number]
'blur': [event: FocusEvent]
'change': [payload: Event]
(e: 'update:modelValue', payload: number): void
(e: 'blur', event: FocusEvent): void
(e: 'change', payload: Event): void
}
export interface InputNumberSlots {

View File

@@ -13,10 +13,6 @@ export interface KbdProps {
*/
as?: any
value?: KbdKey | string
/**
* @defaultValue 'neutral'
*/
color?: Kbd['variants']['color']
/**
* @defaultValue 'outline'
*/
@@ -52,7 +48,7 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.kbd || {}) }
</script>
<template>
<Primitive :as="as" :class="ui({ class: props.class, color: props.color, variant: props.variant, size: props.size })">
<Primitive :as="as" :class="ui({ variant, size, class: props.class })">
<slot>
{{ getKbdKey(value) }}
</slot>

View File

@@ -177,8 +177,6 @@ import UBadge from './Badge.vue'
import UPopover from './Popover.vue'
import UTooltip from './Tooltip.vue'
defineOptions({ inheritAttrs: false })
const props = withDefaults(defineProps<NavigationMenuProps<T>>(), {
orientation: 'horizontal',
contentOrientation: 'horizontal',
@@ -272,7 +270,7 @@ function getAccordionDefaultValue(list: NavigationMenuItem[], level = 0) {
<component :is="orientation === 'vertical' && item.children?.length && !collapsed ? AccordionTrigger : 'span'" v-if="(!collapsed || orientation !== 'vertical') && (item.badge || (orientation === 'horizontal' && (item.children?.length || !!slots[(item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>])) || (orientation === 'vertical' && item.children?.length) || item.trailingIcon || !!slots[(item.slot ? `${item.slot}-trailing` : 'item-trailing') as keyof NavigationMenuSlots<T>])" as="span" :class="ui.linkTrailing({ class: [props.ui?.linkTrailing, item.ui?.linkTrailing] })" @click.stop.prevent>
<slot :name="((item.slot ? `${item.slot}-trailing` : 'item-trailing') as keyof NavigationMenuSlots<T>)" :item="item" :active="active" :index="index">
<UBadge
v-if="item.badge !== undefined"
v-if="item.badge"
color="neutral"
variant="outline"
:size="((item.ui?.linkTrailingBadgeSize || props.ui?.linkTrailingBadgeSize || ui.linkTrailingBadgeSize()) as BadgeProps['size'])"
@@ -394,7 +392,7 @@ function getAccordionDefaultValue(list: NavigationMenuItem[], level = 0) {
</component>
</DefineItemTemplate>
<NavigationMenuRoot v-bind="{ ...rootProps, ...$attrs }" :data-collapsed="collapsed" :class="ui.root({ class: [props.ui?.root, props.class] })">
<NavigationMenuRoot v-bind="rootProps" :data-collapsed="collapsed" :class="ui.root({ class: [props.ui?.root, props.class] })">
<slot name="list-leading" />
<template v-for="(list, listIndex) in lists" :key="`list-${listIndex}`">

View File

@@ -70,9 +70,7 @@ export type RadioGroupEmits = RadioGroupRootEmits & {
change: [payload: Event]
}
type NormalizeItem<T extends RadioGroupItem> = Exclude<T & { id: string }, RadioGroupValue>
type SlotProps<T extends RadioGroupItem> = (props: { item: NormalizeItem<T>, modelValue?: RadioGroupValue }) => any
type SlotProps<T extends RadioGroupItem> = (props: { item: T & { id: string }, modelValue?: RadioGroupValue }) => any
export interface RadioGroupSlots<T extends RadioGroupItem = RadioGroupItem> {
legend(props?: {}): any
@@ -116,21 +114,21 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.radioGroup |
indicator: props.indicator
}))
function normalizeItem(item: T): NormalizeItem<T> {
function normalizeItem(item: any) {
if (item === null) {
return {
id: `${id}:null`,
value: undefined,
label: undefined
} as NormalizeItem<T>
}
}
if (typeof item === 'string' || typeof item === 'number' || typeof item === 'bigint') {
if (typeof item === 'string' || typeof item === 'number') {
return {
id: `${id}:${item}`,
value: String(item),
label: String(item)
} as NormalizeItem<T>
}
}
const value = get(item, props.valueKey as string)
@@ -138,7 +136,7 @@ function normalizeItem(item: T): NormalizeItem<T> {
const description = get(item, props.descriptionKey as string)
return {
...(item as NormalizeItem<T>),
...item,
value,
label,
description,

View File

@@ -234,7 +234,11 @@ function displayValue(value: GetItemValue<T, VK> | GetItemValue<T, VK>[]): strin
return values?.length ? values.join(', ') : undefined
}
const item = items.value.find(item => compare(typeof item === 'object' && props.valueKey ? get(item as Record<string, any>, props.valueKey as string) : item, value))
if (!props.valueKey) {
return value && (typeof value === 'object' ? get(value, props.labelKey as string) : value)
}
const item = items.value.find(item => compare(typeof item === 'object' ? get(item as Record<string, any>, props.valueKey as string) : item, value))
return item && (typeof item === 'object' ? get(item, props.labelKey as string) : item)
}
@@ -256,11 +260,7 @@ const filteredGroups = computed(() => {
const fields = Array.isArray(props.filterFields) ? props.filterFields : [props.labelKey] as string[]
return groups.value.map(items => items.filter((item) => {
if (item === undefined || item === null) {
return false
}
if (typeof item !== 'object') {
if (typeof item !== 'object' || item === null) {
return contains(String(item), searchTerm.value)
}
@@ -268,10 +268,7 @@ const filteredGroups = computed(() => {
return true
}
return fields.some((field) => {
const value = get(item, field)
return value !== undefined && value !== null && contains(String(value), searchTerm.value)
})
return fields.some(field => contains(get(item, field), searchTerm.value))
})).filter(group => group.filter(item =>
!isSelectItem(item) || (!item.type || !['label', 'separator'].includes(item.type))
).length > 0)

View File

@@ -39,8 +39,8 @@ export interface SliderProps extends Pick<SliderRootProps, 'name' | 'disabled' |
}
export interface SliderEmits<T extends number | number[] = number | number[]> {
'update:modelValue': [payload: T]
'change': [payload: Event]
(e: 'update:modelValue', payload: T): void
(e: 'change', payload: Event): void
}
</script>

View File

@@ -45,26 +45,12 @@ declare module '@tanstack/table-core' {
th?: string | ((cell: Header<TData, TValue>) => string)
td?: string | ((cell: Cell<TData, TValue>) => string)
}
style?: {
th?: string | Record<string, string> | ((cell: Header<TData, TValue>) => string | Record<string, string>)
td?: string | Record<string, string> | ((cell: Cell<TData, TValue>) => string | Record<string, string>)
}
colspan?: {
td?: string | ((cell: Cell<TData, TValue>) => string)
}
rowspan?: {
td?: string | ((cell: Cell<TData, TValue>) => string)
}
}
interface TableMeta<TData> {
class?: {
tr?: string | ((row: Row<TData>) => string)
}
style?: {
tr?: string | Record<string, string> | ((row: Row<TData>) => string | Record<string, string>)
}
}
}
@@ -383,14 +369,6 @@ function onRowContextmenu(e: Event, row: TableRow<T>) {
}
}
function resolveValue<T, A = undefined>(prop: T | ((arg: A) => T), arg?: A): T | undefined {
if (typeof prop === 'function') {
// @ts-expect-error: TS can't know if prop is a function here
return prop(arg)
}
return prop
}
watch(
() => props.data, () => {
data.value = props.data ? [...props.data] : []
@@ -420,11 +398,10 @@ defineExpose({
:data-pinned="header.column.getIsPinned()"
:scope="header.colSpan > 1 ? 'colgroup' : 'col'"
:colspan="header.colSpan > 1 ? header.colSpan : undefined"
:rowspan="header.rowSpan > 1 ? header.rowSpan : undefined"
:class="ui.th({
class: [
props.ui?.th,
resolveValue(header.column.columnDef.meta?.class?.th, header)
typeof header.column.columnDef.meta?.class?.th === 'function' ? header.column.columnDef.meta.class.th(header) : header.column.columnDef.meta?.class?.th
],
pinned: !!header.column.getIsPinned()
})"
@@ -452,10 +429,9 @@ defineExpose({
:class="ui.tr({
class: [
props.ui?.tr,
resolveValue(tableApi.options.meta?.class?.tr, row)
typeof tableApi.options.meta?.class?.tr === 'function' ? tableApi.options.meta.class.tr(row) : tableApi.options.meta?.class?.tr
]
})"
:style="resolveValue(tableApi.options.meta?.style?.tr, row)"
@click="onRowSelect($event, row)"
@pointerenter="onRowHover($event, row)"
@pointerleave="onRowHover($event, null)"
@@ -465,16 +441,13 @@ defineExpose({
v-for="cell in row.getVisibleCells()"
:key="cell.id"
:data-pinned="cell.column.getIsPinned()"
:colspan="resolveValue(cell.column.columnDef.meta?.colspan?.td, cell)"
:rowspan="resolveValue(cell.column.columnDef.meta?.rowspan?.td, cell)"
:class="ui.td({
class: [
props.ui?.td,
resolveValue(cell.column.columnDef.meta?.class?.td, cell)
typeof cell.column.columnDef.meta?.class?.td === 'function' ? cell.column.columnDef.meta.class.td(cell) : cell.column.columnDef.meta?.class?.td
],
pinned: !!cell.column.getIsPinned()
})"
:style="resolveValue(cell.column.columnDef.meta?.style?.td, cell)"
>
<slot :name="`${cell.column.id}-cell`" v-bind="cell.getContext()">
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
@@ -515,15 +488,13 @@ defineExpose({
:key="header.id"
:data-pinned="header.column.getIsPinned()"
:colspan="header.colSpan > 1 ? header.colSpan : undefined"
:rowspan="header.rowSpan > 1 ? header.rowSpan : undefined"
:class="ui.th({
class: [
props.ui?.th,
resolveValue(header.column.columnDef.meta?.class?.th, header)
typeof header.column.columnDef.meta?.class?.th === 'function' ? header.column.columnDef.meta.class.th(header) : header.column.columnDef.meta?.class?.th
],
pinned: !!header.column.getIsPinned()
})"
:style="resolveValue(header.column.columnDef.meta?.style?.th, header)"
>
<slot :name="`${header.id}-footer`" v-bind="header.getContext()">
<FlexRender v-if="!header.isPlaceholder" :render="header.column.columnDef.footer" :props="header.getContext()" />

View File

@@ -3,7 +3,7 @@
import type { TabsRootProps, TabsRootEmits } from 'reka-ui'
import type { AppConfig } from '@nuxt/schema'
import theme from '#build/ui/tabs'
import type { AvatarProps, BadgeProps } from '../types'
import type { AvatarProps } from '../types'
import type { DynamicSlots, ComponentConfig } from '../types/utils'
type Tabs = ComponentConfig<typeof theme, AppConfig, 'tabs'>
@@ -15,18 +15,13 @@ export interface TabsItem {
*/
icon?: string
avatar?: AvatarProps
/**
* Display a badge on the item.
* `{ size: 'sm', color: 'neutral', variant: 'outline' }`{lang="ts-type"}
*/
badge?: string | number | BadgeProps
slot?: string
content?: string
/** A unique value for the tab item. Defaults to the index. */
value?: string | number
disabled?: boolean
class?: any
ui?: Pick<Tabs['slots'], 'trigger' | 'leadingIcon' | 'leadingAvatar' | 'leadingAvatarSize' | 'label' | 'trailingBadge' | 'trailingBadgeSize' | 'content'>
ui?: Pick<Tabs['slots'], 'trigger' | 'leadingIcon' | 'leadingAvatar' | 'label' | 'content'>
[key: string]: any
}
@@ -139,23 +134,14 @@ defineExpose({
>
<slot name="leading" :item="item" :index="index">
<UIcon v-if="item.icon" :name="item.icon" :class="ui.leadingIcon({ class: [props.ui?.leadingIcon, item.ui?.leadingIcon] })" />
<UAvatar v-else-if="item.avatar" :size="((item.ui?.leadingAvatarSize || props.ui?.leadingAvatarSize || ui.leadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.leadingAvatar({ class: [props.ui?.leadingAvatar, item.ui?.leadingAvatar] })" />
<UAvatar v-else-if="item.avatar" :size="((props.ui?.leadingAvatarSize || ui.leadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.leadingAvatar({ class: [props.ui?.leadingAvatar, item.ui?.leadingAvatar] })" />
</slot>
<span v-if="get(item, props.labelKey as string) || !!slots.default" :class="ui.label({ class: [props.ui?.label, item.ui?.label] })">
<slot :item="item" :index="index">{{ get(item, props.labelKey as string) }}</slot>
</span>
<slot name="trailing" :item="item" :index="index">
<UBadge
v-if="item.badge !== undefined"
color="neutral"
variant="outline"
:size="((item.ui?.trailingBadgeSize || props.ui?.trailingBadgeSize || ui.trailingBadgeSize()) as BadgeProps['size'])"
v-bind="(typeof item.badge === 'string' || typeof item.badge === 'number') ? { label: item.badge } : item.badge"
:class="ui.trailingBadge({ class: [props.ui?.trailingBadge, item.ui?.trailingBadge] })"
/>
</slot>
<slot name="trailing" :item="item" :index="index" />
</TabsTrigger>
<slot name="list-trailing" />

View File

@@ -55,9 +55,9 @@ export interface TextareaProps<T extends TextareaValue = TextareaValue> extends
}
export interface TextareaEmits<T extends TextareaValue = TextareaValue> {
'update:modelValue': [payload: T]
'blur': [event: FocusEvent]
'change': [event: Event]
(e: 'update:modelValue', payload: T): void
(e: 'blur', event: FocusEvent): void
(e: 'change', event: Event): void
}
export interface TextareaSlots {

View File

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

View File

@@ -122,7 +122,7 @@ export function defineShortcuts(config: MaybeRef<ShortcutsConfig>, options: Shor
if (shortcut.enabled) {
e.preventDefault()
shortcut.handler(e)
shortcut.handler()
}
clearChainedInput()
return

View File

@@ -89,15 +89,10 @@ export function useFormField<T>(props?: Props<T>, opts?: { bind?: boolean, defer
.filter(type => formField?.value?.[type])
.map(type => `${formField?.value.ariaId}-${type}`) || []
const attrs: Record<string, any> = {
return {
'aria-describedby': descriptiveAttrs.join(' '),
'aria-invalid': !!formField?.value.error
}
if (descriptiveAttrs.length > 0) {
attrs['aria-describedby'] = descriptiveAttrs.join(' ')
}
return attrs
})
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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