Compare commits

..

3 Commits

Author SHA1 Message Date
Daniel Roe
c532a6f930 chore: bump to 0.1.9 2025-04-08 08:48:39 -07:00
Daniel Roe
6bbcb40c9e chore: bump to 0.1.8 2025-04-08 08:14:48 -07:00
Daniel Roe
befda895b9 build: bump vue-sfc-transformer 2025-04-07 19:25:13 -07:00
150 changed files with 3053 additions and 4147 deletions

View File

@@ -42,8 +42,6 @@ jobs:
- name: Build application
run: pnpm run docs:build
env:
NODE_OPTIONS: '--max-old-space-size=8192'
- name: Deploy to NuxtHub
uses: nuxt-hub/action@v1

View File

@@ -18,7 +18,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, windows-latest] # macos-latest
os: [ubuntu-latest] # macos-latest, windows-latest
node: [22]
env:
@@ -65,7 +65,6 @@ jobs:
run: pnpm run dev:vue:build
- name: Publish
if: matrix.os != 'windows-latest'
run: pnpx pkg-pr-new publish --compact --no-template --pnpm
starter-nuxt:

1
.npmrc
View File

@@ -1,3 +1,4 @@
shamefully-hoist=true
auto-install-peers=true
ignore-workspace-root-check=true
shell-emulator=true

View File

@@ -6,14 +6,8 @@ export default defineBuildConfig({
'./src/unplugin',
'./src/vite'
],
rollup: {
replace: {
delimiters: ['', ''],
values: {
// Used in development to import directly from theme
'const isUiDev = true': 'const isUiDev = false'
}
}
replace: {
'process.env.DEV': 'false'
},
hooks: {
'mkdist:entry:options'(ctx, entry, options) {

View File

@@ -31,10 +31,13 @@ const component = ({ name, primitive, pro, prose, content }) => {
? `
<script lang="ts">
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/${path}/${prose ? 'prose/' : ''}${content ? 'content/' : ''}${kebabName}'
import type { ComponentConfig } from '../types/utils'
import { tv } from '../utils/tv'
type ${upperName} = ComponentConfig<typeof theme, AppConfig, ${upperName}${pro ? `, '${key}'` : ''}>
const appConfig${camelName} = _appConfig as AppConfig & { ${key}: { ${prose ? 'prose: { ' : ''}${camelName}: Partial<typeof theme> } }${prose ? ' }' : ''}
const ${camelName} = tv({ extend: tv(theme), ...(appConfig${camelName}.${key}?.${prose ? 'prose?.' : ''}${camelName} || {}) })
export interface ${upperName}Props {
/**
@@ -43,7 +46,7 @@ export interface ${upperName}Props {
*/
as?: any
class?: any
ui?: ${upperName}['slots']
ui?: Partial<typeof ${camelName}.slots>
}
export interface ${upperName}Slots {
@@ -52,17 +55,12 @@ export interface ${upperName}Slots {
</script>
<script setup lang="ts">
import { computed } from 'vue'
import { Primitive } from 'reka-ui'
import { useAppConfig } from '#imports'
import { tv } from '../utils/tv'
const props = defineProps<${upperName}Props>()
defineSlots<${upperName}Slots>()
const appConfig = useAppConfig() as ${upperName}['AppConfig']
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.${camelName} || {}) })())
const ui = ${camelName}()
</script>
<template>
@@ -73,16 +71,22 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.${camelName}
`
: `
<script lang="ts">
import type { VariantProps } from 'tailwind-variants'
import type { ${upperName}RootProps, ${upperName}RootEmits } from 'reka-ui'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/${path}/${prose ? 'prose/' : ''}${content ? 'content/' : ''}${kebabName}'
import type { ComponentConfig } from '../types/utils'
import { tv } from '../utils/tv'
type ${upperName} = ComponentConfig<typeof theme, AppConfig, ${upperName}${pro ? `, '${key}'` : ''}>
const appConfig${camelName} = _appConfig as AppConfig & { ${key}: { ${prose ? 'prose: { ' : ''}${camelName}: Partial<typeof theme> } }${prose ? ' }' : ''}
const ${camelName} = tv({ extend: tv(theme), ...(appConfig${camelName}.${key}?.${prose ? 'prose?.' : ''}${camelName} || {}) })
type ${upperName}Variants = VariantProps<typeof ${camelName}>
export interface ${upperName}Props extends Pick<${upperName}RootProps> {
class?: any
ui?: ${upperName}['slots']
ui?: Partial<typeof ${camelName}.slots>
}
export interface ${upperName}Emits extends ${upperName}RootEmits {}
@@ -91,21 +95,16 @@ export interface ${upperName}Slots {}
</script>
<script setup lang="ts">
import { computed } from 'vue'
import { ${upperName}Root, useForwardPropsEmits } from 'reka-ui'
import { reactivePick } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { tv } from '../utils/tv'
const props = defineProps<${upperName}Props>()
const emits = defineEmits<${upperName}Emits>()
const slots = defineSlots<${upperName}Slots>()
const appConfig = useAppConfig() as ${upperName}['AppConfig']
const rootProps = useForwardPropsEmits(reactivePick(props), emits)
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.${camelName} || {}) })())
const ui = ${camelName}()
</script>
<template>

View File

@@ -85,5 +85,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 !justify-end !min-h-96 h-136 max-h-[341px] */
/* 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

@@ -328,7 +328,7 @@ const { data: ast } = await useAsyncData(`component-code-${name}-${hash({ props:
<template>
<div class="my-5">
<div class="relative">
<div>
<div v-if="options.length" class="flex flex-wrap items-center gap-2.5 border border-(--ui-border-muted) border-b-0 relative rounded-t-[calc(var(--ui-radius)*1.5)] px-4 py-2.5 overflow-x-auto">
<template v-for="option in options" :key="option.name">
<UFormField

View File

@@ -22,7 +22,6 @@ function getEmojiFlag(locale: string): string {
hi: 'in', // Hindi -> India
hy: 'am', // Armenian -> Armenia
ja: 'jp', // Japanese -> Japan
kk: 'kz', // Kazakh -> Kazakhstan
km: 'kh', // Khmer -> Cambodia
ko: 'kr', // Korean -> South Korea
nb: 'no', // Norwegian Bokmål -> Norway

View File

@@ -11,6 +11,7 @@ const items = shallowRef<AccordionItem[]>([
{
label: 'Colors',
icon: 'i-lucide-swatch-book',
slot: 'colors' as const,
content: 'Choose a primary and a neutral color from your Tailwind CSS theme.'
},
{

View File

@@ -1,35 +1,26 @@
<script setup lang="ts">
import type { BreadcrumbItem } from '@nuxt/ui'
const items = [
{
label: 'Home',
to: '/'
},
{
slot: 'dropdown' as const,
icon: 'i-lucide-ellipsis',
children: [
{
label: 'Documentation'
},
{
label: 'Themes'
},
{
label: 'GitHub'
}
]
},
{
label: 'Components',
to: '/components'
},
{
label: 'Breadcrumb',
to: '/components/breadcrumb'
}
] satisfies BreadcrumbItem[]
const items = [{
label: 'Home',
to: '/'
}, {
slot: 'dropdown' as const,
icon: 'i-lucide-ellipsis',
children: [{
label: 'Documentation'
}, {
label: 'Themes'
}, {
label: 'GitHub'
}]
}, {
label: 'Components',
to: '/components'
}, {
label: 'Breadcrumb',
to: '/components/breadcrumb'
}] satisfies BreadcrumbItem[]
</script>
<template>

View File

@@ -1,20 +1,16 @@
<script setup lang="ts">
import type { BreadcrumbItem } from '@nuxt/ui'
const items: BreadcrumbItem[] = [
{
label: 'Home',
to: '/'
},
{
label: 'Components',
to: '/components'
},
{
label: 'Breadcrumb',
to: '/components/breadcrumb'
}
]
const items: BreadcrumbItem[] = [{
label: 'Home',
to: '/'
}, {
label: 'Components',
to: '/components'
}, {
label: 'Breadcrumb',
to: '/components/breadcrumb'
}]
</script>
<template>

View File

@@ -1,30 +1,23 @@
<script setup lang="ts">
import type { DropdownMenuItem } from '@nuxt/ui'
const items: DropdownMenuItem[] = [
{
label: 'Team',
icon: 'i-lucide-users'
},
{
label: 'Invite users',
icon: 'i-lucide-user-plus',
children: [
{
label: 'Invite by email',
icon: 'i-lucide-send-horizontal'
},
{
label: 'Invite by link',
icon: 'i-lucide-link'
}
]
},
{
label: 'New team',
icon: 'i-lucide-plus'
}
]
const items: DropdownMenuItem[] = [{
label: 'Team',
icon: 'i-lucide-users'
}, {
label: 'Invite users',
icon: 'i-lucide-user-plus',
children: [{
label: 'Invite by email',
icon: 'i-lucide-send-horizontal'
}, {
label: 'Invite by link',
icon: 'i-lucide-link'
}]
}, {
label: 'New team',
icon: 'i-lucide-plus'
}]
</script>
<template>

View File

@@ -1,79 +1,76 @@
<script setup lang="ts">
const groups = [
{
id: 'settings',
items: [
{
label: 'Profile',
icon: 'i-lucide-user',
kbds: ['meta', 'P']
},
{
label: 'Billing',
icon: 'i-lucide-credit-card',
kbds: ['meta', 'B'],
slot: 'billing' as const
},
{
label: 'Notifications',
icon: 'i-lucide-bell'
},
{
label: 'Security',
icon: 'i-lucide-lock'
}
]
},
{
id: 'users',
label: 'Users',
slot: 'users' as const,
items: [
{
label: 'Benjamin Canac',
suffix: 'benjamincanac',
to: 'https://github.com/benjamincanac',
target: '_blank'
},
{
label: 'Sylvain Marroufin',
suffix: 'smarroufin',
to: 'https://github.com/smarroufin',
target: '_blank'
},
{
label: 'Sébastien Chopin',
suffix: 'atinux',
to: 'https://github.com/atinux',
target: '_blank'
},
{
label: 'Romain Hamel',
suffix: 'romhml',
to: 'https://github.com/romhml',
target: '_blank'
},
{
label: 'Haytham A. Salama',
suffix: 'Haythamasalama',
to: 'https://github.com/Haythamasalama',
target: '_blank'
},
{
label: 'Daniel Roe',
suffix: 'danielroe',
to: 'https://github.com/danielroe',
target: '_blank'
},
{
label: 'Neil Richter',
suffix: 'noook',
to: 'https://github.com/noook',
target: '_blank'
}
]
}
]
const groups = [{
id: 'settings',
items: [
{
label: 'Profile',
icon: 'i-lucide-user',
kbds: ['meta', 'P']
},
{
label: 'Billing',
icon: 'i-lucide-credit-card',
kbds: ['meta', 'B'],
slot: 'billing' as const
},
{
label: 'Notifications',
icon: 'i-lucide-bell'
},
{
label: 'Security',
icon: 'i-lucide-lock'
}
]
}, {
id: 'users',
label: 'Users',
slot: 'users' as const,
items: [
{
label: 'Benjamin Canac',
suffix: 'benjamincanac',
to: 'https://github.com/benjamincanac',
target: '_blank'
},
{
label: 'Sylvain Marroufin',
suffix: 'smarroufin',
to: 'https://github.com/smarroufin',
target: '_blank'
},
{
label: 'Sébastien Chopin',
suffix: 'atinux',
to: 'https://github.com/atinux',
target: '_blank'
},
{
label: 'Romain Hamel',
suffix: 'romhml',
to: 'https://github.com/romhml',
target: '_blank'
},
{
label: 'Haytham A. Salama',
suffix: 'Haythamasalama',
to: 'https://github.com/Haythamasalama',
target: '_blank'
},
{
label: 'Daniel Roe',
suffix: 'danielroe',
to: 'https://github.com/danielroe',
target: '_blank'
},
{
label: 'Neil Richter',
suffix: 'noook',
to: 'https://github.com/noook',
target: '_blank'
}
]
}]
</script>
<template>

View File

@@ -3,18 +3,14 @@ import type { ContextMenuItem } from '@nuxt/ui'
const loading = ref(true)
const items = [
{
label: 'Refresh the Page',
slot: 'refresh' as const
},
{
label: 'Clear Cookies and Refresh'
},
{
label: 'Clear Cache and Refresh'
}
] satisfies ContextMenuItem[]
const items: ContextMenuItem[] = [{
label: 'Refresh the Page',
slot: 'refresh'
}, {
label: 'Clear Cookies and Refresh'
}, {
label: 'Clear Cache and Refresh'
}]
</script>
<template>

View File

@@ -14,7 +14,7 @@ const validate = (state: any): FormError[] => {
}
const toast = useToast()
async function onSubmit(event: FormSubmitEvent<typeof state>) {
async function onSubmit(event: FormSubmitEvent<any>) {
toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' })
console.log(event.data)
}

View File

@@ -34,10 +34,9 @@ const schema = z.object({
pin: z.string().regex(/^\d$/).array().length(5)
})
type Input = z.input<typeof schema>
type Output = z.output<typeof schema>
type Schema = z.input<typeof schema>
const state = reactive<Partial<Input>>({})
const state = reactive<Partial<Schema>>({})
const form = useTemplateRef('form')
@@ -48,7 +47,7 @@ const items = [
]
const toast = useToast()
async function onSubmit(event: FormSubmitEvent<Output>) {
async function onSubmit(event: FormSubmitEvent<any>) {
toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' })
console.log(event.data)
}

View File

@@ -15,7 +15,7 @@ const state = reactive({
})
const toast = useToast()
async function onSubmit(event: FormSubmitEvent<typeof state>) {
async function onSubmit(event: FormSubmitEvent<any>) {
toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' })
console.log(event.data)
}

View File

@@ -18,7 +18,7 @@ type NestedSchema = z.output<typeof nestedSchema>
const state = reactive<Partial<Schema & NestedSchema>>({ })
const toast = useToast()
async function onSubmit(event: FormSubmitEvent<Schema>) {
async function onSubmit(event: FormSubmitEvent<any>) {
toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' })
console.log(event.data)
}
@@ -39,7 +39,7 @@ async function onSubmit(event: FormSubmitEvent<Schema>) {
<UCheckbox v-model="state.news" name="news" label="Register to our newsletter" @update:model-value="state.email = undefined" />
</div>
<UForm v-if="state.news" :state="state" :schema="nestedSchema" nested>
<UForm v-if="state.news" :state="state" :schema="nestedSchema">
<UFormField label="Email" name="email">
<UInput v-model="state.email" placeholder="john@lennon.com" />
</UFormField>

View File

@@ -34,7 +34,7 @@ function removeItem() {
const toast = useToast()
async function onSubmit(event: FormSubmitEvent<Schema>) {
async function onSubmit(event: FormSubmitEvent<any>) {
toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' })
console.log(event.data)
}
@@ -51,14 +51,7 @@ async function onSubmit(event: FormSubmitEvent<Schema>) {
<UInput v-model="state.customer" placeholder="Wonka Industries" />
</UFormField>
<UForm
v-for="item, count in state.items"
:key="count"
:state="item"
:schema="itemSchema"
nested
class="flex gap-2"
>
<UForm v-for="item, count in state.items" :key="count" :state="item" :schema="itemSchema" class="flex gap-2">
<UFormField :label="!count ? 'Description' : undefined" name="description">
<UInput v-model="item.description" />
</UFormField>

View File

@@ -14,7 +14,7 @@ const validate = (state: any): FormError[] => {
}
const toast = useToast()
async function onSubmit(event: FormSubmitEvent<typeof state>) {
async function onSubmit(event: FormSubmitEvent<any>) {
toast.add({ title: 'Success', description: 'The form has been submitted.', color: 'success' })
console.log(event.data)
}

View File

@@ -1,21 +1,23 @@
<script setup lang="ts">
import type { NavigationMenuItem } from '@nuxt/ui'
const items = [
const items: NavigationMenuItem[] = [
{
label: 'Guide',
icon: 'i-lucide-book-open'
},
{
label: 'Composables',
icon: 'i-lucide-database'
},
{
label: 'Components',
icon: 'i-lucide-box',
slot: 'components' as const
slot: 'components'
}
] satisfies NavigationMenuItem[]
]
</script>
<template>

View File

@@ -1,23 +1,23 @@
<script setup lang="ts">
import type { StepperItem } from '@nuxt/ui'
const items = [
const items: StepperItem[] = [
{
slot: 'address' as const,
slot: 'address',
title: 'Address',
description: 'Add your address here',
icon: 'i-lucide-house'
}, {
slot: 'shipping' as const,
slot: 'shipping',
title: 'Shipping',
description: 'Set your preferred shipping method',
icon: 'i-lucide-truck'
}, {
slot: 'checkout' as const,
slot: 'checkout',
title: 'Checkout',
description: 'Confirm your order'
}
] satisfies StepperItem[]
]
</script>
<template>

View File

@@ -3,14 +3,17 @@ import type { StepperItem } from '@nuxt/ui'
const items: StepperItem[] = [
{
slot: 'address',
title: 'Address',
description: 'Add your address here',
icon: 'i-lucide-house'
}, {
slot: 'shipping',
title: 'Shipping',
description: 'Set your preferred shipping method',
icon: 'i-lucide-truck'
}, {
slot: 'checkout',
title: 'Checkout',
description: 'Confirm your order'
}

View File

@@ -6,23 +6,21 @@ const items = [
label: 'app/',
slot: 'app' as const,
defaultExpanded: true,
children: [
{
label: 'composables/',
children: [
{ label: 'useAuth.ts', icon: 'i-vscode-icons-file-type-typescript' },
{ label: 'useUser.ts', icon: 'i-vscode-icons-file-type-typescript' }
]
},
{
label: 'components/',
defaultExpanded: true,
children: [
{ label: 'Card.vue', icon: 'i-vscode-icons-file-type-vue' },
{ label: 'Button.vue', icon: 'i-vscode-icons-file-type-vue' }
]
}
]
children: [{
label: 'composables/',
children: [
{ label: 'useAuth.ts', icon: 'i-vscode-icons-file-type-typescript' },
{ label: 'useUser.ts', icon: 'i-vscode-icons-file-type-typescript' }
]
},
{
label: 'components/',
defaultExpanded: true,
children: [
{ label: 'Card.vue', icon: 'i-vscode-icons-file-type-vue' },
{ label: 'Button.vue', icon: 'i-vscode-icons-file-type-vue' }
]
}]
},
{ label: 'app.vue', icon: 'i-vscode-icons-file-type-vue' },
{ label: 'nuxt.config.ts', icon: 'i-vscode-icons-file-type-nuxt' }

View File

@@ -5,24 +5,22 @@ const items: TreeItem[] = [
{
label: 'app/',
value: 'app',
children: [
{
label: 'composables/',
value: 'composables',
children: [
{ label: 'useAuth.ts', icon: 'i-vscode-icons-file-type-typescript' },
{ label: 'useUser.ts', icon: 'i-vscode-icons-file-type-typescript' }
]
},
{
label: 'components/',
value: 'components',
children: [
{ label: 'Card.vue', icon: 'i-vscode-icons-file-type-vue' },
{ label: 'Button.vue', icon: 'i-vscode-icons-file-type-vue' }
]
}
]
children: [{
label: 'composables/',
value: 'composables',
children: [
{ label: 'useAuth.ts', icon: 'i-vscode-icons-file-type-typescript' },
{ label: 'useUser.ts', icon: 'i-vscode-icons-file-type-typescript' }
]
},
{
label: 'components/',
value: 'components',
children: [
{ label: 'Card.vue', icon: 'i-vscode-icons-file-type-vue' },
{ label: 'Button.vue', icon: 'i-vscode-icons-file-type-vue' }
]
}]
},
{ label: 'app.vue', icon: 'i-vscode-icons-file-type-vue' },
{ label: 'nuxt.config.ts', icon: 'i-vscode-icons-file-type-nuxt' }

View File

@@ -5,23 +5,21 @@ const items: TreeItem[] = [
{
label: 'app/',
defaultExpanded: true,
children: [
{
label: 'composables/',
children: [
{ label: 'useAuth.ts', icon: 'i-vscode-icons-file-type-typescript' },
{ label: 'useUser.ts', icon: 'i-vscode-icons-file-type-typescript' }
]
},
{
label: 'components/',
defaultExpanded: true,
children: [
{ label: 'Card.vue', icon: 'i-vscode-icons-file-type-vue' },
{ label: 'Button.vue', icon: 'i-vscode-icons-file-type-vue' }
]
}
]
children: [{
label: 'composables/',
children: [
{ label: 'useAuth.ts', icon: 'i-vscode-icons-file-type-typescript' },
{ label: 'useUser.ts', icon: 'i-vscode-icons-file-type-typescript' }
]
},
{
label: 'components/',
defaultExpanded: true,
children: [
{ label: 'Card.vue', icon: 'i-vscode-icons-file-type-vue' },
{ label: 'Button.vue', icon: 'i-vscode-icons-file-type-vue' }
]
}]
},
{ label: 'app.vue', icon: 'i-vscode-icons-file-type-vue' },
{ label: 'nuxt.config.ts', icon: 'i-vscode-icons-file-type-nuxt' }

View File

@@ -8,23 +8,21 @@ const items: TreeItem[] = [
onSelect: (e: Event) => {
e.preventDefault()
},
children: [
{
label: 'composables/',
children: [
{ label: 'useAuth.ts', icon: 'i-vscode-icons-file-type-typescript' },
{ label: 'useUser.ts', icon: 'i-vscode-icons-file-type-typescript' }
]
},
{
label: 'components/',
defaultExpanded: true,
children: [
{ label: 'Card.vue', icon: 'i-vscode-icons-file-type-vue' },
{ label: 'Button.vue', icon: 'i-vscode-icons-file-type-vue' }
]
}
]
children: [{
label: 'composables/',
children: [
{ label: 'useAuth.ts', icon: 'i-vscode-icons-file-type-typescript' },
{ label: 'useUser.ts', icon: 'i-vscode-icons-file-type-typescript' }
]
},
{
label: 'components/',
defaultExpanded: true,
children: [
{ label: 'Card.vue', icon: 'i-vscode-icons-file-type-vue' },
{ label: 'Button.vue', icon: 'i-vscode-icons-file-type-vue' }
]
}]
},
{ label: 'app.vue', icon: 'i-vscode-icons-file-type-vue' },
{ label: 'nuxt.config.ts', icon: 'i-vscode-icons-file-type-nuxt' }

View File

@@ -8,23 +8,21 @@ const items: TreeItem[] = [
onToggle: (e: Event) => {
e.preventDefault()
},
children: [
{
label: 'composables/',
children: [
{ label: 'useAuth.ts', icon: 'i-vscode-icons-file-type-typescript' },
{ label: 'useUser.ts', icon: 'i-vscode-icons-file-type-typescript' }
]
},
{
label: 'components/',
defaultExpanded: true,
children: [
{ label: 'Card.vue', icon: 'i-vscode-icons-file-type-vue' },
{ label: 'Button.vue', icon: 'i-vscode-icons-file-type-vue' }
]
}
]
children: [{
label: 'composables/',
children: [
{ label: 'useAuth.ts', icon: 'i-vscode-icons-file-type-typescript' },
{ label: 'useUser.ts', icon: 'i-vscode-icons-file-type-typescript' }
]
},
{
label: 'components/',
defaultExpanded: true,
children: [
{ label: 'Card.vue', icon: 'i-vscode-icons-file-type-vue' },
{ label: 'Button.vue', icon: 'i-vscode-icons-file-type-vue' }
]
}]
},
{ label: 'app.vue', icon: 'i-vscode-icons-file-type-vue' },
{ label: 'nuxt.config.ts', icon: 'i-vscode-icons-file-type-nuxt' }

View File

@@ -33,7 +33,7 @@ const activating = ref(false)
const successMessage = ref()
const errorMessage = ref('')
async function submit(event: FormSubmitEvent<Schema>) {
async function submit(event: FormSubmitEvent<any>) {
activating.value = true
errorMessage.value = ''
successMessage.value = ''

View File

@@ -16,12 +16,12 @@ function handleMessage(message) {
async function handleFormatMessage(message) {
if (!globalThis.prettier) {
await Promise.all([
import('https://cdn.jsdelivr.net/npm/prettier@3.5.2/standalone.js'),
import('https://cdn.jsdelivr.net/npm/prettier@3.5.2/plugins/babel.js'),
import('https://cdn.jsdelivr.net/npm/prettier@3.5.2/plugins/estree.js'),
import('https://cdn.jsdelivr.net/npm/prettier@3.5.2/plugins/html.js'),
import('https://cdn.jsdelivr.net/npm/prettier@3.5.2/plugins/markdown.js'),
import('https://cdn.jsdelivr.net/npm/prettier@3.5.2/plugins/typescript.js')
import('https://unpkg.com/prettier@3.5.2/standalone.js'),
import('https://unpkg.com/prettier@3.5.2/plugins/babel.js'),
import('https://unpkg.com/prettier@3.5.2/plugins/estree.js'),
import('https://unpkg.com/prettier@3.5.2/plugins/html.js'),
import('https://unpkg.com/prettier@3.5.2/plugins/markdown.js'),
import('https://unpkg.com/prettier@3.5.2/plugins/typescript.js')
])
}

View File

@@ -180,7 +180,7 @@ In Nuxt UI v2, we had a mix between a design system with `primary`, `gray`, `err
This change introduces several breaking changes that you need to be aware of:
- The `gray` color has been renamed to `neutral`
1. The `gray` color has been renamed to `neutral`
```diff
<template>
@@ -203,7 +203,7 @@ You can also use the new [design tokens](/getting-started/theme#neutral-palette)
```
::
- The `DEFAULT` shade that let you write `text-primary` no longer exists, you can use [color shades](/getting-started/theme#color-shades) instead:
2. The `DEFAULT` shade that let you write `text-primary` no longer exists, you can use [color shades](/getting-started/theme#color-shades) instead:
```diff
<template>
@@ -212,7 +212,7 @@ You can also use the new [design tokens](/getting-started/theme#neutral-palette)
</template>
```
- The `gray`, `black` and `white` in the `color` props have been removed in favor of `neutral`:
3. The `gray`, `black` and `white` in the `color` props have been removed in favor of `neutral`:
```diff
- <UButton color="black" />
@@ -225,7 +225,7 @@ You can also use the new [design tokens](/getting-started/theme#neutral-palette)
+ <UButton color="neutral" variant="outline" />
```
- You can no longer use Tailwind CSS colors in the `color` props, use the new aliases instead:
4. You can no longer use Tailwind CSS colors in the `color` props, use the new aliases instead:
```diff
- <UButton color="red" />
@@ -236,7 +236,7 @@ You can also use the new [design tokens](/getting-started/theme#neutral-palette)
Learn how to extend the design system to add new color aliases.
::
- The color configuration in `app.config.ts` has been moved into a `colors` object:
5. The color configuration in `app.config.ts` has been moved into a `colors` object:
```diff
export default defineAppConfig({
@@ -255,7 +255,7 @@ export default defineAppConfig({
Nuxt UI components are now styled using the [Tailwind Variants API](/getting-started/theme#components-theme), which makes all the overrides you made using the `app.config.ts` and the `ui` prop obsolete.
- Update your [`app.config.ts`](/getting-started/theme#config) to override components with their new theme:
1. Update your [`app.config.ts`](/getting-started/theme#config) to override components with their new theme:
```diff
export default defineAppConfig({
@@ -278,7 +278,7 @@ export default defineAppConfig({
})
```
- Update your [`ui` props](/getting-started/theme#props) to override each component's slots using their new theme:
2. Update your [`ui` props](/getting-started/theme#props) to override each component's slots using their new theme:
```diff
<template>
@@ -351,7 +351,7 @@ Here are the Nuxt UI Pro components that have been renamed or removed:
In addition to the renamed components, there are lots of changes to the components API. Let's detail the most important ones:
- The `links` and `options` props have been renamed to `items` for consistency:
1. The `links` and `options` props have been renamed to `items` for consistency:
```diff
<template>
@@ -367,25 +367,7 @@ 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`.
::
- The `click` field in different components has been removed in favor of the native Vue `onClick` event:
```diff
<script setup lang="ts">
const items = [{
label: 'Edit',
- click: () => {
+ onClick: () => {
console.log('Edit')
}
}]
</script>
```
::note
This change affects the `Toast` component as well as all component that have `items` links like `NavigationMenu`, `DropdownMenu`, `CommandPalette`, etc.
::
- The global `Modals`, `Slideovers` and `Notifications` components have been removed in favor the [App](/components/app) component:
2. The global `Modals`, `Slideovers` and `Notifications` components have been removed in favor the [App](/components/app) component:
```diff [app.vue]
<template>
@@ -398,7 +380,7 @@ This change affects the `Toast` component as well as all component that have `it
</template>
```
- The `v-model:open` directive and `default-open` prop are now used to control visibility:
3. The `v-model:open` directive and `default-open` prop are now used to control visibility:
```diff
<template>
@@ -411,7 +393,7 @@ This change affects the `Toast` component as well as all component that have `it
This change affects the following components: `ContextMenu`, `Modal` and `Slideover` and enables controlling visibility for `InputMenu`, `Select`, `SelectMenu` and `Tooltip`.
::
- 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):
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">
@@ -438,7 +420,7 @@ This change affects the following components: `ContextMenu`, `Modal` and `Slideo
This change affects the following components: `Modal`, `Popover`, `Slideover`, `Tooltip`.
::
- A `#header`, `#body` and `#footer` slots have been added inside the `#content` slot like the `Card` component:
5. A `#header`, `#body` and `#footer` slots have been added inside the `#content` slot like the `Card` component:
```diff
<template>
@@ -457,9 +439,10 @@ This change affects the following components: `Modal`, `Popover`, `Slideover`, `
This change affects the following components: `Modal`, `Slideover`.
::
### Changed composables
- The `useToast()` composable `timeout` prop has been renamed to `duration`:
1. The `useToast()` composable `timeout` prop has been renamed to `duration`:
```diff
<script setup lang="ts">
@@ -470,7 +453,7 @@ const toast = useToast()
</script>
```
- The `useModal` and `useSlideover` composables have been removed in favor of a more generic `useOverlay` composable:
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

View File

@@ -32,7 +32,6 @@ Use the `items` prop as an array of objects with the following properties:
- [`slot?: string`{lang="ts-type"}](#with-custom-slot)
- `onSelect?(e: Event): void`{lang="ts-type"}
- [`onUpdateChecked?(checked: boolean): void`{lang="ts-type"}](#with-checkbox-items)
- `children?: ContextMenuItem[] | ContextMenuItem[][]`{lang="ts-type"}
You can pass any property from the [Link](/components/link#props) component such as `to`, `target`, etc.

View File

@@ -32,7 +32,6 @@ Use the `items` prop as an array of objects with the following properties:
- [`slot?: string`{lang="ts-type"}](#with-custom-slot)
- `onSelect?(e: Event): void`{lang="ts-type"}
- [`onUpdateChecked?(checked: boolean): void`{lang="ts-type"}](#with-checkbox-items)
- `children?: DropdownMenuItem[] | DropdownMenuItem[][]`{lang="ts-type"}
You can pass any property from the [Link](/components/link#props) component such as `to`, `target`, etc.

View File

@@ -32,7 +32,7 @@ It requires two props:
class: 'w-60'
---
::
::component-example{label="Zod"}
---
name: 'form-example-zod'
@@ -206,7 +206,3 @@ This will give you access to the following:
| `dirtyFields`{lang="ts-type"} | `DeepReadonly<Set<keyof T>>`{lang="ts-type"} Tracks fields that have been modified by the user. |
| `touchedFields`{lang="ts-type"} | `DeepReadonly<Set<keyof T>>`{lang="ts-type"} Tracks fields that the user interacted with. |
| `blurredFields`{lang="ts-type"} | `DeepReadonly<Set<keyof T>>`{lang="ts-type"} Tracks fields blurred by the user. |
## Theme
:component-theme

View File

@@ -28,7 +28,6 @@ Use the `items` prop as an array of objects with the following properties:
- `class?: any`{lang="ts-type"}
- [`slot?: string`{lang="ts-type"}](#with-custom-slot)
- `onSelect?(e: Event): void`{lang="ts-type"}
- `children?: NavigationMenuChildItem[]`{lang="ts-type"}
You can pass any property from the [Link](/components/link#props) component such as `to`, `target`, etc.

View File

@@ -15,8 +15,6 @@ items:
url: https://www.juno.one/
- name: Kassebil
url: https://kassebil.dk/
- name: LearnVue
url: https://learnvue.co/
- name: Mawrble
url: https://mawrble.com/
- name: Meet Sponsors

View File

@@ -175,10 +175,6 @@ export default defineNuxtConfig({
}
},
hub: {
ai: true
},
vite: {
plugins: [
yaml()

View File

@@ -3,23 +3,22 @@
"name": "@nuxt/ui-docs",
"type": "module",
"dependencies": {
"@ai-sdk/vue": "^1.2.8",
"@iconify-json/logos": "^1.2.4",
"@iconify-json/lucide": "^1.2.36",
"@iconify-json/simple-icons": "^1.2.32",
"@iconify-json/vscode-icons": "^1.2.19",
"@iconify-json/lucide": "^1.2.34",
"@iconify-json/simple-icons": "^1.2.30",
"@iconify-json/vscode-icons": "^1.2.18",
"@nuxt/content": "^3.4.0",
"@nuxt/image": "^1.10.0",
"@nuxt/ui": "latest",
"@nuxt/ui-pro": "https://pkg.pr.new/@nuxt/ui-pro@4757a1e",
"@nuxthub/core": "^0.8.24",
"@nuxt/ui-pro": "https://pkg.pr.new/@nuxt/ui-pro@63da8be",
"@nuxthub/core": "^0.8.23",
"@nuxtjs/plausible": "^1.2.0",
"@octokit/rest": "^21.1.1",
"@rollup/plugin-yaml": "^4.1.2",
"@vueuse/integrations": "^13.1.0",
"@vueuse/nuxt": "^13.1.0",
"ai": "^4.3.6",
"@vueuse/nuxt": "^13.0.0",
"capture-website": "^4.2.0",
"@vueuse/integrations": "^13.0.0",
"sortablejs": "^1.15.6",
"joi": "^17.13.3",
"motion-v": "0.13.1",
"nuxt": "^3.16.2",
@@ -28,15 +27,13 @@
"nuxt-og-image": "^5.1.1",
"prettier": "^3.5.3",
"shiki-transformer-color-highlight": "^1.0.0",
"sortablejs": "^1.15.6",
"superstruct": "^2.0.2",
"ufo": "^1.6.1",
"ufo": "^1.5.4",
"valibot": "^1.0.0",
"workers-ai-provider": "^0.3.0",
"yup": "^1.6.1",
"zod": "^3.24.2"
},
"devDependencies": {
"wrangler": "^4.10.0"
"wrangler": "^4.7.2"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 558 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

@@ -1,22 +0,0 @@
import { streamText } from 'ai'
import { createWorkersAI } from 'workers-ai-provider'
export default defineEventHandler(async (event) => {
const { messages } = await readBody(event)
// Enable AI Gateway if defined in environment variables
const gateway = process.env.CLOUDFLARE_AI_GATEWAY_ID
? {
id: process.env.CLOUDFLARE_AI_GATEWAY_ID,
cacheTtl: 60 * 60 * 24 // 24 hours
}
: undefined
const workersAI = createWorkersAI({ binding: hubAI(), gateway })
return streamText({
model: workersAI('@cf/meta/llama-3.2-3b-instruct'),
maxTokens: 10000,
system: 'You are a helpful assistant that can answer questions and help.',
messages
}).toDataStreamResponse()
})

View File

@@ -2,7 +2,7 @@
"name": "@nuxt/ui",
"description": "A UI Library for Modern Web Apps, powered by Vue & Tailwind CSS.",
"version": "3.0.2",
"packageManager": "pnpm@10.8.0",
"packageManager": "pnpm@10.7.1",
"repository": {
"type": "git",
"url": "git+https://github.com/nuxt/ui.git"
@@ -30,10 +30,6 @@
"./runtime/*": "./dist/runtime/*",
"./components/*": "./dist/runtime/components/*",
"./composables/*": "./dist/runtime/composables/*",
"./utils": {
"types": "./dist/runtime/utils/index.d.ts",
"import": "./dist/runtime/utils/index.js"
},
"./utils/*": {
"types": "./dist/runtime/utils/*.d.ts",
"import": "./dist/runtime/utils/*.js"
@@ -66,9 +62,6 @@
"composables/*": [
"./dist/runtime/composables/*"
],
"utils": [
"./dist/runtime/utils/index.d.ts"
],
"utils/*": [
"./dist/runtime/utils/*.d.ts"
],
@@ -96,13 +89,13 @@
"scripts": {
"build": "nuxt-module-build build",
"prepack": "pnpm build",
"dev": "nuxi dev playground",
"dev": "DEV=true nuxi dev playground",
"dev:build": "nuxi build playground",
"dev:vue": "vite playground-vue",
"dev:vue": "DEV=true vite playground-vue",
"dev:vue:build": "vite build playground-vue",
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground && nuxi prepare docs && vite build playground-vue",
"docs": "nuxi dev docs",
"docs:build": "nuxi build docs",
"docs": "DEV=true nuxi dev 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",
@@ -113,10 +106,10 @@
},
"dependencies": {
"@iconify/vue": "^4.3.0",
"@internationalized/date": "^3.8.0",
"@internationalized/number": "^3.6.1",
"@internationalized/date": "^3.7.0",
"@internationalized/number": "^3.6.0",
"@nuxt/fonts": "^0.11.1",
"@nuxt/icon": "^1.12.0",
"@nuxt/icon": "^1.11.0",
"@nuxt/kit": "^3.16.2",
"@nuxt/schema": "^3.16.2",
"@nuxtjs/color-mode": "^3.5.2",
@@ -124,9 +117,9 @@
"@tailwindcss/postcss": "^4.1.3",
"@tailwindcss/vite": "^4.1.3",
"@tanstack/vue-table": "^8.21.2",
"@unhead/vue": "^2.0.5",
"@vueuse/core": "^13.1.0",
"@vueuse/integrations": "^13.1.0",
"@unhead/vue": "^2.0.3",
"@vueuse/core": "^13.0.0",
"@vueuse/integrations": "^13.0.0",
"colortranslator": "^4.1.0",
"consola": "^3.4.2",
"defu": "^6.1.4",
@@ -149,14 +142,16 @@
"tailwind-variants": "^1.0.0",
"tailwindcss": "^4.1.3",
"tinyglobby": "^0.2.12",
"unplugin": "^2.3.2",
"unplugin": "^2.2.2",
"unplugin-auto-import": "^19.1.2",
"unplugin-vue-components": "^28.5.0",
"vaul-vue": "^0.4.1"
"unplugin-vue-components": "^28.4.1",
"vaul-vue": "^0.4.1",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@nuxt/eslint-config": "^1.3.0",
"@nuxt/module-builder": "^1.0.1",
"@nuxt/module-builder": "^1.0.0",
"@nuxt/test-utils": "^3.17.2",
"@release-it/conventional-changelog": "^10.0.0",
"@vue/test-utils": "^2.4.6",
@@ -170,19 +165,14 @@
"vue-tsc": "^2.2.0"
},
"peerDependencies": {
"@inertiajs/vue3": "^2.0.7",
"joi": "^17.13.0",
"superstruct": "^2.0.0",
"typescript": "^5.6.3",
"valibot": "^1.0.0",
"vue-router": "^4.5.0",
"yup": "^1.6.0",
"zod": "^3.24.0"
},
"peerDependenciesMeta": {
"@inertiajs/vue3": {
"optional": true
},
"joi": {
"optional": true
},
@@ -192,9 +182,6 @@
"superstruct": {
"optional": true
},
"vue-router": {
"optional": true
},
"yup": {
"optional": true
},
@@ -207,7 +194,7 @@
"chokidar": "3.6.0",
"debug": "4.3.7",
"rollup": "4.34.9",
"unplugin": "^2.3.2",
"unplugin": "^2.2.2",
"vue-tsc": "2.2.0"
},
"pnpm": {

View File

@@ -18,7 +18,7 @@
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.3",
"typescript": "^5.8.3",
"vite": "^6.2.6",
"vite": "^6.2.5",
"vue-tsc": "^2.2.0"
}
}

View File

@@ -1,71 +0,0 @@
<template>
<UContainer>
<UForm :schema :state @submit="onSubmit">
<UFormField label="A" name="a">
<UInput v-model="state.a" />
</UFormField>
<UFormField label="B" name="b">
<UInput v-model="state.b" />
</UFormField>
<UButton type="submit">
Submit
</UButton>
</UForm>
{{ output }}
</UContainer>
</template>
<script lang="ts" setup>
import type { FormSubmitEvent } from '@nuxt/ui'
import * as v from 'valibot'
const _schemaStringFiltered = v.pipe(v.string(), v.trim())
const schema = v.object({
a: v.string(),
b: v.union([
v.pipe(
v.array(_schemaStringFiltered),
v.filterItems((item, index, array) => (array.indexOf(item) === index || item !== ''))
),
v.pipe(
v.string(),
v.trim(),
v.transform(
(item) => {
if (item === '') return undefined
return item
.split(',')
.map(val => val.trim())
.filter(val => val !== '')
}
)
)
])
})
const state = reactive<{
a: string
b: string
}>({
a: 'hello, world',
b: 'hello, world'
})
const output = reactive<{
a: string
b?: string[]
}>({
a: '',
b: []
})
function onSubmit(event: FormSubmitEvent<v.InferOutput<typeof schema>>) {
console.log('typeof `a`:', typeof event.data.a) // should be string
console.log('typeof `b`:', typeof event.data.b) // should be object (array of strings)
output.a = event.data.a
output.b = event.data.b
}
</script>

View File

@@ -59,14 +59,6 @@ const items = [{
<template #custom="{ item }">
<span class="text-(--ui-text-muted)">Custom: {{ item.content }}</span>
</template>
<template #list-trailing>
<UButton
icon="lucide:refresh-cw"
variant="soft"
class="ml-2"
/>
</template>
</UTabs>
</div>
</div>

View File

@@ -8,10 +8,10 @@
"generate": "nuxi generate"
},
"dependencies": {
"@iconify-json/lucide": "^1.2.36",
"@iconify-json/simple-icons": "^1.2.32",
"@iconify-json/lucide": "^1.2.34",
"@iconify-json/simple-icons": "^1.2.30",
"@nuxt/ui": "latest",
"@nuxthub/core": "^0.8.24",
"@nuxthub/core": "^0.8.23",
"nuxt": "^3.16.2",
"zod": "^3.24.2"
}

1105
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -18,9 +18,6 @@ export default function ComponentImportPlugin(options: NuxtUIOptions & { prefix:
const overrides = globSync('**/*.vue', { cwd: join(runtimeDir, 'vue/components') })
const overrideNames = new Set(overrides.map(c => `${options.prefix}${c.replace(/\.vue$/, '')}`))
const inertiaOverrides = globSync('**/*.vue', { cwd: join(runtimeDir, 'inertia/components') })
const inertiaOverrideNames = new Set(inertiaOverrides.map(c => `${options.prefix}${c.replace(/\.vue$/, '')}`))
const pluginOptions = defu(options.components, <ComponentsOptions>{
dts: options.dts ?? true,
exclude: [
@@ -30,9 +27,6 @@ export default function ComponentImportPlugin(options: NuxtUIOptions & { prefix:
],
resolvers: [
(componentName) => {
if (options.inertia && inertiaOverrideNames.has(componentName)) {
return { name: 'default', from: join(runtimeDir, 'inertia/components', `${componentName.slice(options.prefix.length)}.vue`) }
}
if (overrideNames.has(componentName))
return { name: 'default', from: join(runtimeDir, 'vue/components', `${componentName.slice(options.prefix.length)}.vue`) }
if (componentNames.has(componentName))
@@ -61,9 +55,6 @@ export default function ComponentImportPlugin(options: NuxtUIOptions & { prefix:
}
const filename = id.match(/([^/]+)\.vue$/)?.[1]
if (filename && options.inertia && inertiaOverrideNames.has(`${options.prefix}${filename}`)) {
return join(runtimeDir, 'inertia/components', `${filename}.vue`)
}
if (filename && overrideNames.has(`${options.prefix}${filename}`)) {
return join(runtimeDir, 'vue/components', `${filename}.vue`)
}

View File

@@ -3,13 +3,13 @@ import { normalize } from 'pathe'
import { resolvePathSync } from 'mlly'
import MagicString from 'magic-string'
import { runtimeDir, type NuxtUIOptions } from '../unplugin'
import { runtimeDir } from '../unplugin'
/**
* This plugin normalises Nuxt environment (#imports) and `import.meta.client` within the Nuxt UI components.
*/
export default function NuxtEnvironmentPlugin(options: NuxtUIOptions) {
const stubPath = resolvePathSync(options.inertia ? '../runtime/inertia/stubs' : '../runtime/vue/stubs', { extensions: ['.ts', '.mjs', '.js'], url: import.meta.url })
export default function NuxtEnvironmentPlugin() {
const stubPath = resolvePathSync('../runtime/vue/stubs', { extensions: ['.ts', '.mjs', '.js'], url: import.meta.url })
return {
name: 'nuxt:ui',

View File

@@ -2,10 +2,14 @@
<script lang="ts">
import type { AccordionRootProps, AccordionRootEmits } from 'reka-ui'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/accordion'
import type { DynamicSlots, ComponentConfig } from '../types/utils'
import { tv } from '../utils/tv'
import type { DynamicSlots } from '../types/utils'
type Accordion = ComponentConfig<typeof theme, AppConfig, 'accordion'>
const appConfigAccordion = _appConfig as AppConfig & { ui: { accordion: Partial<typeof theme> } }
const accordion = tv({ extend: tv(theme), ...(appConfigAccordion.ui?.accordion || {}) })
export interface AccordionItem {
label?: string
@@ -44,7 +48,7 @@ export interface AccordionProps<T extends AccordionItem = AccordionItem> extends
*/
labelKey?: string
class?: any
ui?: Accordion['slots']
ui?: Partial<typeof accordion.slots>
}
export interface AccordionEmits extends AccordionRootEmits {}
@@ -67,7 +71,6 @@ import { AccordionRoot, AccordionItem, AccordionHeader, AccordionTrigger, Accord
import { reactivePick } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { get } from '../utils'
import { tv } from '../utils/tv'
import UIcon from './Icon.vue'
const props = withDefaults(defineProps<AccordionProps<T>>(), {
@@ -79,11 +82,10 @@ const props = withDefaults(defineProps<AccordionProps<T>>(), {
const emits = defineEmits<AccordionEmits>()
const slots = defineSlots<AccordionSlots<T>>()
const appConfig = useAppConfig() as Accordion['AppConfig']
const appConfig = useAppConfig()
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'collapsible', 'defaultValue', 'disabled', 'modelValue', 'type', 'unmountOnHide'), emits)
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.accordion || {}) })({
const ui = computed(() => accordion({
disabled: props.disabled
}))
</script>

View File

@@ -1,10 +1,16 @@
<script lang="ts">
import type { VariantProps } from 'tailwind-variants'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/alert'
import { tv } from '../utils/tv'
import type { AvatarProps, ButtonProps } from '../types'
import type { ComponentConfig } from '../types/utils'
type Alert = ComponentConfig<typeof theme, AppConfig, 'alert'>
const appConfigAlert = _appConfig as AppConfig & { ui: { alert: Partial<typeof theme> } }
const alert = tv({ extend: tv(theme), ...(appConfigAlert.ui?.alert || {}) })
type AlertVariants = VariantProps<typeof alert>
export interface AlertProps {
/**
@@ -22,16 +28,16 @@ export interface AlertProps {
/**
* @defaultValue 'primary'
*/
color?: Alert['variants']['color']
color?: AlertVariants['color']
/**
* @defaultValue 'solid'
*/
variant?: Alert['variants']['variant']
variant?: AlertVariants['variant']
/**
* The orientation between the content and the actions.
* @defaultValue 'vertical'
*/
orientation?: Alert['variants']['orientation']
orientation?: AlertVariants['orientation']
/**
* Display a list of actions:
* - under the title and description when orientation is `vertical`
@@ -53,7 +59,7 @@ export interface AlertProps {
*/
closeIcon?: string
class?: any
ui?: Alert['slots']
ui?: Partial<typeof alert.slots>
}
export interface AlertEmits {
@@ -65,7 +71,7 @@ export interface AlertSlots {
title(props?: {}): any
description(props?: {}): any
actions(props?: {}): any
close(props: { ui: { [K in keyof Required<Alert['slots']>]: (props?: Record<string, any>) => string } }): any
close(props: { ui: ReturnType<typeof alert> }): any
}
</script>
@@ -74,7 +80,6 @@ import { computed } from 'vue'
import { Primitive } from 'reka-ui'
import { useAppConfig } from '#imports'
import { useLocale } from '../composables/useLocale'
import { tv } from '../utils/tv'
import UIcon from './Icon.vue'
import UAvatar from './Avatar.vue'
import UButton from './Button.vue'
@@ -86,9 +91,9 @@ const emits = defineEmits<AlertEmits>()
const slots = defineSlots<AlertSlots>()
const { t } = useLocale()
const appConfig = useAppConfig() as Alert['AppConfig']
const appConfig = useAppConfig()
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.alert || {}) })({
const ui = computed(() => alert({
color: props.color,
variant: props.variant,
orientation: props.orientation,

View File

@@ -1,9 +1,15 @@
<script lang="ts">
import type { VariantProps } from 'tailwind-variants'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/avatar'
import type { ComponentConfig } from '../types/utils'
import { tv } from '../utils/tv'
type Avatar = ComponentConfig<typeof theme, AppConfig, 'avatar'>
const appConfigAvatar = _appConfig as AppConfig & { ui: { avatar: Partial<typeof theme> } }
const avatar = tv({ extend: tv(theme), ...(appConfigAvatar.ui?.avatar || {}) })
type AvatarVariants = VariantProps<typeof avatar>
export interface AvatarProps {
/**
@@ -21,10 +27,10 @@ export interface AvatarProps {
/**
* @defaultValue 'md'
*/
size?: Avatar['variants']['size']
size?: AvatarVariants['size']
class?: any
style?: any
ui?: Avatar['slots']
ui?: Partial<typeof avatar.slots>
}
export interface AvatarSlots {
@@ -35,10 +41,8 @@ export interface AvatarSlots {
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { Primitive, Slot } from 'reka-ui'
import { useAppConfig } from '#imports'
import ImageComponent from '#build/ui-image-component'
import { useAvatarGroup } from '../composables/useAvatarGroup'
import { tv } from '../utils/tv'
import UIcon from './Icon.vue'
defineOptions({ inheritAttrs: false })
@@ -47,11 +51,10 @@ const props = withDefaults(defineProps<AvatarProps>(), { as: 'span' })
const fallback = computed(() => props.text || (props.alt || '').split(' ').map(word => word.charAt(0)).join('').substring(0, 2))
const appConfig = useAppConfig() as Avatar['AppConfig']
const { size } = useAvatarGroup(props)
// eslint-disable-next-line vue/no-dupe-keys
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.avatar || {}) })({
const ui = computed(() => avatar({
size: size.value
}))

View File

@@ -1,9 +1,15 @@
<script lang="ts">
import type { VariantProps } from 'tailwind-variants'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/avatar-group'
import type { ComponentConfig } from '../types/utils'
import { tv } from '../utils/tv'
type AvatarGroup = ComponentConfig<typeof theme, AppConfig, 'avatarGroup'>
const appConfigAvatarGroup = _appConfig as AppConfig & { ui: { avatarGroup: Partial<typeof theme> } }
const avatarGroup = tv({ extend: tv(theme), ...(appConfigAvatarGroup.ui?.avatarGroup || {}) })
type AvatarGroupVariants = VariantProps<typeof avatarGroup>
export interface AvatarGroupProps {
/**
@@ -14,13 +20,13 @@ export interface AvatarGroupProps {
/**
* @defaultValue 'md'
*/
size?: AvatarGroup['variants']['size']
size?: AvatarGroupVariants['size']
/**
* The maximum number of avatars to display.
*/
max?: number | string
class?: any
ui?: AvatarGroup['slots']
ui?: Partial<typeof avatarGroup.slots>
}
export interface AvatarGroupSlots {
@@ -31,17 +37,13 @@ export interface AvatarGroupSlots {
<script setup lang="ts">
import { computed, provide } from 'vue'
import { Primitive } from 'reka-ui'
import { useAppConfig } from '#imports'
import { avatarGroupInjectionKey } from '../composables/useAvatarGroup'
import { tv } from '../utils/tv'
import UAvatar from './Avatar.vue'
const props = defineProps<AvatarGroupProps>()
const slots = defineSlots<AvatarGroupSlots>()
const appConfig = useAppConfig() as AvatarGroup['AppConfig']
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.avatarGroup || {}) })({
const ui = computed(() => avatarGroup({
size: props.size
}))

View File

@@ -1,11 +1,17 @@
<script lang="ts">
import type { VariantProps } from 'tailwind-variants'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/badge'
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
import { tv } from '../utils/tv'
import type { AvatarProps } from '../types'
import type { ComponentConfig } from '../types/utils'
type Badge = ComponentConfig<typeof theme, AppConfig, 'badge'>
const appConfigBadge = _appConfig as AppConfig & { ui: { badge: Partial<typeof theme> } }
const badge = tv({ extend: tv(theme), ...(appConfigBadge.ui?.badge || {}) })
type BadgeVariants = VariantProps<typeof badge>
export interface BadgeProps extends Omit<UseComponentIconsProps, 'loading' | 'loadingIcon'> {
/**
@@ -17,17 +23,17 @@ export interface BadgeProps extends Omit<UseComponentIconsProps, 'loading' | 'lo
/**
* @defaultValue 'primary'
*/
color?: Badge['variants']['color']
color?: BadgeVariants['color']
/**
* @defaultValue 'solid'
*/
variant?: Badge['variants']['variant']
variant?: BadgeVariants['variant']
/**
* @defaultValue 'md'
*/
size?: Badge['variants']['size']
size?: BadgeVariants['size']
class?: any
ui?: Badge['slots']
ui?: Partial<typeof badge.slots>
}
export interface BadgeSlots {
@@ -40,10 +46,8 @@ export interface BadgeSlots {
<script setup lang="ts">
import { computed } from 'vue'
import { Primitive } from 'reka-ui'
import { useAppConfig } from '#imports'
import { useButtonGroup } from '../composables/useButtonGroup'
import { useComponentIcons } from '../composables/useComponentIcons'
import { tv } from '../utils/tv'
import UIcon from './Icon.vue'
import UAvatar from './Avatar.vue'
@@ -52,11 +56,10 @@ const props = withDefaults(defineProps<BadgeProps>(), {
})
defineSlots<BadgeSlots>()
const appConfig = useAppConfig() as Badge['AppConfig']
const { orientation, size: buttonGroupSize } = useButtonGroup<BadgeProps>(props)
const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(props)
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.badge || {}) })({
const ui = computed(() => badge({
color: props.color,
variant: props.variant,
size: buttonGroupSize.value || props.size,

View File

@@ -1,11 +1,15 @@
<!-- eslint-disable vue/block-tag-newline -->
<script lang="ts">
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/breadcrumb'
import { tv } from '../utils/tv'
import type { AvatarProps, LinkProps } from '../types'
import type { DynamicSlots, ComponentConfig } from '../types/utils'
import type { DynamicSlots, PartialString } from '../types/utils'
type Breadcrumb = ComponentConfig<typeof theme, AppConfig, 'breadcrumb'>
const appConfigBreadcrumb = _appConfig as AppConfig & { ui: { breadcrumb: Partial<typeof theme> } }
const breadcrumb = tv({ extend: tv(theme), ...(appConfigBreadcrumb.ui?.breadcrumb || {}) })
export interface BreadcrumbItem extends Omit<LinkProps, 'raw' | 'custom'> {
label?: string
@@ -37,7 +41,7 @@ export interface BreadcrumbProps<T extends BreadcrumbItem = BreadcrumbItem> {
*/
labelKey?: string
class?: any
ui?: Breadcrumb['slots']
ui?: PartialString<typeof breadcrumb.slots>
}
type SlotProps<T extends BreadcrumbItem> = (props: { item: T, index: number, active?: boolean }) => any
@@ -58,7 +62,6 @@ import { Primitive } from 'reka-ui'
import { useAppConfig } from '#imports'
import { useLocale } from '../composables/useLocale'
import { get } from '../utils'
import { tv } from '../utils/tv'
import { pickLinkProps } from '../utils/link'
import UIcon from './Icon.vue'
import UAvatar from './Avatar.vue'
@@ -70,14 +73,13 @@ const props = withDefaults(defineProps<BreadcrumbProps<T>>(), {
labelKey: 'label'
})
const slots = defineSlots<BreadcrumbSlots<T>>()
const { dir } = useLocale()
const appConfig = useAppConfig() as Breadcrumb['AppConfig']
const appConfig = useAppConfig()
const separatorIcon = computed(() => props.separatorIcon || (dir.value === 'rtl' ? appConfig.ui.icons.chevronLeft : appConfig.ui.icons.chevronRight))
// eslint-disable-next-line vue/no-dupe-keys
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.breadcrumb || {}) })())
const ui = breadcrumb()
</script>
<template>

View File

@@ -1,29 +1,36 @@
<script lang="ts">
import type { VariantProps } from 'tailwind-variants'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/button'
import type { LinkProps } from './Link.vue'
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
import { tv } from '../utils/tv'
import type { AvatarProps } from '../types'
import type { ComponentConfig } from '../types/utils'
import type { PartialString } from '../types/utils'
type Button = ComponentConfig<typeof theme, AppConfig, 'button'>
const appConfigButton = _appConfig as AppConfig & { ui: { button: Partial<typeof theme> } }
const button = tv({ extend: tv(theme), ...(appConfigButton.ui?.button || {}) })
type ButtonVariants = VariantProps<typeof button>
export interface ButtonProps extends UseComponentIconsProps, Omit<LinkProps, 'raw' | 'custom'> {
label?: string
/**
* @defaultValue 'primary'
*/
color?: Button['variants']['color']
activeColor?: Button['variants']['color']
color?: ButtonVariants['color']
activeColor?: ButtonVariants['color']
/**
* @defaultValue 'solid'
*/
variant?: Button['variants']['variant']
activeVariant?: Button['variants']['variant']
variant?: ButtonVariants['variant']
activeVariant?: ButtonVariants['variant']
/**
* @defaultValue 'md'
*/
size?: Button['variants']['size']
size?: ButtonVariants['size']
/** Render the button with equal padding on all sides. */
square?: boolean
/** Render the button full width. */
@@ -32,7 +39,7 @@ export interface ButtonProps extends UseComponentIconsProps, Omit<LinkProps, 'ra
loadingAuto?: boolean
onClick?: ((event: MouseEvent) => void | Promise<void>) | Array<((event: MouseEvent) => void | Promise<void>)>
class?: any
ui?: Button['slots']
ui?: PartialString<typeof button.slots>
}
export interface ButtonSlots {
@@ -44,14 +51,11 @@ export interface ButtonSlots {
<script setup lang="ts">
import { type Ref, computed, ref, inject } from 'vue'
import { defu } from 'defu'
import { useForwardProps } from 'reka-ui'
import { useAppConfig } from '#imports'
import { useComponentIcons } from '../composables/useComponentIcons'
import { useButtonGroup } from '../composables/useButtonGroup'
import { formLoadingInjectionKey } from '../composables/useFormField'
import { omit } from '../utils'
import { tv } from '../utils/tv'
import { pickLinkProps } from '../utils/link'
import UIcon from './Icon.vue'
import UAvatar from './Avatar.vue'
@@ -65,11 +69,10 @@ const props = withDefaults(defineProps<ButtonProps>(), {
})
const slots = defineSlots<ButtonSlots>()
const appConfig = useAppConfig() as Button['AppConfig']
const { orientation, size: buttonSize } = useButtonGroup<ButtonProps>(props)
const linkProps = useForwardProps(pickLinkProps(props))
const { orientation, size: buttonSize } = useButtonGroup<ButtonProps>(props)
const loadingAutoState = ref(false)
const formLoading = inject<Ref<boolean> | undefined>(formLoadingInjectionKey, undefined)
@@ -92,19 +95,17 @@ const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponen
)
const ui = computed(() => tv({
extend: tv(theme),
...defu({
variants: {
active: {
true: {
base: props.activeClass
},
false: {
base: props.inactiveClass
}
extend: button,
variants: {
active: {
true: {
base: props.activeClass
},
false: {
base: props.inactiveClass
}
}
}, appConfig.ui?.button || {})
}
})({
color: props.color,
variant: props.variant,

View File

@@ -1,9 +1,15 @@
<script lang="ts">
import type { VariantProps } from 'tailwind-variants'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/button-group'
import type { ComponentConfig } from '../types/utils'
import { tv } from '../utils/tv'
type ButtonGroup = ComponentConfig<typeof theme, AppConfig, 'buttonGroup'>
const appConfigButtonGroup = _appConfig as AppConfig & { ui: { buttonGroup: Partial<typeof theme> } }
const buttonGroup = tv({ extend: tv(theme), ...(appConfigButtonGroup.ui?.buttonGroup) })
type ButtonGroupVariants = VariantProps<typeof buttonGroup>
export interface ButtonGroupProps {
/**
@@ -14,14 +20,13 @@ export interface ButtonGroupProps {
/**
* @defaultValue 'md'
*/
size?: ButtonGroup['variants']['size']
size?: ButtonGroupVariants['size']
/**
* The orientation the buttons are laid out.
* @defaultValue 'horizontal'
*/
orientation?: ButtonGroup['variants']['orientation']
orientation?: ButtonGroupVariants['orientation']
class?: any
ui?: ButtonGroup['slots']
}
export interface ButtonGroupSlots {
@@ -32,20 +37,13 @@ export interface ButtonGroupSlots {
<script setup lang="ts">
import { provide, computed } from 'vue'
import { Primitive } from 'reka-ui'
import { useAppConfig } from '#imports'
import { buttonGroupInjectionKey } from '../composables/useButtonGroup'
import { tv } from '../utils/tv'
const props = withDefaults(defineProps<ButtonGroupProps>(), {
orientation: 'horizontal'
})
defineSlots<ButtonGroupSlots>()
const appConfig = useAppConfig() as ButtonGroup['AppConfig']
// eslint-disable-next-line vue/no-dupe-keys
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.buttonGroup || {}) }))
provide(buttonGroupInjectionKey, computed(() => ({
orientation: props.orientation,
size: props.size
@@ -53,7 +51,7 @@ provide(buttonGroupInjectionKey, computed(() => ({
</script>
<template>
<Primitive :as="as" :class="ui({ orientation, class: props.class })">
<Primitive :as="as" :class="buttonGroup({ orientation, class: props.class })">
<slot />
</Primitive>
</template>

View File

@@ -1,12 +1,19 @@
<script lang="ts">
import type { VariantProps } from 'tailwind-variants'
import type { CalendarRootProps, CalendarRootEmits, RangeCalendarRootProps, RangeCalendarRootEmits, DateRange, CalendarCellTriggerProps } from 'reka-ui'
import type { DateValue } from '@internationalized/date'
import type { AppConfig } from '@nuxt/schema'
import theme from '#build/ui/calendar'
import type { ButtonProps } from '../types'
import type { ComponentConfig } from '../types/utils'
import _appConfig from '#build/app.config'
import theme from '#build/ui/calendar'
import { tv } from '../utils/tv'
import type { PartialString } from '../types/utils'
type Calendar = ComponentConfig<typeof theme, AppConfig, 'calendar'>
const appConfigCalendar = _appConfig as AppConfig & { ui: { calendar: Partial<typeof theme> } }
const calendar = tv({ extend: tv(theme), ...(appConfigCalendar.ui?.calendar || {}) })
type CalendarVariants = VariantProps<typeof calendar>
type CalendarDefaultValue<R extends boolean = false, M extends boolean = false> = R extends true
? DateRange
@@ -75,11 +82,11 @@ export interface CalendarProps<R extends boolean = false, M extends boolean = fa
/**
* @defaultValue 'primary'
*/
color?: Calendar['variants']['color']
color?: CalendarVariants['color']
/**
* @defaultValue 'md'
*/
size?: Calendar['variants']['size']
size?: CalendarVariants['size']
/** Whether or not a range of dates can be selected */
range?: R & boolean
/** Whether or not multiple dates can be selected */
@@ -91,7 +98,7 @@ export interface CalendarProps<R extends boolean = false, M extends boolean = fa
defaultValue?: CalendarDefaultValue<R, M>
modelValue?: CalendarModelValue<R, M>
class?: any
ui?: Calendar['slots']
ui?: PartialString<typeof calendar.slots>
}
export interface CalendarEmits<R extends boolean, M extends boolean> extends Omit<CalendarRootEmits & RangeCalendarRootEmits, 'update:modelValue'> {
@@ -112,7 +119,6 @@ import { Calendar as SingleCalendar, RangeCalendar } from 'reka-ui/namespaced'
import { reactiveOmit } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { useLocale } from '../composables/useLocale'
import { tv } from '../utils/tv'
import UButton from './Button.vue'
const props = withDefaults(defineProps<CalendarProps<R, M>>(), {
@@ -123,8 +129,8 @@ const props = withDefaults(defineProps<CalendarProps<R, M>>(), {
const emits = defineEmits<CalendarEmits<R, M>>()
defineSlots<CalendarSlots>()
const appConfig = useAppConfig()
const { code: locale, dir, t } = useLocale()
const appConfig = useAppConfig() as Calendar['AppConfig']
const rootProps = useForwardPropsEmits(reactiveOmit(props, 'range', 'modelValue', 'defaultValue', 'color', 'size', 'monthControls', 'yearControls', 'class', 'ui'), emits)
@@ -133,7 +139,7 @@ const nextMonthIcon = computed(() => props.nextMonthIcon || (dir.value === 'rtl'
const prevYearIcon = computed(() => props.prevYearIcon || (dir.value === 'rtl' ? appConfig.ui.icons.chevronDoubleRight : appConfig.ui.icons.chevronDoubleLeft))
const prevMonthIcon = computed(() => props.prevMonthIcon || (dir.value === 'rtl' ? appConfig.ui.icons.chevronRight : appConfig.ui.icons.chevronLeft))
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.calendar || {}) })({
const ui = computed(() => calendar({
color: props.color,
size: props.size
}))

View File

@@ -1,9 +1,15 @@
<script lang="ts">
import type { VariantProps } from 'tailwind-variants'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/card'
import type { ComponentConfig } from '../types/utils'
import { tv } from '../utils/tv'
type Card = ComponentConfig<typeof theme, AppConfig, 'card'>
const appConfigCard = _appConfig as AppConfig & { ui: { card: Partial<typeof theme> } }
const card = tv({ extend: tv(theme), ...(appConfigCard.ui?.card || {}) })
type CardVariants = VariantProps<typeof card>
export interface CardProps {
/**
@@ -14,9 +20,9 @@ export interface CardProps {
/**
* @defaultValue 'outline'
*/
variant?: Card['variants']['variant']
variant?: CardVariants['variant']
class?: any
ui?: Card['slots']
ui?: Partial<typeof card.slots>
}
export interface CardSlots {
@@ -29,17 +35,11 @@ export interface CardSlots {
<script setup lang="ts">
import { computed } from 'vue'
import { Primitive } from 'reka-ui'
import { useAppConfig } from '#imports'
import { tv } from '../utils/tv'
const props = defineProps<CardProps>()
const slots = defineSlots<CardSlots>()
const appConfig = useAppConfig() as Card['AppConfig']
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.card || {}) })({
variant: props.variant
}))
const ui = computed(() => card({ variant: props.variant }))
</script>
<template>

View File

@@ -1,5 +1,6 @@
<!-- eslint-disable vue/block-tag-newline -->
<script lang="ts">
import type { VariantProps } from 'tailwind-variants'
import type { AppConfig } from '@nuxt/schema'
import type { AcceptableValue } from 'reka-ui'
import type { EmblaCarouselType, EmblaOptionsType, EmblaPluginType } from 'embla-carousel'
@@ -9,11 +10,17 @@ import type { AutoHeightOptionsType } from 'embla-carousel-auto-height'
import type { ClassNamesOptionsType } from 'embla-carousel-class-names'
import type { FadeOptionsType } from 'embla-carousel-fade'
import type { WheelGesturesPluginOptions } from 'embla-carousel-wheel-gestures'
import _appConfig from '#build/app.config'
import theme from '#build/ui/carousel'
import { tv } from '../utils/tv'
import type { ButtonProps } from '../types'
import type { ComponentConfig } from '../types/utils'
import type { PartialString } from '../types/utils'
type Carousel = ComponentConfig<typeof theme, AppConfig, 'carousel'>
const appConfigCarousel = _appConfig as AppConfig & { ui: { carousel: Partial<typeof theme> } }
const carousel = tv({ extend: tv(theme), ...(appConfigCarousel.ui?.carousel || {}) })
type CarouselVariants = VariantProps<typeof carousel>
export type CarouselItem = AcceptableValue
@@ -59,7 +66,7 @@ export interface CarouselProps<T extends CarouselItem = CarouselItem> extends Om
* The orientation of the carousel.
* @defaultValue 'horizontal'
*/
orientation?: Carousel['variants']['orientation']
orientation?: CarouselVariants['orientation']
items?: T[]
/**
* Enable Autoplay plugin
@@ -92,7 +99,7 @@ export interface CarouselProps<T extends CarouselItem = CarouselItem> extends Om
*/
wheelGestures?: boolean | WheelGesturesPluginOptions
class?: any
ui?: Carousel['slots']
ui?: PartialString<typeof carousel.slots>
}
export type CarouselSlots<T extends CarouselItem = CarouselItem> = {
@@ -108,7 +115,6 @@ import { Primitive, useForwardProps } from 'reka-ui'
import { reactivePick, computedAsync } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { useLocale } from '../composables/useLocale'
import { tv } from '../utils/tv'
import UButton from './Button.vue'
const props = withDefaults(defineProps<CarouselProps<T>>(), {
@@ -142,15 +148,14 @@ const props = withDefaults(defineProps<CarouselProps<T>>(), {
})
defineSlots<CarouselSlots<T>>()
const appConfig = useAppConfig()
const { dir, t } = useLocale()
const appConfig = useAppConfig() as Carousel['AppConfig']
const rootProps = useForwardProps(reactivePick(props, 'active', 'align', 'breakpoints', 'containScroll', 'dragFree', 'dragThreshold', 'duration', 'inViewThreshold', 'loop', 'skipSnaps', 'slidesToScroll', 'startIndex', 'watchDrag', 'watchResize', 'watchSlides', 'watchFocus'))
const prevIcon = computed(() => props.prevIcon || (dir.value === 'rtl' ? appConfig.ui.icons.arrowRight : appConfig.ui.icons.arrowLeft))
const nextIcon = computed(() => props.nextIcon || (dir.value === 'rtl' ? appConfig.ui.icons.arrowLeft : appConfig.ui.icons.arrowRight))
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.carousel || {}) })({
const ui = computed(() => carousel({
orientation: props.orientation
}))

View File

@@ -1,10 +1,16 @@
<script lang="ts">
import type { VariantProps } from 'tailwind-variants'
import type { CheckboxRootProps } from 'reka-ui'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/checkbox'
import type { ComponentConfig } from '../types/utils'
import { tv } from '../utils/tv'
type Checkbox = ComponentConfig<typeof theme, AppConfig, 'checkbox'>
const appConfigCheckbox = _appConfig as AppConfig & { ui: { checkbox: Partial<typeof theme> } }
const checkbox = tv({ extend: tv(theme), ...(appConfigCheckbox.ui?.checkbox || {}) })
type CheckboxVariants = VariantProps<typeof checkbox>
export interface CheckboxProps extends Pick<CheckboxRootProps, 'disabled' | 'required' | 'name' | 'value' | 'id' | 'defaultValue'> {
/**
@@ -17,11 +23,11 @@ export interface CheckboxProps extends Pick<CheckboxRootProps, 'disabled' | 'req
/**
* @defaultValue 'primary'
*/
color?: Checkbox['variants']['color']
color?: CheckboxVariants['color']
/**
* @defaultValue 'md'
*/
size?: Checkbox['variants']['size']
size?: CheckboxVariants['size']
/**
* The icon displayed when checked.
* @defaultValue appConfig.ui.icons.check
@@ -35,7 +41,7 @@ export interface CheckboxProps extends Pick<CheckboxRootProps, 'disabled' | 'req
*/
indeterminateIcon?: string
class?: any
ui?: Checkbox['slots']
ui?: Partial<typeof checkbox.slots>
}
export type CheckboxEmits = {
@@ -54,7 +60,6 @@ import { Primitive, CheckboxRoot, CheckboxIndicator, Label, useForwardProps } fr
import { reactivePick } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { useFormField } from '../composables/useFormField'
import { tv } from '../utils/tv'
import UIcon from './Icon.vue'
defineOptions({ inheritAttrs: false })
@@ -65,14 +70,13 @@ const emits = defineEmits<CheckboxEmits>()
const modelValue = defineModel<boolean | 'indeterminate'>({ default: undefined })
const appConfig = useAppConfig() as Checkbox['AppConfig']
const rootProps = useForwardProps(reactivePick(props, 'required', 'value', 'defaultValue'))
const appConfig = useAppConfig()
const { id: _id, emitFormChange, emitFormInput, size, color, name, disabled, ariaAttrs } = useFormField<CheckboxProps>(props)
const id = _id.value ?? useId()
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.checkbox || {}) })({
const ui = computed(() => checkbox({
size: size.value,
color: color.value,
required: props.required,

View File

@@ -1,9 +1,15 @@
<script lang="ts">
import type { VariantProps } from 'tailwind-variants'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/chip'
import type { ComponentConfig } from '../types/utils'
import { tv } from '../utils/tv'
type Chip = ComponentConfig<typeof theme, AppConfig, 'chip'>
const appConfigChip = _appConfig as AppConfig & { ui: { chip: Partial<typeof theme> } }
const chip = tv({ extend: tv(theme), ...(appConfigChip.ui?.chip || {}) })
type ChipVariants = VariantProps<typeof chip>
export interface ChipProps {
/**
@@ -16,22 +22,22 @@ export interface ChipProps {
/**
* @defaultValue 'primary'
*/
color?: Chip['variants']['color']
color?: ChipVariants['color']
/**
* @defaultValue 'md'
*/
size?: Chip['variants']['size']
size?: ChipVariants['size']
/**
* The position of the chip.
* @defaultValue 'top-right'
*/
position?: Chip['variants']['position']
position?: ChipVariants['position']
/** When `true`, keep the chip inside the component for rounded elements. */
inset?: boolean
/** When `true`, render the chip relatively to the parent. */
standalone?: boolean
class?: any
ui?: Chip['slots']
ui?: Partial<typeof chip.slots>
}
export interface ChipEmits {
@@ -47,9 +53,7 @@ export interface ChipSlots {
<script setup lang="ts">
import { computed } from 'vue'
import { Primitive, Slot } from 'reka-ui'
import { useAppConfig } from '#imports'
import { useAvatarGroup } from '../composables/useAvatarGroup'
import { tv } from '../utils/tv'
defineOptions({ inheritAttrs: false })
@@ -62,9 +66,8 @@ defineSlots<ChipSlots>()
const show = defineModel<boolean>('show', { default: true })
const { size } = useAvatarGroup(props)
const appConfig = useAppConfig() as Chip['AppConfig']
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.chip || {}) })({
const ui = computed(() => chip({
color: props.color,
size: size.value,
position: props.position,

View File

@@ -1,10 +1,13 @@
<script lang="ts">
import type { CollapsibleRootProps, CollapsibleRootEmits } from 'reka-ui'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/collapsible'
import type { ComponentConfig } from '../types/utils'
import { tv } from '../utils/tv'
type Collapsible = ComponentConfig<typeof theme, AppConfig, 'collapsible'>
const appConfigCollapsible = _appConfig as AppConfig & { ui: { collapsible: Partial<typeof theme> } }
const collapsible = tv({ extend: tv(theme), ...(appConfigCollapsible.ui?.collapsible || {}) })
export interface CollapsibleProps extends Pick<CollapsibleRootProps, 'defaultOpen' | 'open' | 'disabled' | 'unmountOnHide'> {
/**
@@ -13,7 +16,7 @@ export interface CollapsibleProps extends Pick<CollapsibleRootProps, 'defaultOpe
*/
as?: any
class?: any
ui?: Collapsible['slots']
ui?: Partial<typeof collapsible.slots>
}
export interface CollapsibleEmits extends CollapsibleRootEmits {}
@@ -25,11 +28,8 @@ export interface CollapsibleSlots {
</script>
<script setup lang="ts">
import { computed } from 'vue'
import { CollapsibleRoot, CollapsibleTrigger, CollapsibleContent, useForwardPropsEmits } from 'reka-ui'
import { reactivePick } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { tv } from '../utils/tv'
const props = withDefaults(defineProps<CollapsibleProps>(), {
unmountOnHide: true
@@ -37,12 +37,10 @@ const props = withDefaults(defineProps<CollapsibleProps>(), {
const emits = defineEmits<CollapsibleEmits>()
const slots = defineSlots<CollapsibleSlots>()
const appConfig = useAppConfig() as Collapsible['AppConfig']
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'defaultOpen', 'open', 'disabled', 'unmountOnHide'), emits)
// eslint-disable-next-line vue/no-dupe-keys
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.collapsible || {}) })())
const ui = collapsible()
</script>
<template>

View File

@@ -1,12 +1,18 @@
<!-- eslint-disable vue/block-tag-newline -->
<script lang="ts">
import type { VariantProps } from 'tailwind-variants'
import type { MaybeRefOrGetter } from '@vueuse/shared'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/color-picker'
import { tv } from '../utils/tv'
import type { HSLObject } from 'colortranslator'
import type { ComponentConfig } from '../types/utils'
type ColorPicker = ComponentConfig<typeof theme, AppConfig, 'colorPicker'>
const appConfigColorPicker = _appConfig as AppConfig & { ui: { colorPicker: Partial<typeof theme> } }
const colorPicker = tv({ extend: tv(theme), ...(appConfigColorPicker.ui?.colorPicker || {}) })
type ColorPickerVariants = VariantProps<typeof colorPicker>
type HSVColor = {
h: number
@@ -61,9 +67,9 @@ export type ColorPickerProps = {
/**
* @defaultValue 'md'
*/
size?: ColorPicker['variants']['size']
size?: ColorPickerVariants['size']
class?: any
ui?: ColorPicker['slots']
ui?: Partial<typeof colorPicker.slots>
}
</script>
@@ -74,8 +80,6 @@ import { Primitive } from 'reka-ui'
import { useEventListener, useElementBounding, watchThrottled, watchPausable } from '@vueuse/core'
import { isClient } from '@vueuse/shared'
import { ColorTranslator } from 'colortranslator'
import { useAppConfig } from '#imports'
import { tv } from '../utils/tv'
const props = withDefaults(defineProps<ColorPickerProps>(), {
format: 'hex',
@@ -84,12 +88,6 @@ const props = withDefaults(defineProps<ColorPickerProps>(), {
})
const modelValue = defineModel<string>(undefined)
const appConfig = useAppConfig() as ColorPicker['AppConfig']
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.colorPicker || {}) })({
size: props.size
}))
const pickedColor = computed<HSVColor>({
get() {
try {
@@ -260,6 +258,10 @@ const trackThumbStyle = computed(() => ({
backgroundColor: trackThumbColor.value,
top: `${trackThumbPosition.value.y}%`
}))
const ui = computed(() => colorPicker({
size: props.size
}))
</script>
<template>

View File

@@ -4,12 +4,16 @@ import type { ListboxRootProps, ListboxRootEmits } from 'reka-ui'
import type { FuseResult } from 'fuse.js'
import type { AppConfig } from '@nuxt/schema'
import type { UseFuseOptions } from '@vueuse/integrations/useFuse'
import _appConfig from '#build/app.config'
import theme from '#build/ui/command-palette'
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
import { tv } from '../utils/tv'
import type { AvatarProps, ButtonProps, ChipProps, KbdProps, InputProps, LinkProps } from '../types'
import type { ComponentConfig } from '../types/utils'
import type { PartialString } from '../types/utils'
type CommandPalette = ComponentConfig<typeof theme, AppConfig, 'commandPalette'>
const appConfigCommandPalette = _appConfig as AppConfig & { ui: { commandPalette: Partial<typeof theme> } }
const commandPalette = tv({ extend: tv(theme), ...(appConfigCommandPalette.ui?.commandPalette || {}) })
export interface CommandPaletteItem extends Omit<LinkProps, 'type' | 'raw' | 'custom'> {
prefix?: string
@@ -111,7 +115,7 @@ export interface CommandPaletteProps<G, T> extends Pick<ListboxRootProps, 'multi
*/
labelKey?: string
class?: any
ui?: CommandPalette['slots']
ui?: PartialString<typeof commandPalette.slots>
}
export type CommandPaletteEmits<T> = ListboxRootEmits<T> & {
@@ -122,7 +126,7 @@ type SlotProps<T> = (props: { item: T, index: number }) => any
export type CommandPaletteSlots<G extends { slot?: string }, T extends { slot?: string }> = {
'empty'(props: { searchTerm?: string }): any
'close'(props: { ui: { [K in keyof Required<CommandPalette['slots']>]: (props?: Record<string, any>) => string } }): any
'close'(props: { ui: ReturnType<typeof commandPalette> }): any
'item': SlotProps<T>
'item-leading': SlotProps<T>
'item-label': SlotProps<T>
@@ -140,7 +144,6 @@ import { useFuse } from '@vueuse/integrations/useFuse'
import { useAppConfig } from '#imports'
import { useLocale } from '../composables/useLocale'
import { omit, get } from '../utils'
import { tv } from '../utils/tv'
import { highlight } from '../utils/fuse'
import { pickLinkProps } from '../utils/link'
import UIcon from './Icon.vue'
@@ -163,13 +166,13 @@ const slots = defineSlots<CommandPaletteSlots<G, T>>()
const searchTerm = defineModel<string>('searchTerm', { default: '' })
const { t } = useLocale()
const appConfig = useAppConfig() as CommandPalette['AppConfig']
const appConfig = useAppConfig()
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'disabled', 'multiple', 'modelValue', 'defaultValue', 'highlightOnHover'), emits)
const inputProps = useForwardProps(reactivePick(props, 'loading', 'loadingIcon'))
// eslint-disable-next-line vue/no-dupe-keys
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.commandPalette || {}) })())
const ui = commandPalette()
const fuse = computed(() => defu({}, props.fuse, {
fuseOptions: {
@@ -255,6 +258,7 @@ const groups = computed(() => {
:placeholder="placeholder || t('commandPalette.placeholder')"
variant="none"
:autofocus="autofocus"
size="lg"
v-bind="inputProps"
:icon="icon || appConfig.ui.icons.search"
:class="ui.input({ class: props.ui?.input })"

View File

@@ -1,9 +1,12 @@
<script lang="ts">
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/container'
import type { ComponentConfig } from '../types/utils'
import { tv } from '../utils/tv'
type Container = ComponentConfig<typeof theme, AppConfig, 'container'>
const appConfigContainer = _appConfig as AppConfig & { ui: { container: Partial<typeof theme> } }
const container = tv({ extend: tv(theme), ...(appConfigContainer.ui?.container || {}) })
export interface ContainerProps {
/**
@@ -20,21 +23,14 @@ export interface ContainerSlots {
</script>
<script setup lang="ts">
import { computed } from 'vue'
import { Primitive } from 'reka-ui'
import { useAppConfig } from '#imports'
import { tv } from '../utils/tv'
const props = defineProps<ContainerProps>()
defineSlots<ContainerSlots>()
const appConfig = useAppConfig() as Container['AppConfig']
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.container || {}) }))
</script>
<template>
<Primitive :as="as" :class="ui({ class: props.class })">
<Primitive :as="as" :class="container({ class: props.class })">
<slot />
</Primitive>
</template>

View File

@@ -1,12 +1,26 @@
<!-- eslint-disable vue/block-tag-newline -->
<script lang="ts">
import type { VariantProps } from 'tailwind-variants'
import type { ContextMenuRootProps, ContextMenuRootEmits, ContextMenuContentProps, ContextMenuContentEmits } from 'reka-ui'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/context-menu'
import { tv } from '../utils/tv'
import type { AvatarProps, KbdProps, LinkProps } from '../types'
import type { ArrayOrNested, DynamicSlots, MergeTypes, NestedItem, EmitsToProps, ComponentConfig } from '../types/utils'
import type {
ArrayOrNested,
DynamicSlots,
MergeTypes,
NestedItem,
PartialString,
EmitsToProps
} from '../types/utils'
type ContextMenu = ComponentConfig<typeof theme, AppConfig, 'contextMenu'>
const appConfigContextMenu = _appConfig as AppConfig & { ui: { contextMenu: Partial<typeof theme> } }
const contextMenu = tv({ extend: tv(theme), ...(appConfigContextMenu.ui?.contextMenu || {}) })
type ContextMenuVariants = VariantProps<typeof contextMenu>
export interface ContextMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'custom'> {
label?: string
@@ -14,7 +28,7 @@ export interface ContextMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'custo
* @IconifyIcon
*/
icon?: string
color?: ContextMenu['variants']['color']
color?: ContextMenuVariants['color']
avatar?: AvatarProps
content?: Omit<ContextMenuContentProps, 'as' | 'asChild' | 'forceMount'> & Partial<EmitsToProps<ContextMenuContentEmits>>
kbds?: KbdProps['value'][] | KbdProps[]
@@ -39,7 +53,7 @@ export interface ContextMenuProps<T extends ArrayOrNested<ContextMenuItem> = Arr
/**
* @defaultValue 'md'
*/
size?: ContextMenu['variants']['size']
size?: ContextMenuVariants['size']
items?: T
/**
* The icon displayed when an item is checked.
@@ -74,7 +88,7 @@ export interface ContextMenuProps<T extends ArrayOrNested<ContextMenuItem> = Arr
labelKey?: keyof NestedItem<T>
disabled?: boolean
class?: any
ui?: ContextMenu['slots']
ui?: PartialString<typeof contextMenu.slots>
}
export interface ContextMenuEmits extends ContextMenuRootEmits {}
@@ -98,9 +112,7 @@ export type ContextMenuSlots<
import { computed, toRef } from 'vue'
import { ContextMenuRoot, ContextMenuTrigger, useForwardPropsEmits } from 'reka-ui'
import { reactivePick } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { omit } from '../utils'
import { tv } from '../utils/tv'
import UContextMenuContent from './ContextMenuContent.vue'
const props = withDefaults(defineProps<ContextMenuProps<T>>(), {
@@ -112,13 +124,12 @@ const props = withDefaults(defineProps<ContextMenuProps<T>>(), {
const emits = defineEmits<ContextMenuEmits>()
const slots = defineSlots<ContextMenuSlots<T>>()
const appConfig = useAppConfig() as ContextMenu['AppConfig']
const rootProps = useForwardPropsEmits(reactivePick(props, 'modal'), emits)
const contentProps = toRef(() => props.content)
const proxySlots = omit(slots, ['default'])
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.contextMenu || {}) })({
const ui = computed(() => contextMenu({
size: props.size
}))
</script>

View File

@@ -1,11 +1,11 @@
<script lang="ts">
import type { ContextMenuContentProps as RekaContextMenuContentProps, ContextMenuContentEmits as RekaContextMenuContentEmits } from 'reka-ui'
import type { AppConfig } from '@nuxt/schema'
import type theme from '#build/ui/context-menu'
import theme from '#build/ui/context-menu'
import { tv } from '../utils/tv'
import type { AvatarProps, ContextMenuItem, ContextMenuSlots, KbdProps } from '../types'
import type { ArrayOrNested, NestedItem, ComponentConfig } from '../types/utils'
import type { ArrayOrNested, NestedItem } from '../types/utils'
type ContextMenu = ComponentConfig<typeof theme, AppConfig, 'contextMenu'>
const _contextMenu = tv(theme)()
interface ContextMenuContentProps<T extends ArrayOrNested<ContextMenuItem>> extends Omit<RekaContextMenuContentProps, 'as' | 'asChild' | 'forceMount'> {
items?: T
@@ -25,8 +25,8 @@ interface ContextMenuContentProps<T extends ArrayOrNested<ContextMenuItem>> exte
*/
externalIcon?: boolean | string
class?: any
ui: { [K in keyof Required<ContextMenu['slots']>]: (props?: Record<string, any>) => string }
uiOverride?: ContextMenu['slots']
ui: typeof _contextMenu
uiOverride?: any
}
interface ContextMenuContentEmits extends RekaContextMenuContentEmits {}
@@ -53,9 +53,8 @@ const props = defineProps<ContextMenuContentProps<T>>()
const emits = defineEmits<ContextMenuContentEmits>()
const slots = defineSlots<ContextMenuSlots<T>>()
const { dir } = useLocale()
const appConfig = useAppConfig()
const { dir } = useLocale()
const contentProps = useForwardPropsEmits(reactiveOmit(props, 'sub', 'items', 'portal', 'labelKey', 'checkedIcon', 'loadingIcon', 'externalIcon', 'class', 'ui', 'uiOverride'), emits)
const proxySlots = omit(slots, ['default'])

View File

@@ -2,10 +2,14 @@
import type { DrawerRootProps, DrawerRootEmits } from 'vaul-vue'
import type { DialogContentProps, DialogContentEmits } from 'reka-ui'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/drawer'
import type { EmitsToProps, ComponentConfig } from '../types/utils'
import { tv } from '../utils/tv'
import type { EmitsToProps } from '../types/utils'
type Drawer = ComponentConfig<typeof theme, AppConfig, 'drawer'>
const appConfigDrawer = _appConfig as AppConfig & { ui: { drawer: Partial<typeof theme> } }
const drawer = tv({ extend: tv(theme), ...(appConfigDrawer.ui?.drawer || {}) })
export interface DrawerProps extends Pick<DrawerRootProps, 'activeSnapPoint' | 'closeThreshold' | 'shouldScaleBackground' | 'setBackgroundColorOnScale' | 'scrollLockTimeout' | 'fixed' | 'dismissible' | 'modal' | 'open' | 'defaultOpen' | 'nested' | 'direction' | 'noBodyStyles' | 'handleOnly' | 'preventScrollRestoration' | 'snapPoints'> {
/**
@@ -38,7 +42,7 @@ export interface DrawerProps extends Pick<DrawerRootProps, 'activeSnapPoint' | '
*/
portal?: boolean
class?: any
ui?: Drawer['slots']
ui?: Partial<typeof drawer.slots>
}
export interface DrawerEmits extends DrawerRootEmits {}
@@ -59,8 +63,6 @@ import { computed, toRef } from 'vue'
import { useForwardPropsEmits } from 'reka-ui'
import { DrawerRoot, DrawerTrigger, DrawerPortal, DrawerOverlay, DrawerContent, DrawerTitle, DrawerDescription, DrawerHandle } from 'vaul-vue'
import { reactivePick } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { tv } from '../utils/tv'
const props = withDefaults(defineProps<DrawerProps>(), {
direction: 'bottom',
@@ -73,15 +75,13 @@ const props = withDefaults(defineProps<DrawerProps>(), {
const emits = defineEmits<DrawerEmits>()
const slots = defineSlots<DrawerSlots>()
const appConfig = useAppConfig() as Drawer['AppConfig']
const rootProps = useForwardPropsEmits(reactivePick(props, 'activeSnapPoint', 'closeThreshold', 'shouldScaleBackground', 'setBackgroundColorOnScale', 'scrollLockTimeout', 'fixed', 'dismissible', 'modal', 'open', 'defaultOpen', 'nested', 'direction', 'noBodyStyles', 'handleOnly', 'preventScrollRestoration', 'snapPoints'), emits)
const contentProps = toRef(() => props.content)
const contentEvents = {
closeAutoFocus: (e: Event) => e.preventDefault()
}
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.drawer || {}) })({
const ui = computed(() => drawer({
direction: props.direction,
inset: props.inset
}))

View File

@@ -1,12 +1,26 @@
<!-- eslint-disable vue/block-tag-newline -->
<script lang="ts">
import type { VariantProps } from 'tailwind-variants'
import type { DropdownMenuRootProps, DropdownMenuRootEmits, DropdownMenuContentProps, DropdownMenuContentEmits, DropdownMenuArrowProps } from 'reka-ui'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/dropdown-menu'
import { tv } from '../utils/tv'
import type { AvatarProps, KbdProps, LinkProps } from '../types'
import type { ArrayOrNested, DynamicSlots, MergeTypes, NestedItem, EmitsToProps, ComponentConfig } from '../types/utils'
import type {
ArrayOrNested,
DynamicSlots,
MergeTypes,
NestedItem,
PartialString,
EmitsToProps
} from '../types/utils'
type DropdownMenu = ComponentConfig<typeof theme, AppConfig, 'dropdownMenu'>
const appConfigDropdownMenu = _appConfig as AppConfig & { ui: { dropdownMenu: Partial<typeof theme> } }
const dropdownMenu = tv({ extend: tv(theme), ...(appConfigDropdownMenu.ui?.dropdownMenu || {}) })
type DropdownMenuVariants = VariantProps<typeof dropdownMenu>
export interface DropdownMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'custom'> {
label?: string
@@ -14,7 +28,7 @@ export interface DropdownMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'cust
* @IconifyIcon
*/
icon?: string
color?: DropdownMenu['variants']['color']
color?: DropdownMenuVariants['color']
avatar?: AvatarProps
content?: Omit<DropdownMenuContentProps, 'as' | 'asChild' | 'forceMount'> & Partial<EmitsToProps<DropdownMenuContentEmits>>
kbds?: KbdProps['value'][] | KbdProps[]
@@ -39,7 +53,7 @@ export interface DropdownMenuProps<T extends ArrayOrNested<DropdownMenuItem> = A
/**
* @defaultValue 'md'
*/
size?: DropdownMenu['variants']['size']
size?: DropdownMenuVariants['size']
items?: T
/**
* The icon displayed when an item is checked.
@@ -82,7 +96,7 @@ export interface DropdownMenuProps<T extends ArrayOrNested<DropdownMenuItem> = A
labelKey?: keyof NestedItem<T>
disabled?: boolean
class?: any
ui?: DropdownMenu['slots']
ui?: PartialString<typeof dropdownMenu.slots>
}
export interface DropdownMenuEmits extends DropdownMenuRootEmits {}
@@ -107,9 +121,7 @@ import { computed, toRef } from 'vue'
import { defu } from 'defu'
import { DropdownMenuRoot, DropdownMenuTrigger, DropdownMenuArrow, useForwardPropsEmits } from 'reka-ui'
import { reactivePick } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { omit } from '../utils'
import { tv } from '../utils/tv'
import UDropdownMenuContent from './DropdownMenuContent.vue'
const props = withDefaults(defineProps<DropdownMenuProps<T>>(), {
@@ -121,14 +133,12 @@ const props = withDefaults(defineProps<DropdownMenuProps<T>>(), {
const emits = defineEmits<DropdownMenuEmits>()
const slots = defineSlots<DropdownMenuSlots<T>>()
const appConfig = useAppConfig() as DropdownMenu['AppConfig']
const rootProps = useForwardPropsEmits(reactivePick(props, 'defaultOpen', 'open', 'modal'), emits)
const contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffset: 8, collisionPadding: 8 }) as DropdownMenuContentProps)
const arrowProps = toRef(() => props.arrow as DropdownMenuArrowProps)
const proxySlots = omit(slots, ['default'])
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.dropdownMenu || {}) })({
const ui = computed(() => dropdownMenu({
size: props.size
}))
</script>

View File

@@ -1,12 +1,12 @@
<!-- eslint-disable vue/block-tag-newline -->
<script lang="ts">
import type { DropdownMenuContentProps as RekaDropdownMenuContentProps, DropdownMenuContentEmits as RekaDropdownMenuContentEmits } from 'reka-ui'
import type { AppConfig } from '@nuxt/schema'
import type theme from '#build/ui/dropdown-menu'
import theme from '#build/ui/dropdown-menu'
import { tv } from '../utils/tv'
import type { KbdProps, AvatarProps, DropdownMenuItem, DropdownMenuSlots } from '../types'
import type { ArrayOrNested, NestedItem, ComponentConfig } from '../types/utils'
import type { ArrayOrNested, NestedItem } from '../types/utils'
type DropdownMenu = ComponentConfig<typeof theme, AppConfig, 'dropdownMenu'>
const _dropdownMenu = tv(theme)()
interface DropdownMenuContentProps<T extends ArrayOrNested<DropdownMenuItem>> extends Omit<RekaDropdownMenuContentProps, 'as' | 'asChild' | 'forceMount'> {
items?: T
@@ -26,8 +26,8 @@ interface DropdownMenuContentProps<T extends ArrayOrNested<DropdownMenuItem>> ex
*/
externalIcon?: boolean | string
class?: any
ui: { [K in keyof Required<DropdownMenu['slots']>]: (props?: Record<string, any>) => string }
uiOverride?: DropdownMenu['slots']
ui: typeof _dropdownMenu
uiOverride?: any
}
interface DropdownMenuContentEmits extends RekaDropdownMenuContentEmits {}
@@ -59,9 +59,8 @@ const props = defineProps<DropdownMenuContentProps<T>>()
const emits = defineEmits<DropdownMenuContentEmits>()
const slots = defineSlots<DropdownMenuContentSlots<T>>()
const { dir } = useLocale()
const appConfig = useAppConfig()
const { dir } = useLocale()
const contentProps = useForwardPropsEmits(reactiveOmit(props, 'sub', 'items', 'portal', 'labelKey', 'checkedIcon', 'loadingIcon', 'externalIcon', 'class', 'ui', 'uiOverride'), emits)
const proxySlots = omit(slots, ['default'])

View File

@@ -1,25 +1,27 @@
<script lang="ts">
import type { DeepReadonly } from 'vue'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/form'
import { tv } from '../utils/tv'
import type { FormSchema, FormError, FormInputEvents, FormErrorEvent, FormSubmitEvent, FormEvent, Form, FormErrorWithId } from '../types/form'
import type { ComponentConfig } from '../types/utils'
import type { DeepReadonly } from 'vue'
type FormConfig = ComponentConfig<typeof theme, AppConfig, 'form'>
const appConfigForm = _appConfig as AppConfig & { ui: { form: Partial<typeof theme> } }
export interface FormProps<I extends object, O extends object = I> {
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<I, O>
schema?: FormSchema<T>
/** An object representing the current state of the form. */
state: Partial<I>
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<I>) => Promise<FormError[]> | FormError[]
validate?: (state: Partial<T>) => Promise<FormError[]> | FormError[]
/**
* The list of input events that trigger the form validation.
* @defaultValue `['blur', 'change', 'input']`
@@ -32,23 +34,17 @@ export interface FormProps<I extends object, O extends object = I> {
* @defaultValue `300`
*/
validateOnInputDelay?: number
/**
* If true and nested in another form, this form will attach to its parent and validate at the same time.
*/
nested?: boolean
/**
* When `true`, all form elements will be disabled on `@submit` event.
* This will cause any focused input elements to lose their focus state.
* If true, schema transformations will be applied to the state on submit.
* @defaultValue `true`
*/
loadingAuto?: boolean
transform?: boolean
class?: any
onSubmit?: ((event: FormSubmitEvent<O>) => void | Promise<void>) | (() => void | Promise<void>)
onSubmit?: ((event: FormSubmitEvent<T>) => void | Promise<void>) | (() => void | Promise<void>)
}
export interface FormEmits<I extends object, O extends object = I> {
(e: 'submit', payload: FormSubmitEvent<O>): void
export interface FormEmits<T extends object> {
(e: 'submit', payload: FormSubmitEvent<T>): void
(e: 'error', payload: FormErrorEvent): void
}
@@ -57,33 +53,27 @@ export interface FormSlots {
}
</script>
<script lang="ts" setup generic="I extends object, O extends object = I">
<script lang="ts" setup generic="T extends object">
import { provide, inject, nextTick, ref, onUnmounted, onMounted, computed, useId, readonly } from 'vue'
import { useEventBus } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { formOptionsInjectionKey, formInputsInjectionKey, formBusInjectionKey, formLoadingInjectionKey } from '../composables/useFormField'
import { tv } from '../utils/tv'
import { validateSchema } from '../utils/form'
import { FormValidationException } from '../types/form'
const props = withDefaults(defineProps<FormProps<I, O>>(), {
const props = withDefaults(defineProps<FormProps<T>>(), {
validateOn() {
return ['input', 'blur', 'change'] as FormInputEvents[]
},
validateOnInputDelay: 300,
loadingAuto: true
transform: true
})
const emits = defineEmits<FormEmits<I, O>>()
const emits = defineEmits<FormEmits<T>>()
defineSlots<FormSlots>()
const appConfig = useAppConfig() as FormConfig['AppConfig']
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.form || {}) }))
const formId = props.id ?? useId() as string
const bus = useEventBus<FormEvent<I>>(`form-${formId}`)
const bus = useEventBus<FormEvent<T>>(`form-${formId}`)
const parentBus = inject(
formBusInjectionKey,
undefined
@@ -126,14 +116,14 @@ onUnmounted(() => {
})
onMounted(async () => {
if (props.nested && parentBus) {
if (parentBus) {
await nextTick()
parentBus.emit({ type: 'attach', validate: _validate, formId })
}
})
onUnmounted(() => {
if (props.nested && parentBus) {
if (parentBus) {
parentBus.emit({ type: 'detach', formId })
}
})
@@ -141,12 +131,12 @@ onUnmounted(() => {
const errors = ref<FormErrorWithId[]>([])
provide('form-errors', errors)
const inputs = ref<{ [P in keyof I]?: { id?: string, pattern?: RegExp } }>({})
const inputs = ref<{ [P in keyof T]?: { id?: string, pattern?: RegExp } }>({})
provide(formInputsInjectionKey, inputs as any)
const dirtyFields = new Set<keyof I>()
const touchedFields = new Set<keyof I>()
const blurredFields = new Set<keyof I>()
const dirtyFields = new Set<keyof T>()
const touchedFields = new Set<keyof T>()
const blurredFields = new Set<keyof T>()
function resolveErrorIds(errs: FormError[]): FormErrorWithId[] {
return errs.map(err => ({
@@ -155,7 +145,7 @@ function resolveErrorIds(errs: FormError[]): FormErrorWithId[] {
}))
}
const transformedState = ref<I | null>(null)
const transformedState = ref<T | null>(null)
async function getErrors(): Promise<FormErrorWithId[]> {
let errs = props.validate ? (await props.validate(props.state)) ?? [] : []
@@ -172,8 +162,8 @@ async function getErrors(): Promise<FormErrorWithId[]> {
return resolveErrorIds(errs)
}
async function _validate(opts: { name?: keyof I | (keyof I)[], silent?: boolean, nested?: boolean } = { silent: false, nested: true }): Promise<O | false> {
const names = opts.name && !Array.isArray(opts.name) ? [opts.name] : opts.name as (keyof I)[]
async function _validate(opts: { name?: keyof T | (keyof T)[], silent?: boolean, nested?: boolean, transform?: boolean } = { silent: false, nested: true, transform: false }): Promise<T | false> {
const names = opts.name && !Array.isArray(opts.name) ? [opts.name] : opts.name as (keyof T)[]
const nestedValidatePromises = !names && opts.nested
? Array.from(nestedForms.value.values()).map(
@@ -209,19 +199,23 @@ async function _validate(opts: { name?: keyof I | (keyof I)[], silent?: boolean,
throw new FormValidationException(formId, errors.value, childErrors)
}
return transformedState.value
if (opts.transform) {
Object.assign(props.state, transformedState.value)
}
return props.state as T
}
const loading = ref(false)
provide(formLoadingInjectionKey, readonly(loading))
async function onSubmitWrapper(payload: Event) {
loading.value = props.loadingAuto && true
loading.value = true
const event = payload as FormSubmitEvent<any>
try {
event.data = await _validate({ nested: true })
event.data = await _validate({ nested: true, transform: props.transform })
await props.onSubmit?.(event)
dirtyFields.clear()
} catch (error) {
@@ -247,11 +241,11 @@ provide(formOptionsInjectionKey, computed(() => ({
validateOnInputDelay: props.validateOnInputDelay
})))
defineExpose<Form<I, O>>({
defineExpose<Form<T>>({
validate: _validate,
errors,
setErrors(errs: FormError[], name?: keyof I) {
setErrors(errs: FormError[], name?: keyof T) {
if (name) {
errors.value = errors.value
.filter(error => error.name !== name)
@@ -265,7 +259,7 @@ defineExpose<Form<I, O>>({
await onSubmitWrapper(new Event('submit'))
},
getErrors(name?: keyof I) {
getErrors(name?: keyof T) {
if (name) {
return errors.value.filter(err => err.name === name)
}
@@ -281,12 +275,11 @@ defineExpose<Form<I, O>>({
},
disabled,
loading,
dirty: computed(() => !!dirtyFields.size),
dirtyFields: readonly(dirtyFields) as DeepReadonly<Set<keyof I>>,
blurredFields: readonly(blurredFields) as DeepReadonly<Set<keyof I>>,
touchedFields: readonly(touchedFields) as DeepReadonly<Set<keyof I>>
dirtyFields: readonly(dirtyFields) as DeepReadonly<Set<keyof T>>,
blurredFields: readonly(blurredFields) as DeepReadonly<Set<keyof T>>,
touchedFields: readonly(touchedFields) as DeepReadonly<Set<keyof T>>
})
</script>
@@ -294,7 +287,7 @@ defineExpose<Form<I, O>>({
<component
:is="parentBus ? 'div' : 'form'"
:id="formId"
:class="ui({ class: props.class })"
:class="form({ class: props.class })"
@submit.prevent="onSubmitWrapper"
>
<slot :errors="errors" />

View File

@@ -1,9 +1,15 @@
<script lang="ts">
import type { VariantProps } from 'tailwind-variants'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/form-field'
import type { ComponentConfig } from '../types/utils'
import { tv } from '../utils/tv'
type FormField = ComponentConfig<typeof theme, AppConfig, 'formField'>
const appConfigFormField = _appConfig as AppConfig & { ui: { formField: Partial<typeof theme> } }
const formField = tv({ extend: tv(theme), ...(appConfigFormField.ui?.formField || {}) })
type FormFieldVariants = VariantProps<typeof formField>
export interface FormFieldProps {
/**
@@ -23,7 +29,7 @@ export interface FormFieldProps {
/**
* @defaultValue 'md'
*/
size?: FormField['variants']['size']
size?: FormFieldVariants['size']
required?: boolean
/** If true, validation on input will be active immediately instead of waiting for a blur event. */
eagerValidation?: boolean
@@ -33,7 +39,7 @@ export interface FormFieldProps {
*/
validateOnInputDelay?: number
class?: any
ui?: FormField['slots']
ui?: Partial<typeof formField.slots>
}
export interface FormFieldSlots {
@@ -49,17 +55,13 @@ export interface FormFieldSlots {
<script setup lang="ts">
import { computed, ref, inject, provide, type Ref, useId } from 'vue'
import { Primitive, Label } from 'reka-ui'
import { useAppConfig } from '#imports'
import { formFieldInjectionKey, inputIdInjectionKey } from '../composables/useFormField'
import { tv } from '../utils/tv'
import type { FormError, FormFieldInjectedOptions } from '../types/form'
const props = defineProps<FormFieldProps>()
const slots = defineSlots<FormFieldSlots>()
const appConfig = useAppConfig() as FormField['AppConfig']
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.formField || {}) })({
const ui = computed(() => formField({
size: props.size,
required: props.required
}))

View File

@@ -1,12 +1,19 @@
<script lang="ts">
import type { InputHTMLAttributes } from 'vue'
import type { VariantProps } from 'tailwind-variants'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/input'
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
import { tv } from '../utils/tv'
import type { AvatarProps } from '../types'
import type { ComponentConfig } from '../types/utils'
import type { PartialString } from '../types/utils'
type Input = ComponentConfig<typeof theme, AppConfig, 'input'>
const appConfigInput = _appConfig as AppConfig & { ui: { input: Partial<typeof theme> } }
const input = tv({ extend: tv(theme), ...(appConfigInput.ui?.input || {}) })
type InputVariants = VariantProps<typeof input>
export interface InputProps extends UseComponentIconsProps {
/**
@@ -22,15 +29,15 @@ export interface InputProps extends UseComponentIconsProps {
/**
* @defaultValue 'primary'
*/
color?: Input['variants']['color']
color?: InputVariants['color']
/**
* @defaultValue 'outline'
*/
variant?: Input['variants']['variant']
variant?: InputVariants['variant']
/**
* @defaultValue 'md'
*/
size?: Input['variants']['size']
size?: InputVariants['size']
required?: boolean
autocomplete?: InputHTMLAttributes['autocomplete']
autofocus?: boolean
@@ -39,7 +46,7 @@ export interface InputProps extends UseComponentIconsProps {
/** Highlight the ring color like a focus state. */
highlight?: boolean
class?: any
ui?: Input['slots']
ui?: PartialString<typeof input.slots>
}
export interface InputEmits {
@@ -58,12 +65,10 @@ export interface InputSlots {
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { Primitive } from 'reka-ui'
import { useAppConfig } from '#imports'
import { useButtonGroup } from '../composables/useButtonGroup'
import { useComponentIcons } from '../composables/useComponentIcons'
import { useFormField } from '../composables/useFormField'
import { looseToNumber } from '../utils'
import { tv } from '../utils/tv'
import UIcon from './Icon.vue'
import UAvatar from './Avatar.vue'
@@ -79,15 +84,14 @@ const slots = defineSlots<InputSlots>()
const [modelValue, modelModifiers] = defineModel<string | number | null>()
const appConfig = useAppConfig() as Input['AppConfig']
const { emitFormBlur, emitFormInput, emitFormChange, size: formGroupSize, color, id, name, highlight, disabled, emitFormFocus, ariaAttrs } = useFormField<InputProps>(props, { deferInputValidation: true })
const { orientation, size: buttonGroupSize } = useButtonGroup<InputProps>(props)
const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(props)
const inputSize = computed(() => buttonGroupSize.value || formGroupSize.value)
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.input || {}) })({
type: props.type as Input['variants']['type'],
const ui = computed(() => input({
type: props.type as InputVariants['type'],
color: color.value,
variant: props.variant,
size: inputSize?.value,

View File

@@ -1,13 +1,27 @@
<script lang="ts">
import type { InputHTMLAttributes } from 'vue'
import type { VariantProps } from 'tailwind-variants'
import type { ComboboxRootProps, ComboboxRootEmits, ComboboxContentProps, ComboboxContentEmits, ComboboxArrowProps } from 'reka-ui'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/input-menu'
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
import { tv } from '../utils/tv'
import type { AvatarProps, ChipProps, InputProps } from '../types'
import type { AcceptableValue, ArrayOrNested, GetItemKeys, GetModelValue, GetModelValueEmits, NestedItem, EmitsToProps, ComponentConfig } from '../types/utils'
import type {
AcceptableValue,
ArrayOrNested,
GetItemKeys,
GetModelValue,
GetModelValueEmits,
NestedItem,
PartialString,
EmitsToProps
} from '../types/utils'
type InputMenu = ComponentConfig<typeof theme, AppConfig, 'inputMenu'>
const appConfigInputMenu = _appConfig as AppConfig & { ui: { inputMenu: Partial<typeof theme> } }
const inputMenu = tv({ extend: tv(theme), ...(appConfigInputMenu.ui?.inputMenu || {}) })
interface _InputMenuItem {
label?: string
@@ -28,7 +42,9 @@ interface _InputMenuItem {
}
export type InputMenuItem = _InputMenuItem | AcceptableValue | boolean
export interface InputMenuProps<T extends ArrayOrNested<InputMenuItem> = ArrayOrNested<InputMenuItem>, VK extends GetItemKeys<T> | undefined = undefined, M extends boolean = false> extends Pick<ComboboxRootProps<T>, 'open' | 'defaultOpen' | 'disabled' | 'name' | 'resetSearchTermOnBlur' | 'resetSearchTermOnSelect' | 'highlightOnHover'>, UseComponentIconsProps {
type InputMenuVariants = VariantProps<typeof inputMenu>
export interface InputMenuProps<T extends ArrayOrNested<InputMenuItem> = ArrayOrNested<InputMenuItem>, VK extends GetItemKeys<T> | undefined = undefined, M extends boolean = false> extends Pick<ComboboxRootProps<T>, 'open' | 'defaultOpen' | 'disabled' | 'name' | 'resetSearchTermOnBlur' | 'highlightOnHover'>, UseComponentIconsProps {
/**
* The element or component this component should render as.
* @defaultValue 'div'
@@ -41,15 +57,15 @@ export interface InputMenuProps<T extends ArrayOrNested<InputMenuItem> = ArrayOr
/**
* @defaultValue 'primary'
*/
color?: InputMenu['variants']['color']
color?: InputMenuVariants['color']
/**
* @defaultValue 'outline'
*/
variant?: InputMenu['variants']['variant']
variant?: InputMenuVariants['variant']
/**
* @defaultValue 'md'
*/
size?: InputMenu['variants']['size']
size?: InputMenuVariants['size']
required?: boolean
autofocus?: boolean
autofocusDelay?: number
@@ -122,7 +138,7 @@ export interface InputMenuProps<T extends ArrayOrNested<InputMenuItem> = ArrayOr
*/
ignoreFilter?: boolean
class?: any
ui?: InputMenu['slots']
ui?: PartialString<typeof inputMenu.slots>
}
export type InputMenuEmits<A extends ArrayOrNested<InputMenuItem>, VK extends GetItemKeys<A> | undefined, M extends boolean> = Pick<ComboboxRootEmits, 'update:open'> & {
@@ -145,16 +161,8 @@ export interface InputMenuSlots<
M extends boolean = false,
T extends NestedItem<A> = NestedItem<A>
> {
'leading'(props: {
modelValue?: GetModelValue<A, VK, M>
open: boolean
ui: { [K in keyof Required<InputMenu['slots']>]: (props?: Record<string, any>) => string }
}): any
'trailing'(props: {
modelValue?: GetModelValue<A, VK, M>
open: boolean
ui: { [K in keyof Required<InputMenu['slots']>]: (props?: Record<string, any>) => string }
}): any
'leading'(props: { modelValue?: GetModelValue<A, VK, M>, open: boolean, ui: ReturnType<typeof inputMenu> }): any
'trailing'(props: { modelValue?: GetModelValue<A, VK, M>, open: boolean, ui: ReturnType<typeof inputMenu> }): any
'empty'(props: { searchTerm?: string }): any
'item': SlotProps<T>
'item-leading': SlotProps<T>
@@ -167,7 +175,7 @@ export interface InputMenuSlots<
</script>
<script setup lang="ts" generic="T extends ArrayOrNested<InputMenuItem>, VK extends GetItemKeys<T> | undefined = undefined, M extends boolean = false">
import { computed, ref, toRef, onMounted, toRaw } from 'vue'
import { computed, ref, toRef, onMounted, toRaw, nextTick } from 'vue'
import { ComboboxRoot, ComboboxArrow, ComboboxAnchor, ComboboxInput, ComboboxTrigger, ComboboxPortal, ComboboxContent, ComboboxViewport, ComboboxEmpty, ComboboxGroup, ComboboxLabel, ComboboxSeparator, ComboboxItem, ComboboxItemIndicator, TagsInputRoot, TagsInputItem, TagsInputItemText, TagsInputItemDelete, TagsInputInput, useForwardPropsEmits, useFilter } from 'reka-ui'
import { defu } from 'defu'
import { isEqual } from 'ohash/utils'
@@ -178,7 +186,6 @@ import { useComponentIcons } from '../composables/useComponentIcons'
import { useFormField } from '../composables/useFormField'
import { useLocale } from '../composables/useLocale'
import { compare, get, isArrayOfArray } from '../utils'
import { tv } from '../utils/tv'
import UIcon from './Icon.vue'
import UAvatar from './Avatar.vue'
import UChip from './Chip.vue'
@@ -189,9 +196,7 @@ const props = withDefaults(defineProps<InputMenuProps<T, VK, M>>(), {
type: 'text',
autofocusDelay: 0,
portal: true,
labelKey: 'label' as never,
resetSearchTermOnBlur: true,
resetSearchTermOnSelect: true
labelKey: 'label' as never
})
const emits = defineEmits<InputMenuEmits<T, VK, M>>()
const slots = defineSlots<InputMenuSlots<T, VK, M>>()
@@ -199,10 +204,10 @@ const slots = defineSlots<InputMenuSlots<T, VK, M>>()
const searchTerm = defineModel<string>('searchTerm', { default: '' })
const { t } = useLocale()
const appConfig = useAppConfig() as InputMenu['AppConfig']
const appConfig = useAppConfig()
const { contains } = useFilter({ sensitivity: 'base' })
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'open', 'defaultOpen', 'required', 'multiple', 'resetSearchTermOnBlur', 'resetSearchTermOnSelect', 'highlightOnHover', 'ignoreFilter'), emits)
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'open', 'defaultOpen', 'required', 'multiple', 'resetSearchTermOnBlur', 'highlightOnHover', 'ignoreFilter'), emits)
const contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffset: 8, collisionPadding: 8, position: 'popper' }) as ComboboxContentProps)
const arrowProps = toRef(() => props.arrow as ComboboxArrowProps)
@@ -214,7 +219,7 @@ const inputSize = computed(() => buttonGroupSize.value || formGroupSize.value)
const [DefineCreateItemTemplate, ReuseCreateItemTemplate] = createReusableTemplate()
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.inputMenu || {}) })({
const ui = computed(() => inputMenu({
color: color.value,
variant: props.variant,
size: inputSize?.value,
@@ -306,10 +311,6 @@ function onUpdate(value: any) {
emits('change', event)
emitFormChange()
emitFormInput()
if (props.resetSearchTermOnSelect) {
searchTerm.value = ''
}
}
function onBlur(event: FocusEvent) {
@@ -323,29 +324,18 @@ function onFocus(event: FocusEvent) {
}
function onUpdateOpen(value: boolean) {
let timeoutId
if (!value) {
const event = new FocusEvent('blur')
emits('blur', event)
emitFormBlur()
// Since we use `displayValue` prop inside ComboboxInput we should reset searchTerm manually
// https://reka-ui.com/docs/components/combobox#api-reference
if (props.resetSearchTermOnBlur) {
const STATE_ANIMATION_DELAY_MS = 100
timeoutId = setTimeout(() => {
searchTerm.value = ''
}, STATE_ANIMATION_DELAY_MS)
}
} else {
const event = new FocusEvent('focus')
emits('focus', event)
emitFormFocus()
clearTimeout(timeoutId)
}
nextTick(() => {
searchTerm.value = ''
})
}
function onRemoveTag(event: any) {
@@ -357,22 +347,10 @@ function onRemoveTag(event: any) {
}
}
function onSelect(e: Event, item: InputMenuItem) {
if (!isInputItem(item)) {
return
}
if (item.disabled) {
e.preventDefault()
return
}
item.onSelect?.(e)
}
function isInputItem(item: InputMenuItem): item is _InputMenuItem {
return typeof item === 'object' && item !== null
}
defineExpose({
inputRef
})
@@ -436,7 +414,7 @@ defineExpose({
</TagsInputItemDelete>
</TagsInputItem>
<ComboboxInput v-model="searchTerm" as-child>
<ComboboxInput as-child @update:model-value="searchTerm = $event">
<TagsInputInput
ref="inputRef"
v-bind="{ ...$attrs, ...ariaAttrs }"
@@ -498,7 +476,7 @@ defineExpose({
:class="ui.item({ class: props.ui?.item })"
:disabled="isInputItem(item) && item.disabled"
:value="props.valueKey && isInputItem(item) ? get(item, String(props.valueKey)) : item"
@select="onSelect($event, item)"
@select="isInputItem(item) && item.onSelect?.($event)"
>
<slot name="item" :item="(item as NestedItem<T>)" :index="index">
<slot name="item-leading" :item="(item as NestedItem<T>)" :index="index">

View File

@@ -1,11 +1,18 @@
<script lang="ts">
import type { VariantProps } from 'tailwind-variants'
import type { NumberFieldRootProps } from 'reka-ui'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/input-number'
import { tv } from '../utils/tv'
import type { ButtonProps } from '../types'
import type { ComponentConfig } from '../types/utils'
import type { PartialString } from '../types/utils'
type InputNumber = ComponentConfig<typeof theme, AppConfig, 'inputNumber'>
const appConfigInputNumber = _appConfig as AppConfig & { ui: { inputNumber: Partial<typeof theme> } }
const inputNumber = tv({ extend: tv(theme), ...(appConfigInputNumber.ui?.inputNumber || {}) })
type InputNumberVariants = VariantProps<typeof inputNumber>
export interface InputNumberProps extends Pick<NumberFieldRootProps, 'modelValue' | 'defaultValue' | 'min' | 'max' | 'step' | 'stepSnapping' | 'disabled' | 'required' | 'id' | 'name' | 'formatOptions' | 'disableWheelChange'> {
/**
@@ -15,9 +22,9 @@ export interface InputNumberProps extends Pick<NumberFieldRootProps, 'modelValue
as?: any
/** The placeholder text when the input is empty. */
placeholder?: string
color?: InputNumber['variants']['color']
variant?: InputNumber['variants']['variant']
size?: InputNumber['variants']['size']
color?: InputNumberVariants['color']
variant?: InputNumberVariants['variant']
size?: InputNumberVariants['size']
/** Highlight the ring color like a focus state. */
highlight?: boolean
/**
@@ -55,7 +62,7 @@ export interface InputNumberProps extends Pick<NumberFieldRootProps, 'modelValue
*/
locale?: string
class?: any
ui?: InputNumber['slots']
ui?: PartialString<typeof inputNumber.slots>
}
export interface InputNumberEmits {
@@ -77,7 +84,6 @@ import { reactivePick } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { useFormField } from '../composables/useFormField'
import { useLocale } from '../composables/useLocale'
import { tv } from '../utils/tv'
import UButton from './Button.vue'
defineOptions({ inheritAttrs: false })
@@ -88,16 +94,15 @@ const props = withDefaults(defineProps<InputNumberProps>(), {
const emits = defineEmits<InputNumberEmits>()
defineSlots<InputNumberSlots>()
const appConfig = useAppConfig() as InputNumber['AppConfig']
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'min', 'max', 'step', 'stepSnapping', 'formatOptions', 'disableWheelChange'), emits)
const appConfig = useAppConfig()
const { emitFormBlur, emitFormFocus, emitFormChange, emitFormInput, id, color, size, name, highlight, disabled, ariaAttrs } = useFormField<InputNumberProps>(props)
const { t, code: codeLocale } = useLocale()
const locale = computed(() => props.locale || codeLocale.value)
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.inputNumber || {}) })({
const ui = computed(() => inputNumber({
color: color.value,
variant: props.variant,
size: size.value,

View File

@@ -1,10 +1,16 @@
<script lang="ts">
import type { VariantProps } from 'tailwind-variants'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/kbd'
import type { KbdKey } from '../composables/useKbd'
import type { ComponentConfig } from '../types/utils'
import { tv } from '../utils/tv'
type Kbd = ComponentConfig<typeof theme, AppConfig, 'kbd'>
const appConfigKbd = _appConfig as AppConfig & { ui: { kbd: Partial<typeof theme> } }
const kbd = tv({ extend: tv(theme), ...(appConfigKbd.ui?.kbd || {}) })
type KbdVariants = VariantProps<typeof kbd>
export interface KbdProps {
/**
@@ -16,11 +22,11 @@ export interface KbdProps {
/**
* @defaultValue 'outline'
*/
variant?: Kbd['variants']['variant']
variant?: KbdVariants['variant']
/**
* @defaultValue 'md'
*/
size?: Kbd['variants']['size']
size?: KbdVariants['size']
class?: any
}
@@ -30,11 +36,8 @@ export interface KbdSlots {
</script>
<script setup lang="ts">
import { computed } from 'vue'
import { Primitive } from 'reka-ui'
import { useAppConfig } from '#imports'
import { useKbd } from '../composables/useKbd'
import { tv } from '../utils/tv'
const props = withDefaults(defineProps<KbdProps>(), {
as: 'kbd'
@@ -42,13 +45,10 @@ const props = withDefaults(defineProps<KbdProps>(), {
defineSlots<KbdSlots>()
const { getKbdKey } = useKbd()
const appConfig = useAppConfig() as Kbd['AppConfig']
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.kbd || {}) }))
</script>
<template>
<Primitive :as="as" :class="ui({ variant, size, class: props.class })">
<Primitive :as="as" :class="kbd({ variant, size, class: props.class })">
<slot>
{{ getKbdKey(value) }}
</slot>

View File

@@ -1,11 +1,10 @@
<script lang="ts">
import type { ButtonHTMLAttributes } from 'vue'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import type { RouterLinkProps, RouteLocationRaw } from 'vue-router'
import theme from '#build/ui/link'
import type { ComponentConfig } from '../types/utils'
type Link = ComponentConfig<typeof theme, AppConfig, 'link'>
import { tv } from '../utils/tv'
interface NuxtLinkProps extends Omit<RouterLinkProps, 'to'> {
/**
@@ -53,6 +52,10 @@ interface NuxtLinkProps extends Omit<RouterLinkProps, 'to'> {
noPrefetch?: boolean
}
const appConfigLink = _appConfig as AppConfig & { ui: { link: Partial<typeof theme> } }
const link = tv({ extend: tv(theme), ...(appConfigLink.ui?.link || {}) })
export interface LinkProps extends NuxtLinkProps {
/**
* The element or component this component should render as when not a link.
@@ -88,12 +91,10 @@ export interface LinkSlots {
<script setup lang="ts">
import { computed } from 'vue'
import { defu } from 'defu'
import { isEqual, diff } from 'ohash/utils'
import { useForwardProps } from 'reka-ui'
import { reactiveOmit } from '@vueuse/core'
import { useRoute, useAppConfig } from '#imports'
import { tv } from '../utils/tv'
import { useRoute } from '#imports'
import ULinkBase from './LinkBase.vue'
defineOptions({ inheritAttrs: false })
@@ -109,20 +110,16 @@ const props = withDefaults(defineProps<LinkProps>(), {
defineSlots<LinkSlots>()
const route = useRoute()
const appConfig = useAppConfig() as Link['AppConfig']
const nuxtLinkProps = useForwardProps(reactiveOmit(props, 'as', 'type', 'disabled', 'active', 'exact', 'exactQuery', 'exactHash', 'activeClass', 'inactiveClass', 'raw', 'class'))
const ui = computed(() => tv({
extend: tv(theme),
...defu({
variants: {
active: {
true: props.activeClass,
false: props.inactiveClass
}
extend: link,
variants: {
active: {
true: props.activeClass,
false: props.inactiveClass
}
}, appConfig.ui?.link || {})
}
}))
function isPartiallyEqual(item1: any, item2: any) {

View File

@@ -1,11 +1,15 @@
<script lang="ts">
import type { DialogRootProps, DialogRootEmits, DialogContentProps, DialogContentEmits } from 'reka-ui'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/modal'
import { tv } from '../utils/tv'
import type { ButtonProps } from '../types'
import type { EmitsToProps, ComponentConfig } from '../types/utils'
import type { EmitsToProps } from '../types/utils'
type Modal = ComponentConfig<typeof theme, AppConfig, 'modal'>
const appConfigModal = _appConfig as AppConfig & { ui: { modal: Partial<typeof theme> } }
const modal = tv({ extend: tv(theme), ...(appConfigModal.ui?.modal || {}) })
export interface ModalProps extends DialogRootProps {
title?: string
@@ -50,7 +54,7 @@ export interface ModalProps extends DialogRootProps {
*/
dismissible?: boolean
class?: any
ui?: Modal['slots']
ui?: Partial<typeof modal.slots>
}
export interface ModalEmits extends DialogRootEmits {
@@ -63,7 +67,7 @@ export interface ModalSlots {
header(props?: {}): any
title(props?: {}): any
description(props?: {}): any
close(props: { ui: { [K in keyof Required<Modal['slots']>]: (props?: Record<string, any>) => string } }): any
close(props: { ui: ReturnType<typeof modal> }): any
body(props?: {}): any
footer(props?: {}): any
}
@@ -75,7 +79,6 @@ import { DialogRoot, DialogTrigger, DialogPortal, DialogOverlay, DialogContent,
import { reactivePick } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { useLocale } from '../composables/useLocale'
import { tv } from '../utils/tv'
import UButton from './Button.vue'
const props = withDefaults(defineProps<ModalProps>(), {
@@ -90,7 +93,7 @@ const emits = defineEmits<ModalEmits>()
const slots = defineSlots<ModalSlots>()
const { t } = useLocale()
const appConfig = useAppConfig() as Modal['AppConfig']
const appConfig = useAppConfig()
const rootProps = useForwardPropsEmits(reactivePick(props, 'open', 'defaultOpen', 'modal'), emits)
const contentProps = toRef(() => props.content)
@@ -111,7 +114,7 @@ const contentEvents = computed(() => {
return events
})
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.modal || {}) })({
const ui = computed(() => modal({
transition: props.transition,
fullscreen: props.fullscreen
}))

View File

@@ -1,12 +1,24 @@
<!-- eslint-disable vue/block-tag-newline -->
<script lang="ts">
import type { VariantProps } from 'tailwind-variants'
import type { NavigationMenuRootProps, NavigationMenuRootEmits, NavigationMenuContentProps, NavigationMenuContentEmits, CollapsibleRootProps } from 'reka-ui'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/navigation-menu'
import { tv } from '../utils/tv'
import type { AvatarProps, BadgeProps, LinkProps } from '../types'
import type { ArrayOrNested, DynamicSlots, MergeTypes, NestedItem, EmitsToProps, ComponentConfig } from '../types/utils'
import type {
ArrayOrNested,
DynamicSlots,
MergeTypes,
NestedItem,
PartialString,
EmitsToProps
} from '../types/utils'
type NavigationMenu = ComponentConfig<typeof theme, AppConfig, 'navigationMenu'>
const appConfigNavigationMenu = _appConfig as AppConfig & { ui: { navigationMenu: Partial<typeof theme> } }
const navigationMenu = tv({ extend: tv(theme), ...(appConfigNavigationMenu.ui?.navigationMenu || {}) })
export interface NavigationMenuChildItem extends Omit<NavigationMenuItem, 'type'> {
/** Description is only used when `orientation` is `horizontal`. */
@@ -43,6 +55,8 @@ export interface NavigationMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'cu
[key: string]: any
}
type NavigationMenuVariants = VariantProps<typeof navigationMenu>
export interface NavigationMenuProps<T extends ArrayOrNested<NavigationMenuItem> = ArrayOrNested<NavigationMenuItem>> extends Pick<NavigationMenuRootProps, 'modelValue' | 'defaultValue' | 'delayDuration' | 'disableClickTrigger' | 'disableHoverTrigger' | 'skipDelayDuration' | 'disablePointerLeaveClose' | 'unmountOnHide'> {
/**
* The element or component this component should render as.
@@ -66,11 +80,11 @@ export interface NavigationMenuProps<T extends ArrayOrNested<NavigationMenuItem>
/**
* @defaultValue 'primary'
*/
color?: NavigationMenu['variants']['color']
color?: NavigationMenuVariants['color']
/**
* @defaultValue 'pill'
*/
variant?: NavigationMenu['variants']['variant']
variant?: NavigationMenuVariants['variant']
/**
* The orientation of the menu.
* @defaultValue 'horizontal'
@@ -87,7 +101,7 @@ export interface NavigationMenuProps<T extends ArrayOrNested<NavigationMenuItem>
/**
* @defaultValue 'primary'
*/
highlightColor?: NavigationMenu['variants']['highlightColor']
highlightColor?: NavigationMenuVariants['highlightColor']
/** The content of the menu. */
content?: Omit<NavigationMenuContentProps, 'as' | 'asChild' | 'forceMount'> & Partial<EmitsToProps<NavigationMenuContentEmits>>
/**
@@ -95,7 +109,7 @@ export interface NavigationMenuProps<T extends ArrayOrNested<NavigationMenuItem>
* Only works when `orientation` is `horizontal`.
* @defaultValue 'horizontal'
*/
contentOrientation?: NavigationMenu['variants']['contentOrientation']
contentOrientation?: NavigationMenuVariants['contentOrientation']
/**
* Display an arrow alongside the menu.
* @defaultValue false
@@ -107,7 +121,7 @@ export interface NavigationMenuProps<T extends ArrayOrNested<NavigationMenuItem>
*/
labelKey?: keyof NestedItem<T>
class?: any
ui?: NavigationMenu['slots']
ui?: PartialString<typeof navigationMenu.slots>
}
export interface NavigationMenuEmits extends NavigationMenuRootEmits {}
@@ -133,7 +147,6 @@ import { NavigationMenuRoot, NavigationMenuList, NavigationMenuItem, NavigationM
import { createReusableTemplate } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { get, isArrayOfArray } from '../utils'
import { tv } from '../utils/tv'
import { pickLinkProps } from '../utils/link'
import ULinkBase from './LinkBase.vue'
import ULink from './Link.vue'
@@ -153,8 +166,6 @@ const props = withDefaults(defineProps<NavigationMenuProps<T>>(), {
const emits = defineEmits<NavigationMenuEmits>()
const slots = defineSlots<NavigationMenuSlots<T>>()
const appConfig = useAppConfig() as NavigationMenu['AppConfig']
const rootProps = useForwardPropsEmits(computed(() => ({
as: props.as,
modelValue: props.modelValue,
@@ -167,8 +178,11 @@ const rootProps = useForwardPropsEmits(computed(() => ({
disablePointerLeaveClose: props.disablePointerLeaveClose,
unmountOnHide: props.unmountOnHide
})), emits)
const contentProps = toRef(() => props.content)
const appConfig = useAppConfig()
const [DefineLinkTemplate, ReuseLinkTemplate] = createReusableTemplate<{ item: NavigationMenuItem, index: number, active?: boolean }>()
const [DefineItemTemplate, ReuseItemTemplate] = createReusableTemplate<{ item: NavigationMenuItem, index: number, level?: number }>({
props: {
@@ -178,7 +192,7 @@ const [DefineItemTemplate, ReuseItemTemplate] = createReusableTemplate<{ item: N
}
})
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.navigationMenu || {}) })({
const ui = computed(() => navigationMenu({
orientation: props.orientation,
contentOrientation: props.contentOrientation,
collapsed: props.collapsed,

View File

@@ -22,7 +22,7 @@ const onClose = (id: symbol, value: any) => {
v-for="overlay in mountedOverlays"
:key="overlay.id"
v-bind="overlay.props"
v-model:open="overlay.isOpen"
v-model:open="overlay.modelValue"
@close="(value:any) => onClose(overlay.id, value)"
@after:leave="onAfterLeave(overlay.id)"
/>

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