mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-29 11:20:36 +01:00
fix(components): improve generic types (#3331)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
<!-- eslint-disable no-useless-escape -->
|
<!-- eslint-disable no-useless-escape -->
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { ChipProps } from '@nuxt/ui'
|
||||||
import json5 from 'json5'
|
import json5 from 'json5'
|
||||||
import { upperFirst, camelCase, kebabCase } from 'scule'
|
import { upperFirst, camelCase, kebabCase } from 'scule'
|
||||||
import { hash } from 'ohash'
|
import { hash } from 'ohash'
|
||||||
@@ -53,6 +54,8 @@ const props = defineProps<{
|
|||||||
hide?: string[]
|
hide?: string[]
|
||||||
/** List of props to externalize in script setup */
|
/** List of props to externalize in script setup */
|
||||||
external?: string[]
|
external?: string[]
|
||||||
|
/** The types of the externalized props */
|
||||||
|
externalTypes?: string[]
|
||||||
/** List of props to use with `v-model` */
|
/** List of props to use with `v-model` */
|
||||||
model?: string[]
|
model?: string[]
|
||||||
/** List of props to cast from code and selection */
|
/** List of props to cast from code and selection */
|
||||||
@@ -209,11 +212,21 @@ ${props.slots?.default}
|
|||||||
code += `
|
code += `
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
`
|
`
|
||||||
for (const key of props.external) {
|
if (props.externalTypes?.length) {
|
||||||
|
const removeArrayBrackets = (type: string): string => type.endsWith('[]') ? removeArrayBrackets(type.slice(0, -2)) : type
|
||||||
|
|
||||||
|
const types = props.externalTypes.map(type => removeArrayBrackets(type))
|
||||||
|
code += `import type { ${types.join(', ')} } from '@nuxt/ui${props.pro ? '-pro' : ''}'
|
||||||
|
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [i, key] of props.external.entries()) {
|
||||||
const cast = props.cast?.[key]
|
const cast = props.cast?.[key]
|
||||||
const value = cast ? castMap[cast]!.template(componentProps[key]) : json5.stringify(componentProps[key], null, 2)?.replace(/,([ |\t\n]+[}|\]])/g, '$1')
|
const value = cast ? castMap[cast]!.template(componentProps[key]) : json5.stringify(componentProps[key], null, 2)?.replace(/,([ |\t\n]+[}|\]])/g, '$1')
|
||||||
|
const type = props.externalTypes?.[i] ? `<${props.externalTypes[i]}>` : ''
|
||||||
|
|
||||||
code += `const ${key === 'modelValue' ? 'value' : key} = ref(${value})
|
code += `const ${key === 'modelValue' ? 'value' : key} = ref${type}(${value})
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
code += `<\/script>
|
code += `<\/script>
|
||||||
@@ -346,7 +359,7 @@ const { data: ast } = await useAsyncData(`component-code-${name}-${hash({ props:
|
|||||||
inset
|
inset
|
||||||
standalone
|
standalone
|
||||||
:color="(modelValue as any)"
|
:color="(modelValue as any)"
|
||||||
:size="ui.itemLeadingChipSize()"
|
:size="(ui.itemLeadingChipSize() as ChipProps['size'])"
|
||||||
class="size-2"
|
class="size-2"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { ChipProps } from '@nuxt/ui'
|
||||||
import { camelCase } from 'scule'
|
import { camelCase } from 'scule'
|
||||||
import { useElementSize } from '@vueuse/core'
|
import { useElementSize } from '@vueuse/core'
|
||||||
import { get, set } from '#ui/utils'
|
import { get, set } from '#ui/utils'
|
||||||
@@ -185,7 +186,7 @@ const urlSearchParams = computed(() => {
|
|||||||
inset
|
inset
|
||||||
standalone
|
standalone
|
||||||
:color="(modelValue as any)"
|
:color="(modelValue as any)"
|
||||||
:size="ui.itemLeadingChipSize()"
|
:size="(ui.itemLeadingChipSize() as ChipProps['size'])"
|
||||||
class="size-2"
|
class="size-2"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const items = [
|
import type { AccordionItem } from '@nuxt/ui'
|
||||||
|
|
||||||
|
const items: AccordionItem[] = [
|
||||||
{
|
{
|
||||||
label: 'Icons',
|
label: 'Icons',
|
||||||
icon: 'i-lucide-smile'
|
icon: 'i-lucide-smile'
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const items = [
|
import type { AccordionItem } from '@nuxt/ui'
|
||||||
|
|
||||||
|
const items: AccordionItem[] = [
|
||||||
{
|
{
|
||||||
label: 'Icons',
|
label: 'Icons',
|
||||||
icon: 'i-lucide-smile'
|
icon: 'i-lucide-smile'
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { AccordionItem } from '@nuxt/ui'
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
label: 'Icons',
|
label: 'Icons',
|
||||||
@@ -8,7 +10,7 @@ const items = [
|
|||||||
{
|
{
|
||||||
label: 'Colors',
|
label: 'Colors',
|
||||||
icon: 'i-lucide-swatch-book',
|
icon: 'i-lucide-swatch-book',
|
||||||
slot: 'colors',
|
slot: 'colors' as const,
|
||||||
content: 'Choose a primary and a neutral color from your Tailwind CSS theme.'
|
content: 'Choose a primary and a neutral color from your Tailwind CSS theme.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -16,7 +18,7 @@ const items = [
|
|||||||
icon: 'i-lucide-box',
|
icon: 'i-lucide-box',
|
||||||
content: 'You can customize components by using the `class` / `ui` props or in your app.config.ts.'
|
content: 'You can customize components by using the `class` / `ui` props or in your app.config.ts.'
|
||||||
}
|
}
|
||||||
]
|
] satisfies AccordionItem[]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const items = [
|
import type { AccordionItem } from '@nuxt/ui'
|
||||||
|
|
||||||
|
const items: AccordionItem[] = [
|
||||||
{
|
{
|
||||||
label: 'Icons',
|
label: 'Icons',
|
||||||
icon: 'i-lucide-smile',
|
icon: 'i-lucide-smile',
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { BreadcrumbItem } from '@nuxt/ui'
|
||||||
|
|
||||||
const items = [{
|
const items = [{
|
||||||
label: 'Home',
|
label: 'Home',
|
||||||
to: '/'
|
to: '/'
|
||||||
}, {
|
}, {
|
||||||
slot: 'dropdown',
|
slot: 'dropdown' as const,
|
||||||
icon: 'i-lucide-ellipsis',
|
icon: 'i-lucide-ellipsis',
|
||||||
children: [{
|
children: [{
|
||||||
label: 'Documentation'
|
label: 'Documentation'
|
||||||
@@ -18,7 +20,7 @@ const items = [{
|
|||||||
}, {
|
}, {
|
||||||
label: 'Breadcrumb',
|
label: 'Breadcrumb',
|
||||||
to: '/components/breadcrumb'
|
to: '/components/breadcrumb'
|
||||||
}]
|
}] satisfies BreadcrumbItem[]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const items = [{
|
import type { BreadcrumbItem } from '@nuxt/ui'
|
||||||
|
|
||||||
|
const items: BreadcrumbItem[] = [{
|
||||||
label: 'Home',
|
label: 'Home',
|
||||||
to: '/'
|
to: '/'
|
||||||
}, {
|
}, {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const items = [{
|
import type { DropdownMenuItem } from '@nuxt/ui'
|
||||||
|
|
||||||
|
const items: DropdownMenuItem[] = [{
|
||||||
label: 'Team',
|
label: 'Team',
|
||||||
icon: 'i-lucide-users'
|
icon: 'i-lucide-users'
|
||||||
}, {
|
}, {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const groups = [{
|
|||||||
label: 'Billing',
|
label: 'Billing',
|
||||||
icon: 'i-lucide-credit-card',
|
icon: 'i-lucide-credit-card',
|
||||||
kbds: ['meta', 'B'],
|
kbds: ['meta', 'B'],
|
||||||
slot: 'billing'
|
slot: 'billing' as const
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Notifications',
|
label: 'Notifications',
|
||||||
@@ -25,7 +25,7 @@ const groups = [{
|
|||||||
}, {
|
}, {
|
||||||
id: 'users',
|
id: 'users',
|
||||||
label: 'Users',
|
label: 'Users',
|
||||||
slot: 'users',
|
slot: 'users' as const,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
label: 'Benjamin Canac',
|
label: 'Benjamin Canac',
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { ContextMenuItem } from '@nuxt/ui'
|
||||||
|
|
||||||
const showSidebar = ref(true)
|
const showSidebar = ref(true)
|
||||||
const showToolbar = ref(false)
|
const showToolbar = ref(false)
|
||||||
|
|
||||||
const items = computed(() => [{
|
const items = computed<ContextMenuItem[]>(() => [{
|
||||||
label: 'View',
|
label: 'View',
|
||||||
type: 'label' as const
|
type: 'label' as const
|
||||||
}, {
|
}, {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const items = [
|
import type { ContextMenuItem } from '@nuxt/ui'
|
||||||
|
|
||||||
|
const items: ContextMenuItem[][] = [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
label: 'View',
|
label: 'View',
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { ContextMenuItem } from '@nuxt/ui'
|
||||||
|
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
|
|
||||||
const items = [{
|
const items: ContextMenuItem[] = [{
|
||||||
label: 'Refresh the Page',
|
label: 'Refresh the Page',
|
||||||
slot: 'refresh'
|
slot: 'refresh'
|
||||||
}, {
|
}, {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { DropdownMenuItem } from '@nuxt/ui'
|
||||||
|
|
||||||
const showBookmarks = ref(true)
|
const showBookmarks = ref(true)
|
||||||
const showHistory = ref(false)
|
const showHistory = ref(false)
|
||||||
const showDownloads = ref(false)
|
const showDownloads = ref(false)
|
||||||
@@ -36,7 +38,7 @@ const items = computed(() => [{
|
|||||||
onUpdateChecked(checked: boolean) {
|
onUpdateChecked(checked: boolean) {
|
||||||
showDownloads.value = checked
|
showDownloads.value = checked
|
||||||
}
|
}
|
||||||
}])
|
}] satisfies DropdownMenuItem[])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const items = [
|
import type { DropdownMenuItem } from '@nuxt/ui'
|
||||||
|
|
||||||
|
const items: DropdownMenuItem[][] = [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
label: 'View',
|
label: 'View',
|
||||||
@@ -17,7 +19,7 @@ const items = [
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
label: 'Delete',
|
label: 'Delete',
|
||||||
color: 'error' as const,
|
color: 'error',
|
||||||
icon: 'i-lucide-trash'
|
icon: 'i-lucide-trash'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -27,9 +29,5 @@ const items = [
|
|||||||
<template>
|
<template>
|
||||||
<UDropdownMenu :items="items" :ui="{ content: 'w-48' }">
|
<UDropdownMenu :items="items" :ui="{ content: 'w-48' }">
|
||||||
<UButton label="Open" color="neutral" variant="outline" icon="i-lucide-menu" />
|
<UButton label="Open" color="neutral" variant="outline" icon="i-lucide-menu" />
|
||||||
|
|
||||||
<template #profile-trailing>
|
|
||||||
<UIcon name="i-lucide-badge-check" class="shrink-0 size-5 text-(--ui-primary)" />
|
|
||||||
</template>
|
|
||||||
</UDropdownMenu>
|
</UDropdownMenu>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const items = [{
|
import type { DropdownMenuItem } from '@nuxt/ui'
|
||||||
label: 'Profile',
|
|
||||||
icon: 'i-lucide-user',
|
const items = [
|
||||||
slot: 'profile'
|
{
|
||||||
}, {
|
label: 'Profile',
|
||||||
label: 'Billing',
|
icon: 'i-lucide-user',
|
||||||
icon: 'i-lucide-credit-card'
|
slot: 'profile' as const
|
||||||
}, {
|
}, {
|
||||||
label: 'Settings',
|
label: 'Billing',
|
||||||
icon: 'i-lucide-cog'
|
icon: 'i-lucide-credit-card'
|
||||||
}]
|
}, {
|
||||||
|
label: 'Settings',
|
||||||
|
icon: 'i-lucide-cog'
|
||||||
|
}
|
||||||
|
] satisfies DropdownMenuItem[]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -1,20 +1,24 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { DropdownMenuItem } from '@nuxt/ui'
|
||||||
|
|
||||||
const open = ref(false)
|
const open = ref(false)
|
||||||
|
|
||||||
defineShortcuts({
|
defineShortcuts({
|
||||||
o: () => open.value = !open.value
|
o: () => open.value = !open.value
|
||||||
})
|
})
|
||||||
|
|
||||||
const items = [{
|
const items: DropdownMenuItem[] = [
|
||||||
label: 'Profile',
|
{
|
||||||
icon: 'i-lucide-user'
|
label: 'Profile',
|
||||||
}, {
|
icon: 'i-lucide-user'
|
||||||
label: 'Billing',
|
}, {
|
||||||
icon: 'i-lucide-credit-card'
|
label: 'Billing',
|
||||||
}, {
|
icon: 'i-lucide-credit-card'
|
||||||
label: 'Settings',
|
}, {
|
||||||
icon: 'i-lucide-cog'
|
label: 'Settings',
|
||||||
}]
|
icon: 'i-lucide-cog'
|
||||||
|
}
|
||||||
|
]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ function onOpen() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UInputMenu
|
<UInputMenu
|
||||||
:items="countries || []"
|
:items="countries"
|
||||||
:loading="status === 'pending'"
|
:loading="status === 'pending'"
|
||||||
label-key="name"
|
label-key="name"
|
||||||
:search-input="{ icon: 'i-lucide-search' }"
|
:search-input="{ icon: 'i-lucide-search' }"
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { AvatarProps } from '@nuxt/ui'
|
||||||
|
|
||||||
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
|
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
|
||||||
key: 'typicode-users',
|
key: 'typicode-users',
|
||||||
transform: (data: { id: number, name: string }[]) => {
|
transform: (data: { id: number, name: string }[]) => {
|
||||||
@@ -6,7 +8,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
|||||||
label: user.name,
|
label: user.name,
|
||||||
value: String(user.id),
|
value: String(user.id),
|
||||||
avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` }
|
avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` }
|
||||||
})) || []
|
}))
|
||||||
},
|
},
|
||||||
lazy: true
|
lazy: true
|
||||||
})
|
})
|
||||||
@@ -14,7 +16,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UInputMenu
|
<UInputMenu
|
||||||
:items="users || []"
|
:items="users"
|
||||||
:loading="status === 'pending'"
|
:loading="status === 'pending'"
|
||||||
icon="i-lucide-user"
|
icon="i-lucide-user"
|
||||||
placeholder="Select user"
|
placeholder="Select user"
|
||||||
@@ -23,7 +25,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
|||||||
<UAvatar
|
<UAvatar
|
||||||
v-if="modelValue"
|
v-if="modelValue"
|
||||||
v-bind="modelValue.avatar"
|
v-bind="modelValue.avatar"
|
||||||
:size="ui.leadingAvatarSize()"
|
:size="(ui.leadingAvatarSize() as AvatarProps['size'])"
|
||||||
:class="ui.leadingAvatar()"
|
:class="ui.leadingAvatar()"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { AvatarProps } from '@nuxt/ui'
|
||||||
|
|
||||||
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
|
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
|
||||||
key: 'typicode-users-email',
|
key: 'typicode-users-email',
|
||||||
transform: (data: { id: number, name: string, email: string }[]) => {
|
transform: (data: { id: number, name: string, email: string }[]) => {
|
||||||
@@ -7,7 +9,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
|||||||
email: user.email,
|
email: user.email,
|
||||||
value: String(user.id),
|
value: String(user.id),
|
||||||
avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` }
|
avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` }
|
||||||
})) || []
|
}))
|
||||||
},
|
},
|
||||||
lazy: true
|
lazy: true
|
||||||
})
|
})
|
||||||
@@ -15,7 +17,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UInputMenu
|
<UInputMenu
|
||||||
:items="users || []"
|
:items="users"
|
||||||
:loading="status === 'pending'"
|
:loading="status === 'pending'"
|
||||||
:filter-fields="['label', 'email']"
|
:filter-fields="['label', 'email']"
|
||||||
icon="i-lucide-user"
|
icon="i-lucide-user"
|
||||||
@@ -26,7 +28,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
|||||||
<UAvatar
|
<UAvatar
|
||||||
v-if="modelValue"
|
v-if="modelValue"
|
||||||
v-bind="modelValue.avatar"
|
v-bind="modelValue.avatar"
|
||||||
:size="ui.leadingAvatarSize()"
|
:size="(ui.leadingAvatarSize() as AvatarProps['size'])"
|
||||||
:class="ui.leadingAvatar()"
|
:class="ui.leadingAvatar()"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { AvatarProps } from '@nuxt/ui'
|
||||||
|
|
||||||
const searchTerm = ref('')
|
const searchTerm = ref('')
|
||||||
const searchTermDebounced = refDebounced(searchTerm, 200)
|
const searchTermDebounced = refDebounced(searchTerm, 200)
|
||||||
|
|
||||||
@@ -10,7 +12,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
|||||||
label: user.name,
|
label: user.name,
|
||||||
value: String(user.id),
|
value: String(user.id),
|
||||||
avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` }
|
avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` }
|
||||||
})) || []
|
}))
|
||||||
},
|
},
|
||||||
lazy: true
|
lazy: true
|
||||||
})
|
})
|
||||||
@@ -19,7 +21,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
|||||||
<template>
|
<template>
|
||||||
<UInputMenu
|
<UInputMenu
|
||||||
v-model:search-term="searchTerm"
|
v-model:search-term="searchTerm"
|
||||||
:items="users || []"
|
:items="users"
|
||||||
:loading="status === 'pending'"
|
:loading="status === 'pending'"
|
||||||
ignore-filter
|
ignore-filter
|
||||||
icon="i-lucide-user"
|
icon="i-lucide-user"
|
||||||
@@ -29,7 +31,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
|||||||
<UAvatar
|
<UAvatar
|
||||||
v-if="modelValue"
|
v-if="modelValue"
|
||||||
v-bind="modelValue.avatar"
|
v-bind="modelValue.avatar"
|
||||||
:size="ui.leadingAvatarSize()"
|
:size="(ui.leadingAvatarSize() as AvatarProps['size'])"
|
||||||
:class="ui.leadingAvatar()"
|
:class="ui.leadingAvatar()"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { InputMenuItem } from '@nuxt/ui'
|
||||||
|
|
||||||
const items = ref([
|
const items = ref([
|
||||||
{
|
{
|
||||||
label: 'benjamincanac',
|
label: 'benjamincanac',
|
||||||
@@ -23,8 +25,16 @@ const items = ref([
|
|||||||
src: 'https://github.com/noook.png',
|
src: 'https://github.com/noook.png',
|
||||||
alt: 'noook'
|
alt: 'noook'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'sandros94',
|
||||||
|
value: 'sandros94',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://github.com/sandros94.png',
|
||||||
|
alt: 'sandros94'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
])
|
] satisfies InputMenuItem[])
|
||||||
const value = ref(items.value[0])
|
const value = ref(items.value[0])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,30 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { InputMenuItem, ChipProps } from '@nuxt/ui'
|
||||||
|
|
||||||
const items = ref([
|
const items = ref([
|
||||||
{
|
{
|
||||||
label: 'bug',
|
label: 'bug',
|
||||||
value: 'bug',
|
value: 'bug',
|
||||||
chip: {
|
chip: {
|
||||||
color: 'error' as const
|
color: 'error'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'feature',
|
label: 'feature',
|
||||||
value: 'feature',
|
value: 'feature',
|
||||||
chip: {
|
chip: {
|
||||||
color: 'success' as const
|
color: 'success'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'enhancement',
|
label: 'enhancement',
|
||||||
value: 'enhancement',
|
value: 'enhancement',
|
||||||
chip: {
|
chip: {
|
||||||
color: 'info' as const
|
color: 'info'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
])
|
] satisfies InputMenuItem[])
|
||||||
|
|
||||||
const value = ref(items.value[0])
|
const value = ref(items.value[0])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -33,7 +36,7 @@ const value = ref(items.value[0])
|
|||||||
v-bind="modelValue.chip"
|
v-bind="modelValue.chip"
|
||||||
inset
|
inset
|
||||||
standalone
|
standalone
|
||||||
:size="ui.itemLeadingChipSize()"
|
:size="(ui.itemLeadingChipSize() as ChipProps['size'])"
|
||||||
:class="ui.itemLeadingChip()"
|
:class="ui.itemLeadingChip()"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { InputMenuItem } from '@nuxt/ui'
|
||||||
|
|
||||||
const items = ref([
|
const items = ref([
|
||||||
{
|
{
|
||||||
label: 'Backlog',
|
label: 'Backlog',
|
||||||
@@ -20,7 +22,8 @@ const items = ref([
|
|||||||
value: 'done',
|
value: 'done',
|
||||||
icon: 'i-lucide-circle-check'
|
icon: 'i-lucide-circle-check'
|
||||||
}
|
}
|
||||||
])
|
] satisfies InputMenuItem[])
|
||||||
|
|
||||||
const value = ref(items.value[0])
|
const value = ref(items.value[0])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { NavigationMenuItem } from '@nuxt/ui'
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
label: 'Docs',
|
label: 'Docs',
|
||||||
icon: 'i-lucide-book-open',
|
icon: 'i-lucide-book-open',
|
||||||
slot: 'docs',
|
slot: 'docs' as const,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
label: 'Icons',
|
label: 'Icons',
|
||||||
@@ -22,7 +24,7 @@ const items = [
|
|||||||
{
|
{
|
||||||
label: 'Components',
|
label: 'Components',
|
||||||
icon: 'i-lucide-box',
|
icon: 'i-lucide-box',
|
||||||
slot: 'components',
|
slot: 'components' as const,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
label: 'Link',
|
label: 'Link',
|
||||||
@@ -54,7 +56,7 @@ const items = [
|
|||||||
label: 'GitHub',
|
label: 'GitHub',
|
||||||
icon: 'i-simple-icons-github'
|
icon: 'i-simple-icons-github'
|
||||||
}
|
}
|
||||||
]
|
] satisfies NavigationMenuItem[]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const items = [
|
import type { NavigationMenuItem } from '@nuxt/ui'
|
||||||
|
|
||||||
|
const items: NavigationMenuItem[] = [
|
||||||
{
|
{
|
||||||
label: 'Guide',
|
label: 'Guide',
|
||||||
icon: 'i-lucide-book-open'
|
icon: 'i-lucide-book-open'
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const items = [
|
import type { NavigationMenuItem } from '@nuxt/ui'
|
||||||
|
|
||||||
|
const items: NavigationMenuItem[] = [
|
||||||
{
|
{
|
||||||
label: 'Guide',
|
label: 'Guide',
|
||||||
icon: 'i-lucide-book-open',
|
icon: 'i-lucide-book-open',
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ const { data: countries, status, execute } = await useLazyFetch<{
|
|||||||
code: string
|
code: string
|
||||||
emoji: string
|
emoji: string
|
||||||
}[]>('/api/countries.json', {
|
}[]>('/api/countries.json', {
|
||||||
immediate: false,
|
immediate: false
|
||||||
default: () => []
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function onOpen() {
|
function onOpen() {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { AvatarProps } from '@nuxt/ui'
|
||||||
|
|
||||||
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
|
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
|
||||||
key: 'typicode-users',
|
key: 'typicode-users',
|
||||||
transform: (data: { id: number, name: string }[]) => {
|
transform: (data: { id: number, name: string }[]) => {
|
||||||
@@ -6,7 +8,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
|||||||
label: user.name,
|
label: user.name,
|
||||||
value: String(user.id),
|
value: String(user.id),
|
||||||
avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` }
|
avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` }
|
||||||
})) || []
|
}))
|
||||||
},
|
},
|
||||||
lazy: true
|
lazy: true
|
||||||
})
|
})
|
||||||
@@ -14,7 +16,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<USelectMenu
|
<USelectMenu
|
||||||
:items="users || []"
|
:items="users"
|
||||||
:loading="status === 'pending'"
|
:loading="status === 'pending'"
|
||||||
icon="i-lucide-user"
|
icon="i-lucide-user"
|
||||||
placeholder="Select user"
|
placeholder="Select user"
|
||||||
@@ -24,7 +26,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
|||||||
<UAvatar
|
<UAvatar
|
||||||
v-if="modelValue"
|
v-if="modelValue"
|
||||||
v-bind="modelValue.avatar"
|
v-bind="modelValue.avatar"
|
||||||
:size="ui.leadingAvatarSize()"
|
:size="(ui.leadingAvatarSize() as AvatarProps['size'])"
|
||||||
:class="ui.leadingAvatar()"
|
:class="ui.leadingAvatar()"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { AvatarProps } from '@nuxt/ui'
|
||||||
|
|
||||||
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
|
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
|
||||||
key: 'typicode-users-email',
|
key: 'typicode-users-email',
|
||||||
transform: (data: { id: number, name: string, email: string }[]) => {
|
transform: (data: { id: number, name: string, email: string }[]) => {
|
||||||
@@ -7,7 +9,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
|||||||
email: user.email,
|
email: user.email,
|
||||||
value: String(user.id),
|
value: String(user.id),
|
||||||
avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` }
|
avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` }
|
||||||
})) || []
|
}))
|
||||||
},
|
},
|
||||||
lazy: true
|
lazy: true
|
||||||
})
|
})
|
||||||
@@ -15,7 +17,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<USelectMenu
|
<USelectMenu
|
||||||
:items="users || []"
|
:items="users"
|
||||||
:loading="status === 'pending'"
|
:loading="status === 'pending'"
|
||||||
:filter-fields="['label', 'email']"
|
:filter-fields="['label', 'email']"
|
||||||
icon="i-lucide-user"
|
icon="i-lucide-user"
|
||||||
@@ -26,7 +28,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
|||||||
<UAvatar
|
<UAvatar
|
||||||
v-if="modelValue"
|
v-if="modelValue"
|
||||||
v-bind="modelValue.avatar"
|
v-bind="modelValue.avatar"
|
||||||
:size="ui.leadingAvatarSize()"
|
:size="(ui.leadingAvatarSize() as AvatarProps['size'])"
|
||||||
:class="ui.leadingAvatar()"
|
:class="ui.leadingAvatar()"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { AvatarProps } from '@nuxt/ui'
|
||||||
|
|
||||||
const searchTerm = ref('')
|
const searchTerm = ref('')
|
||||||
const searchTermDebounced = refDebounced(searchTerm, 200)
|
const searchTermDebounced = refDebounced(searchTerm, 200)
|
||||||
|
|
||||||
@@ -10,7 +12,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
|||||||
label: user.name,
|
label: user.name,
|
||||||
value: String(user.id),
|
value: String(user.id),
|
||||||
avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` }
|
avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` }
|
||||||
})) || []
|
}))
|
||||||
},
|
},
|
||||||
lazy: true
|
lazy: true
|
||||||
})
|
})
|
||||||
@@ -19,7 +21,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
|||||||
<template>
|
<template>
|
||||||
<USelectMenu
|
<USelectMenu
|
||||||
v-model:search-term="searchTerm"
|
v-model:search-term="searchTerm"
|
||||||
:items="users || []"
|
:items="users"
|
||||||
:loading="status === 'pending'"
|
:loading="status === 'pending'"
|
||||||
ignore-filter
|
ignore-filter
|
||||||
icon="i-lucide-user"
|
icon="i-lucide-user"
|
||||||
@@ -30,7 +32,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
|||||||
<UAvatar
|
<UAvatar
|
||||||
v-if="modelValue"
|
v-if="modelValue"
|
||||||
v-bind="modelValue.avatar"
|
v-bind="modelValue.avatar"
|
||||||
:size="ui.leadingAvatarSize()"
|
:size="(ui.leadingAvatarSize() as AvatarProps['size'])"
|
||||||
:class="ui.leadingAvatar()"
|
:class="ui.leadingAvatar()"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { SelectMenuItem } from '@nuxt/ui'
|
||||||
|
|
||||||
const items = ref([
|
const items = ref([
|
||||||
{
|
{
|
||||||
label: 'benjamincanac',
|
label: 'benjamincanac',
|
||||||
@@ -23,8 +25,16 @@ const items = ref([
|
|||||||
src: 'https://github.com/noook.png',
|
src: 'https://github.com/noook.png',
|
||||||
alt: 'noook'
|
alt: 'noook'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'sandros94',
|
||||||
|
value: 'sandros94',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://github.com/sandros94.png',
|
||||||
|
alt: 'sandros94'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
])
|
] satisfies SelectMenuItem[])
|
||||||
const value = ref(items.value[0])
|
const value = ref(items.value[0])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,29 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { SelectMenuItem, ChipProps } from '@nuxt/ui'
|
||||||
|
|
||||||
const items = ref([
|
const items = ref([
|
||||||
{
|
{
|
||||||
label: 'bug',
|
label: 'bug',
|
||||||
value: 'bug',
|
value: 'bug',
|
||||||
chip: {
|
chip: {
|
||||||
color: 'error' as const
|
color: 'error'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'feature',
|
label: 'feature',
|
||||||
value: 'feature',
|
value: 'feature',
|
||||||
chip: {
|
chip: {
|
||||||
color: 'success' as const
|
color: 'success'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'enhancement',
|
label: 'enhancement',
|
||||||
value: 'enhancement',
|
value: 'enhancement',
|
||||||
chip: {
|
chip: {
|
||||||
color: 'info' as const
|
color: 'info'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
])
|
] satisfies SelectMenuItem[])
|
||||||
const value = ref(items.value[0])
|
const value = ref(items.value[0])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -33,7 +35,7 @@ const value = ref(items.value[0])
|
|||||||
v-bind="modelValue.chip"
|
v-bind="modelValue.chip"
|
||||||
inset
|
inset
|
||||||
standalone
|
standalone
|
||||||
:size="ui.itemLeadingChipSize()"
|
:size="(ui.itemLeadingChipSize() as ChipProps['size'])"
|
||||||
:class="ui.itemLeadingChip()"
|
:class="ui.itemLeadingChip()"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { SelectMenuItem } from '@nuxt/ui'
|
||||||
|
|
||||||
const items = ref([
|
const items = ref([
|
||||||
{
|
{
|
||||||
label: 'Backlog',
|
label: 'Backlog',
|
||||||
@@ -20,7 +22,7 @@ const items = ref([
|
|||||||
value: 'done',
|
value: 'done',
|
||||||
icon: 'i-lucide-circle-check'
|
icon: 'i-lucide-circle-check'
|
||||||
}
|
}
|
||||||
])
|
] satisfies SelectMenuItem[])
|
||||||
const value = ref(items.value[0])
|
const value = ref(items.value[0])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { AvatarProps } from '@nuxt/ui'
|
||||||
|
|
||||||
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
|
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
|
||||||
key: 'typicode-users',
|
key: 'typicode-users',
|
||||||
transform: (data: { id: number, name: string }[]) => {
|
transform: (data: { id: number, name: string }[]) => {
|
||||||
@@ -6,7 +8,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
|||||||
label: user.name,
|
label: user.name,
|
||||||
value: String(user.id),
|
value: String(user.id),
|
||||||
avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` }
|
avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` }
|
||||||
})) || []
|
}))
|
||||||
},
|
},
|
||||||
lazy: true
|
lazy: true
|
||||||
})
|
})
|
||||||
@@ -18,17 +20,18 @@ function getUserAvatar(value: string) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<USelect
|
<USelect
|
||||||
:items="users || []"
|
:items="users"
|
||||||
:loading="status === 'pending'"
|
:loading="status === 'pending'"
|
||||||
icon="i-lucide-user"
|
icon="i-lucide-user"
|
||||||
placeholder="Select user"
|
placeholder="Select user"
|
||||||
class="w-48"
|
class="w-48"
|
||||||
|
value-key="value"
|
||||||
>
|
>
|
||||||
<template #leading="{ modelValue, ui }">
|
<template #leading="{ modelValue, ui }">
|
||||||
<UAvatar
|
<UAvatar
|
||||||
v-if="modelValue"
|
v-if="modelValue"
|
||||||
v-bind="getUserAvatar(modelValue as string)"
|
v-bind="getUserAvatar(modelValue)"
|
||||||
:size="ui.leadingAvatarSize()"
|
:size="(ui.leadingAvatarSize() as AvatarProps['size'])"
|
||||||
:class="ui.leadingAvatar()"
|
:class="ui.leadingAvatar()"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { SelectItem } from '@nuxt/ui'
|
||||||
|
|
||||||
const items = ref([
|
const items = ref([
|
||||||
{
|
{
|
||||||
label: 'benjamincanac',
|
label: 'benjamincanac',
|
||||||
@@ -23,13 +25,21 @@ const items = ref([
|
|||||||
src: 'https://github.com/noook.png',
|
src: 'https://github.com/noook.png',
|
||||||
alt: 'noook'
|
alt: 'noook'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'sandros94',
|
||||||
|
value: 'sandros94',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://github.com/sandros94.png',
|
||||||
|
alt: 'sandros94'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
])
|
] satisfies SelectItem[])
|
||||||
const value = ref(items.value[0]?.value)
|
const value = ref(items.value[0]?.value)
|
||||||
|
|
||||||
const avatar = computed(() => items.value.find(item => item.value === value.value)?.avatar)
|
const avatar = computed(() => items.value.find(item => item.value === value.value)?.avatar)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<USelect v-model="value" :avatar="avatar" :items="items" class="w-48" />
|
<USelect v-model="value" :items="items" value-key="value" :avatar="avatar" class="w-48" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,27 +1,30 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { SelectItem, ChipProps } from '@nuxt/ui'
|
||||||
|
|
||||||
const items = ref([
|
const items = ref([
|
||||||
{
|
{
|
||||||
label: 'bug',
|
label: 'bug',
|
||||||
value: 'bug',
|
value: 'bug',
|
||||||
chip: {
|
chip: {
|
||||||
color: 'error' as const
|
color: 'error'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'feature',
|
label: 'feature',
|
||||||
value: 'feature',
|
value: 'feature',
|
||||||
chip: {
|
chip: {
|
||||||
color: 'success' as const
|
color: 'success'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'enhancement',
|
label: 'enhancement',
|
||||||
value: 'enhancement',
|
value: 'enhancement',
|
||||||
chip: {
|
chip: {
|
||||||
color: 'info' as const
|
color: 'info'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
])
|
] satisfies SelectItem[])
|
||||||
|
|
||||||
const value = ref(items.value[0]?.value)
|
const value = ref(items.value[0]?.value)
|
||||||
|
|
||||||
function getChip(value: string) {
|
function getChip(value: string) {
|
||||||
@@ -30,14 +33,14 @@ function getChip(value: string) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<USelect v-model="value" :items="items" class="w-48">
|
<USelect v-model="value" :items="items" value-key="value" class="w-48">
|
||||||
<template #leading="{ modelValue, ui }">
|
<template #leading="{ modelValue, ui }">
|
||||||
<UChip
|
<UChip
|
||||||
v-if="modelValue"
|
v-if="modelValue"
|
||||||
v-bind="getChip(modelValue as string)"
|
v-bind="getChip(modelValue)"
|
||||||
inset
|
inset
|
||||||
standalone
|
standalone
|
||||||
:size="ui.itemLeadingChipSize()"
|
:size="(ui.itemLeadingChipSize() as ChipProps['size'])"
|
||||||
:class="ui.itemLeadingChip()"
|
:class="ui.itemLeadingChip()"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { SelectItem } from '@nuxt/ui'
|
||||||
|
|
||||||
const items = ref([
|
const items = ref([
|
||||||
{
|
{
|
||||||
label: 'Backlog',
|
label: 'Backlog',
|
||||||
@@ -20,12 +22,12 @@ const items = ref([
|
|||||||
value: 'done',
|
value: 'done',
|
||||||
icon: 'i-lucide-circle-check'
|
icon: 'i-lucide-circle-check'
|
||||||
}
|
}
|
||||||
])
|
] satisfies SelectItem[])
|
||||||
const value = ref(items.value[0]?.value)
|
const value = ref(items.value[0]?.value)
|
||||||
|
|
||||||
const icon = computed(() => items.value.find(item => item.value === value.value)?.icon)
|
const icon = computed(() => items.value.find(item => item.value === value.value)?.icon)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<USelect v-model="value" :icon="icon" :items="items" class="w-48" />
|
<USelect v-model="value" :items="items" value-key="value" :icon="icon" class="w-48" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const items = [
|
import type { StepperItem } from '@nuxt/ui'
|
||||||
|
|
||||||
|
const items: StepperItem[] = [
|
||||||
{
|
{
|
||||||
title: 'Address',
|
title: 'Address',
|
||||||
description: 'Add your address here',
|
description: 'Add your address here',
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const items = [
|
import type { StepperItem } from '@nuxt/ui'
|
||||||
|
|
||||||
|
const items: StepperItem[] = [
|
||||||
{
|
{
|
||||||
slot: 'address',
|
slot: 'address',
|
||||||
title: 'Address',
|
title: 'Address',
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { StepperItem } from '@nuxt/ui'
|
||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
|
|
||||||
const items = [
|
const items: StepperItem[] = [
|
||||||
{
|
{
|
||||||
title: 'Address',
|
title: 'Address',
|
||||||
description: 'Add your address here',
|
description: 'Add your address here',
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const items = [
|
import type { StepperItem } from '@nuxt/ui'
|
||||||
|
|
||||||
|
const items: StepperItem[] = [
|
||||||
{
|
{
|
||||||
slot: 'address',
|
slot: 'address',
|
||||||
title: 'Address',
|
title: 'Address',
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const items = [
|
import type { TabsItem } from '@nuxt/ui'
|
||||||
|
|
||||||
|
const items: TabsItem[] = [
|
||||||
{
|
{
|
||||||
label: 'Account',
|
label: 'Account',
|
||||||
icon: 'i-lucide-user'
|
icon: 'i-lucide-user'
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { TabsItem } from '@nuxt/ui'
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
label: 'Account',
|
label: 'Account',
|
||||||
description: 'Make changes to your account here. Click save when you\'re done.',
|
description: 'Make changes to your account here. Click save when you\'re done.',
|
||||||
icon: 'i-lucide-user',
|
icon: 'i-lucide-user',
|
||||||
slot: 'account'
|
slot: 'account' as const
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Password',
|
label: 'Password',
|
||||||
description: 'Change your password here. After saving, you\'ll be logged out.',
|
description: 'Change your password here. After saving, you\'ll be logged out.',
|
||||||
icon: 'i-lucide-lock',
|
icon: 'i-lucide-lock',
|
||||||
slot: 'password'
|
slot: 'password' as const
|
||||||
}
|
}
|
||||||
]
|
] satisfies TabsItem[]
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
name: 'Benjamin Canac',
|
name: 'Benjamin Canac',
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const items = [
|
import type { TabsItem } from '@nuxt/ui'
|
||||||
|
|
||||||
|
const items: TabsItem[] = [
|
||||||
{
|
{
|
||||||
label: 'Account'
|
label: 'Account'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { TreeItem } from '@nuxt/ui'
|
import type { TreeItem } from '@nuxt/ui'
|
||||||
|
|
||||||
const items: TreeItem[] = [
|
const items = [
|
||||||
{
|
{
|
||||||
label: 'app/',
|
label: 'app/',
|
||||||
slot: 'app',
|
slot: 'app' as const,
|
||||||
defaultExpanded: true,
|
defaultExpanded: true,
|
||||||
children: [{
|
children: [{
|
||||||
label: 'composables/',
|
label: 'composables/',
|
||||||
@@ -24,7 +24,7 @@ const items: TreeItem[] = [
|
|||||||
},
|
},
|
||||||
{ label: 'app.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' }
|
{ label: 'nuxt.config.ts', icon: 'i-vscode-icons-file-type-nuxt' }
|
||||||
]
|
] satisfies TreeItem[]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const items: TreeItem[] = [
|
|||||||
{ label: 'nuxt.config.ts', icon: 'i-vscode-icons-file-type-nuxt' }
|
{ label: 'nuxt.config.ts', icon: 'i-vscode-icons-file-type-nuxt' }
|
||||||
]
|
]
|
||||||
|
|
||||||
const value = ref(items[items.length - 1])
|
const value = ref()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ ignore:
|
|||||||
- items
|
- items
|
||||||
external:
|
external:
|
||||||
- items
|
- items
|
||||||
|
externalTypes:
|
||||||
|
- AccordionItem[]
|
||||||
hide:
|
hide:
|
||||||
- class
|
- class
|
||||||
props:
|
props:
|
||||||
@@ -58,6 +60,8 @@ ignore:
|
|||||||
- items
|
- items
|
||||||
external:
|
external:
|
||||||
- items
|
- items
|
||||||
|
externalTypes:
|
||||||
|
- AccordionItem[]
|
||||||
hide:
|
hide:
|
||||||
- class
|
- class
|
||||||
props:
|
props:
|
||||||
@@ -87,6 +91,8 @@ ignore:
|
|||||||
- items
|
- items
|
||||||
external:
|
external:
|
||||||
- items
|
- items
|
||||||
|
externalTypes:
|
||||||
|
- AccordionItem[]
|
||||||
hide:
|
hide:
|
||||||
- class
|
- class
|
||||||
props:
|
props:
|
||||||
@@ -115,6 +121,8 @@ ignore:
|
|||||||
- items
|
- items
|
||||||
external:
|
external:
|
||||||
- items
|
- items
|
||||||
|
externalTypes:
|
||||||
|
- AccordionItem[]
|
||||||
hide:
|
hide:
|
||||||
- class
|
- class
|
||||||
props:
|
props:
|
||||||
@@ -149,6 +157,8 @@ ignore:
|
|||||||
- items
|
- items
|
||||||
external:
|
external:
|
||||||
- items
|
- items
|
||||||
|
externalTypes:
|
||||||
|
- AccordionItem[]
|
||||||
hide:
|
hide:
|
||||||
- class
|
- class
|
||||||
props:
|
props:
|
||||||
@@ -182,6 +192,8 @@ ignore:
|
|||||||
- items
|
- items
|
||||||
external:
|
external:
|
||||||
- items
|
- items
|
||||||
|
externalTypes:
|
||||||
|
- AccordionItem[]
|
||||||
hide:
|
hide:
|
||||||
- class
|
- class
|
||||||
props:
|
props:
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ ignore:
|
|||||||
- items
|
- items
|
||||||
external:
|
external:
|
||||||
- items
|
- items
|
||||||
|
externalTypes:
|
||||||
|
- BreadcrumbItem[]
|
||||||
props:
|
props:
|
||||||
items:
|
items:
|
||||||
- label: 'Home'
|
- label: 'Home'
|
||||||
@@ -54,6 +56,8 @@ ignore:
|
|||||||
- items
|
- items
|
||||||
external:
|
external:
|
||||||
- items
|
- items
|
||||||
|
externalTypes:
|
||||||
|
- BreadcrumbItem[]
|
||||||
props:
|
props:
|
||||||
separatorIcon: 'i-lucide-arrow-right'
|
separatorIcon: 'i-lucide-arrow-right'
|
||||||
items:
|
items:
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ ignore:
|
|||||||
- ui.content
|
- ui.content
|
||||||
external:
|
external:
|
||||||
- items
|
- items
|
||||||
|
externalTypes:
|
||||||
|
- ContextMenuItem[][]
|
||||||
props:
|
props:
|
||||||
items:
|
items:
|
||||||
- - label: Appearance
|
- - label: Appearance
|
||||||
@@ -124,6 +126,8 @@ ignore:
|
|||||||
- ui.content
|
- ui.content
|
||||||
external:
|
external:
|
||||||
- items
|
- items
|
||||||
|
externalTypes:
|
||||||
|
- ContextMenuItem[]
|
||||||
props:
|
props:
|
||||||
size: xl
|
size: xl
|
||||||
items:
|
items:
|
||||||
@@ -158,6 +162,8 @@ ignore:
|
|||||||
- ui.content
|
- ui.content
|
||||||
external:
|
external:
|
||||||
- items
|
- items
|
||||||
|
externalTypes:
|
||||||
|
- ContextMenuItem[]
|
||||||
props:
|
props:
|
||||||
disabled: true
|
disabled: true
|
||||||
items:
|
items:
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ ignore:
|
|||||||
- ui.content
|
- ui.content
|
||||||
external:
|
external:
|
||||||
- items
|
- items
|
||||||
|
externalTypes:
|
||||||
|
- DropdownMenuItem[][]
|
||||||
props:
|
props:
|
||||||
items:
|
items:
|
||||||
- - label: Benjamin
|
- - label: Benjamin
|
||||||
@@ -123,6 +125,8 @@ ignore:
|
|||||||
- ui.content
|
- ui.content
|
||||||
external:
|
external:
|
||||||
- items
|
- items
|
||||||
|
externalTypes:
|
||||||
|
- DropdownMenuItem[]
|
||||||
items:
|
items:
|
||||||
content.align:
|
content.align:
|
||||||
- start
|
- start
|
||||||
@@ -169,6 +173,8 @@ ignore:
|
|||||||
- ui.content
|
- ui.content
|
||||||
external:
|
external:
|
||||||
- items
|
- items
|
||||||
|
externalTypes:
|
||||||
|
- DropdownMenuItem[]
|
||||||
props:
|
props:
|
||||||
arrow: true
|
arrow: true
|
||||||
items:
|
items:
|
||||||
@@ -202,6 +208,8 @@ ignore:
|
|||||||
- ui.content
|
- ui.content
|
||||||
external:
|
external:
|
||||||
- items
|
- items
|
||||||
|
externalTypes:
|
||||||
|
- DropdownMenuItem[]
|
||||||
props:
|
props:
|
||||||
size: xl
|
size: xl
|
||||||
items:
|
items:
|
||||||
@@ -244,6 +252,8 @@ ignore:
|
|||||||
- ui.content
|
- ui.content
|
||||||
external:
|
external:
|
||||||
- items
|
- items
|
||||||
|
externalTypes:
|
||||||
|
- DropdownMenuItem[]
|
||||||
props:
|
props:
|
||||||
disabled: true
|
disabled: true
|
||||||
items:
|
items:
|
||||||
@@ -334,7 +344,9 @@ Inside the `defineShortcuts` composable, there is an `extractShortcuts` utility
|
|||||||
|
|
||||||
```vue
|
```vue
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const items = [{
|
import type { DropdownMenuItem } from '@nuxt/ui'
|
||||||
|
|
||||||
|
const items: DropdownMenuItem[] = [{
|
||||||
label: 'Invite users',
|
label: 'Invite users',
|
||||||
icon: 'i-lucide-user-plus',
|
icon: 'i-lucide-user-plus',
|
||||||
children: [{
|
children: [{
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ ignore:
|
|||||||
- class
|
- class
|
||||||
external:
|
external:
|
||||||
- items
|
- items
|
||||||
|
externalTypes:
|
||||||
|
- NavigationMenuItem[]
|
||||||
props:
|
props:
|
||||||
items:
|
items:
|
||||||
- label: Guide
|
- label: Guide
|
||||||
@@ -148,6 +150,8 @@ ignore:
|
|||||||
- class
|
- class
|
||||||
external:
|
external:
|
||||||
- items
|
- items
|
||||||
|
externalTypes:
|
||||||
|
- NavigationMenuItem[][]
|
||||||
props:
|
props:
|
||||||
orientation: 'vertical'
|
orientation: 'vertical'
|
||||||
items:
|
items:
|
||||||
@@ -247,6 +251,8 @@ ignore:
|
|||||||
- class
|
- class
|
||||||
external:
|
external:
|
||||||
- items
|
- items
|
||||||
|
externalTypes:
|
||||||
|
- NavigationMenuItem[][]
|
||||||
props:
|
props:
|
||||||
highlight: true
|
highlight: true
|
||||||
highlightColor: 'primary'
|
highlightColor: 'primary'
|
||||||
@@ -346,6 +352,8 @@ ignore:
|
|||||||
- class
|
- class
|
||||||
external:
|
external:
|
||||||
- items
|
- items
|
||||||
|
externalTypes:
|
||||||
|
- NavigationMenuItem[][]
|
||||||
props:
|
props:
|
||||||
color: neutral
|
color: neutral
|
||||||
items:
|
items:
|
||||||
@@ -379,6 +387,8 @@ ignore:
|
|||||||
- class
|
- class
|
||||||
external:
|
external:
|
||||||
- items
|
- items
|
||||||
|
externalTypes:
|
||||||
|
- NavigationMenuItem[][]
|
||||||
props:
|
props:
|
||||||
color: neutral
|
color: neutral
|
||||||
variant: link
|
variant: link
|
||||||
@@ -423,6 +433,8 @@ ignore:
|
|||||||
- class
|
- class
|
||||||
external:
|
external:
|
||||||
- items
|
- items
|
||||||
|
externalTypes:
|
||||||
|
- NavigationMenuItem[]
|
||||||
props:
|
props:
|
||||||
trailingIcon: 'i-lucide-arrow-down'
|
trailingIcon: 'i-lucide-arrow-down'
|
||||||
items:
|
items:
|
||||||
@@ -519,6 +531,8 @@ ignore:
|
|||||||
- class
|
- class
|
||||||
external:
|
external:
|
||||||
- items
|
- items
|
||||||
|
externalTypes:
|
||||||
|
- NavigationMenuItem[]
|
||||||
props:
|
props:
|
||||||
arrow: true
|
arrow: true
|
||||||
items:
|
items:
|
||||||
@@ -611,6 +625,8 @@ ignore:
|
|||||||
- class
|
- class
|
||||||
external:
|
external:
|
||||||
- items
|
- items
|
||||||
|
externalTypes:
|
||||||
|
- NavigationMenuItem[]
|
||||||
props:
|
props:
|
||||||
arrow: true
|
arrow: true
|
||||||
contentOrientation: 'vertical'
|
contentOrientation: 'vertical'
|
||||||
@@ -682,6 +698,8 @@ ignore:
|
|||||||
- class
|
- class
|
||||||
external:
|
external:
|
||||||
- items
|
- items
|
||||||
|
externalTypes:
|
||||||
|
- NavigationMenuItem[]
|
||||||
props:
|
props:
|
||||||
unmountOnHide: false
|
unmountOnHide: false
|
||||||
items:
|
items:
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ ignore:
|
|||||||
external:
|
external:
|
||||||
- items
|
- items
|
||||||
- modelValue
|
- modelValue
|
||||||
|
externalTypes:
|
||||||
|
- RadioGroupItem[]
|
||||||
|
- RadioGroupValue
|
||||||
props:
|
props:
|
||||||
modelValue: 'System'
|
modelValue: 'System'
|
||||||
items:
|
items:
|
||||||
@@ -52,6 +55,9 @@ ignore:
|
|||||||
external:
|
external:
|
||||||
- items
|
- items
|
||||||
- modelValue
|
- modelValue
|
||||||
|
externalTypes:
|
||||||
|
- RadioGroupItem[]
|
||||||
|
- RadioGroupValue
|
||||||
props:
|
props:
|
||||||
modelValue: 'system'
|
modelValue: 'system'
|
||||||
items:
|
items:
|
||||||
@@ -84,6 +90,9 @@ ignore:
|
|||||||
external:
|
external:
|
||||||
- items
|
- items
|
||||||
- modelValue
|
- modelValue
|
||||||
|
externalTypes:
|
||||||
|
- RadioGroupItem[]
|
||||||
|
- RadioGroupValue
|
||||||
props:
|
props:
|
||||||
modelValue: 'light'
|
modelValue: 'light'
|
||||||
valueKey: 'id'
|
valueKey: 'id'
|
||||||
@@ -112,6 +121,8 @@ ignore:
|
|||||||
- items
|
- items
|
||||||
external:
|
external:
|
||||||
- items
|
- items
|
||||||
|
externalTypes:
|
||||||
|
- RadioGroupItem[]
|
||||||
props:
|
props:
|
||||||
legend: 'Theme'
|
legend: 'Theme'
|
||||||
defaultValue: 'System'
|
defaultValue: 'System'
|
||||||
@@ -134,6 +145,8 @@ ignore:
|
|||||||
- items
|
- items
|
||||||
external:
|
external:
|
||||||
- items
|
- items
|
||||||
|
externalTypes:
|
||||||
|
- RadioGroupItem[]
|
||||||
props:
|
props:
|
||||||
orientation: 'horizontal'
|
orientation: 'horizontal'
|
||||||
defaultValue: 'System'
|
defaultValue: 'System'
|
||||||
@@ -156,6 +169,8 @@ ignore:
|
|||||||
- items
|
- items
|
||||||
external:
|
external:
|
||||||
- items
|
- items
|
||||||
|
externalTypes:
|
||||||
|
- RadioGroupItem[]
|
||||||
props:
|
props:
|
||||||
color: neutral
|
color: neutral
|
||||||
defaultValue: 'System'
|
defaultValue: 'System'
|
||||||
@@ -178,6 +193,8 @@ ignore:
|
|||||||
- items
|
- items
|
||||||
external:
|
external:
|
||||||
- items
|
- items
|
||||||
|
externalTypes:
|
||||||
|
- RadioGroupItem[]
|
||||||
props:
|
props:
|
||||||
size: 'xl'
|
size: 'xl'
|
||||||
defaultValue: 'System'
|
defaultValue: 'System'
|
||||||
@@ -200,6 +217,8 @@ ignore:
|
|||||||
- items
|
- items
|
||||||
external:
|
external:
|
||||||
- items
|
- items
|
||||||
|
externalTypes:
|
||||||
|
- RadioGroupItem[]
|
||||||
props:
|
props:
|
||||||
disabled: true
|
disabled: true
|
||||||
defaultValue: 'System'
|
defaultValue: 'System'
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ ignore:
|
|||||||
- class
|
- class
|
||||||
external:
|
external:
|
||||||
- items
|
- items
|
||||||
|
externalTypes:
|
||||||
|
- StepperItem[]
|
||||||
props:
|
props:
|
||||||
items:
|
items:
|
||||||
- title: 'Address'
|
- title: 'Address'
|
||||||
@@ -61,6 +63,8 @@ ignore:
|
|||||||
- class
|
- class
|
||||||
external:
|
external:
|
||||||
- items
|
- items
|
||||||
|
externalTypes:
|
||||||
|
- StepperItem[]
|
||||||
props:
|
props:
|
||||||
color: neutral
|
color: neutral
|
||||||
items:
|
items:
|
||||||
@@ -88,6 +92,8 @@ ignore:
|
|||||||
- class
|
- class
|
||||||
external:
|
external:
|
||||||
- items
|
- items
|
||||||
|
externalTypes:
|
||||||
|
- StepperItem[]
|
||||||
props:
|
props:
|
||||||
size: xl
|
size: xl
|
||||||
items:
|
items:
|
||||||
@@ -115,6 +121,8 @@ ignore:
|
|||||||
- class
|
- class
|
||||||
external:
|
external:
|
||||||
- items
|
- items
|
||||||
|
externalTypes:
|
||||||
|
- StepperItem[]
|
||||||
props:
|
props:
|
||||||
orientation: vertical
|
orientation: vertical
|
||||||
items:
|
items:
|
||||||
@@ -142,6 +150,8 @@ ignore:
|
|||||||
- class
|
- class
|
||||||
external:
|
external:
|
||||||
- items
|
- items
|
||||||
|
externalTypes:
|
||||||
|
- StepperItem[]
|
||||||
props:
|
props:
|
||||||
disabled: true
|
disabled: true
|
||||||
items:
|
items:
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ ignore:
|
|||||||
- class
|
- class
|
||||||
external:
|
external:
|
||||||
- items
|
- items
|
||||||
|
externalTypes:
|
||||||
|
- TabsItem[]
|
||||||
props:
|
props:
|
||||||
items:
|
items:
|
||||||
- label: Account
|
- label: Account
|
||||||
@@ -55,6 +57,8 @@ ignore:
|
|||||||
- class
|
- class
|
||||||
external:
|
external:
|
||||||
- items
|
- items
|
||||||
|
externalTypes:
|
||||||
|
- TabsItem[]
|
||||||
props:
|
props:
|
||||||
content: false
|
content: false
|
||||||
items:
|
items:
|
||||||
@@ -80,6 +84,8 @@ ignore:
|
|||||||
- class
|
- class
|
||||||
external:
|
external:
|
||||||
- items
|
- items
|
||||||
|
externalTypes:
|
||||||
|
- TabsItem[]
|
||||||
props:
|
props:
|
||||||
unmountOnHide: false
|
unmountOnHide: false
|
||||||
items:
|
items:
|
||||||
@@ -109,6 +115,8 @@ ignore:
|
|||||||
- class
|
- class
|
||||||
external:
|
external:
|
||||||
- items
|
- items
|
||||||
|
externalTypes:
|
||||||
|
- TabsItem[]
|
||||||
props:
|
props:
|
||||||
color: neutral
|
color: neutral
|
||||||
content: false
|
content: false
|
||||||
@@ -131,6 +139,8 @@ ignore:
|
|||||||
- class
|
- class
|
||||||
external:
|
external:
|
||||||
- items
|
- items
|
||||||
|
externalTypes:
|
||||||
|
- TabsItem[]
|
||||||
props:
|
props:
|
||||||
color: neutral
|
color: neutral
|
||||||
variant: link
|
variant: link
|
||||||
@@ -154,6 +164,8 @@ ignore:
|
|||||||
- class
|
- class
|
||||||
external:
|
external:
|
||||||
- items
|
- items
|
||||||
|
externalTypes:
|
||||||
|
- TabsItem[]
|
||||||
props:
|
props:
|
||||||
size: md
|
size: md
|
||||||
variant: pill
|
variant: pill
|
||||||
@@ -177,6 +189,8 @@ ignore:
|
|||||||
- class
|
- class
|
||||||
external:
|
external:
|
||||||
- items
|
- items
|
||||||
|
externalTypes:
|
||||||
|
- TabsItem[]
|
||||||
props:
|
props:
|
||||||
orientation: vertical
|
orientation: vertical
|
||||||
variant: pill
|
variant: pill
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"@nuxt/content": "^3.4.0",
|
"@nuxt/content": "^3.4.0",
|
||||||
"@nuxt/image": "^1.10.0",
|
"@nuxt/image": "^1.10.0",
|
||||||
"@nuxt/ui": "latest",
|
"@nuxt/ui": "latest",
|
||||||
"@nuxt/ui-pro": "https://pkg.pr.new/@nuxt/ui-pro@d96a086",
|
"@nuxt/ui-pro": "https://pkg.pr.new/@nuxt/ui-pro@e524f08",
|
||||||
"@nuxthub/core": "^0.8.18",
|
"@nuxthub/core": "^0.8.18",
|
||||||
"@nuxtjs/plausible": "^1.2.0",
|
"@nuxtjs/plausible": "^1.2.0",
|
||||||
"@octokit/rest": "^21.1.1",
|
"@octokit/rest": "^21.1.1",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const items = [{
|
|||||||
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.'
|
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.'
|
||||||
}, {
|
}, {
|
||||||
label: 'Components',
|
label: 'Components',
|
||||||
|
slot: 'test' as const,
|
||||||
icon: 'i-lucide-layers-3',
|
icon: 'i-lucide-layers-3',
|
||||||
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.'
|
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.'
|
||||||
}, {
|
}, {
|
||||||
@@ -37,6 +38,11 @@ const items = [{
|
|||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template #custom="{ item }">
|
||||||
|
<p class="text-(--ui-text-muted)">
|
||||||
|
Custom: {{ item.content }}
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
<template #custom-body="{ item }">
|
<template #custom-body="{ item }">
|
||||||
<p class="text-(--ui-text-muted)">
|
<p class="text-(--ui-text-muted)">
|
||||||
Custom: {{ item.content }}
|
Custom: {{ item.content }}
|
||||||
|
|||||||
@@ -150,10 +150,6 @@ defineShortcuts(extractShortcuts(items.value))
|
|||||||
|
|
||||||
<UDropdownMenu :items="itemsWithColor" :size="size" arrow :content="{ side: 'bottom', align: 'start' }" :ui="{ content: 'w-48' }">
|
<UDropdownMenu :items="itemsWithColor" :size="size" arrow :content="{ side: 'bottom', align: 'start' }" :ui="{ content: 'w-48' }">
|
||||||
<UButton label="Color" color="neutral" variant="outline" icon="i-lucide-menu" />
|
<UButton label="Color" color="neutral" variant="outline" icon="i-lucide-menu" />
|
||||||
|
|
||||||
<template #custom-trailing>
|
|
||||||
<UIcon name="i-lucide-badge-check" class="shrink-0 size-5 text-(--ui-primary)" />
|
|
||||||
</template>
|
|
||||||
</UDropdownMenu>
|
</UDropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { InputMenuItem, AvatarProps } from '@nuxt/ui'
|
||||||
|
|
||||||
import { upperFirst } from 'scule'
|
import { upperFirst } from 'scule'
|
||||||
import { refDebounced } from '@vueuse/core'
|
import { refDebounced } from '@vueuse/core'
|
||||||
import type { User } from '~/types'
|
import type { User } from '~/types'
|
||||||
@@ -10,7 +12,7 @@ const variants = Object.keys(theme.variants.variant) as Array<keyof typeof theme
|
|||||||
const fruits = ['Apple', 'Banana', 'Blueberry', 'Grapes', 'Pineapple']
|
const fruits = ['Apple', 'Banana', 'Blueberry', 'Grapes', 'Pineapple']
|
||||||
const vegetables = ['Aubergine', 'Broccoli', 'Carrot', 'Courgette', 'Leek']
|
const vegetables = ['Aubergine', 'Broccoli', 'Carrot', 'Courgette', 'Leek']
|
||||||
|
|
||||||
const items = [[{ label: 'Fruits', type: 'label' }, ...fruits], [{ label: 'Vegetables', type: 'label' }, ...vegetables]]
|
const items = [[{ label: 'Fruits', type: 'label' as const }, ...fruits], [{ label: 'Vegetables', type: 'label' as const }, ...vegetables]]
|
||||||
const selectedItems = ref([fruits[0]!, vegetables[0]!])
|
const selectedItems = ref([fruits[0]!, vegetables[0]!])
|
||||||
|
|
||||||
const statuses = [{
|
const statuses = [{
|
||||||
@@ -28,7 +30,7 @@ const statuses = [{
|
|||||||
}, {
|
}, {
|
||||||
label: 'Canceled',
|
label: 'Canceled',
|
||||||
icon: 'i-lucide-circle-x'
|
icon: 'i-lucide-circle-x'
|
||||||
}]
|
}] satisfies InputMenuItem[]
|
||||||
|
|
||||||
const searchTerm = ref('')
|
const searchTerm = ref('')
|
||||||
const searchTermDebounced = refDebounced(searchTerm, 200)
|
const searchTermDebounced = refDebounced(searchTerm, 200)
|
||||||
@@ -126,7 +128,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
|||||||
class="w-48"
|
class="w-48"
|
||||||
>
|
>
|
||||||
<template #leading="{ modelValue, ui }">
|
<template #leading="{ modelValue, ui }">
|
||||||
<UAvatar v-if="modelValue?.avatar" :size="ui.itemLeadingAvatarSize()" v-bind="modelValue.avatar" />
|
<UAvatar v-if="modelValue?.avatar" :size="(ui.itemLeadingAvatarSize() as AvatarProps['size'])" v-bind="modelValue.avatar" />
|
||||||
</template>
|
</template>
|
||||||
</UInputMenu>
|
</UInputMenu>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { SelectMenuItem, AvatarProps } from '@nuxt/ui'
|
||||||
|
|
||||||
import { upperFirst } from 'scule'
|
import { upperFirst } from 'scule'
|
||||||
import { refDebounced } from '@vueuse/core'
|
import { refDebounced } from '@vueuse/core'
|
||||||
import theme from '#build/ui/select-menu'
|
import theme from '#build/ui/select-menu'
|
||||||
@@ -10,7 +12,7 @@ const variants = Object.keys(theme.variants.variant) as Array<keyof typeof theme
|
|||||||
const fruits = ['Apple', 'Banana', 'Blueberry', 'Grapes', 'Pineapple']
|
const fruits = ['Apple', 'Banana', 'Blueberry', 'Grapes', 'Pineapple']
|
||||||
const vegetables = ['Aubergine', 'Broccoli', 'Carrot', 'Courgette', 'Leek']
|
const vegetables = ['Aubergine', 'Broccoli', 'Carrot', 'Courgette', 'Leek']
|
||||||
|
|
||||||
const items = [[{ label: 'Fruits', type: 'label' }, ...fruits], [{ label: 'Vegetables', type: 'label' }, ...vegetables]]
|
const items = [[{ label: 'Fruits', type: 'label' }, ...fruits], [{ label: 'Vegetables', type: 'label' }, ...vegetables]] satisfies SelectMenuItem[][]
|
||||||
const selectedItems = ref([fruits[0]!, vegetables[0]!])
|
const selectedItems = ref([fruits[0]!, vegetables[0]!])
|
||||||
|
|
||||||
const statuses = [{
|
const statuses = [{
|
||||||
@@ -33,7 +35,7 @@ const statuses = [{
|
|||||||
label: 'Canceled',
|
label: 'Canceled',
|
||||||
value: 'canceled',
|
value: 'canceled',
|
||||||
icon: 'i-lucide-circle-x'
|
icon: 'i-lucide-circle-x'
|
||||||
}]
|
}] satisfies SelectMenuItem[]
|
||||||
|
|
||||||
const searchTerm = ref('')
|
const searchTerm = ref('')
|
||||||
const searchTermDebounced = refDebounced(searchTerm, 200)
|
const searchTermDebounced = refDebounced(searchTerm, 200)
|
||||||
@@ -41,7 +43,7 @@ const searchTermDebounced = refDebounced(searchTerm, 200)
|
|||||||
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
|
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
|
||||||
params: { q: searchTermDebounced },
|
params: { q: searchTermDebounced },
|
||||||
transform: (data: User[]) => {
|
transform: (data: User[]) => {
|
||||||
return data?.map(user => ({ id: user.id, label: user.name, avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` } })) || []
|
return data?.map(user => ({ id: user.id, label: user.name, avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` } }))
|
||||||
},
|
},
|
||||||
lazy: true
|
lazy: true
|
||||||
})
|
})
|
||||||
@@ -122,7 +124,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
|||||||
v-for="size in sizes"
|
v-for="size in sizes"
|
||||||
:key="size"
|
:key="size"
|
||||||
v-model:search-term="searchTerm"
|
v-model:search-term="searchTerm"
|
||||||
:items="users || []"
|
:items="users"
|
||||||
:loading="status === 'pending'"
|
:loading="status === 'pending'"
|
||||||
ignore-filter
|
ignore-filter
|
||||||
icon="i-lucide-user"
|
icon="i-lucide-user"
|
||||||
@@ -132,7 +134,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
|||||||
@update:open="searchTerm = ''"
|
@update:open="searchTerm = ''"
|
||||||
>
|
>
|
||||||
<template #leading="{ modelValue, ui }">
|
<template #leading="{ modelValue, ui }">
|
||||||
<UAvatar v-if="modelValue?.avatar" :size="ui.itemLeadingAvatarSize()" v-bind="modelValue.avatar" />
|
<UAvatar v-if="modelValue?.avatar" :size="(ui.itemLeadingAvatarSize() as AvatarProps['size'])" v-bind="modelValue.avatar" />
|
||||||
</template>
|
</template>
|
||||||
</USelectMenu>
|
</USelectMenu>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { SelectItem, AvatarProps } from '@nuxt/ui'
|
||||||
import { upperFirst } from 'scule'
|
import { upperFirst } from 'scule'
|
||||||
import theme from '#build/ui/select'
|
import theme from '#build/ui/select'
|
||||||
import type { User } from '~/types'
|
import type { User } from '~/types'
|
||||||
@@ -9,7 +10,7 @@ const variants = Object.keys(theme.variants.variant) as Array<keyof typeof theme
|
|||||||
const fruits = ['Apple', 'Banana', 'Blueberry', 'Grapes', 'Pineapple']
|
const fruits = ['Apple', 'Banana', 'Blueberry', 'Grapes', 'Pineapple']
|
||||||
const vegetables = ['Aubergine', 'Broccoli', 'Carrot', 'Courgette', 'Leek']
|
const vegetables = ['Aubergine', 'Broccoli', 'Carrot', 'Courgette', 'Leek']
|
||||||
|
|
||||||
const items = [[{ label: 'Fruits', type: 'label' }, ...fruits], [{ label: 'Vegetables', type: 'label' }, ...vegetables]]
|
const items = [[{ label: 'Fruits', type: 'label' as const }, ...fruits], [{ label: 'Vegetables', type: 'label' as const }, ...vegetables]]
|
||||||
const selectedItems = ref([fruits[0]!, vegetables[0]!])
|
const selectedItems = ref([fruits[0]!, vegetables[0]!])
|
||||||
|
|
||||||
const statuses = [{
|
const statuses = [{
|
||||||
@@ -32,7 +33,7 @@ const statuses = [{
|
|||||||
label: 'Canceled',
|
label: 'Canceled',
|
||||||
value: 'canceled',
|
value: 'canceled',
|
||||||
icon: 'i-lucide-circle-x'
|
icon: 'i-lucide-circle-x'
|
||||||
}]
|
}] satisfies SelectItem[]
|
||||||
|
|
||||||
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
|
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
|
||||||
transform: (data: User[]) => {
|
transform: (data: User[]) => {
|
||||||
@@ -114,9 +115,10 @@ function getUserAvatar(value: string) {
|
|||||||
trailing-icon="i-lucide-chevrons-up-down"
|
trailing-icon="i-lucide-chevrons-up-down"
|
||||||
:size="size"
|
:size="size"
|
||||||
class="w-48"
|
class="w-48"
|
||||||
|
value-key="value"
|
||||||
>
|
>
|
||||||
<template #leading="{ modelValue, ui }">
|
<template #leading="{ modelValue, ui }">
|
||||||
<UIcon v-if="modelValue" :name="getStatusIcon(modelValue as string)" :class="ui.leadingIcon()" />
|
<UIcon v-if="modelValue" :name="getStatusIcon(modelValue)" :class="ui.leadingIcon()" />
|
||||||
</template>
|
</template>
|
||||||
</USelect>
|
</USelect>
|
||||||
</div>
|
</div>
|
||||||
@@ -130,9 +132,10 @@ function getUserAvatar(value: string) {
|
|||||||
placeholder="Search users..."
|
placeholder="Search users..."
|
||||||
:size="size"
|
:size="size"
|
||||||
class="w-48"
|
class="w-48"
|
||||||
|
value-key="value"
|
||||||
>
|
>
|
||||||
<template #leading="{ modelValue, ui }">
|
<template #leading="{ modelValue, ui }">
|
||||||
<UAvatar v-if="modelValue" :size="ui.itemLeadingAvatarSize()" v-bind="getUserAvatar(modelValue as string)" />
|
<UAvatar v-if="modelValue" :size="(ui.itemLeadingAvatarSize() as AvatarProps['size'])" v-bind="getUserAvatar(modelValue)" />
|
||||||
</template>
|
</template>
|
||||||
</USelect>
|
</USelect>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,22 +11,22 @@ const size = ref('md' as const)
|
|||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
slot: 'address',
|
slot: 'address' as const,
|
||||||
title: 'Address',
|
title: 'Address',
|
||||||
description: 'Add your address here',
|
description: 'Add your address here',
|
||||||
icon: 'i-lucide-house'
|
icon: 'i-lucide-house'
|
||||||
}, {
|
}, {
|
||||||
slot: 'shipping',
|
slot: 'shipping' as const,
|
||||||
title: 'Shipping',
|
title: 'Shipping',
|
||||||
description: 'Set your preferred shipping method',
|
description: 'Set your preferred shipping method',
|
||||||
icon: 'i-lucide-truck'
|
icon: 'i-lucide-truck'
|
||||||
}, {
|
}, {
|
||||||
slot: 'payment',
|
slot: 'payment' as const,
|
||||||
title: 'Payment',
|
title: 'Payment',
|
||||||
description: 'Select your payment method',
|
description: 'Select your payment method',
|
||||||
icon: 'i-lucide-credit-card'
|
icon: 'i-lucide-credit-card'
|
||||||
}, {
|
}, {
|
||||||
slot: 'checkout',
|
slot: 'checkout' as const,
|
||||||
title: 'Checkout',
|
title: 'Checkout',
|
||||||
description: 'Confirm your order'
|
description: 'Confirm your order'
|
||||||
}
|
}
|
||||||
@@ -50,27 +50,27 @@ const stepper = useTemplateRef('stepper')
|
|||||||
:orientation="orientation"
|
:orientation="orientation"
|
||||||
:size="size"
|
:size="size"
|
||||||
>
|
>
|
||||||
<template #address>
|
<template #address="{ item }">
|
||||||
<Placeholder class="size-full min-h-60 min-w-60">
|
<Placeholder class="size-full min-h-60 min-w-60">
|
||||||
Address
|
{{ item.title }}
|
||||||
</Placeholder>
|
</Placeholder>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #shipping>
|
<template #shipping="{ item }">
|
||||||
<Placeholder class="size-full min-h-60 min-w-60">
|
<Placeholder class="size-full min-h-60 min-w-60">
|
||||||
Shipping
|
{{ item.title }}
|
||||||
</Placeholder>
|
</Placeholder>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #payment>
|
<template #payment="{ item }">
|
||||||
<Placeholder class="size-full min-h-60 min-w-60">
|
<Placeholder class="size-full min-h-60 min-w-60">
|
||||||
Payment
|
{{ item.title }}
|
||||||
</Placeholder>
|
</Placeholder>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #checkout>
|
<template #checkout="{ item }">
|
||||||
<Placeholder class="size-full min-h-60 min-w-60">
|
<Placeholder class="size-full min-h-60 min-w-60">
|
||||||
Checkout
|
{{ item.title }}
|
||||||
</Placeholder>
|
</Placeholder>
|
||||||
</template>
|
</template>
|
||||||
</UStepper>
|
</UStepper>
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ const itemsWithMappedId = [
|
|||||||
{ id: 'id3', title: 'obiwan kenobi' }
|
{ id: 'id3', title: 'obiwan kenobi' }
|
||||||
]
|
]
|
||||||
|
|
||||||
const modelValue = ref<TreeItem>()
|
const modelValue = ref<string>()
|
||||||
const modelValues = ref<TreeItem[]>([])
|
const modelValues = ref<string[]>([])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -64,22 +64,14 @@ const modelValues = ref<TreeItem[]>([])
|
|||||||
|
|
||||||
<!-- Typescript tests -->
|
<!-- Typescript tests -->
|
||||||
<template v-if="false">
|
<template v-if="false">
|
||||||
<!-- @vue-expect-error - multiple props should type modelValue to array. -->
|
<UTree :model-value="modelValues" :items="items" multiple />
|
||||||
<UTree :model-value="modelValue" :items="items" multiple />
|
<UTree :default-value="modelValues" :items="items" multiple />
|
||||||
<!-- @vue-expect-error - multiple props should type defaultValue to array. -->
|
<UTree :items="items" multiple @update:model-value="(payload) => payload" />
|
||||||
<UTree :default-value="modelValue" :items="items" multiple />
|
<UTree :model-value="modelValue" :items="items" />
|
||||||
<!-- @vue-expect-error - multiple props should type @update:modelValue to array. -->
|
<UTree :default-value="modelValue" :items="items" />
|
||||||
<UTree :items="items" multiple @update:model-value="(payload: TreeItem) => payload" />
|
<UTree :items="items" @update:model-value="(payload) => payload" />
|
||||||
<!-- @vue-expect-error - default should type modelValue to single item. -->
|
|
||||||
<UTree :model-value="modelValues" :items="items" />
|
|
||||||
<!-- @vue-expect-error - default should type defaultValue to single item. -->
|
|
||||||
<UTree :default-value="modelValues" :items="items" />
|
|
||||||
<!-- @vue-expect-error - default should type @update:modelValue to single item. -->
|
|
||||||
<UTree :items="items" @update:model-value="(payload: TreeItem[]) => payload" />
|
|
||||||
|
|
||||||
<!-- @vue-expect-error - value key should type v-model. -->
|
|
||||||
<UTree v-model="modelValue" :items="itemsWithMappedId" value-key="id" />
|
<UTree v-model="modelValue" :items="itemsWithMappedId" value-key="id" />
|
||||||
<!-- @vue-expect-error - label key should type v-model. -->
|
|
||||||
<UTree v-model="modelValue" :items="itemsWithMappedId" label-key="title" />
|
<UTree v-model="modelValue" :items="itemsWithMappedId" label-key="title" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
30
pnpm-lock.yaml
generated
30
pnpm-lock.yaml
generated
@@ -240,8 +240,8 @@ importers:
|
|||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:..
|
version: link:..
|
||||||
'@nuxt/ui-pro':
|
'@nuxt/ui-pro':
|
||||||
specifier: https://pkg.pr.new/@nuxt/ui-pro@d96a086
|
specifier: https://pkg.pr.new/@nuxt/ui-pro@e524f08
|
||||||
version: https://pkg.pr.new/@nuxt/ui-pro@d96a086(@babel/parser@7.26.10)(magicast@0.3.5)(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2))
|
version: https://pkg.pr.new/@nuxt/ui-pro@e524f08(@babel/parser@7.26.10)(magicast@0.3.5)(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2))
|
||||||
'@nuxthub/core':
|
'@nuxthub/core':
|
||||||
specifier: ^0.8.18
|
specifier: ^0.8.18
|
||||||
version: 0.8.18(db0@0.3.1(better-sqlite3@11.9.1))(ioredis@5.6.0)(magicast@0.3.5)(vite@6.2.3(@types/node@22.13.12)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(yaml@2.7.0))
|
version: 0.8.18(db0@0.3.1(better-sqlite3@11.9.1))(ioredis@5.6.0)(magicast@0.3.5)(vite@6.2.3(@types/node@22.13.12)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(yaml@2.7.0))
|
||||||
@@ -1555,9 +1555,9 @@ packages:
|
|||||||
vitest:
|
vitest:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@nuxt/ui-pro@https://pkg.pr.new/@nuxt/ui-pro@d96a086':
|
'@nuxt/ui-pro@https://pkg.pr.new/@nuxt/ui-pro@e524f08':
|
||||||
resolution: {tarball: https://pkg.pr.new/@nuxt/ui-pro@d96a086}
|
resolution: {tarball: https://pkg.pr.new/@nuxt/ui-pro@e524f08}
|
||||||
version: 3.0.0
|
version: 3.0.1
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: ^5.6.3
|
typescript: ^5.6.3
|
||||||
|
|
||||||
@@ -4046,10 +4046,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==}
|
resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
git-config-path@2.0.0:
|
|
||||||
resolution: {integrity: sha512-qc8h1KIQbJpp+241id3GuAtkdyJ+IK+LIVtkiFTRKRrmddDzs3SI9CvP1QYmWBFvm1I/PWRwj//of8bgAc0ltA==}
|
|
||||||
engines: {node: '>=4'}
|
|
||||||
|
|
||||||
git-raw-commits@5.0.0:
|
git-raw-commits@5.0.0:
|
||||||
resolution: {integrity: sha512-I2ZXrXeOc0KrCvC7swqtIFXFN+rbjnC7b2T943tvemIOVNl+XP8YnA9UVwqFhzzLClnSA60KR/qEjLpXzs73Qg==}
|
resolution: {integrity: sha512-I2ZXrXeOc0KrCvC7swqtIFXFN+rbjnC7b2T943tvemIOVNl+XP8YnA9UVwqFhzzLClnSA60KR/qEjLpXzs73Qg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -5330,10 +5326,6 @@ packages:
|
|||||||
parse-entities@4.0.2:
|
parse-entities@4.0.2:
|
||||||
resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==}
|
resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==}
|
||||||
|
|
||||||
parse-git-config@3.0.0:
|
|
||||||
resolution: {integrity: sha512-wXoQGL1D+2COYWCD35/xbiKma1Z15xvZL8cI25wvxzled58V51SJM04Urt/uznS900iQor7QO04SgdfT/XlbuA==}
|
|
||||||
engines: {node: '>=8'}
|
|
||||||
|
|
||||||
parse-imports@2.2.1:
|
parse-imports@2.2.1:
|
||||||
resolution: {integrity: sha512-OL/zLggRp8mFhKL0rNORUTR4yBYujK/uU+xZL+/0Rgm2QE4nLO9v8PzEweSJEbMGKmDRjJE4R3IMJlL2di4JeQ==}
|
resolution: {integrity: sha512-OL/zLggRp8mFhKL0rNORUTR4yBYujK/uU+xZL+/0Rgm2QE4nLO9v8PzEweSJEbMGKmDRjJE4R3IMJlL2di4JeQ==}
|
||||||
engines: {node: '>= 18'}
|
engines: {node: '>= 18'}
|
||||||
@@ -8415,7 +8407,7 @@ snapshots:
|
|||||||
- typescript
|
- typescript
|
||||||
- yaml
|
- yaml
|
||||||
|
|
||||||
'@nuxt/ui-pro@https://pkg.pr.new/@nuxt/ui-pro@d96a086(@babel/parser@7.26.10)(magicast@0.3.5)(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2))':
|
'@nuxt/ui-pro@https://pkg.pr.new/@nuxt/ui-pro@e524f08(@babel/parser@7.26.10)(magicast@0.3.5)(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nuxt/kit': 3.16.1(magicast@0.3.5)
|
'@nuxt/kit': 3.16.1(magicast@0.3.5)
|
||||||
'@nuxt/schema': 3.16.1
|
'@nuxt/schema': 3.16.1
|
||||||
@@ -8427,9 +8419,8 @@ snapshots:
|
|||||||
git-url-parse: 16.0.1
|
git-url-parse: 16.0.1
|
||||||
ofetch: 1.4.1
|
ofetch: 1.4.1
|
||||||
ohash: 2.0.11
|
ohash: 2.0.11
|
||||||
parse-git-config: 3.0.0
|
|
||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
pkg-types: 1.3.1
|
pkg-types: 2.1.0
|
||||||
scule: 1.3.0
|
scule: 1.3.0
|
||||||
tinyglobby: 0.2.12
|
tinyglobby: 0.2.12
|
||||||
typescript: 5.8.2
|
typescript: 5.8.2
|
||||||
@@ -11147,8 +11138,6 @@ snapshots:
|
|||||||
nypm: 0.6.0
|
nypm: 0.6.0
|
||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
|
|
||||||
git-config-path@2.0.0: {}
|
|
||||||
|
|
||||||
git-raw-commits@5.0.0(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.1.0):
|
git-raw-commits@5.0.0(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.1.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@conventional-changelog/git-client': 1.0.1(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.1.0)
|
'@conventional-changelog/git-client': 1.0.1(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.1.0)
|
||||||
@@ -12988,11 +12977,6 @@ snapshots:
|
|||||||
is-decimal: 2.0.1
|
is-decimal: 2.0.1
|
||||||
is-hexadecimal: 2.0.1
|
is-hexadecimal: 2.0.1
|
||||||
|
|
||||||
parse-git-config@3.0.0:
|
|
||||||
dependencies:
|
|
||||||
git-config-path: 2.0.0
|
|
||||||
ini: 1.3.8
|
|
||||||
|
|
||||||
parse-imports@2.2.1:
|
parse-imports@2.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
es-module-lexer: 1.6.0
|
es-module-lexer: 1.6.0
|
||||||
|
|||||||
@@ -26,9 +26,10 @@ export interface AccordionItem {
|
|||||||
/** A unique value for the accordion item. Defaults to the index. */
|
/** A unique value for the accordion item. Defaults to the index. */
|
||||||
value?: string
|
value?: string
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
|
[key: string]: any
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AccordionProps<T> extends Pick<AccordionRootProps, 'collapsible' | 'defaultValue' | 'modelValue' | 'type' | 'disabled' | 'unmountOnHide'> {
|
export interface AccordionProps<T extends AccordionItem = AccordionItem> extends Pick<AccordionRootProps, 'collapsible' | 'defaultValue' | 'modelValue' | 'type' | 'disabled' | 'unmountOnHide'> {
|
||||||
/**
|
/**
|
||||||
* The element or component this component should render as.
|
* The element or component this component should render as.
|
||||||
* @defaultValue 'div'
|
* @defaultValue 'div'
|
||||||
@@ -52,15 +53,15 @@ export interface AccordionProps<T> extends Pick<AccordionRootProps, 'collapsible
|
|||||||
|
|
||||||
export interface AccordionEmits extends AccordionRootEmits {}
|
export interface AccordionEmits extends AccordionRootEmits {}
|
||||||
|
|
||||||
type SlotProps<T> = (props: { item: T, index: number, open: boolean }) => any
|
type SlotProps<T extends AccordionItem> = (props: { item: T, index: number, open: boolean }) => any
|
||||||
|
|
||||||
export type AccordionSlots<T extends { slot?: string }> = {
|
export type AccordionSlots<T extends AccordionItem = AccordionItem> = {
|
||||||
leading: SlotProps<T>
|
leading: SlotProps<T>
|
||||||
default: SlotProps<T>
|
default: SlotProps<T>
|
||||||
trailing: SlotProps<T>
|
trailing: SlotProps<T>
|
||||||
content: SlotProps<T>
|
content: SlotProps<T>
|
||||||
body: SlotProps<T>
|
body: SlotProps<T>
|
||||||
} & DynamicSlots<T, SlotProps<T>>
|
} & DynamicSlots<T, 'body', { index: number, open: boolean }>
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -92,7 +93,7 @@ const ui = computed(() => accordion({
|
|||||||
<template>
|
<template>
|
||||||
<AccordionRoot v-bind="rootProps" :class="ui.root({ class: [props.class, props.ui?.root] })">
|
<AccordionRoot v-bind="rootProps" :class="ui.root({ class: [props.class, props.ui?.root] })">
|
||||||
<AccordionItem
|
<AccordionItem
|
||||||
v-for="(item, index) in items"
|
v-for="(item, index) in props.items"
|
||||||
v-slot="{ open }"
|
v-slot="{ open }"
|
||||||
:key="index"
|
:key="index"
|
||||||
:value="item.value || String(index)"
|
:value="item.value || String(index)"
|
||||||
@@ -115,10 +116,10 @@ const ui = computed(() => accordion({
|
|||||||
</AccordionTrigger>
|
</AccordionTrigger>
|
||||||
</AccordionHeader>
|
</AccordionHeader>
|
||||||
|
|
||||||
<AccordionContent v-if="item.content || !!slots.content || (item.slot && !!slots[item.slot]) || !!slots.body || (item.slot && !!slots[`${item.slot}-body`])" :class="ui.content({ class: props.ui?.content })">
|
<AccordionContent v-if="item.content || !!slots.content || (item.slot && !!slots[item.slot as keyof AccordionSlots<T>]) || !!slots.body || (item.slot && !!slots[`${item.slot}-body` as keyof AccordionSlots<T>])" :class="ui.content({ class: props.ui?.content })">
|
||||||
<slot :name="item.slot || 'content'" :item="item" :index="index" :open="open">
|
<slot :name="((item.slot || 'content') as keyof AccordionSlots<T>)" :item="item" :index="index" :open="open">
|
||||||
<div :class="ui.body({ class: props.ui?.body })">
|
<div :class="ui.body({ class: props.ui?.body })">
|
||||||
<slot :name="item.slot ? `${item.slot}-body`: 'body'" :item="item" :index="index" :open="open">
|
<slot :name="((item.slot ? `${item.slot}-body`: 'body') as keyof AccordionSlots<T>)" :item="item" :index="index" :open="open">
|
||||||
{{ item.content }}
|
{{ item.content }}
|
||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export interface AlertSlots {
|
|||||||
title(props?: {}): any
|
title(props?: {}): any
|
||||||
description(props?: {}): any
|
description(props?: {}): any
|
||||||
actions(props?: {}): any
|
actions(props?: {}): any
|
||||||
close(props: { ui: any }): any
|
close(props: { ui: ReturnType<typeof alert> }): any
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export default {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script setup lang="ts" generic="T extends Messages = Messages">
|
<script setup lang="ts" generic="T extends Messages">
|
||||||
import { toRef, useId, provide } from 'vue'
|
import { toRef, useId, provide } from 'vue'
|
||||||
import { ConfigProvider, TooltipProvider, useForwardProps } from 'reka-ui'
|
import { ConfigProvider, TooltipProvider, useForwardProps } from 'reka-ui'
|
||||||
import { reactivePick } from '@vueuse/core'
|
import { reactivePick } from '@vueuse/core'
|
||||||
|
|||||||
@@ -19,9 +19,10 @@ export interface BreadcrumbItem extends Omit<LinkProps, 'raw' | 'custom'> {
|
|||||||
icon?: string
|
icon?: string
|
||||||
avatar?: AvatarProps
|
avatar?: AvatarProps
|
||||||
slot?: string
|
slot?: string
|
||||||
|
[key: string]: any
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BreadcrumbProps<T> {
|
export interface BreadcrumbProps<T extends BreadcrumbItem = BreadcrumbItem> {
|
||||||
/**
|
/**
|
||||||
* The element or component this component should render as.
|
* The element or component this component should render as.
|
||||||
* @defaultValue 'nav'
|
* @defaultValue 'nav'
|
||||||
@@ -43,15 +44,15 @@ export interface BreadcrumbProps<T> {
|
|||||||
ui?: PartialString<typeof breadcrumb.slots>
|
ui?: PartialString<typeof breadcrumb.slots>
|
||||||
}
|
}
|
||||||
|
|
||||||
type SlotProps<T> = (props: { item: T, index: number, active?: boolean }) => any
|
type SlotProps<T extends BreadcrumbItem> = (props: { item: T, index: number, active?: boolean }) => any
|
||||||
|
|
||||||
export type BreadcrumbSlots<T extends { slot?: string }> = {
|
export type BreadcrumbSlots<T extends BreadcrumbItem = BreadcrumbItem> = {
|
||||||
'item': SlotProps<T>
|
'item': SlotProps<T>
|
||||||
'item-leading': SlotProps<T>
|
'item-leading': SlotProps<T>
|
||||||
'item-label': SlotProps<T>
|
'item-label': SlotProps<T>
|
||||||
'item-trailing': SlotProps<T>
|
'item-trailing': SlotProps<T>
|
||||||
'separator'(props?: {}): any
|
'separator': any
|
||||||
} & DynamicSlots<T, SlotProps<T>>
|
} & DynamicSlots<T, 'leading' | 'label' | 'trailing', { index: number, active?: boolean }>
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -88,19 +89,19 @@ const ui = breadcrumb()
|
|||||||
<li :class="ui.item({ class: props.ui?.item })">
|
<li :class="ui.item({ class: props.ui?.item })">
|
||||||
<ULink v-slot="{ active, ...slotProps }" v-bind="pickLinkProps(item)" custom>
|
<ULink v-slot="{ active, ...slotProps }" v-bind="pickLinkProps(item)" custom>
|
||||||
<ULinkBase v-bind="slotProps" as="span" :aria-current="active && (index === items!.length - 1) ? 'page' : undefined" :class="ui.link({ class: [props.ui?.link, item.class], active: index === items!.length - 1, disabled: !!item.disabled, to: !!item.to })">
|
<ULinkBase v-bind="slotProps" as="span" :aria-current="active && (index === items!.length - 1) ? 'page' : undefined" :class="ui.link({ class: [props.ui?.link, item.class], active: index === items!.length - 1, disabled: !!item.disabled, to: !!item.to })">
|
||||||
<slot :name="item.slot || 'item'" :item="item" :index="index">
|
<slot :name="((item.slot || 'item') as keyof BreadcrumbSlots<T>)" :item="item" :index="index">
|
||||||
<slot :name="item.slot ? `${item.slot}-leading`: 'item-leading'" :item="item" :active="index === items!.length - 1" :index="index">
|
<slot :name="((item.slot ? `${item.slot}-leading`: 'item-leading') as keyof BreadcrumbSlots<T>)" :item="item" :active="index === items!.length - 1" :index="index">
|
||||||
<UIcon v-if="item.icon" :name="item.icon" :class="ui.linkLeadingIcon({ class: props.ui?.linkLeadingIcon, active: index === items!.length - 1 })" />
|
<UIcon v-if="item.icon" :name="item.icon" :class="ui.linkLeadingIcon({ class: props.ui?.linkLeadingIcon, active: index === items!.length - 1 })" />
|
||||||
<UAvatar v-else-if="item.avatar" :size="((props.ui?.linkLeadingAvatarSize || ui.linkLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.linkLeadingAvatar({ class: props.ui?.linkLeadingAvatar, active: index === items!.length - 1 })" />
|
<UAvatar v-else-if="item.avatar" :size="((props.ui?.linkLeadingAvatarSize || ui.linkLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.linkLeadingAvatar({ class: props.ui?.linkLeadingAvatar, active: index === items!.length - 1 })" />
|
||||||
</slot>
|
</slot>
|
||||||
|
|
||||||
<span v-if="get(item, props.labelKey as string) || !!slots[item.slot ? `${item.slot}-label`: 'item-label']" :class="ui.linkLabel({ class: props.ui?.linkLabel })">
|
<span v-if="get(item, props.labelKey as string) || !!slots[(item.slot ? `${item.slot}-label`: 'item-label') as keyof BreadcrumbSlots<T>]" :class="ui.linkLabel({ class: props.ui?.linkLabel })">
|
||||||
<slot :name="item.slot ? `${item.slot}-label`: 'item-label'" :item="item" :active="index === items!.length - 1" :index="index">
|
<slot :name="((item.slot ? `${item.slot}-label`: 'item-label') as keyof BreadcrumbSlots<T>)" :item="item" :active="index === items!.length - 1" :index="index">
|
||||||
{{ get(item, props.labelKey as string) }}
|
{{ get(item, props.labelKey as string) }}
|
||||||
</slot>
|
</slot>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<slot :name="item.slot ? `${item.slot}-trailing`: 'item-trailing'" :item="item" :active="index === items!.length - 1" :index="index" />
|
<slot :name="((item.slot ? `${item.slot}-trailing`: 'item-trailing') as keyof BreadcrumbSlots<T>)" :item="item" :active="index === items!.length - 1" :index="index" />
|
||||||
</slot>
|
</slot>
|
||||||
</ULinkBase>
|
</ULinkBase>
|
||||||
</ULink>
|
</ULink>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { VariantProps } from 'tailwind-variants'
|
import type { VariantProps } from 'tailwind-variants'
|
||||||
import type { CalendarRootProps, CalendarRootEmits, RangeCalendarRootEmits, DateRange, CalendarCellTriggerProps } from 'reka-ui'
|
import type { CalendarRootProps, CalendarRootEmits, RangeCalendarRootProps, RangeCalendarRootEmits, DateRange, CalendarCellTriggerProps } from 'reka-ui'
|
||||||
import type { DateValue } from '@internationalized/date'
|
import type { DateValue } from '@internationalized/date'
|
||||||
import type { AppConfig } from '@nuxt/schema'
|
import type { AppConfig } from '@nuxt/schema'
|
||||||
import type { ButtonProps } from '../types'
|
import type { ButtonProps } from '../types'
|
||||||
@@ -15,13 +15,21 @@ const calendar = tv({ extend: tv(theme), ...(appConfigCalendar.ui?.calendar || {
|
|||||||
|
|
||||||
type CalendarVariants = VariantProps<typeof calendar>
|
type CalendarVariants = VariantProps<typeof calendar>
|
||||||
|
|
||||||
type CalendarModelValue<R extends boolean = false, M extends boolean = false> = R extends true
|
type CalendarDefaultValue<R extends boolean = false, M extends boolean = false> = R extends true
|
||||||
? DateRange
|
? DateRange
|
||||||
: M extends true
|
: M extends true
|
||||||
? DateValue[]
|
? DateValue[]
|
||||||
: DateValue
|
: DateValue
|
||||||
|
type CalendarModelValue<R extends boolean = false, M extends boolean = false> = R extends true
|
||||||
|
? (DateRange | null)
|
||||||
|
: M extends true
|
||||||
|
? (DateValue[] | undefined)
|
||||||
|
: (DateValue | undefined)
|
||||||
|
|
||||||
export interface CalendarProps<R extends boolean, M extends boolean> extends Omit<CalendarRootProps, 'as' | 'asChild' | 'modelValue' | 'defaultValue' | 'dir' | 'locale' | 'calendarLabel' | 'multiple'> {
|
type _CalendarRootProps = Omit<CalendarRootProps, 'as' | 'asChild' | 'modelValue' | 'defaultValue' | 'dir' | 'locale' | 'calendarLabel' | 'multiple'>
|
||||||
|
type _RangeCalendarRootProps = Omit<RangeCalendarRootProps, 'as' | 'asChild' | 'modelValue' | 'defaultValue' | 'dir' | 'locale' | 'calendarLabel' | 'multiple'>
|
||||||
|
|
||||||
|
export interface CalendarProps<R extends boolean = false, M extends boolean = false> extends _RangeCalendarRootProps, _CalendarRootProps {
|
||||||
/**
|
/**
|
||||||
* The element or component this component should render as.
|
* The element or component this component should render as.
|
||||||
* @defaultValue 'div'
|
* @defaultValue 'div'
|
||||||
@@ -87,7 +95,7 @@ export interface CalendarProps<R extends boolean, M extends boolean> extends Omi
|
|||||||
monthControls?: boolean
|
monthControls?: boolean
|
||||||
/** Show year controls */
|
/** Show year controls */
|
||||||
yearControls?: boolean
|
yearControls?: boolean
|
||||||
defaultValue?: CalendarModelValue<R, M>
|
defaultValue?: CalendarDefaultValue<R, M>
|
||||||
modelValue?: CalendarModelValue<R, M>
|
modelValue?: CalendarModelValue<R, M>
|
||||||
class?: any
|
class?: any
|
||||||
ui?: PartialString<typeof calendar.slots>
|
ui?: PartialString<typeof calendar.slots>
|
||||||
@@ -104,7 +112,7 @@ export interface CalendarSlots {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script setup lang="ts" generic="R extends boolean = false, M extends boolean = false">
|
<script setup lang="ts" generic="R extends boolean, M extends boolean">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useForwardPropsEmits } from 'reka-ui'
|
import { useForwardPropsEmits } from 'reka-ui'
|
||||||
import { Calendar as SingleCalendar, RangeCalendar } from 'reka-ui/namespaced'
|
import { Calendar as SingleCalendar, RangeCalendar } from 'reka-ui/namespaced'
|
||||||
@@ -151,8 +159,8 @@ const Calendar = computed(() => props.range ? RangeCalendar : SingleCalendar)
|
|||||||
<Calendar.Root
|
<Calendar.Root
|
||||||
v-slot="{ weekDays, grid }"
|
v-slot="{ weekDays, grid }"
|
||||||
v-bind="rootProps"
|
v-bind="rootProps"
|
||||||
:model-value="(modelValue as CalendarModelValue<true & false>)"
|
:model-value="modelValue"
|
||||||
:default-value="(defaultValue as CalendarModelValue<true & false>)"
|
:default-value="defaultValue"
|
||||||
:locale="locale"
|
:locale="locale"
|
||||||
:dir="dir"
|
:dir="dir"
|
||||||
:class="ui.root({ class: [props.class, props.ui?.root] })"
|
:class="ui.root({ class: [props.class, props.ui?.root] })"
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
<!-- eslint-disable vue/block-tag-newline -->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { VariantProps } from 'tailwind-variants'
|
import type { VariantProps } from 'tailwind-variants'
|
||||||
import type { AppConfig } from '@nuxt/schema'
|
import type { AppConfig } from '@nuxt/schema'
|
||||||
@@ -21,7 +22,9 @@ const carousel = tv({ extend: tv(theme), ...(appConfigCarousel.ui?.carousel || {
|
|||||||
|
|
||||||
type CarouselVariants = VariantProps<typeof carousel>
|
type CarouselVariants = VariantProps<typeof carousel>
|
||||||
|
|
||||||
export interface CarouselProps<T> extends Omit<EmblaOptionsType, 'axis' | 'container' | 'slides' | 'direction'> {
|
export type CarouselItem = AcceptableValue
|
||||||
|
|
||||||
|
export interface CarouselProps<T extends CarouselItem = CarouselItem> extends Omit<EmblaOptionsType, 'axis' | 'container' | 'slides' | 'direction'> {
|
||||||
/**
|
/**
|
||||||
* The element or component this component should render as.
|
* The element or component this component should render as.
|
||||||
* @defaultValue 'div'
|
* @defaultValue 'div'
|
||||||
@@ -99,12 +102,13 @@ export interface CarouselProps<T> extends Omit<EmblaOptionsType, 'axis' | 'conta
|
|||||||
ui?: PartialString<typeof carousel.slots>
|
ui?: PartialString<typeof carousel.slots>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CarouselSlots<T> = {
|
export type CarouselSlots<T extends CarouselItem = CarouselItem> = {
|
||||||
default(props: { item: T, index: number }): any
|
default(props: { item: T, index: number }): any
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script setup lang="ts" generic="T extends AcceptableValue">
|
<script setup lang="ts" generic="T extends CarouselItem">
|
||||||
import { computed, ref, watch, onMounted } from 'vue'
|
import { computed, ref, watch, onMounted } from 'vue'
|
||||||
import useEmblaCarousel from 'embla-carousel-vue'
|
import useEmblaCarousel from 'embla-carousel-vue'
|
||||||
import { Primitive, useForwardProps } from 'reka-ui'
|
import { Primitive, useForwardProps } from 'reka-ui'
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import theme from '#build/ui/command-palette'
|
|||||||
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
|
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
|
||||||
import { tv } from '../utils/tv'
|
import { tv } from '../utils/tv'
|
||||||
import type { AvatarProps, ButtonProps, ChipProps, KbdProps, InputProps, LinkProps } from '../types'
|
import type { AvatarProps, ButtonProps, ChipProps, KbdProps, InputProps, LinkProps } from '../types'
|
||||||
import type { DynamicSlots, PartialString } from '../types/utils'
|
import type { PartialString } from '../types/utils'
|
||||||
|
|
||||||
const appConfigCommandPalette = _appConfig as AppConfig & { ui: { commandPalette: Partial<typeof theme> } }
|
const appConfigCommandPalette = _appConfig as AppConfig & { ui: { commandPalette: Partial<typeof theme> } }
|
||||||
|
|
||||||
@@ -31,6 +31,7 @@ export interface CommandPaletteItem extends Omit<LinkProps, 'type' | 'raw' | 'cu
|
|||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
slot?: string
|
slot?: string
|
||||||
onSelect?(e?: Event): void
|
onSelect?(e?: Event): void
|
||||||
|
[key: string]: any
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CommandPaletteGroup<T> {
|
export interface CommandPaletteGroup<T> {
|
||||||
@@ -125,12 +126,12 @@ type SlotProps<T> = (props: { item: T, index: number }) => any
|
|||||||
|
|
||||||
export type CommandPaletteSlots<G extends { slot?: string }, T extends { slot?: string }> = {
|
export type CommandPaletteSlots<G extends { slot?: string }, T extends { slot?: string }> = {
|
||||||
'empty'(props: { searchTerm?: string }): any
|
'empty'(props: { searchTerm?: string }): any
|
||||||
'close'(props: { ui: any }): any
|
'close'(props: { ui: ReturnType<typeof commandPalette> }): any
|
||||||
'item': SlotProps<T>
|
'item': SlotProps<T>
|
||||||
'item-leading': SlotProps<T>
|
'item-leading': SlotProps<T>
|
||||||
'item-label': SlotProps<T>
|
'item-label': SlotProps<T>
|
||||||
'item-trailing': SlotProps<T>
|
'item-trailing': SlotProps<T>
|
||||||
} & DynamicSlots<G, SlotProps<T>> & DynamicSlots<T, SlotProps<T>>
|
} & Record<string, SlotProps<G>> & Record<string, SlotProps<T>>
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -297,8 +298,8 @@ const groups = computed(() => {
|
|||||||
>
|
>
|
||||||
<ULink v-slot="{ active, ...slotProps }" v-bind="pickLinkProps(item)" custom>
|
<ULink v-slot="{ active, ...slotProps }" v-bind="pickLinkProps(item)" custom>
|
||||||
<ULinkBase v-bind="slotProps" :class="ui.item({ class: props.ui?.item, active: active || item.active })">
|
<ULinkBase v-bind="slotProps" :class="ui.item({ class: props.ui?.item, active: active || item.active })">
|
||||||
<slot :name="item.slot || group.slot || 'item'" :item="item" :index="index">
|
<slot :name="((item.slot || group.slot || 'item') as keyof CommandPaletteSlots<G, T>)" :item="(item as any)" :index="index">
|
||||||
<slot :name="item.slot ? `${item.slot}-leading` : group.slot ? `${group.slot}-leading` : `item-leading`" :item="item" :index="index">
|
<slot :name="((item.slot ? `${item.slot}-leading` : group.slot ? `${group.slot}-leading` : `item-leading`) as keyof CommandPaletteSlots<G, T>)" :item="(item as any)" :index="index">
|
||||||
<UIcon v-if="item.loading" :name="loadingIcon || appConfig.ui.icons.loading" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon, loading: true })" />
|
<UIcon v-if="item.loading" :name="loadingIcon || appConfig.ui.icons.loading" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon, loading: true })" />
|
||||||
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon, active: active || item.active })" />
|
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon, active: active || item.active })" />
|
||||||
<UAvatar v-else-if="item.avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar, active: active || item.active })" />
|
<UAvatar v-else-if="item.avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar, active: active || item.active })" />
|
||||||
@@ -312,8 +313,8 @@ const groups = computed(() => {
|
|||||||
/>
|
/>
|
||||||
</slot>
|
</slot>
|
||||||
|
|
||||||
<span v-if="item.labelHtml || get(item, props.labelKey as string) || !!slots[item.slot ? `${item.slot}-label` : group.slot ? `${group.slot}-label` : `item-label`]" :class="ui.itemLabel({ class: props.ui?.itemLabel, active: active || item.active })">
|
<span v-if="item.labelHtml || get(item, props.labelKey as string) || !!slots[(item.slot ? `${item.slot}-label` : group.slot ? `${group.slot}-label` : `item-label`) as keyof CommandPaletteSlots<G, T>]" :class="ui.itemLabel({ class: props.ui?.itemLabel, active: active || item.active })">
|
||||||
<slot :name="item.slot ? `${item.slot}-label` : group.slot ? `${group.slot}-label` : `item-label`" :item="item" :index="index">
|
<slot :name="((item.slot ? `${item.slot}-label` : group.slot ? `${group.slot}-label` : `item-label`) as keyof CommandPaletteSlots<G, T>)" :item="(item as any)" :index="index">
|
||||||
<span v-if="item.prefix" :class="ui.itemLabelPrefix({ class: props.ui?.itemLabelPrefix })">{{ item.prefix }}</span>
|
<span v-if="item.prefix" :class="ui.itemLabelPrefix({ class: props.ui?.itemLabelPrefix })">{{ item.prefix }}</span>
|
||||||
|
|
||||||
<span :class="ui.itemLabelBase({ class: props.ui?.itemLabelBase, active: active || item.active })" v-html="item.labelHtml || get(item, props.labelKey as string)" />
|
<span :class="ui.itemLabelBase({ class: props.ui?.itemLabelBase, active: active || item.active })" v-html="item.labelHtml || get(item, props.labelKey as string)" />
|
||||||
@@ -323,7 +324,7 @@ const groups = computed(() => {
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span :class="ui.itemTrailing({ class: props.ui?.itemTrailing })">
|
<span :class="ui.itemTrailing({ class: props.ui?.itemTrailing })">
|
||||||
<slot :name="item.slot ? `${item.slot}-trailing` : group.slot ? `${group.slot}-trailing` : `item-trailing`" :item="item" :index="index">
|
<slot :name="((item.slot ? `${item.slot}-trailing` : group.slot ? `${group.slot}-trailing` : `item-trailing`) as keyof CommandPaletteSlots<G, T>)" :item="(item as any)" :index="index">
|
||||||
<span v-if="item.kbds?.length" :class="ui.itemTrailingKbds({ class: props.ui?.itemTrailingKbds })">
|
<span v-if="item.kbds?.length" :class="ui.itemTrailingKbds({ class: props.ui?.itemTrailingKbds })">
|
||||||
<UKbd v-for="(kbd, kbdIndex) in item.kbds" :key="kbdIndex" :size="((props.ui?.itemTrailingKbdsSize || ui.itemTrailingKbdsSize()) as KbdProps['size'])" v-bind="typeof kbd === 'string' ? { value: kbd } : kbd" />
|
<UKbd v-for="(kbd, kbdIndex) in item.kbds" :key="kbdIndex" :size="((props.ui?.itemTrailingKbdsSize || ui.itemTrailingKbdsSize()) as KbdProps['size'])" v-bind="typeof kbd === 'string' ? { value: kbd } : kbd" />
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -7,7 +7,14 @@ import _appConfig from '#build/app.config'
|
|||||||
import theme from '#build/ui/context-menu'
|
import theme from '#build/ui/context-menu'
|
||||||
import { tv } from '../utils/tv'
|
import { tv } from '../utils/tv'
|
||||||
import type { AvatarProps, KbdProps, LinkProps } from '../types'
|
import type { AvatarProps, KbdProps, LinkProps } from '../types'
|
||||||
import type { DynamicSlots, PartialString, EmitsToProps } from '../types/utils'
|
import type {
|
||||||
|
ArrayOrNested,
|
||||||
|
DynamicSlots,
|
||||||
|
MergeTypes,
|
||||||
|
NestedItem,
|
||||||
|
PartialString,
|
||||||
|
EmitsToProps
|
||||||
|
} from '../types/utils'
|
||||||
|
|
||||||
const appConfigContextMenu = _appConfig as AppConfig & { ui: { contextMenu: Partial<typeof theme> } }
|
const appConfigContextMenu = _appConfig as AppConfig & { ui: { contextMenu: Partial<typeof theme> } }
|
||||||
|
|
||||||
@@ -36,17 +43,18 @@ export interface ContextMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'custo
|
|||||||
checked?: boolean
|
checked?: boolean
|
||||||
open?: boolean
|
open?: boolean
|
||||||
defaultOpen?: boolean
|
defaultOpen?: boolean
|
||||||
children?: ContextMenuItem[] | ContextMenuItem[][]
|
children?: ArrayOrNested<ContextMenuItem>
|
||||||
onSelect?(e: Event): void
|
onSelect?(e: Event): void
|
||||||
onUpdateChecked?(checked: boolean): void
|
onUpdateChecked?(checked: boolean): void
|
||||||
|
[key: string]: any
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ContextMenuProps<T> extends Omit<ContextMenuRootProps, 'dir'> {
|
export interface ContextMenuProps<T extends ArrayOrNested<ContextMenuItem> = ArrayOrNested<ContextMenuItem>> extends Omit<ContextMenuRootProps, 'dir'> {
|
||||||
/**
|
/**
|
||||||
* @defaultValue 'md'
|
* @defaultValue 'md'
|
||||||
*/
|
*/
|
||||||
size?: ContextMenuVariants['size']
|
size?: ContextMenuVariants['size']
|
||||||
items?: T[] | T[][]
|
items?: T
|
||||||
/**
|
/**
|
||||||
* The icon displayed when an item is checked.
|
* The icon displayed when an item is checked.
|
||||||
* @defaultValue appConfig.ui.icons.check
|
* @defaultValue appConfig.ui.icons.check
|
||||||
@@ -77,7 +85,7 @@ export interface ContextMenuProps<T> extends Omit<ContextMenuRootProps, 'dir'> {
|
|||||||
* The key used to get the label from the item.
|
* The key used to get the label from the item.
|
||||||
* @defaultValue 'label'
|
* @defaultValue 'label'
|
||||||
*/
|
*/
|
||||||
labelKey?: string
|
labelKey?: keyof NestedItem<T>
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
class?: any
|
class?: any
|
||||||
ui?: PartialString<typeof contextMenu.slots>
|
ui?: PartialString<typeof contextMenu.slots>
|
||||||
@@ -85,19 +93,22 @@ export interface ContextMenuProps<T> extends Omit<ContextMenuRootProps, 'dir'> {
|
|||||||
|
|
||||||
export interface ContextMenuEmits extends ContextMenuRootEmits {}
|
export interface ContextMenuEmits extends ContextMenuRootEmits {}
|
||||||
|
|
||||||
type SlotProps<T> = (props: { item: T, active?: boolean, index: number }) => any
|
type SlotProps<T extends ContextMenuItem> = (props: { item: T, active?: boolean, index: number }) => any
|
||||||
|
|
||||||
export type ContextMenuSlots<T extends { slot?: string }> = {
|
export type ContextMenuSlots<
|
||||||
|
A extends ArrayOrNested<ContextMenuItem> = ArrayOrNested<ContextMenuItem>,
|
||||||
|
T extends NestedItem<A> = NestedItem<A>
|
||||||
|
> = {
|
||||||
'default'(props?: {}): any
|
'default'(props?: {}): any
|
||||||
'item': SlotProps<T>
|
'item': SlotProps<T>
|
||||||
'item-leading': SlotProps<T>
|
'item-leading': SlotProps<T>
|
||||||
'item-label': SlotProps<T>
|
'item-label': SlotProps<T>
|
||||||
'item-trailing': SlotProps<T>
|
'item-trailing': SlotProps<T>
|
||||||
} & DynamicSlots<T, SlotProps<T>>
|
} & DynamicSlots<MergeTypes<T>, 'leading' | 'label' | 'trailing', { active?: boolean, index: number }>
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script setup lang="ts" generic="T extends ContextMenuItem">
|
<script setup lang="ts" generic="T extends ArrayOrNested<ContextMenuItem>">
|
||||||
import { computed, toRef } from 'vue'
|
import { computed, toRef } from 'vue'
|
||||||
import { ContextMenuRoot, ContextMenuTrigger, useForwardPropsEmits } from 'reka-ui'
|
import { ContextMenuRoot, ContextMenuTrigger, useForwardPropsEmits } from 'reka-ui'
|
||||||
import { reactivePick } from '@vueuse/core'
|
import { reactivePick } from '@vueuse/core'
|
||||||
@@ -114,8 +125,9 @@ const emits = defineEmits<ContextMenuEmits>()
|
|||||||
const slots = defineSlots<ContextMenuSlots<T>>()
|
const slots = defineSlots<ContextMenuSlots<T>>()
|
||||||
|
|
||||||
const rootProps = useForwardPropsEmits(reactivePick(props, 'modal'), emits)
|
const rootProps = useForwardPropsEmits(reactivePick(props, 'modal'), emits)
|
||||||
|
|
||||||
const contentProps = toRef(() => props.content)
|
const contentProps = toRef(() => props.content)
|
||||||
const proxySlots = omit(slots, ['default']) as Record<string, ContextMenuSlots<T>[string]>
|
const proxySlots = omit(slots, ['default'])
|
||||||
|
|
||||||
const ui = computed(() => contextMenu({
|
const ui = computed(() => contextMenu({
|
||||||
size: props.size
|
size: props.size
|
||||||
@@ -135,13 +147,13 @@ const ui = computed(() => contextMenu({
|
|||||||
v-bind="contentProps"
|
v-bind="contentProps"
|
||||||
:items="items"
|
:items="items"
|
||||||
:portal="portal"
|
:portal="portal"
|
||||||
:label-key="labelKey"
|
:label-key="(labelKey as keyof NestedItem<T>)"
|
||||||
:checked-icon="checkedIcon"
|
:checked-icon="checkedIcon"
|
||||||
:loading-icon="loadingIcon"
|
:loading-icon="loadingIcon"
|
||||||
:external-icon="externalIcon"
|
:external-icon="externalIcon"
|
||||||
>
|
>
|
||||||
<template v-for="(_, name) in proxySlots" #[name]="slotData">
|
<template v-for="(_, name) in proxySlots" #[name]="slotData">
|
||||||
<slot :name="name" v-bind="slotData" />
|
<slot :name="(name as keyof ContextMenuSlots<T>)" v-bind="slotData" />
|
||||||
</template>
|
</template>
|
||||||
</UContextMenuContent>
|
</UContextMenuContent>
|
||||||
</ContextMenuRoot>
|
</ContextMenuRoot>
|
||||||
|
|||||||
@@ -2,15 +2,16 @@
|
|||||||
import type { ContextMenuContentProps as RekaContextMenuContentProps, ContextMenuContentEmits as RekaContextMenuContentEmits } from 'reka-ui'
|
import type { ContextMenuContentProps as RekaContextMenuContentProps, ContextMenuContentEmits as RekaContextMenuContentEmits } from 'reka-ui'
|
||||||
import theme from '#build/ui/context-menu'
|
import theme from '#build/ui/context-menu'
|
||||||
import { tv } from '../utils/tv'
|
import { tv } from '../utils/tv'
|
||||||
import type { KbdProps, AvatarProps, ContextMenuItem, ContextMenuSlots } from '../types'
|
import type { AvatarProps, ContextMenuItem, ContextMenuSlots, KbdProps } from '../types'
|
||||||
|
import type { ArrayOrNested, NestedItem } from '../types/utils'
|
||||||
|
|
||||||
const _contextMenu = tv(theme)()
|
const _contextMenu = tv(theme)()
|
||||||
|
|
||||||
interface ContextMenuContentProps<T> extends Omit<RekaContextMenuContentProps, 'as' | 'asChild' | 'forceMount'> {
|
interface ContextMenuContentProps<T extends ArrayOrNested<ContextMenuItem>> extends Omit<RekaContextMenuContentProps, 'as' | 'asChild' | 'forceMount'> {
|
||||||
items?: T[] | T[][]
|
items?: T
|
||||||
portal?: boolean
|
portal?: boolean
|
||||||
sub?: boolean
|
sub?: boolean
|
||||||
labelKey: string
|
labelKey: keyof NestedItem<T>
|
||||||
/**
|
/**
|
||||||
* @IconifyIcon
|
* @IconifyIcon
|
||||||
*/
|
*/
|
||||||
@@ -31,13 +32,13 @@ interface ContextMenuContentProps<T> extends Omit<RekaContextMenuContentProps, '
|
|||||||
interface ContextMenuContentEmits extends RekaContextMenuContentEmits {}
|
interface ContextMenuContentEmits extends RekaContextMenuContentEmits {}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script setup lang="ts" generic="T extends ContextMenuItem">
|
<script setup lang="ts" generic="T extends ArrayOrNested<ContextMenuItem>">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { ContextMenu } from 'reka-ui/namespaced'
|
import { ContextMenu } from 'reka-ui/namespaced'
|
||||||
import { useForwardPropsEmits } from 'reka-ui'
|
import { useForwardPropsEmits } from 'reka-ui'
|
||||||
import { reactiveOmit, createReusableTemplate } from '@vueuse/core'
|
import { reactiveOmit, createReusableTemplate } from '@vueuse/core'
|
||||||
import { useAppConfig } from '#imports'
|
import { useAppConfig } from '#imports'
|
||||||
import { omit, get } from '../utils'
|
import { omit, get, isArrayOfArray } from '../utils'
|
||||||
import { pickLinkProps } from '../utils/link'
|
import { pickLinkProps } from '../utils/link'
|
||||||
import ULinkBase from './LinkBase.vue'
|
import ULinkBase from './LinkBase.vue'
|
||||||
import ULink from './Link.vue'
|
import ULink from './Link.vue'
|
||||||
@@ -53,24 +54,30 @@ const slots = defineSlots<ContextMenuSlots<T>>()
|
|||||||
|
|
||||||
const appConfig = useAppConfig()
|
const appConfig = useAppConfig()
|
||||||
const contentProps = useForwardPropsEmits(reactiveOmit(props, 'sub', 'items', 'portal', 'labelKey', 'checkedIcon', 'loadingIcon', 'externalIcon', 'class', 'ui', 'uiOverride'), emits)
|
const contentProps = useForwardPropsEmits(reactiveOmit(props, 'sub', 'items', 'portal', 'labelKey', 'checkedIcon', 'loadingIcon', 'externalIcon', 'class', 'ui', 'uiOverride'), emits)
|
||||||
const proxySlots = omit(slots, ['default']) as Record<string, ContextMenuSlots<T>[string]>
|
const proxySlots = omit(slots, ['default'])
|
||||||
|
|
||||||
const [DefineItemTemplate, ReuseItemTemplate] = createReusableTemplate<{ item: ContextMenuItem, active?: boolean, index: number }>()
|
const [DefineItemTemplate, ReuseItemTemplate] = createReusableTemplate<{ item: ContextMenuItem, active?: boolean, index: number }>()
|
||||||
|
|
||||||
const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0]) ? props.items : [props.items]) as T[][] : [])
|
const groups = computed<ContextMenuItem[][]>(() =>
|
||||||
|
props.items?.length
|
||||||
|
? isArrayOfArray(props.items)
|
||||||
|
? props.items
|
||||||
|
: [props.items]
|
||||||
|
: []
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DefineItemTemplate v-slot="{ item, active, index }">
|
<DefineItemTemplate v-slot="{ item, active, index }">
|
||||||
<slot :name="item.slot || 'item'" :item="(item as T)" :index="index">
|
<slot :name="((item.slot || 'item') as keyof ContextMenuSlots<T>)" :item="item" :index="index">
|
||||||
<slot :name="item.slot ? `${item.slot}-leading`: 'item-leading'" :item="(item as T)" :active="active" :index="index">
|
<slot :name="((item.slot ? `${item.slot}-leading`: 'item-leading') as keyof ContextMenuSlots<T>)" :item="item" :active="active" :index="index">
|
||||||
<UIcon v-if="item.loading" :name="loadingIcon || appConfig.ui.icons.loading" :class="ui.itemLeadingIcon({ class: uiOverride?.itemLeadingIcon, color: item?.color, loading: true })" />
|
<UIcon v-if="item.loading" :name="loadingIcon || appConfig.ui.icons.loading" :class="ui.itemLeadingIcon({ class: uiOverride?.itemLeadingIcon, color: item?.color, loading: true })" />
|
||||||
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: uiOverride?.itemLeadingIcon, color: item?.color, active })" />
|
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: uiOverride?.itemLeadingIcon, color: item?.color, active })" />
|
||||||
<UAvatar v-else-if="item.avatar" :size="((props.uiOverride?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: uiOverride?.itemLeadingAvatar, active })" />
|
<UAvatar v-else-if="item.avatar" :size="((props.uiOverride?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: uiOverride?.itemLeadingAvatar, active })" />
|
||||||
</slot>
|
</slot>
|
||||||
|
|
||||||
<span v-if="get(item, props.labelKey as string) || !!slots[item.slot ? `${item.slot}-label`: 'item-label']" :class="ui.itemLabel({ class: uiOverride?.itemLabel, active })">
|
<span v-if="get(item, props.labelKey as string) || !!slots[(item.slot ? `${item.slot}-label`: 'item-label') as keyof ContextMenuSlots<T>]" :class="ui.itemLabel({ class: uiOverride?.itemLabel, active })">
|
||||||
<slot :name="item.slot ? `${item.slot}-label`: 'item-label'" :item="(item as T)" :active="active" :index="index">
|
<slot :name="((item.slot ? `${item.slot}-label`: 'item-label') as keyof ContextMenuSlots<T>)" :item="item" :active="active" :index="index">
|
||||||
{{ get(item, props.labelKey as string) }}
|
{{ get(item, props.labelKey as string) }}
|
||||||
</slot>
|
</slot>
|
||||||
|
|
||||||
@@ -78,7 +85,7 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span :class="ui.itemTrailing({ class: uiOverride?.itemTrailing })">
|
<span :class="ui.itemTrailing({ class: uiOverride?.itemTrailing })">
|
||||||
<slot :name="item.slot ? `${item.slot}-trailing`: 'item-trailing'" :item="(item as T)" :active="active" :index="index">
|
<slot :name="((item.slot ? `${item.slot}-trailing`: 'item-trailing') as keyof ContextMenuSlots<T>)" :item="item" :active="active" :index="index">
|
||||||
<UIcon v-if="item.children?.length" :name="appConfig.ui.icons.chevronRight" :class="ui.itemTrailingIcon({ class: uiOverride?.itemTrailingIcon, color: item?.color, active })" />
|
<UIcon v-if="item.children?.length" :name="appConfig.ui.icons.chevronRight" :class="ui.itemTrailingIcon({ class: uiOverride?.itemTrailingIcon, color: item?.color, active })" />
|
||||||
<span v-else-if="item.kbds?.length" :class="ui.itemTrailingKbds({ class: uiOverride?.itemTrailingKbds })">
|
<span v-else-if="item.kbds?.length" :class="ui.itemTrailingKbds({ class: uiOverride?.itemTrailingKbds })">
|
||||||
<UKbd v-for="(kbd, kbdIndex) in item.kbds" :key="kbdIndex" :size="((props.uiOverride?.itemTrailingKbdsSize || ui.itemTrailingKbdsSize()) as KbdProps['size'])" v-bind="typeof kbd === 'string' ? { value: kbd } : kbd" />
|
<UKbd v-for="(kbd, kbdIndex) in item.kbds" :key="kbdIndex" :size="((props.uiOverride?.itemTrailingKbdsSize || ui.itemTrailingKbdsSize()) as KbdProps['size'])" v-bind="typeof kbd === 'string' ? { value: kbd } : kbd" />
|
||||||
@@ -117,7 +124,7 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
|
|||||||
:ui="ui"
|
:ui="ui"
|
||||||
:ui-override="uiOverride"
|
:ui-override="uiOverride"
|
||||||
:portal="portal"
|
:portal="portal"
|
||||||
:items="item.children"
|
:items="(item.children as T)"
|
||||||
:align-offset="-4"
|
:align-offset="-4"
|
||||||
:label-key="labelKey"
|
:label-key="labelKey"
|
||||||
:checked-icon="checkedIcon"
|
:checked-icon="checkedIcon"
|
||||||
@@ -126,7 +133,7 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
|
|||||||
v-bind="item.content"
|
v-bind="item.content"
|
||||||
>
|
>
|
||||||
<template v-for="(_, name) in proxySlots" #[name]="slotData: any">
|
<template v-for="(_, name) in proxySlots" #[name]="slotData: any">
|
||||||
<slot :name="name" v-bind="slotData" />
|
<slot :name="(name as keyof ContextMenuSlots<T>)" v-bind="slotData" />
|
||||||
</template>
|
</template>
|
||||||
</UContextMenuContent>
|
</UContextMenuContent>
|
||||||
</ContextMenu.Sub>
|
</ContextMenu.Sub>
|
||||||
|
|||||||
@@ -7,7 +7,14 @@ import _appConfig from '#build/app.config'
|
|||||||
import theme from '#build/ui/dropdown-menu'
|
import theme from '#build/ui/dropdown-menu'
|
||||||
import { tv } from '../utils/tv'
|
import { tv } from '../utils/tv'
|
||||||
import type { AvatarProps, KbdProps, LinkProps } from '../types'
|
import type { AvatarProps, KbdProps, LinkProps } from '../types'
|
||||||
import type { DynamicSlots, PartialString, EmitsToProps } from '../types/utils'
|
import type {
|
||||||
|
ArrayOrNested,
|
||||||
|
DynamicSlots,
|
||||||
|
MergeTypes,
|
||||||
|
NestedItem,
|
||||||
|
PartialString,
|
||||||
|
EmitsToProps
|
||||||
|
} from '../types/utils'
|
||||||
|
|
||||||
const appConfigDropdownMenu = _appConfig as AppConfig & { ui: { dropdownMenu: Partial<typeof theme> } }
|
const appConfigDropdownMenu = _appConfig as AppConfig & { ui: { dropdownMenu: Partial<typeof theme> } }
|
||||||
|
|
||||||
@@ -36,17 +43,18 @@ export interface DropdownMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'cust
|
|||||||
checked?: boolean
|
checked?: boolean
|
||||||
open?: boolean
|
open?: boolean
|
||||||
defaultOpen?: boolean
|
defaultOpen?: boolean
|
||||||
children?: DropdownMenuItem[] | DropdownMenuItem[][]
|
children?: ArrayOrNested<DropdownMenuItem>
|
||||||
onSelect?(e: Event): void
|
onSelect?(e: Event): void
|
||||||
onUpdateChecked?(checked: boolean): void
|
onUpdateChecked?(checked: boolean): void
|
||||||
|
[key: string]: any
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DropdownMenuProps<T> extends Omit<DropdownMenuRootProps, 'dir'> {
|
export interface DropdownMenuProps<T extends ArrayOrNested<DropdownMenuItem> = ArrayOrNested<DropdownMenuItem>> extends Omit<DropdownMenuRootProps, 'dir'> {
|
||||||
/**
|
/**
|
||||||
* @defaultValue 'md'
|
* @defaultValue 'md'
|
||||||
*/
|
*/
|
||||||
size?: DropdownMenuVariants['size']
|
size?: DropdownMenuVariants['size']
|
||||||
items?: T[] | T[][]
|
items?: T
|
||||||
/**
|
/**
|
||||||
* The icon displayed when an item is checked.
|
* The icon displayed when an item is checked.
|
||||||
* @defaultValue appConfig.ui.icons.check
|
* @defaultValue appConfig.ui.icons.check
|
||||||
@@ -85,7 +93,7 @@ export interface DropdownMenuProps<T> extends Omit<DropdownMenuRootProps, 'dir'>
|
|||||||
* The key used to get the label from the item.
|
* The key used to get the label from the item.
|
||||||
* @defaultValue 'label'
|
* @defaultValue 'label'
|
||||||
*/
|
*/
|
||||||
labelKey?: string
|
labelKey?: keyof NestedItem<T>
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
class?: any
|
class?: any
|
||||||
ui?: PartialString<typeof dropdownMenu.slots>
|
ui?: PartialString<typeof dropdownMenu.slots>
|
||||||
@@ -93,19 +101,22 @@ export interface DropdownMenuProps<T> extends Omit<DropdownMenuRootProps, 'dir'>
|
|||||||
|
|
||||||
export interface DropdownMenuEmits extends DropdownMenuRootEmits {}
|
export interface DropdownMenuEmits extends DropdownMenuRootEmits {}
|
||||||
|
|
||||||
type SlotProps<T> = (props: { item: T, active?: boolean, index: number }) => any
|
type SlotProps<T extends DropdownMenuItem> = (props: { item: T, active?: boolean, index: number }) => any
|
||||||
|
|
||||||
export type DropdownMenuSlots<T extends { slot?: string }> = {
|
export type DropdownMenuSlots<
|
||||||
|
A extends ArrayOrNested<DropdownMenuItem> = ArrayOrNested<DropdownMenuItem>,
|
||||||
|
T extends NestedItem<A> = NestedItem<A>
|
||||||
|
> = {
|
||||||
'default'(props: { open: boolean }): any
|
'default'(props: { open: boolean }): any
|
||||||
'item': SlotProps<T>
|
'item': SlotProps<T>
|
||||||
'item-leading': SlotProps<T>
|
'item-leading': SlotProps<T>
|
||||||
'item-label': SlotProps<T>
|
'item-label': SlotProps<T>
|
||||||
'item-trailing': SlotProps<T>
|
'item-trailing': SlotProps<T>
|
||||||
} & DynamicSlots<T, SlotProps<T>>
|
} & DynamicSlots<MergeTypes<T>, 'leading' | 'label' | 'trailing', { active?: boolean, index: number }>
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script setup lang="ts" generic="T extends DropdownMenuItem">
|
<script setup lang="ts" generic="T extends ArrayOrNested<DropdownMenuItem>">
|
||||||
import { computed, toRef } from 'vue'
|
import { computed, toRef } from 'vue'
|
||||||
import { defu } from 'defu'
|
import { defu } from 'defu'
|
||||||
import { DropdownMenuRoot, DropdownMenuTrigger, DropdownMenuArrow, useForwardPropsEmits } from 'reka-ui'
|
import { DropdownMenuRoot, DropdownMenuTrigger, DropdownMenuArrow, useForwardPropsEmits } from 'reka-ui'
|
||||||
@@ -125,7 +136,7 @@ const slots = defineSlots<DropdownMenuSlots<T>>()
|
|||||||
const rootProps = useForwardPropsEmits(reactivePick(props, 'defaultOpen', 'open', 'modal'), emits)
|
const rootProps = useForwardPropsEmits(reactivePick(props, 'defaultOpen', 'open', 'modal'), emits)
|
||||||
const contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffset: 8, collisionPadding: 8 }) as DropdownMenuContentProps)
|
const contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffset: 8, collisionPadding: 8 }) as DropdownMenuContentProps)
|
||||||
const arrowProps = toRef(() => props.arrow as DropdownMenuArrowProps)
|
const arrowProps = toRef(() => props.arrow as DropdownMenuArrowProps)
|
||||||
const proxySlots = omit(slots, ['default']) as Record<string, DropdownMenuSlots<T>[string]>
|
const proxySlots = omit(slots, ['default'])
|
||||||
|
|
||||||
const ui = computed(() => dropdownMenu({
|
const ui = computed(() => dropdownMenu({
|
||||||
size: props.size
|
size: props.size
|
||||||
@@ -145,13 +156,13 @@ const ui = computed(() => dropdownMenu({
|
|||||||
v-bind="contentProps"
|
v-bind="contentProps"
|
||||||
:items="items"
|
:items="items"
|
||||||
:portal="portal"
|
:portal="portal"
|
||||||
:label-key="labelKey"
|
:label-key="(labelKey as keyof NestedItem<T>)"
|
||||||
:checked-icon="checkedIcon"
|
:checked-icon="checkedIcon"
|
||||||
:loading-icon="loadingIcon"
|
:loading-icon="loadingIcon"
|
||||||
:external-icon="externalIcon"
|
:external-icon="externalIcon"
|
||||||
>
|
>
|
||||||
<template v-for="(_, name) in proxySlots" #[name]="slotData">
|
<template v-for="(_, name) in proxySlots" #[name]="slotData">
|
||||||
<slot :name="name" v-bind="slotData" />
|
<slot :name="(name as keyof DropdownMenuSlots<T>)" v-bind="slotData" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<DropdownMenuArrow v-if="!!arrow" v-bind="arrowProps" :class="ui.arrow({ class: props.ui?.arrow })" />
|
<DropdownMenuArrow v-if="!!arrow" v-bind="arrowProps" :class="ui.arrow({ class: props.ui?.arrow })" />
|
||||||
|
|||||||
@@ -4,14 +4,15 @@ import type { DropdownMenuContentProps as RekaDropdownMenuContentProps, Dropdown
|
|||||||
import theme from '#build/ui/dropdown-menu'
|
import theme from '#build/ui/dropdown-menu'
|
||||||
import { tv } from '../utils/tv'
|
import { tv } from '../utils/tv'
|
||||||
import type { KbdProps, AvatarProps, DropdownMenuItem, DropdownMenuSlots } from '../types'
|
import type { KbdProps, AvatarProps, DropdownMenuItem, DropdownMenuSlots } from '../types'
|
||||||
|
import type { ArrayOrNested, NestedItem } from '../types/utils'
|
||||||
|
|
||||||
const _dropdownMenu = tv(theme)()
|
const _dropdownMenu = tv(theme)()
|
||||||
|
|
||||||
interface DropdownMenuContentProps<T> extends Omit<RekaDropdownMenuContentProps, 'as' | 'asChild' | 'forceMount'> {
|
interface DropdownMenuContentProps<T extends ArrayOrNested<DropdownMenuItem>> extends Omit<RekaDropdownMenuContentProps, 'as' | 'asChild' | 'forceMount'> {
|
||||||
items?: T[] | T[][]
|
items?: T
|
||||||
portal?: boolean
|
portal?: boolean
|
||||||
sub?: boolean
|
sub?: boolean
|
||||||
labelKey: string
|
labelKey: keyof NestedItem<T>
|
||||||
/**
|
/**
|
||||||
* @IconifyIcon
|
* @IconifyIcon
|
||||||
*/
|
*/
|
||||||
@@ -31,19 +32,19 @@ interface DropdownMenuContentProps<T> extends Omit<RekaDropdownMenuContentProps,
|
|||||||
|
|
||||||
interface DropdownMenuContentEmits extends RekaDropdownMenuContentEmits {}
|
interface DropdownMenuContentEmits extends RekaDropdownMenuContentEmits {}
|
||||||
|
|
||||||
type DropdownMenuContentSlots<T extends { slot?: string }> = Omit<DropdownMenuSlots<T>, 'default'> & {
|
type DropdownMenuContentSlots<T extends ArrayOrNested<DropdownMenuItem>> = Omit<DropdownMenuSlots<T>, 'default'> & {
|
||||||
default(props?: {}): any
|
default(props?: {}): any
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script setup lang="ts" generic="T extends DropdownMenuItem">
|
<script setup lang="ts" generic="T extends ArrayOrNested<DropdownMenuItem>">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { DropdownMenu } from 'reka-ui/namespaced'
|
import { DropdownMenu } from 'reka-ui/namespaced'
|
||||||
import { useForwardPropsEmits } from 'reka-ui'
|
import { useForwardPropsEmits } from 'reka-ui'
|
||||||
import { reactiveOmit, createReusableTemplate } from '@vueuse/core'
|
import { reactiveOmit, createReusableTemplate } from '@vueuse/core'
|
||||||
import { useAppConfig } from '#imports'
|
import { useAppConfig } from '#imports'
|
||||||
import { omit, get } from '../utils'
|
import { omit, get, isArrayOfArray } from '../utils'
|
||||||
import { pickLinkProps } from '../utils/link'
|
import { pickLinkProps } from '../utils/link'
|
||||||
import ULinkBase from './LinkBase.vue'
|
import ULinkBase from './LinkBase.vue'
|
||||||
import ULink from './Link.vue'
|
import ULink from './Link.vue'
|
||||||
@@ -59,24 +60,30 @@ const slots = defineSlots<DropdownMenuContentSlots<T>>()
|
|||||||
|
|
||||||
const appConfig = useAppConfig()
|
const appConfig = useAppConfig()
|
||||||
const contentProps = useForwardPropsEmits(reactiveOmit(props, 'sub', 'items', 'portal', 'labelKey', 'checkedIcon', 'loadingIcon', 'externalIcon', 'class', 'ui', 'uiOverride'), emits)
|
const contentProps = useForwardPropsEmits(reactiveOmit(props, 'sub', 'items', 'portal', 'labelKey', 'checkedIcon', 'loadingIcon', 'externalIcon', 'class', 'ui', 'uiOverride'), emits)
|
||||||
const proxySlots = omit(slots, ['default']) as Record<string, DropdownMenuContentSlots<T>[string]>
|
const proxySlots = omit(slots, ['default'])
|
||||||
|
|
||||||
const [DefineItemTemplate, ReuseItemTemplate] = createReusableTemplate<{ item: DropdownMenuItem, active?: boolean, index: number }>()
|
const [DefineItemTemplate, ReuseItemTemplate] = createReusableTemplate<{ item: DropdownMenuItem, active?: boolean, index: number }>()
|
||||||
|
|
||||||
const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0]) ? props.items : [props.items]) as T[][] : [])
|
const groups = computed<DropdownMenuItem[][]>(() =>
|
||||||
|
props.items?.length
|
||||||
|
? isArrayOfArray(props.items)
|
||||||
|
? props.items
|
||||||
|
: [props.items]
|
||||||
|
: []
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DefineItemTemplate v-slot="{ item, active, index }">
|
<DefineItemTemplate v-slot="{ item, active, index }">
|
||||||
<slot :name="item.slot || 'item'" :item="(item as T)" :index="index">
|
<slot :name="((item.slot || 'item') as keyof DropdownMenuContentSlots<T>)" :item="(item as Extract<NestedItem<T>, { slot: string; }>)" :index="index">
|
||||||
<slot :name="item.slot ? `${item.slot}-leading`: 'item-leading'" :item="(item as T)" :active="active" :index="index">
|
<slot :name="((item.slot ? `${item.slot}-leading`: 'item-leading') as keyof DropdownMenuContentSlots<T>)" :item="(item as Extract<NestedItem<T>, { slot: string; }>)" :active="active" :index="index">
|
||||||
<UIcon v-if="item.loading" :name="loadingIcon || appConfig.ui.icons.loading" :class="ui.itemLeadingIcon({ class: uiOverride?.itemLeadingIcon, color: item?.color, loading: true })" />
|
<UIcon v-if="item.loading" :name="loadingIcon || appConfig.ui.icons.loading" :class="ui.itemLeadingIcon({ class: uiOverride?.itemLeadingIcon, color: item?.color, loading: true })" />
|
||||||
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: uiOverride?.itemLeadingIcon, color: item?.color, active })" />
|
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: uiOverride?.itemLeadingIcon, color: item?.color, active })" />
|
||||||
<UAvatar v-else-if="item.avatar" :size="((props.uiOverride?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: uiOverride?.itemLeadingAvatar, active })" />
|
<UAvatar v-else-if="item.avatar" :size="((props.uiOverride?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: uiOverride?.itemLeadingAvatar, active })" />
|
||||||
</slot>
|
</slot>
|
||||||
|
|
||||||
<span v-if="get(item, props.labelKey as string) || !!slots[item.slot ? `${item.slot}-label`: 'item-label']" :class="ui.itemLabel({ class: uiOverride?.itemLabel, active })">
|
<span v-if="get(item, props.labelKey as string) || !!slots[(item.slot ? `${item.slot}-label`: 'item-label') as keyof DropdownMenuContentSlots<T>]" :class="ui.itemLabel({ class: uiOverride?.itemLabel, active })">
|
||||||
<slot :name="item.slot ? `${item.slot}-label`: 'item-label'" :item="(item as T)" :active="active" :index="index">
|
<slot :name="((item.slot ? `${item.slot}-label`: 'item-label') as keyof DropdownMenuContentSlots<T>)" :item="(item as Extract<NestedItem<T>, { slot: string; }>)" :active="active" :index="index">
|
||||||
{{ get(item, props.labelKey as string) }}
|
{{ get(item, props.labelKey as string) }}
|
||||||
</slot>
|
</slot>
|
||||||
|
|
||||||
@@ -84,7 +91,7 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span :class="ui.itemTrailing({ class: uiOverride?.itemTrailing })">
|
<span :class="ui.itemTrailing({ class: uiOverride?.itemTrailing })">
|
||||||
<slot :name="item.slot ? `${item.slot}-trailing`: 'item-trailing'" :item="(item as T)" :active="active" :index="index">
|
<slot :name="((item.slot ? `${item.slot}-trailing`: 'item-trailing') as keyof DropdownMenuContentSlots<T>)" :item="(item as Extract<NestedItem<T>, { slot: string; }>)" :active="active" :index="index">
|
||||||
<UIcon v-if="item.children?.length" :name="appConfig.ui.icons.chevronRight" :class="ui.itemTrailingIcon({ class: uiOverride?.itemTrailingIcon, color: item?.color, active })" />
|
<UIcon v-if="item.children?.length" :name="appConfig.ui.icons.chevronRight" :class="ui.itemTrailingIcon({ class: uiOverride?.itemTrailingIcon, color: item?.color, active })" />
|
||||||
<span v-else-if="item.kbds?.length" :class="ui.itemTrailingKbds({ class: uiOverride?.itemTrailingKbds })">
|
<span v-else-if="item.kbds?.length" :class="ui.itemTrailingKbds({ class: uiOverride?.itemTrailingKbds })">
|
||||||
<UKbd v-for="(kbd, kbdIndex) in item.kbds" :key="kbdIndex" :size="((props.uiOverride?.itemTrailingKbdsSize || ui.itemTrailingKbdsSize()) as KbdProps['size'])" v-bind="typeof kbd === 'string' ? { value: kbd } : kbd" />
|
<UKbd v-for="(kbd, kbdIndex) in item.kbds" :key="kbdIndex" :size="((props.uiOverride?.itemTrailingKbdsSize || ui.itemTrailingKbdsSize()) as KbdProps['size'])" v-bind="typeof kbd === 'string' ? { value: kbd } : kbd" />
|
||||||
@@ -123,7 +130,7 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
|
|||||||
:ui="ui"
|
:ui="ui"
|
||||||
:ui-override="uiOverride"
|
:ui-override="uiOverride"
|
||||||
:portal="portal"
|
:portal="portal"
|
||||||
:items="item.children"
|
:items="(item.children as T)"
|
||||||
side="right"
|
side="right"
|
||||||
align="start"
|
align="start"
|
||||||
:align-offset="-4"
|
:align-offset="-4"
|
||||||
@@ -135,7 +142,7 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
|
|||||||
v-bind="item.content"
|
v-bind="item.content"
|
||||||
>
|
>
|
||||||
<template v-for="(_, name) in proxySlots" #[name]="slotData: any">
|
<template v-for="(_, name) in proxySlots" #[name]="slotData: any">
|
||||||
<slot :name="name" v-bind="slotData" />
|
<slot :name="(name as keyof DropdownMenuContentSlots<T>)" v-bind="slotData" />
|
||||||
</template>
|
</template>
|
||||||
</UDropdownMenuContent>
|
</UDropdownMenuContent>
|
||||||
</DropdownMenu.Sub>
|
</DropdownMenu.Sub>
|
||||||
|
|||||||
@@ -1,20 +1,29 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { InputHTMLAttributes } from 'vue'
|
import type { InputHTMLAttributes } from 'vue'
|
||||||
import type { VariantProps } from 'tailwind-variants'
|
import type { VariantProps } from 'tailwind-variants'
|
||||||
import type { ComboboxRootProps, ComboboxRootEmits, ComboboxContentProps, ComboboxContentEmits, ComboboxArrowProps, AcceptableValue } from 'reka-ui'
|
import type { ComboboxRootProps, ComboboxRootEmits, ComboboxContentProps, ComboboxContentEmits, ComboboxArrowProps } from 'reka-ui'
|
||||||
import type { AppConfig } from '@nuxt/schema'
|
import type { AppConfig } from '@nuxt/schema'
|
||||||
import _appConfig from '#build/app.config'
|
import _appConfig from '#build/app.config'
|
||||||
import theme from '#build/ui/input-menu'
|
import theme from '#build/ui/input-menu'
|
||||||
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
|
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
|
||||||
import { tv } from '../utils/tv'
|
import { tv } from '../utils/tv'
|
||||||
import type { AvatarProps, ChipProps, InputProps } from '../types'
|
import type { AvatarProps, ChipProps, InputProps } from '../types'
|
||||||
import type { PartialString, MaybeArrayOfArray, MaybeArrayOfArrayItem, SelectModelValue, SelectModelValueEmits, SelectItemKey, EmitsToProps } from '../types/utils'
|
import type {
|
||||||
|
AcceptableValue,
|
||||||
|
ArrayOrNested,
|
||||||
|
GetItemKeys,
|
||||||
|
GetModelValue,
|
||||||
|
GetModelValueEmits,
|
||||||
|
NestedItem,
|
||||||
|
PartialString,
|
||||||
|
EmitsToProps
|
||||||
|
} from '../types/utils'
|
||||||
|
|
||||||
const appConfigInputMenu = _appConfig as AppConfig & { ui: { inputMenu: Partial<typeof theme> } }
|
const appConfigInputMenu = _appConfig as AppConfig & { ui: { inputMenu: Partial<typeof theme> } }
|
||||||
|
|
||||||
const inputMenu = tv({ extend: tv(theme), ...(appConfigInputMenu.ui?.inputMenu || {}) })
|
const inputMenu = tv({ extend: tv(theme), ...(appConfigInputMenu.ui?.inputMenu || {}) })
|
||||||
|
|
||||||
export interface InputMenuItem {
|
interface _InputMenuItem {
|
||||||
label?: string
|
label?: string
|
||||||
/**
|
/**
|
||||||
* @IconifyIcon
|
* @IconifyIcon
|
||||||
@@ -27,13 +36,16 @@ export interface InputMenuItem {
|
|||||||
* @defaultValue 'item'
|
* @defaultValue 'item'
|
||||||
*/
|
*/
|
||||||
type?: 'label' | 'separator' | 'item'
|
type?: 'label' | 'separator' | 'item'
|
||||||
|
value?: string | number
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
onSelect?(e?: Event): void
|
onSelect?(e?: Event): void
|
||||||
|
[key: string]: any
|
||||||
}
|
}
|
||||||
|
export type InputMenuItem = _InputMenuItem | AcceptableValue | boolean
|
||||||
|
|
||||||
type InputMenuVariants = VariantProps<typeof inputMenu>
|
type InputMenuVariants = VariantProps<typeof inputMenu>
|
||||||
|
|
||||||
export interface InputMenuProps<T extends MaybeArrayOfArrayItem<I>, I extends MaybeArrayOfArray<InputMenuItem | AcceptableValue | boolean> = MaybeArrayOfArray<InputMenuItem | AcceptableValue | boolean>, V extends SelectItemKey<T> | undefined = undefined, M extends boolean = false> extends Pick<ComboboxRootProps<T>, 'open' | 'defaultOpen' | 'disabled' | 'name' | 'resetSearchTermOnBlur' | 'highlightOnHover'>, UseComponentIconsProps {
|
export interface InputMenuProps<T extends ArrayOrNested<InputMenuItem> = ArrayOrNested<InputMenuItem>, VK extends GetItemKeys<T> | undefined = undefined, M extends boolean = false> extends Pick<ComboboxRootProps<T>, 'open' | 'defaultOpen' | 'disabled' | 'name' | 'resetSearchTermOnBlur' | 'highlightOnHover'>, UseComponentIconsProps {
|
||||||
/**
|
/**
|
||||||
* The element or component this component should render as.
|
* The element or component this component should render as.
|
||||||
* @defaultValue 'div'
|
* @defaultValue 'div'
|
||||||
@@ -96,17 +108,17 @@ export interface InputMenuProps<T extends MaybeArrayOfArrayItem<I>, I extends Ma
|
|||||||
* When `items` is an array of objects, select the field to use as the value instead of the object itself.
|
* When `items` is an array of objects, select the field to use as the value instead of the object itself.
|
||||||
* @defaultValue undefined
|
* @defaultValue undefined
|
||||||
*/
|
*/
|
||||||
valueKey?: V
|
valueKey?: VK
|
||||||
/**
|
/**
|
||||||
* When `items` is an array of objects, select the field to use as the label.
|
* When `items` is an array of objects, select the field to use as the label.
|
||||||
* @defaultValue 'label'
|
* @defaultValue 'label'
|
||||||
*/
|
*/
|
||||||
labelKey?: V
|
labelKey?: keyof NestedItem<T>
|
||||||
items?: I
|
items?: T
|
||||||
/** The value of the InputMenu when initially rendered. Use when you do not need to control the state of the InputMenu. */
|
/** The value of the InputMenu when initially rendered. Use when you do not need to control the state of the InputMenu. */
|
||||||
defaultValue?: SelectModelValue<T, V, M>
|
defaultValue?: GetModelValue<T, VK, M>
|
||||||
/** The controlled value of the InputMenu. Can be binded-with with `v-model`. */
|
/** The controlled value of the InputMenu. Can be binded-with with `v-model`. */
|
||||||
modelValue?: SelectModelValue<T, V, M>
|
modelValue?: GetModelValue<T, VK, M>
|
||||||
/** Whether multiple options can be selected or not. */
|
/** Whether multiple options can be selected or not. */
|
||||||
multiple?: M & boolean
|
multiple?: M & boolean
|
||||||
/** Highlight the ring color like a focus state. */
|
/** Highlight the ring color like a focus state. */
|
||||||
@@ -130,18 +142,28 @@ export interface InputMenuProps<T extends MaybeArrayOfArrayItem<I>, I extends Ma
|
|||||||
ui?: PartialString<typeof inputMenu.slots>
|
ui?: PartialString<typeof inputMenu.slots>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type InputMenuEmits<T, V, M extends boolean> = Omit<ComboboxRootEmits<T>, 'update:modelValue'> & {
|
export type InputMenuEmits<A extends ArrayOrNested<InputMenuItem>, VK extends GetItemKeys<A> | undefined, M extends boolean> = Pick<ComboboxRootEmits, 'update:open'> & {
|
||||||
change: [payload: Event]
|
change: [payload: Event]
|
||||||
blur: [payload: FocusEvent]
|
blur: [payload: FocusEvent]
|
||||||
focus: [payload: FocusEvent]
|
focus: [payload: FocusEvent]
|
||||||
create: [item: string]
|
create: [item: string]
|
||||||
} & SelectModelValueEmits<T, V, M>
|
/** Event handler when highlighted element changes. */
|
||||||
|
highlight: [payload: {
|
||||||
|
ref: HTMLElement
|
||||||
|
value: GetModelValue<A, VK, M>
|
||||||
|
} | undefined]
|
||||||
|
} & GetModelValueEmits<A, VK, M>
|
||||||
|
|
||||||
type SlotProps<T> = (props: { item: T, index: number }) => any
|
type SlotProps<T extends InputMenuItem> = (props: { item: T, index: number }) => any
|
||||||
|
|
||||||
export interface InputMenuSlots<T, M extends boolean> {
|
export interface InputMenuSlots<
|
||||||
'leading'(props: { modelValue?: M extends true ? T[] : T, open: boolean, ui: any }): any
|
A extends ArrayOrNested<InputMenuItem> = ArrayOrNested<InputMenuItem>,
|
||||||
'trailing'(props: { modelValue?: M extends true ? T[] : T, open: boolean, ui: any }): any
|
VK extends GetItemKeys<A> | undefined = undefined,
|
||||||
|
M extends boolean = false,
|
||||||
|
T extends NestedItem<A> = NestedItem<A>
|
||||||
|
> {
|
||||||
|
'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
|
'empty'(props: { searchTerm?: string }): any
|
||||||
'item': SlotProps<T>
|
'item': SlotProps<T>
|
||||||
'item-leading': SlotProps<T>
|
'item-leading': SlotProps<T>
|
||||||
@@ -153,7 +175,7 @@ export interface InputMenuSlots<T, M extends boolean> {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script setup lang="ts" generic="T extends MaybeArrayOfArrayItem<I>, I extends MaybeArrayOfArray<InputMenuItem | AcceptableValue | boolean> = MaybeArrayOfArray<InputMenuItem | AcceptableValue | boolean>, V extends SelectItemKey<T> | undefined = undefined, M extends boolean = false">
|
<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 } 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 { 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 { defu } from 'defu'
|
||||||
@@ -164,21 +186,21 @@ import { useButtonGroup } from '../composables/useButtonGroup'
|
|||||||
import { useComponentIcons } from '../composables/useComponentIcons'
|
import { useComponentIcons } from '../composables/useComponentIcons'
|
||||||
import { useFormField } from '../composables/useFormField'
|
import { useFormField } from '../composables/useFormField'
|
||||||
import { useLocale } from '../composables/useLocale'
|
import { useLocale } from '../composables/useLocale'
|
||||||
import { get, compare } from '../utils'
|
import { compare, get, isArrayOfArray } from '../utils'
|
||||||
import UIcon from './Icon.vue'
|
import UIcon from './Icon.vue'
|
||||||
import UAvatar from './Avatar.vue'
|
import UAvatar from './Avatar.vue'
|
||||||
import UChip from './Chip.vue'
|
import UChip from './Chip.vue'
|
||||||
|
|
||||||
defineOptions({ inheritAttrs: false })
|
defineOptions({ inheritAttrs: false })
|
||||||
|
|
||||||
const props = withDefaults(defineProps<InputMenuProps<T, I, V, M>>(), {
|
const props = withDefaults(defineProps<InputMenuProps<T, VK, M>>(), {
|
||||||
type: 'text',
|
type: 'text',
|
||||||
autofocusDelay: 0,
|
autofocusDelay: 0,
|
||||||
portal: true,
|
portal: true,
|
||||||
labelKey: 'label' as never
|
labelKey: 'label' as never
|
||||||
})
|
})
|
||||||
const emits = defineEmits<InputMenuEmits<T, V, M>>()
|
const emits = defineEmits<InputMenuEmits<T, VK, M>>()
|
||||||
const slots = defineSlots<InputMenuSlots<T, M>>()
|
const slots = defineSlots<InputMenuSlots<T, VK, M>>()
|
||||||
|
|
||||||
const searchTerm = defineModel<string>('searchTerm', { default: '' })
|
const searchTerm = defineModel<string>('searchTerm', { default: '' })
|
||||||
|
|
||||||
@@ -219,9 +241,15 @@ function displayValue(value: T): string {
|
|||||||
return item && (typeof item === 'object' ? get(item, props.labelKey as string) : item)
|
return item && (typeof item === 'object' ? get(item, props.labelKey as string) : item)
|
||||||
}
|
}
|
||||||
|
|
||||||
const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0]) ? props.items : [props.items]) as InputMenuItem[][] : [])
|
const groups = computed<InputMenuItem[][]>(() =>
|
||||||
|
props.items?.length
|
||||||
|
? isArrayOfArray(props.items)
|
||||||
|
? props.items
|
||||||
|
: [props.items]
|
||||||
|
: []
|
||||||
|
)
|
||||||
// eslint-disable-next-line vue/no-dupe-keys
|
// eslint-disable-next-line vue/no-dupe-keys
|
||||||
const items = computed(() => groups.value.flatMap(group => group) as T[])
|
const items = computed(() => groups.value.flatMap(group => group))
|
||||||
|
|
||||||
const filteredGroups = computed(() => {
|
const filteredGroups = computed(() => {
|
||||||
if (props.ignoreFilter || !searchTerm.value) {
|
if (props.ignoreFilter || !searchTerm.value) {
|
||||||
@@ -230,9 +258,9 @@ const filteredGroups = computed(() => {
|
|||||||
|
|
||||||
const fields = Array.isArray(props.filterFields) ? props.filterFields : [props.labelKey] as string[]
|
const fields = Array.isArray(props.filterFields) ? props.filterFields : [props.labelKey] as string[]
|
||||||
|
|
||||||
return groups.value.map(items => items.filter((item) => {
|
return groups.value.map(group => group.filter((item) => {
|
||||||
if (typeof item !== 'object') {
|
if (typeof item !== 'object' || item === null) {
|
||||||
return contains(item, searchTerm.value)
|
return contains(String(item), searchTerm.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.type && ['label', 'separator'].includes(item.type)) {
|
if (item.type && ['label', 'separator'].includes(item.type)) {
|
||||||
@@ -240,19 +268,21 @@ const filteredGroups = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return fields.some(field => contains(get(item, field), searchTerm.value))
|
return fields.some(field => contains(get(item, field), searchTerm.value))
|
||||||
})).filter(group => group.filter(item => !item.type || !['label', 'separator'].includes(item.type)).length > 0)
|
})).filter(group => group.filter(item =>
|
||||||
|
isInputItem(item) && (!item.type || !['label', 'separator'].includes(item.type))
|
||||||
|
).length > 0)
|
||||||
})
|
})
|
||||||
const filteredItems = computed(() => filteredGroups.value.flatMap(group => group) as T[])
|
const filteredItems = computed(() => filteredGroups.value.flatMap(group => group))
|
||||||
|
|
||||||
const createItem = computed(() => {
|
const createItem = computed(() => {
|
||||||
if (!props.createItem || !searchTerm.value) {
|
if (!props.createItem || !searchTerm.value) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const newItem = props.valueKey ? { [props.valueKey]: searchTerm.value } as T : searchTerm.value
|
const newItem = props.valueKey ? { [props.valueKey]: searchTerm.value } as NestedItem<T> : searchTerm.value
|
||||||
|
|
||||||
if ((typeof props.createItem === 'object' && props.createItem.when === 'always') || props.createItem === 'always') {
|
if ((typeof props.createItem === 'object' && props.createItem.when === 'always') || props.createItem === 'always') {
|
||||||
return !filteredItems.value.find(item => compare(item, newItem, props.valueKey))
|
return !filteredItems.value.find(item => compare(item, newItem, String(props.valueKey)))
|
||||||
}
|
}
|
||||||
|
|
||||||
return !filteredItems.value.length
|
return !filteredItems.value.length
|
||||||
@@ -307,12 +337,16 @@ function onUpdateOpen(value: boolean) {
|
|||||||
|
|
||||||
function onRemoveTag(event: any) {
|
function onRemoveTag(event: any) {
|
||||||
if (props.multiple) {
|
if (props.multiple) {
|
||||||
const modelValue = props.modelValue as SelectModelValue<T, V, true>
|
const modelValue = props.modelValue as GetModelValue<T, VK, true>
|
||||||
const filteredValue = modelValue.filter(value => !isEqual(value, event))
|
const filteredValue = modelValue.filter(value => !isEqual(value, event))
|
||||||
emits('update:modelValue', filteredValue as SelectModelValue<T, V, M>)
|
emits('update:modelValue', filteredValue as GetModelValue<T, VK, M>)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isInputItem(item: InputMenuItem): item is _InputMenuItem {
|
||||||
|
return typeof item === 'object' && item !== null
|
||||||
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
inputRef
|
inputRef
|
||||||
})
|
})
|
||||||
@@ -362,15 +396,15 @@ defineExpose({
|
|||||||
@focus="onFocus"
|
@focus="onFocus"
|
||||||
@remove-tag="onRemoveTag"
|
@remove-tag="onRemoveTag"
|
||||||
>
|
>
|
||||||
<TagsInputItem v-for="(item, index) in tags" :key="index" :value="(item as string)" :class="ui.tagsItem({ class: props.ui?.tagsItem })">
|
<TagsInputItem v-for="(item, index) in tags" :key="index" :value="item" :class="ui.tagsItem({ class: props.ui?.tagsItem })">
|
||||||
<TagsInputItemText :class="ui.tagsItemText({ class: props.ui?.tagsItemText })">
|
<TagsInputItemText :class="ui.tagsItemText({ class: props.ui?.tagsItemText })">
|
||||||
<slot name="tags-item-text" :item="(item as T)" :index="index">
|
<slot name="tags-item-text" :item="(item as NestedItem<T>)" :index="index">
|
||||||
{{ displayValue(item as T) }}
|
{{ displayValue(item as T) }}
|
||||||
</slot>
|
</slot>
|
||||||
</TagsInputItemText>
|
</TagsInputItemText>
|
||||||
|
|
||||||
<TagsInputItemDelete :class="ui.tagsItemDelete({ class: props.ui?.tagsItemDelete })" :disabled="disabled">
|
<TagsInputItemDelete :class="ui.tagsItemDelete({ class: props.ui?.tagsItemDelete })" :disabled="disabled">
|
||||||
<slot name="tags-item-delete" :item="(item as T)" :index="index">
|
<slot name="tags-item-delete" :item="(item as NestedItem<T>)" :index="index">
|
||||||
<UIcon :name="deleteIcon || appConfig.ui.icons.close" :class="ui.tagsItemDeleteIcon({ class: props.ui?.tagsItemDeleteIcon })" />
|
<UIcon :name="deleteIcon || appConfig.ui.icons.close" :class="ui.tagsItemDeleteIcon({ class: props.ui?.tagsItemDeleteIcon })" />
|
||||||
</slot>
|
</slot>
|
||||||
</TagsInputItemDelete>
|
</TagsInputItemDelete>
|
||||||
@@ -401,14 +435,14 @@ defineExpose({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<span v-if="isLeading || !!avatar || !!slots.leading" :class="ui.leading({ class: props.ui?.leading })">
|
<span v-if="isLeading || !!avatar || !!slots.leading" :class="ui.leading({ class: props.ui?.leading })">
|
||||||
<slot name="leading" :model-value="(modelValue as M extends true ? T[] : T)" :open="open" :ui="ui">
|
<slot name="leading" :model-value="(modelValue as GetModelValue<T, VK, M>)" :open="open" :ui="ui">
|
||||||
<UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
|
<UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
|
||||||
<UAvatar v-else-if="!!avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
|
<UAvatar v-else-if="!!avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
|
||||||
</slot>
|
</slot>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<ComboboxTrigger v-if="isTrailing || !!slots.trailing" :class="ui.trailing({ class: props.ui?.trailing })">
|
<ComboboxTrigger v-if="isTrailing || !!slots.trailing" :class="ui.trailing({ class: props.ui?.trailing })">
|
||||||
<slot name="trailing" :model-value="(modelValue as M extends true ? T[] : T)" :open="open" :ui="ui">
|
<slot name="trailing" :model-value="(modelValue as GetModelValue<T, VK, M>)" :open="open" :ui="ui">
|
||||||
<UIcon v-if="trailingIconName" :name="trailingIconName" :class="ui.trailingIcon({ class: props.ui?.trailingIcon })" />
|
<UIcon v-if="trailingIconName" :name="trailingIconName" :class="ui.trailingIcon({ class: props.ui?.trailingIcon })" />
|
||||||
</slot>
|
</slot>
|
||||||
</ComboboxTrigger>
|
</ComboboxTrigger>
|
||||||
@@ -427,25 +461,25 @@ defineExpose({
|
|||||||
|
|
||||||
<ComboboxGroup v-for="(group, groupIndex) in filteredGroups" :key="`group-${groupIndex}`" :class="ui.group({ class: props.ui?.group })">
|
<ComboboxGroup v-for="(group, groupIndex) in filteredGroups" :key="`group-${groupIndex}`" :class="ui.group({ class: props.ui?.group })">
|
||||||
<template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
|
<template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
|
||||||
<ComboboxLabel v-if="item?.type === 'label'" :class="ui.label({ class: props.ui?.label })">
|
<ComboboxLabel v-if="isInputItem(item) && item.type === 'label'" :class="ui.label({ class: props.ui?.label })">
|
||||||
{{ get(item, props.labelKey as string) }}
|
{{ get(item, props.labelKey as string) }}
|
||||||
</ComboboxLabel>
|
</ComboboxLabel>
|
||||||
|
|
||||||
<ComboboxSeparator v-else-if="item?.type === 'separator'" :class="ui.separator({ class: props.ui?.separator })" />
|
<ComboboxSeparator v-else-if="isInputItem(item) && item.type === 'separator'" :class="ui.separator({ class: props.ui?.separator })" />
|
||||||
|
|
||||||
<ComboboxItem
|
<ComboboxItem
|
||||||
v-else
|
v-else
|
||||||
:class="ui.item({ class: props.ui?.item })"
|
:class="ui.item({ class: props.ui?.item })"
|
||||||
:disabled="item.disabled"
|
:disabled="isInputItem(item) && item.disabled"
|
||||||
:value="valueKey && typeof item === 'object' ? get(item, props.valueKey as string) : item"
|
:value="props.valueKey && isInputItem(item) ? get(item, String(props.valueKey)) : item"
|
||||||
@select="item.onSelect"
|
@select="isInputItem(item) && item.onSelect"
|
||||||
>
|
>
|
||||||
<slot name="item" :item="(item as T)" :index="index">
|
<slot name="item" :item="(item as NestedItem<T>)" :index="index">
|
||||||
<slot name="item-leading" :item="(item as T)" :index="index">
|
<slot name="item-leading" :item="(item as NestedItem<T>)" :index="index">
|
||||||
<UIcon v-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon })" />
|
<UIcon v-if="isInputItem(item) && item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon })" />
|
||||||
<UAvatar v-else-if="item.avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
|
<UAvatar v-else-if="isInputItem(item) && item.avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
|
||||||
<UChip
|
<UChip
|
||||||
v-else-if="item.chip"
|
v-else-if="isInputItem(item) && item.chip"
|
||||||
:size="((props.ui?.itemLeadingChipSize || ui.itemLeadingChipSize()) as ChipProps['size'])"
|
:size="((props.ui?.itemLeadingChipSize || ui.itemLeadingChipSize()) as ChipProps['size'])"
|
||||||
inset
|
inset
|
||||||
standalone
|
standalone
|
||||||
@@ -455,13 +489,13 @@ defineExpose({
|
|||||||
</slot>
|
</slot>
|
||||||
|
|
||||||
<span :class="ui.itemLabel({ class: props.ui?.itemLabel })">
|
<span :class="ui.itemLabel({ class: props.ui?.itemLabel })">
|
||||||
<slot name="item-label" :item="(item as T)" :index="index">
|
<slot name="item-label" :item="(item as NestedItem<T>)" :index="index">
|
||||||
{{ typeof item === 'object' ? get(item, props.labelKey as string) : item }}
|
{{ isInputItem(item) ? get(item, props.labelKey as string) : item }}
|
||||||
</slot>
|
</slot>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span :class="ui.itemTrailing({ class: props.ui?.itemTrailing })">
|
<span :class="ui.itemTrailing({ class: props.ui?.itemTrailing })">
|
||||||
<slot name="item-trailing" :item="(item as T)" :index="index" />
|
<slot name="item-trailing" :item="(item as NestedItem<T>)" :index="index" />
|
||||||
|
|
||||||
<ComboboxItemIndicator as-child>
|
<ComboboxItemIndicator as-child>
|
||||||
<UIcon :name="selectedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: props.ui?.itemTrailingIcon })" />
|
<UIcon :name="selectedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: props.ui?.itemTrailingIcon })" />
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export interface ModalSlots {
|
|||||||
header(props?: {}): any
|
header(props?: {}): any
|
||||||
title(props?: {}): any
|
title(props?: {}): any
|
||||||
description(props?: {}): any
|
description(props?: {}): any
|
||||||
close(props: { ui: any }): any
|
close(props: { ui: ReturnType<typeof modal> }): any
|
||||||
body(props?: {}): any
|
body(props?: {}): any
|
||||||
footer(props?: {}): any
|
footer(props?: {}): any
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,15 +7,23 @@ import _appConfig from '#build/app.config'
|
|||||||
import theme from '#build/ui/navigation-menu'
|
import theme from '#build/ui/navigation-menu'
|
||||||
import { tv } from '../utils/tv'
|
import { tv } from '../utils/tv'
|
||||||
import type { AvatarProps, BadgeProps, LinkProps } from '../types'
|
import type { AvatarProps, BadgeProps, LinkProps } from '../types'
|
||||||
import type { DynamicSlots, MaybeArrayOfArray, MaybeArrayOfArrayItem, PartialString, EmitsToProps } from '../types/utils'
|
import type {
|
||||||
|
ArrayOrNested,
|
||||||
|
DynamicSlots,
|
||||||
|
MergeTypes,
|
||||||
|
NestedItem,
|
||||||
|
PartialString,
|
||||||
|
EmitsToProps
|
||||||
|
} from '../types/utils'
|
||||||
|
|
||||||
const appConfigNavigationMenu = _appConfig as AppConfig & { ui: { navigationMenu: Partial<typeof theme> } }
|
const appConfigNavigationMenu = _appConfig as AppConfig & { ui: { navigationMenu: Partial<typeof theme> } }
|
||||||
|
|
||||||
const navigationMenu = tv({ extend: tv(theme), ...(appConfigNavigationMenu.ui?.navigationMenu || {}) })
|
const navigationMenu = tv({ extend: tv(theme), ...(appConfigNavigationMenu.ui?.navigationMenu || {}) })
|
||||||
|
|
||||||
export interface NavigationMenuChildItem extends Omit<NavigationMenuItem, 'children' | 'type'> {
|
export interface NavigationMenuChildItem extends Omit<NavigationMenuItem, 'type'> {
|
||||||
/** Description is only used when `orientation` is `horizontal`. */
|
/** Description is only used when `orientation` is `horizontal`. */
|
||||||
description?: string
|
description?: string
|
||||||
|
[key: string]: any
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NavigationMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'custom'>, Pick<CollapsibleRootProps, 'defaultOpen' | 'open'> {
|
export interface NavigationMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'custom'>, Pick<CollapsibleRootProps, 'defaultOpen' | 'open'> {
|
||||||
@@ -44,11 +52,12 @@ export interface NavigationMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'cu
|
|||||||
value?: string
|
value?: string
|
||||||
children?: NavigationMenuChildItem[]
|
children?: NavigationMenuChildItem[]
|
||||||
onSelect?(e: Event): void
|
onSelect?(e: Event): void
|
||||||
|
[key: string]: any
|
||||||
}
|
}
|
||||||
|
|
||||||
type NavigationMenuVariants = VariantProps<typeof navigationMenu>
|
type NavigationMenuVariants = VariantProps<typeof navigationMenu>
|
||||||
|
|
||||||
export interface NavigationMenuProps<T> extends Pick<NavigationMenuRootProps, 'modelValue' | 'defaultValue' | 'delayDuration' | 'disableClickTrigger' | 'disableHoverTrigger' | 'skipDelayDuration' | 'disablePointerLeaveClose' | 'unmountOnHide'> {
|
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.
|
* The element or component this component should render as.
|
||||||
* @defaultValue 'div'
|
* @defaultValue 'div'
|
||||||
@@ -110,31 +119,34 @@ export interface NavigationMenuProps<T> extends Pick<NavigationMenuRootProps, 'm
|
|||||||
* The key used to get the label from the item.
|
* The key used to get the label from the item.
|
||||||
* @defaultValue 'label'
|
* @defaultValue 'label'
|
||||||
*/
|
*/
|
||||||
labelKey?: string
|
labelKey?: keyof NestedItem<T>
|
||||||
class?: any
|
class?: any
|
||||||
ui?: PartialString<typeof navigationMenu.slots>
|
ui?: PartialString<typeof navigationMenu.slots>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NavigationMenuEmits extends NavigationMenuRootEmits {}
|
export interface NavigationMenuEmits extends NavigationMenuRootEmits {}
|
||||||
|
|
||||||
type SlotProps<T> = (props: { item: T, index: number, active?: boolean }) => any
|
type SlotProps<T extends NavigationMenuItem> = (props: { item: T, index: number, active?: boolean }) => any
|
||||||
|
|
||||||
export type NavigationMenuSlots<T extends { slot?: string }> = {
|
export type NavigationMenuSlots<
|
||||||
|
A extends ArrayOrNested<NavigationMenuItem> = ArrayOrNested<NavigationMenuItem>,
|
||||||
|
T extends NestedItem<A> = NestedItem<A>
|
||||||
|
> = {
|
||||||
'item': SlotProps<T>
|
'item': SlotProps<T>
|
||||||
'item-leading': SlotProps<T>
|
'item-leading': SlotProps<T>
|
||||||
'item-label': SlotProps<T>
|
'item-label': SlotProps<T>
|
||||||
'item-trailing': SlotProps<T>
|
'item-trailing': SlotProps<T>
|
||||||
'item-content': SlotProps<T>
|
'item-content': SlotProps<T>
|
||||||
} & DynamicSlots<T, SlotProps<T>>
|
} & DynamicSlots<MergeTypes<T>, 'leading' | 'label' | 'trailing' | 'content', { index: number, active?: boolean }>
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script setup lang="ts" generic="T extends MaybeArrayOfArrayItem<I>, I extends MaybeArrayOfArray<NavigationMenuItem>">
|
<script setup lang="ts" generic="T extends ArrayOrNested<NavigationMenuItem>">
|
||||||
import { computed, toRef } from 'vue'
|
import { computed, toRef } from 'vue'
|
||||||
import { NavigationMenuRoot, NavigationMenuList, NavigationMenuItem, NavigationMenuTrigger, NavigationMenuContent, NavigationMenuLink, NavigationMenuIndicator, NavigationMenuViewport, useForwardPropsEmits } from 'reka-ui'
|
import { NavigationMenuRoot, NavigationMenuList, NavigationMenuItem, NavigationMenuTrigger, NavigationMenuContent, NavigationMenuLink, NavigationMenuIndicator, NavigationMenuViewport, useForwardPropsEmits } from 'reka-ui'
|
||||||
import { createReusableTemplate } from '@vueuse/core'
|
import { createReusableTemplate } from '@vueuse/core'
|
||||||
import { useAppConfig } from '#imports'
|
import { useAppConfig } from '#imports'
|
||||||
import { get } from '../utils'
|
import { get, isArrayOfArray } from '../utils'
|
||||||
import { pickLinkProps } from '../utils/link'
|
import { pickLinkProps } from '../utils/link'
|
||||||
import ULinkBase from './LinkBase.vue'
|
import ULinkBase from './LinkBase.vue'
|
||||||
import ULink from './Link.vue'
|
import ULink from './Link.vue'
|
||||||
@@ -143,7 +155,7 @@ import UIcon from './Icon.vue'
|
|||||||
import UBadge from './Badge.vue'
|
import UBadge from './Badge.vue'
|
||||||
import UCollapsible from './Collapsible.vue'
|
import UCollapsible from './Collapsible.vue'
|
||||||
|
|
||||||
const props = withDefaults(defineProps<NavigationMenuProps<I>>(), {
|
const props = withDefaults(defineProps<NavigationMenuProps<T>>(), {
|
||||||
orientation: 'horizontal',
|
orientation: 'horizontal',
|
||||||
contentOrientation: 'horizontal',
|
contentOrientation: 'horizontal',
|
||||||
externalIcon: true,
|
externalIcon: true,
|
||||||
@@ -170,8 +182,14 @@ const rootProps = useForwardPropsEmits(computed(() => ({
|
|||||||
const contentProps = toRef(() => props.content)
|
const contentProps = toRef(() => props.content)
|
||||||
|
|
||||||
const appConfig = useAppConfig()
|
const appConfig = useAppConfig()
|
||||||
const [DefineLinkTemplate, ReuseLinkTemplate] = createReusableTemplate<{ item: NavigationMenuItem, index: number, active?: boolean }>()
|
const [DefineLinkTemplate, ReuseLinkTemplate] = createReusableTemplate<
|
||||||
const [DefineItemTemplate, ReuseItemTemplate] = createReusableTemplate<{ item: NavigationMenuItem, index: number, level?: number }>({
|
{ item: NavigationMenuItem, index: number, active?: boolean },
|
||||||
|
NavigationMenuSlots<T>
|
||||||
|
>()
|
||||||
|
const [DefineItemTemplate, ReuseItemTemplate] = createReusableTemplate<
|
||||||
|
{ item: NavigationMenuItem, index: number, level?: number },
|
||||||
|
NavigationMenuSlots<T>
|
||||||
|
>({
|
||||||
props: {
|
props: {
|
||||||
item: Object,
|
item: Object,
|
||||||
index: Number,
|
index: Number,
|
||||||
@@ -189,30 +207,36 @@ const ui = computed(() => navigationMenu({
|
|||||||
highlightColor: props.highlightColor || props.color
|
highlightColor: props.highlightColor || props.color
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const lists = computed(() => props.items?.length ? (Array.isArray(props.items[0]) ? props.items : [props.items]) as T[][] : [])
|
const lists = computed<NavigationMenuItem[][]>(() =>
|
||||||
|
props.items?.length
|
||||||
|
? isArrayOfArray(props.items)
|
||||||
|
? props.items
|
||||||
|
: [props.items]
|
||||||
|
: []
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DefineLinkTemplate v-slot="{ item, active, index }">
|
<DefineLinkTemplate v-slot="{ item, active, index }">
|
||||||
<slot :name="item.slot || 'item'" :item="(item as T)" :index="index">
|
<slot :name="((item.slot || 'item') as keyof NavigationMenuSlots<T>)" :item="item" :index="index">
|
||||||
<slot :name="item.slot ? `${item.slot}-leading` : 'item-leading'" :item="(item as T)" :active="active" :index="index">
|
<slot :name="((item.slot ? `${item.slot}-leading` : 'item-leading') as keyof NavigationMenuSlots<T>)" :item="item" :active="active" :index="index">
|
||||||
<UAvatar v-if="item.avatar" :size="((props.ui?.linkLeadingAvatarSize || ui.linkLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.linkLeadingAvatar({ class: props.ui?.linkLeadingAvatar, active, disabled: !!item.disabled })" />
|
<UAvatar v-if="item.avatar" :size="((props.ui?.linkLeadingAvatarSize || ui.linkLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.linkLeadingAvatar({ class: props.ui?.linkLeadingAvatar, active, disabled: !!item.disabled })" />
|
||||||
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.linkLeadingIcon({ class: props.ui?.linkLeadingIcon, active, disabled: !!item.disabled })" />
|
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.linkLeadingIcon({ class: props.ui?.linkLeadingIcon, active, disabled: !!item.disabled })" />
|
||||||
</slot>
|
</slot>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
v-if="(!collapsed || orientation !== 'vertical') && (get(item, props.labelKey as string) || !!slots[item.slot ? `${item.slot}-label` : 'item-label'])"
|
v-if="(!collapsed || orientation !== 'vertical') && (get(item, props.labelKey as string) || !!slots[(item.slot ? `${item.slot}-label` : 'item-label') as keyof NavigationMenuSlots<T>])"
|
||||||
:class="ui.linkLabel({ class: props.ui?.linkLabel })"
|
:class="ui.linkLabel({ class: props.ui?.linkLabel })"
|
||||||
>
|
>
|
||||||
<slot :name="item.slot ? `${item.slot}-label` : 'item-label'" :item="(item as T)" :active="active" :index="index">
|
<slot :name="((item.slot ? `${item.slot}-label` : 'item-label') as keyof NavigationMenuSlots<T>)" :item="item" :active="active" :index="index">
|
||||||
{{ get(item, props.labelKey as string) }}
|
{{ get(item, props.labelKey as string) }}
|
||||||
</slot>
|
</slot>
|
||||||
|
|
||||||
<UIcon v-if="item.target === '_blank' && externalIcon !== false" :name="typeof externalIcon === 'string' ? externalIcon : appConfig.ui.icons.external" :class="ui.linkLabelExternalIcon({ class: props.ui?.linkLabelExternalIcon, active })" />
|
<UIcon v-if="item.target === '_blank' && externalIcon !== false" :name="typeof externalIcon === 'string' ? externalIcon : appConfig.ui.icons.external" :class="ui.linkLabelExternalIcon({ class: props.ui?.linkLabelExternalIcon, active })" />
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span v-if="(!collapsed || orientation !== 'vertical') && (item.badge || (orientation === 'horizontal' && (item.children?.length || !!slots[item.slot ? `${item.slot}-content` : 'item-content'])) || (orientation === 'vertical' && item.children?.length) || item.trailingIcon || !!slots[item.slot ? `${item.slot}-trailing` : 'item-trailing'])" :class="ui.linkTrailing({ class: props.ui?.linkTrailing })">
|
<span v-if="(!collapsed || orientation !== 'vertical') && (item.badge || (orientation === 'horizontal' && (item.children?.length || !!slots[(item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>])) || (orientation === 'vertical' && item.children?.length) || item.trailingIcon || !!slots[(item.slot ? `${item.slot}-trailing` : 'item-trailing') as keyof NavigationMenuSlots<T>])" :class="ui.linkTrailing({ class: props.ui?.linkTrailing })">
|
||||||
<slot :name="item.slot ? `${item.slot}-trailing` : 'item-trailing'" :item="(item as T)" :active="active" :index="index">
|
<slot :name="((item.slot ? `${item.slot}-trailing` : 'item-trailing') as keyof NavigationMenuSlots<T>)" :item="item" :active="active" :index="index">
|
||||||
<UBadge
|
<UBadge
|
||||||
v-if="item.badge"
|
v-if="item.badge"
|
||||||
color="neutral"
|
color="neutral"
|
||||||
@@ -222,7 +246,7 @@ const lists = computed(() => props.items?.length ? (Array.isArray(props.items[0]
|
|||||||
:class="ui.linkTrailingBadge({ class: props.ui?.linkTrailingBadge })"
|
:class="ui.linkTrailingBadge({ class: props.ui?.linkTrailingBadge })"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<UIcon v-if="(orientation === 'horizontal' && (item.children?.length || !!slots[item.slot ? `${item.slot}-content` : 'item-content'])) || (orientation === 'vertical' && item.children?.length)" :name="item.trailingIcon || trailingIcon || appConfig.ui.icons.chevronDown" :class="ui.linkTrailingIcon({ class: props.ui?.linkTrailingIcon, active })" />
|
<UIcon v-if="(orientation === 'horizontal' && (item.children?.length || !!slots[(item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>])) || (orientation === 'vertical' && item.children?.length)" :name="item.trailingIcon || trailingIcon || appConfig.ui.icons.chevronDown" :class="ui.linkTrailingIcon({ class: props.ui?.linkTrailingIcon, active })" />
|
||||||
<UIcon v-else-if="item.trailingIcon" :name="item.trailingIcon" :class="ui.linkTrailingIcon({ class: props.ui?.linkTrailingIcon, active })" />
|
<UIcon v-else-if="item.trailingIcon" :name="item.trailingIcon" :class="ui.linkTrailingIcon({ class: props.ui?.linkTrailingIcon, active })" />
|
||||||
</slot>
|
</slot>
|
||||||
</span>
|
</span>
|
||||||
@@ -239,23 +263,23 @@ const lists = computed(() => props.items?.length ? (Array.isArray(props.items[0]
|
|||||||
:open="item.open"
|
:open="item.open"
|
||||||
>
|
>
|
||||||
<div v-if="orientation === 'vertical' && item.type === 'label'" :class="ui.label({ class: props.ui?.label })">
|
<div v-if="orientation === 'vertical' && item.type === 'label'" :class="ui.label({ class: props.ui?.label })">
|
||||||
<ReuseLinkTemplate :item="(item as T)" :index="index" />
|
<ReuseLinkTemplate :item="item" :index="index" />
|
||||||
</div>
|
</div>
|
||||||
<ULink v-else-if="item.type !== 'label'" v-slot="{ active, ...slotProps }" v-bind="(orientation === 'vertical' && item.children?.length && !collapsed) ? {} : pickLinkProps(item as Omit<NavigationMenuItem, 'type'>)" custom>
|
<ULink v-else-if="item.type !== 'label'" v-slot="{ active, ...slotProps }" v-bind="(orientation === 'vertical' && item.children?.length && !collapsed) ? {} : pickLinkProps(item as Omit<NavigationMenuItem, 'type'>)" custom>
|
||||||
<component
|
<component
|
||||||
:is="(orientation === 'horizontal' && (item.children?.length || !!slots[item.slot ? `${item.slot}-content` : 'item-content'])) ? NavigationMenuTrigger : NavigationMenuLink"
|
:is="(orientation === 'horizontal' && (item.children?.length || !!slots[(item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>])) ? NavigationMenuTrigger : NavigationMenuLink"
|
||||||
as-child
|
as-child
|
||||||
:active="active || item.active"
|
:active="active || item.active"
|
||||||
:disabled="item.disabled"
|
:disabled="item.disabled"
|
||||||
@select="item.onSelect"
|
@select="item.onSelect"
|
||||||
>
|
>
|
||||||
<ULinkBase v-bind="slotProps" :class="ui.link({ class: [props.ui?.link, item.class], active: active || item.active, disabled: !!item.disabled, level: orientation === 'horizontal' || level > 0 })">
|
<ULinkBase v-bind="slotProps" :class="ui.link({ class: [props.ui?.link, item.class], active: active || item.active, disabled: !!item.disabled, level: orientation === 'horizontal' || level > 0 })">
|
||||||
<ReuseLinkTemplate :item="(item as T)" :active="active || item.active" :index="index" />
|
<ReuseLinkTemplate :item="item" :active="active || item.active" :index="index" />
|
||||||
</ULinkBase>
|
</ULinkBase>
|
||||||
</component>
|
</component>
|
||||||
|
|
||||||
<NavigationMenuContent v-if="orientation === 'horizontal' && (item.children?.length || !!slots[item.slot ? `${item.slot}-content` : 'item-content'])" v-bind="contentProps" :class="ui.content({ class: props.ui?.content })">
|
<NavigationMenuContent v-if="orientation === 'horizontal' && (item.children?.length || !!slots[(item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>])" v-bind="contentProps" :class="ui.content({ class: props.ui?.content })">
|
||||||
<slot :name="item.slot ? `${item.slot}-content` : 'item-content'" :item="(item as T)" :active="active" :index="index">
|
<slot :name="((item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>)" :item="item" :active="active" :index="index">
|
||||||
<ul :class="ui.childList({ class: props.ui?.childList })">
|
<ul :class="ui.childList({ class: props.ui?.childList })">
|
||||||
<li v-for="(childItem, childIndex) in item.children" :key="childIndex" :class="ui.childItem({ class: props.ui?.childItem })">
|
<li v-for="(childItem, childIndex) in item.children" :key="childIndex" :class="ui.childItem({ class: props.ui?.childItem })">
|
||||||
<ULink v-slot="{ active: childActive, ...childSlotProps }" v-bind="pickLinkProps(childItem)" custom>
|
<ULink v-slot="{ active: childActive, ...childSlotProps }" v-bind="pickLinkProps(childItem)" custom>
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { VariantProps } from 'tailwind-variants'
|
import type { VariantProps } from 'tailwind-variants'
|
||||||
import type { RadioGroupRootProps, RadioGroupRootEmits, AcceptableValue } from 'reka-ui'
|
import type { RadioGroupRootProps, RadioGroupRootEmits } from 'reka-ui'
|
||||||
import type { AppConfig } from '@nuxt/schema'
|
import type { AppConfig } from '@nuxt/schema'
|
||||||
import _appConfig from '#build/app.config'
|
import _appConfig from '#build/app.config'
|
||||||
import theme from '#build/ui/radio-group'
|
import theme from '#build/ui/radio-group'
|
||||||
import { tv } from '../utils/tv'
|
import { tv } from '../utils/tv'
|
||||||
|
import type { AcceptableValue } from '../types/utils'
|
||||||
|
|
||||||
const appConfigRadioGroup = _appConfig as AppConfig & { ui: { radioGroup: Partial<typeof theme> } }
|
const appConfigRadioGroup = _appConfig as AppConfig & { ui: { radioGroup: Partial<typeof theme> } }
|
||||||
|
|
||||||
@@ -12,14 +13,16 @@ const radioGroup = tv({ extend: tv(theme), ...(appConfigRadioGroup.ui?.radioGrou
|
|||||||
|
|
||||||
type RadioGroupVariants = VariantProps<typeof radioGroup>
|
type RadioGroupVariants = VariantProps<typeof radioGroup>
|
||||||
|
|
||||||
export interface RadioGroupItem {
|
export type RadioGroupValue = AcceptableValue
|
||||||
|
export type RadioGroupItem = {
|
||||||
label?: string
|
label?: string
|
||||||
description?: string
|
description?: string
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
value?: string
|
value?: string
|
||||||
}
|
[key: string]: any
|
||||||
|
} | RadioGroupValue
|
||||||
|
|
||||||
export interface RadioGroupProps<T> extends Pick<RadioGroupRootProps, 'defaultValue' | 'disabled' | 'loop' | 'modelValue' | 'name' | 'required'> {
|
export interface RadioGroupProps<T extends RadioGroupItem = RadioGroupItem> extends Pick<RadioGroupRootProps, 'defaultValue' | 'disabled' | 'loop' | 'modelValue' | 'name' | 'required'> {
|
||||||
/**
|
/**
|
||||||
* The element or component this component should render as.
|
* The element or component this component should render as.
|
||||||
* @defaultValue 'div'
|
* @defaultValue 'div'
|
||||||
@@ -63,16 +66,16 @@ export type RadioGroupEmits = RadioGroupRootEmits & {
|
|||||||
change: [payload: Event]
|
change: [payload: Event]
|
||||||
}
|
}
|
||||||
|
|
||||||
type SlotProps<T> = (props: { item: T, modelValue?: AcceptableValue }) => any
|
type SlotProps<T extends RadioGroupItem> = (props: { item: T & { id: string }, modelValue?: RadioGroupValue }) => any
|
||||||
|
|
||||||
export interface RadioGroupSlots<T> {
|
export interface RadioGroupSlots<T extends RadioGroupItem = RadioGroupItem> {
|
||||||
legend(props?: {}): any
|
legend(props?: {}): any
|
||||||
label: SlotProps<T>
|
label: SlotProps<T>
|
||||||
description: SlotProps<T>
|
description: SlotProps<T>
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script setup lang="ts" generic="T extends RadioGroupItem | AcceptableValue">
|
<script setup lang="ts" generic="T extends RadioGroupItem">
|
||||||
import { computed, useId } from 'vue'
|
import { computed, useId } from 'vue'
|
||||||
import { RadioGroupRoot, RadioGroupItem, RadioGroupIndicator, Label, useForwardPropsEmits } from 'reka-ui'
|
import { RadioGroupRoot, RadioGroupItem, RadioGroupIndicator, Label, useForwardPropsEmits } from 'reka-ui'
|
||||||
import { reactivePick } from '@vueuse/core'
|
import { reactivePick } from '@vueuse/core'
|
||||||
@@ -102,11 +105,19 @@ const ui = computed(() => radioGroup({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
function normalizeItem(item: any) {
|
function normalizeItem(item: any) {
|
||||||
if (['string', 'number', 'boolean'].includes(typeof item)) {
|
if (item === null) {
|
||||||
|
return {
|
||||||
|
id: `${id}:null`,
|
||||||
|
value: undefined,
|
||||||
|
label: undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof item === 'string' || typeof item === 'number') {
|
||||||
return {
|
return {
|
||||||
id: `${id}:${item}`,
|
id: `${id}:${item}`,
|
||||||
value: item,
|
value: String(item),
|
||||||
label: item
|
label: String(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,10 +181,10 @@ function onUpdate(value: any) {
|
|||||||
|
|
||||||
<div :class="ui.wrapper({ class: props.ui?.wrapper })">
|
<div :class="ui.wrapper({ class: props.ui?.wrapper })">
|
||||||
<Label :class="ui.label({ class: props.ui?.label })" :for="item.id">
|
<Label :class="ui.label({ class: props.ui?.label })" :for="item.id">
|
||||||
<slot name="label" :item="item" :model-value="modelValue">{{ item.label }}</slot>
|
<slot name="label" :item="item" :model-value="(modelValue as RadioGroupValue)">{{ item.label }}</slot>
|
||||||
</Label>
|
</Label>
|
||||||
<p v-if="item.description || !!slots.description" :class="ui.description({ class: props.ui?.description })">
|
<p v-if="item.description || !!slots.description" :class="ui.description({ class: props.ui?.description })">
|
||||||
<slot name="description" :item="item" :model-value="modelValue">
|
<slot name="description" :item="item" :model-value="(modelValue as RadioGroupValue)">
|
||||||
{{ item.description }}
|
{{ item.description }}
|
||||||
</slot>
|
</slot>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,19 +1,29 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { VariantProps } from 'tailwind-variants'
|
import type { VariantProps } from 'tailwind-variants'
|
||||||
import type { SelectRootProps, SelectRootEmits, SelectContentProps, SelectContentEmits, SelectArrowProps, AcceptableValue } from 'reka-ui'
|
import type { SelectRootProps, SelectRootEmits, SelectContentProps, SelectContentEmits, SelectArrowProps } from 'reka-ui'
|
||||||
import type { AppConfig } from '@nuxt/schema'
|
import type { AppConfig } from '@nuxt/schema'
|
||||||
import _appConfig from '#build/app.config'
|
import _appConfig from '#build/app.config'
|
||||||
import theme from '#build/ui/select'
|
import theme from '#build/ui/select'
|
||||||
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
|
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
|
||||||
import { tv } from '../utils/tv'
|
import { tv } from '../utils/tv'
|
||||||
import type { AvatarProps, ChipProps, InputProps } from '../types'
|
import type { AvatarProps, ChipProps, InputProps } from '../types'
|
||||||
import type { PartialString, MaybeArrayOfArray, MaybeArrayOfArrayItem, SelectModelValue, SelectModelValueEmits, SelectItemKey, EmitsToProps } from '../types/utils'
|
import type {
|
||||||
|
AcceptableValue,
|
||||||
|
ArrayOrNested,
|
||||||
|
GetItemKeys,
|
||||||
|
GetItemValue,
|
||||||
|
GetModelValue,
|
||||||
|
GetModelValueEmits,
|
||||||
|
NestedItem,
|
||||||
|
PartialString,
|
||||||
|
EmitsToProps
|
||||||
|
} from '../types/utils'
|
||||||
|
|
||||||
const appConfigSelect = _appConfig as AppConfig & { ui: { select: Partial<typeof theme> } }
|
const appConfigSelect = _appConfig as AppConfig & { ui: { select: Partial<typeof theme> } }
|
||||||
|
|
||||||
const select = tv({ extend: tv(theme), ...(appConfigSelect.ui?.select || {}) })
|
const select = tv({ extend: tv(theme), ...(appConfigSelect.ui?.select || {}) })
|
||||||
|
|
||||||
export interface SelectItem {
|
interface SelectItemBase {
|
||||||
label?: string
|
label?: string
|
||||||
/**
|
/**
|
||||||
* @IconifyIcon
|
* @IconifyIcon
|
||||||
@@ -26,13 +36,15 @@ export interface SelectItem {
|
|||||||
* @defaultValue 'item'
|
* @defaultValue 'item'
|
||||||
*/
|
*/
|
||||||
type?: 'label' | 'separator' | 'item'
|
type?: 'label' | 'separator' | 'item'
|
||||||
value?: string
|
value?: string | number
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
|
[key: string]: any
|
||||||
}
|
}
|
||||||
|
export type SelectItem = SelectItemBase | AcceptableValue | boolean
|
||||||
|
|
||||||
type SelectVariants = VariantProps<typeof select>
|
type SelectVariants = VariantProps<typeof select>
|
||||||
|
|
||||||
export interface SelectProps<T extends MaybeArrayOfArrayItem<I>, I extends MaybeArrayOfArray<SelectItem | AcceptableValue | boolean> = MaybeArrayOfArray<SelectItem | AcceptableValue | boolean>, V extends SelectItemKey<T> | undefined = undefined, M extends boolean = false> extends Omit<SelectRootProps<T>, 'dir' | 'multiple' | 'modelValue' | 'defaultValue' | 'by'>, UseComponentIconsProps {
|
export interface SelectProps<T extends ArrayOrNested<SelectItem> = ArrayOrNested<SelectItem>, VK extends GetItemKeys<T> = 'value', M extends boolean = false> extends Omit<SelectRootProps<T>, 'dir' | 'multiple' | 'modelValue' | 'defaultValue' | 'by'>, UseComponentIconsProps {
|
||||||
id?: string
|
id?: string
|
||||||
/** The placeholder text when the select is empty. */
|
/** The placeholder text when the select is empty. */
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
@@ -79,17 +91,17 @@ export interface SelectProps<T extends MaybeArrayOfArrayItem<I>, I extends Maybe
|
|||||||
* When `items` is an array of objects, select the field to use as the value.
|
* When `items` is an array of objects, select the field to use as the value.
|
||||||
* @defaultValue 'value'
|
* @defaultValue 'value'
|
||||||
*/
|
*/
|
||||||
valueKey?: V
|
valueKey?: VK
|
||||||
/**
|
/**
|
||||||
* When `items` is an array of objects, select the field to use as the label.
|
* When `items` is an array of objects, select the field to use as the label.
|
||||||
* @defaultValue 'label'
|
* @defaultValue 'label'
|
||||||
*/
|
*/
|
||||||
labelKey?: V
|
labelKey?: keyof NestedItem<T>
|
||||||
items?: I
|
items?: T
|
||||||
/** The value of the Select when initially rendered. Use when you do not need to control the state of the Select. */
|
/** The value of the Select when initially rendered. Use when you do not need to control the state of the Select. */
|
||||||
defaultValue?: SelectModelValue<T, V, M, T extends { value: infer U } ? U : never>
|
defaultValue?: GetModelValue<T, VK, M>
|
||||||
/** The controlled value of the Select. Can be bind as `v-model`. */
|
/** The controlled value of the Select. Can be bind as `v-model`. */
|
||||||
modelValue?: SelectModelValue<T, V, M, T extends { value: infer U } ? U : never>
|
modelValue?: GetModelValue<T, VK, M>
|
||||||
/** Whether multiple options can be selected or not. */
|
/** Whether multiple options can be selected or not. */
|
||||||
multiple?: M & boolean
|
multiple?: M & boolean
|
||||||
/** Highlight the ring color like a focus state. */
|
/** Highlight the ring color like a focus state. */
|
||||||
@@ -98,18 +110,23 @@ export interface SelectProps<T extends MaybeArrayOfArrayItem<I>, I extends Maybe
|
|||||||
ui?: PartialString<typeof select.slots>
|
ui?: PartialString<typeof select.slots>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SelectEmits<T, V, M extends boolean> = Omit<SelectRootEmits<T>, 'update:modelValue'> & {
|
export type SelectEmits<A extends ArrayOrNested<SelectItem>, VK extends GetItemKeys<A> | undefined, M extends boolean> = Omit<SelectRootEmits, 'update:modelValue'> & {
|
||||||
change: [payload: Event]
|
change: [payload: Event]
|
||||||
blur: [payload: FocusEvent]
|
blur: [payload: FocusEvent]
|
||||||
focus: [payload: FocusEvent]
|
focus: [payload: FocusEvent]
|
||||||
} & SelectModelValueEmits<T, V, M, T extends { value: infer U } ? U : never>
|
} & GetModelValueEmits<A, VK, M>
|
||||||
|
|
||||||
type SlotProps<T> = (props: { item: T, index: number }) => any
|
type SlotProps<T extends SelectItem> = (props: { item: T, index: number }) => any
|
||||||
|
|
||||||
export interface SelectSlots<T, M extends boolean> {
|
export interface SelectSlots<
|
||||||
'leading'(props: { modelValue?: M extends true ? AcceptableValue[] : AcceptableValue, open: boolean, ui: any }): any
|
A extends ArrayOrNested<SelectItem> = ArrayOrNested<SelectItem>,
|
||||||
'default'(props: { modelValue?: M extends true ? AcceptableValue[] : AcceptableValue, open: boolean }): any
|
VK extends GetItemKeys<A> | undefined = undefined,
|
||||||
'trailing'(props: { modelValue?: M extends true ? AcceptableValue[] : AcceptableValue, open: boolean, ui: any }): any
|
M extends boolean = false,
|
||||||
|
T extends NestedItem<A> = NestedItem<A>
|
||||||
|
> {
|
||||||
|
'leading'(props: { modelValue?: GetModelValue<A, VK, M>, open: boolean, ui: ReturnType<typeof select> }): any
|
||||||
|
'default'(props: { modelValue?: GetModelValue<A, VK, M>, open: boolean }): any
|
||||||
|
'trailing'(props: { modelValue?: GetModelValue<A, VK, M>, open: boolean, ui: ReturnType<typeof select> }): any
|
||||||
'item': SlotProps<T>
|
'item': SlotProps<T>
|
||||||
'item-leading': SlotProps<T>
|
'item-leading': SlotProps<T>
|
||||||
'item-label': SlotProps<T>
|
'item-label': SlotProps<T>
|
||||||
@@ -117,7 +134,7 @@ export interface SelectSlots<T, M extends boolean> {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script setup lang="ts" generic="T extends MaybeArrayOfArrayItem<I>, I extends MaybeArrayOfArray<SelectItem | AcceptableValue | boolean> = MaybeArrayOfArray<SelectItem | AcceptableValue | boolean>, V extends SelectItemKey<T> | undefined = undefined, M extends boolean = false">
|
<script setup lang="ts" generic="T extends ArrayOrNested<SelectItem>, VK extends GetItemKeys<T> = 'value', M extends boolean = false">
|
||||||
import { computed, toRef } from 'vue'
|
import { computed, toRef } from 'vue'
|
||||||
import { SelectRoot, SelectArrow, SelectTrigger, SelectPortal, SelectContent, SelectViewport, SelectLabel, SelectGroup, SelectItem, SelectItemIndicator, SelectItemText, SelectSeparator, useForwardPropsEmits } from 'reka-ui'
|
import { SelectRoot, SelectArrow, SelectTrigger, SelectPortal, SelectContent, SelectViewport, SelectLabel, SelectGroup, SelectItem, SelectItemIndicator, SelectItemText, SelectSeparator, useForwardPropsEmits } from 'reka-ui'
|
||||||
import { defu } from 'defu'
|
import { defu } from 'defu'
|
||||||
@@ -126,20 +143,20 @@ import { useAppConfig } from '#imports'
|
|||||||
import { useButtonGroup } from '../composables/useButtonGroup'
|
import { useButtonGroup } from '../composables/useButtonGroup'
|
||||||
import { useComponentIcons } from '../composables/useComponentIcons'
|
import { useComponentIcons } from '../composables/useComponentIcons'
|
||||||
import { useFormField } from '../composables/useFormField'
|
import { useFormField } from '../composables/useFormField'
|
||||||
import { get, compare } from '../utils'
|
import { compare, get, isArrayOfArray } from '../utils'
|
||||||
import UIcon from './Icon.vue'
|
import UIcon from './Icon.vue'
|
||||||
import UAvatar from './Avatar.vue'
|
import UAvatar from './Avatar.vue'
|
||||||
import UChip from './Chip.vue'
|
import UChip from './Chip.vue'
|
||||||
|
|
||||||
defineOptions({ inheritAttrs: false })
|
defineOptions({ inheritAttrs: false })
|
||||||
|
|
||||||
const props = withDefaults(defineProps<SelectProps<T, I, V, M>>(), {
|
const props = withDefaults(defineProps<SelectProps<T, VK, M>>(), {
|
||||||
valueKey: 'value' as never,
|
valueKey: 'value' as never,
|
||||||
labelKey: 'label' as never,
|
labelKey: 'label' as never,
|
||||||
portal: true
|
portal: true
|
||||||
})
|
})
|
||||||
const emits = defineEmits<SelectEmits<T, V, M>>()
|
const emits = defineEmits<SelectEmits<T, VK, M>>()
|
||||||
const slots = defineSlots<SelectSlots<T, M>>()
|
const slots = defineSlots<SelectSlots<T, VK, M>>()
|
||||||
|
|
||||||
const appConfig = useAppConfig()
|
const appConfig = useAppConfig()
|
||||||
const rootProps = useForwardPropsEmits(reactivePick(props, 'open', 'defaultOpen', 'disabled', 'autocomplete', 'required', 'multiple'), emits)
|
const rootProps = useForwardPropsEmits(reactivePick(props, 'open', 'defaultOpen', 'disabled', 'autocomplete', 'required', 'multiple'), emits)
|
||||||
@@ -163,11 +180,17 @@ const ui = computed(() => select({
|
|||||||
buttonGroup: orientation.value
|
buttonGroup: orientation.value
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0]) ? props.items : [props.items]) as SelectItem[][] : [])
|
const groups = computed<SelectItem[][]>(() =>
|
||||||
|
props.items?.length
|
||||||
|
? isArrayOfArray(props.items)
|
||||||
|
? props.items
|
||||||
|
: [props.items]
|
||||||
|
: []
|
||||||
|
)
|
||||||
// eslint-disable-next-line vue/no-dupe-keys
|
// eslint-disable-next-line vue/no-dupe-keys
|
||||||
const items = computed(() => groups.value.flatMap(group => group) as T[])
|
const items = computed(() => groups.value.flatMap(group => group) as T[])
|
||||||
|
|
||||||
function displayValue(value?: AcceptableValue | AcceptableValue[]): string | undefined {
|
function displayValue(value?: GetItemValue<T, VK> | GetItemValue<T, VK>[]): string {
|
||||||
if (props.multiple && Array.isArray(value)) {
|
if (props.multiple && Array.isArray(value)) {
|
||||||
return value.map(v => displayValue(v)).filter(Boolean).join(', ')
|
return value.map(v => displayValue(v)).filter(Boolean).join(', ')
|
||||||
}
|
}
|
||||||
@@ -195,6 +218,10 @@ function onUpdateOpen(value: boolean) {
|
|||||||
emitFormFocus()
|
emitFormFocus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isSelectItem(item: SelectItem): item is SelectItemBase {
|
||||||
|
return typeof item === 'object' && item !== null
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- eslint-disable vue/no-template-shadow -->
|
<!-- eslint-disable vue/no-template-shadow -->
|
||||||
@@ -205,21 +232,21 @@ function onUpdateOpen(value: boolean) {
|
|||||||
v-bind="rootProps"
|
v-bind="rootProps"
|
||||||
:autocomplete="autocomplete"
|
:autocomplete="autocomplete"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:default-value="(defaultValue as (AcceptableValue | AcceptableValue[] | undefined))"
|
:default-value="(defaultValue as (AcceptableValue | AcceptableValue[]))"
|
||||||
:model-value="(modelValue as (AcceptableValue | AcceptableValue[] | undefined))"
|
:model-value="(modelValue as (AcceptableValue | AcceptableValue[]))"
|
||||||
@update:model-value="onUpdate"
|
@update:model-value="onUpdate"
|
||||||
@update:open="onUpdateOpen"
|
@update:open="onUpdateOpen"
|
||||||
>
|
>
|
||||||
<SelectTrigger :id="id" :class="ui.base({ class: [props.class, props.ui?.base] })" v-bind="{ ...$attrs, ...ariaAttrs }">
|
<SelectTrigger :id="id" :class="ui.base({ class: [props.class, props.ui?.base] })" v-bind="{ ...$attrs, ...ariaAttrs }">
|
||||||
<span v-if="isLeading || !!avatar || !!slots.leading" :class="ui.leading({ class: props.ui?.leading })">
|
<span v-if="isLeading || !!avatar || !!slots.leading" :class="ui.leading({ class: props.ui?.leading })">
|
||||||
<slot name="leading" :model-value="(modelValue as M extends true ? AcceptableValue[] : AcceptableValue)" :open="open" :ui="ui">
|
<slot name="leading" :model-value="(modelValue as GetModelValue<T, VK, M>)" :open="open" :ui="ui">
|
||||||
<UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
|
<UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
|
||||||
<UAvatar v-else-if="!!avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
|
<UAvatar v-else-if="!!avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
|
||||||
</slot>
|
</slot>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<slot :model-value="(modelValue as M extends true ? AcceptableValue[] : AcceptableValue)" :open="open">
|
<slot :model-value="(modelValue as GetModelValue<T, VK, M>)" :open="open">
|
||||||
<template v-for="displayedModelValue in [displayValue(modelValue)]" :key="displayedModelValue">
|
<template v-for="displayedModelValue in [displayValue(modelValue as GetModelValue<T, VK, M>)]" :key="displayedModelValue">
|
||||||
<span v-if="displayedModelValue" :class="ui.value({ class: props.ui?.value })">
|
<span v-if="displayedModelValue" :class="ui.value({ class: props.ui?.value })">
|
||||||
{{ displayedModelValue }}
|
{{ displayedModelValue }}
|
||||||
</span>
|
</span>
|
||||||
@@ -230,7 +257,7 @@ function onUpdateOpen(value: boolean) {
|
|||||||
</slot>
|
</slot>
|
||||||
|
|
||||||
<span v-if="isTrailing || !!slots.trailing" :class="ui.trailing({ class: props.ui?.trailing })">
|
<span v-if="isTrailing || !!slots.trailing" :class="ui.trailing({ class: props.ui?.trailing })">
|
||||||
<slot name="trailing" :model-value="(modelValue as M extends true ? AcceptableValue[] : AcceptableValue)" :open="open" :ui="ui">
|
<slot name="trailing" :model-value="(modelValue as GetModelValue<T, VK, M>)" :open="open" :ui="ui">
|
||||||
<UIcon v-if="trailingIconName" :name="trailingIconName" :class="ui.trailingIcon({ class: props.ui?.trailingIcon })" />
|
<UIcon v-if="trailingIconName" :name="trailingIconName" :class="ui.trailingIcon({ class: props.ui?.trailingIcon })" />
|
||||||
</slot>
|
</slot>
|
||||||
</span>
|
</span>
|
||||||
@@ -241,24 +268,24 @@ function onUpdateOpen(value: boolean) {
|
|||||||
<SelectViewport :class="ui.viewport({ class: props.ui?.viewport })">
|
<SelectViewport :class="ui.viewport({ class: props.ui?.viewport })">
|
||||||
<SelectGroup v-for="(group, groupIndex) in groups" :key="`group-${groupIndex}`" :class="ui.group({ class: props.ui?.group })">
|
<SelectGroup v-for="(group, groupIndex) in groups" :key="`group-${groupIndex}`" :class="ui.group({ class: props.ui?.group })">
|
||||||
<template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
|
<template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
|
||||||
<SelectLabel v-if="item?.type === 'label'" :class="ui.label({ class: props.ui?.label })">
|
<SelectLabel v-if="isSelectItem(item) && item.type === 'label'" :class="ui.label({ class: props.ui?.label })">
|
||||||
{{ get(item, props.labelKey as string) }}
|
{{ get(item, props.labelKey as string) }}
|
||||||
</SelectLabel>
|
</SelectLabel>
|
||||||
|
|
||||||
<SelectSeparator v-else-if="item?.type === 'separator'" :class="ui.separator({ class: props.ui?.separator })" />
|
<SelectSeparator v-else-if="isSelectItem(item) && item.type === 'separator'" :class="ui.separator({ class: props.ui?.separator })" />
|
||||||
|
|
||||||
<SelectItem
|
<SelectItem
|
||||||
v-else
|
v-else
|
||||||
:class="ui.item({ class: props.ui?.item })"
|
:class="ui.item({ class: props.ui?.item })"
|
||||||
:disabled="item.disabled"
|
:disabled="isSelectItem(item) && item.disabled"
|
||||||
:value="typeof item === 'object' ? get(item, props.valueKey as string) : item"
|
:value="isSelectItem(item) ? get(item, props.valueKey as string) : item"
|
||||||
>
|
>
|
||||||
<slot name="item" :item="(item as T)" :index="index">
|
<slot name="item" :item="(item as NestedItem<T>)" :index="index">
|
||||||
<slot name="item-leading" :item="(item as T)" :index="index">
|
<slot name="item-leading" :item="(item as NestedItem<T>)" :index="index">
|
||||||
<UIcon v-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon })" />
|
<UIcon v-if="isSelectItem(item) && item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon })" />
|
||||||
<UAvatar v-else-if="item.avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
|
<UAvatar v-else-if="isSelectItem(item) && item.avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
|
||||||
<UChip
|
<UChip
|
||||||
v-else-if="item.chip"
|
v-else-if="isSelectItem(item) && item.chip"
|
||||||
:size="((props.ui?.itemLeadingChipSize || ui.itemLeadingChipSize()) as ChipProps['size'])"
|
:size="((props.ui?.itemLeadingChipSize || ui.itemLeadingChipSize()) as ChipProps['size'])"
|
||||||
inset
|
inset
|
||||||
standalone
|
standalone
|
||||||
@@ -268,13 +295,13 @@ function onUpdateOpen(value: boolean) {
|
|||||||
</slot>
|
</slot>
|
||||||
|
|
||||||
<SelectItemText :class="ui.itemLabel({ class: props.ui?.itemLabel })">
|
<SelectItemText :class="ui.itemLabel({ class: props.ui?.itemLabel })">
|
||||||
<slot name="item-label" :item="(item as T)" :index="index">
|
<slot name="item-label" :item="(item as NestedItem<T>)" :index="index">
|
||||||
{{ typeof item === 'object' ? get(item, props.labelKey as string) : item }}
|
{{ isSelectItem(item) ? get(item, props.labelKey as string) : item }}
|
||||||
</slot>
|
</slot>
|
||||||
</SelectItemText>
|
</SelectItemText>
|
||||||
|
|
||||||
<span :class="ui.itemTrailing({ class: props.ui?.itemTrailing })">
|
<span :class="ui.itemTrailing({ class: props.ui?.itemTrailing })">
|
||||||
<slot name="item-trailing" :item="(item as T)" :index="index" />
|
<slot name="item-trailing" :item="(item as NestedItem<T>)" :index="index" />
|
||||||
|
|
||||||
<SelectItemIndicator as-child>
|
<SelectItemIndicator as-child>
|
||||||
<UIcon :name="selectedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: props.ui?.itemTrailingIcon })" />
|
<UIcon :name="selectedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: props.ui?.itemTrailingIcon })" />
|
||||||
|
|||||||
@@ -1,19 +1,29 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { VariantProps } from 'tailwind-variants'
|
import type { VariantProps } from 'tailwind-variants'
|
||||||
import type { ComboboxRootProps, ComboboxRootEmits, ComboboxContentProps, ComboboxContentEmits, ComboboxArrowProps, AcceptableValue } from 'reka-ui'
|
import type { ComboboxRootProps, ComboboxRootEmits, ComboboxContentProps, ComboboxContentEmits, ComboboxArrowProps } from 'reka-ui'
|
||||||
import type { AppConfig } from '@nuxt/schema'
|
import type { AppConfig } from '@nuxt/schema'
|
||||||
import _appConfig from '#build/app.config'
|
import _appConfig from '#build/app.config'
|
||||||
import theme from '#build/ui/select-menu'
|
import theme from '#build/ui/select-menu'
|
||||||
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
|
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
|
||||||
import { tv } from '../utils/tv'
|
import { tv } from '../utils/tv'
|
||||||
import type { AvatarProps, ChipProps, InputProps } from '../types'
|
import type { AvatarProps, ChipProps, InputProps } from '../types'
|
||||||
import type { PartialString, MaybeArrayOfArray, MaybeArrayOfArrayItem, SelectModelValue, SelectModelValueEmits, SelectItemKey, EmitsToProps } from '../types/utils'
|
import type {
|
||||||
|
AcceptableValue,
|
||||||
|
ArrayOrNested,
|
||||||
|
GetItemKeys,
|
||||||
|
GetItemValue,
|
||||||
|
GetModelValue,
|
||||||
|
GetModelValueEmits,
|
||||||
|
NestedItem,
|
||||||
|
PartialString,
|
||||||
|
EmitsToProps
|
||||||
|
} from '../types/utils'
|
||||||
|
|
||||||
const appConfigSelectMenu = _appConfig as AppConfig & { ui: { selectMenu: Partial<typeof theme> } }
|
const appConfigSelectMenu = _appConfig as AppConfig & { ui: { selectMenu: Partial<typeof theme> } }
|
||||||
|
|
||||||
const selectMenu = tv({ extend: tv(theme), ...(appConfigSelectMenu.ui?.selectMenu || {}) })
|
const selectMenu = tv({ extend: tv(theme), ...(appConfigSelectMenu.ui?.selectMenu || {}) })
|
||||||
|
|
||||||
export interface SelectMenuItem {
|
interface _SelectMenuItem {
|
||||||
label?: string
|
label?: string
|
||||||
/**
|
/**
|
||||||
* @IconifyIcon
|
* @IconifyIcon
|
||||||
@@ -26,13 +36,16 @@ export interface SelectMenuItem {
|
|||||||
* @defaultValue 'item'
|
* @defaultValue 'item'
|
||||||
*/
|
*/
|
||||||
type?: 'label' | 'separator' | 'item'
|
type?: 'label' | 'separator' | 'item'
|
||||||
|
value?: string | number
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
onSelect?(e?: Event): void
|
onSelect?(e?: Event): void
|
||||||
|
[key: string]: any
|
||||||
}
|
}
|
||||||
|
export type SelectMenuItem = _SelectMenuItem | AcceptableValue | boolean
|
||||||
|
|
||||||
type SelectMenuVariants = VariantProps<typeof selectMenu>
|
type SelectMenuVariants = VariantProps<typeof selectMenu>
|
||||||
|
|
||||||
export interface SelectMenuProps<T extends MaybeArrayOfArrayItem<I>, I extends MaybeArrayOfArray<SelectMenuItem | AcceptableValue | boolean> = MaybeArrayOfArray<SelectMenuItem | AcceptableValue | boolean>, V extends SelectItemKey<T> | undefined = undefined, M extends boolean = false> extends Pick<ComboboxRootProps<T>, 'open' | 'defaultOpen' | 'disabled' | 'name' | 'resetSearchTermOnBlur' | 'highlightOnHover'>, UseComponentIconsProps {
|
export interface SelectMenuProps<T extends ArrayOrNested<SelectMenuItem> = ArrayOrNested<SelectMenuItem>, VK extends GetItemKeys<T> | undefined = undefined, M extends boolean = false> extends Pick<ComboboxRootProps<T>, 'open' | 'defaultOpen' | 'disabled' | 'name' | 'resetSearchTermOnBlur' | 'highlightOnHover'>, UseComponentIconsProps {
|
||||||
id?: string
|
id?: string
|
||||||
/** The placeholder text when the select is empty. */
|
/** The placeholder text when the select is empty. */
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
@@ -88,17 +101,17 @@ export interface SelectMenuProps<T extends MaybeArrayOfArrayItem<I>, I extends M
|
|||||||
* When `items` is an array of objects, select the field to use as the value instead of the object itself.
|
* When `items` is an array of objects, select the field to use as the value instead of the object itself.
|
||||||
* @defaultValue undefined
|
* @defaultValue undefined
|
||||||
*/
|
*/
|
||||||
valueKey?: V
|
valueKey?: VK
|
||||||
/**
|
/**
|
||||||
* When `items` is an array of objects, select the field to use as the label.
|
* When `items` is an array of objects, select the field to use as the label.
|
||||||
* @defaultValue 'label'
|
* @defaultValue 'label'
|
||||||
*/
|
*/
|
||||||
labelKey?: V
|
labelKey?: keyof NestedItem<T>
|
||||||
items?: I
|
items?: T
|
||||||
/** The value of the SelectMenu when initially rendered. Use when you do not need to control the state of the SelectMenu. */
|
/** The value of the SelectMenu when initially rendered. Use when you do not need to control the state of the SelectMenu. */
|
||||||
defaultValue?: SelectModelValue<T, V, M>
|
defaultValue?: GetModelValue<T, VK, M>
|
||||||
/** The controlled value of the SelectMenu. Can be binded-with with `v-model`. */
|
/** The controlled value of the SelectMenu. Can be binded-with with `v-model`. */
|
||||||
modelValue?: SelectModelValue<T, V, M>
|
modelValue?: GetModelValue<T, VK, M>
|
||||||
/** Whether multiple options can be selected or not. */
|
/** Whether multiple options can be selected or not. */
|
||||||
multiple?: M & boolean
|
multiple?: M & boolean
|
||||||
/** Highlight the ring color like a focus state. */
|
/** Highlight the ring color like a focus state. */
|
||||||
@@ -122,19 +135,29 @@ export interface SelectMenuProps<T extends MaybeArrayOfArrayItem<I>, I extends M
|
|||||||
ui?: PartialString<typeof selectMenu.slots>
|
ui?: PartialString<typeof selectMenu.slots>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SelectMenuEmits<T, V, M extends boolean> = Omit<ComboboxRootEmits<T>, 'update:modelValue'> & {
|
export type SelectMenuEmits<A extends ArrayOrNested<SelectMenuItem>, VK extends GetItemKeys<A> | undefined, M extends boolean> = Pick<ComboboxRootEmits, 'update:open'> & {
|
||||||
change: [payload: Event]
|
change: [payload: Event]
|
||||||
blur: [payload: FocusEvent]
|
blur: [payload: FocusEvent]
|
||||||
focus: [payload: FocusEvent]
|
focus: [payload: FocusEvent]
|
||||||
create: [item: string]
|
create: [item: string]
|
||||||
} & SelectModelValueEmits<T, V, M>
|
/** Event handler when highlighted element changes. */
|
||||||
|
highlight: [payload: {
|
||||||
|
ref: HTMLElement
|
||||||
|
value: GetModelValue<A, VK, M>
|
||||||
|
} | undefined]
|
||||||
|
} & GetModelValueEmits<A, VK, M>
|
||||||
|
|
||||||
type SlotProps<T> = (props: { item: T, index: number }) => any
|
type SlotProps<T extends SelectMenuItem> = (props: { item: T, index: number }) => any
|
||||||
|
|
||||||
export interface SelectMenuSlots<T, M extends boolean> {
|
export interface SelectMenuSlots<
|
||||||
'leading'(props: { modelValue?: M extends true ? T[] : T, open: boolean, ui: any }): any
|
A extends ArrayOrNested<SelectMenuItem> = ArrayOrNested<SelectMenuItem>,
|
||||||
'default'(props: { modelValue?: M extends true ? T[] : T, open: boolean }): any
|
VK extends GetItemKeys<A> | undefined = undefined,
|
||||||
'trailing'(props: { modelValue?: M extends true ? T[] : T, open: boolean, ui: any }): any
|
M extends boolean = false,
|
||||||
|
T extends NestedItem<A> = NestedItem<A>
|
||||||
|
> {
|
||||||
|
'leading'(props: { modelValue?: GetModelValue<A, VK, M>, open: boolean, ui: ReturnType<typeof selectMenu> }): any
|
||||||
|
'default'(props: { modelValue?: GetModelValue<A, VK, M>, open: boolean }): any
|
||||||
|
'trailing'(props: { modelValue?: GetModelValue<A, VK, M>, open: boolean, ui: ReturnType<typeof selectMenu> }): any
|
||||||
'empty'(props: { searchTerm?: string }): any
|
'empty'(props: { searchTerm?: string }): any
|
||||||
'item': SlotProps<T>
|
'item': SlotProps<T>
|
||||||
'item-leading': SlotProps<T>
|
'item-leading': SlotProps<T>
|
||||||
@@ -144,7 +167,7 @@ export interface SelectMenuSlots<T, M extends boolean> {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script setup lang="ts" generic="T extends MaybeArrayOfArrayItem<I>, I extends MaybeArrayOfArray<SelectMenuItem | AcceptableValue | boolean> = MaybeArrayOfArray<SelectMenuItem | AcceptableValue | boolean>, V extends SelectItemKey<T> | undefined = undefined, M extends boolean = false">
|
<script setup lang="ts" generic="T extends ArrayOrNested<SelectMenuItem>, VK extends GetItemKeys<T> | undefined = undefined, M extends boolean = false">
|
||||||
import { computed, toRef, toRaw } from 'vue'
|
import { computed, toRef, toRaw } from 'vue'
|
||||||
import { ComboboxRoot, ComboboxArrow, ComboboxAnchor, ComboboxInput, ComboboxTrigger, ComboboxPortal, ComboboxContent, ComboboxViewport, ComboboxEmpty, ComboboxGroup, ComboboxLabel, ComboboxSeparator, ComboboxItem, ComboboxItemIndicator, FocusScope, useForwardPropsEmits, useFilter } from 'reka-ui'
|
import { ComboboxRoot, ComboboxArrow, ComboboxAnchor, ComboboxInput, ComboboxTrigger, ComboboxPortal, ComboboxContent, ComboboxViewport, ComboboxEmpty, ComboboxGroup, ComboboxLabel, ComboboxSeparator, ComboboxItem, ComboboxItemIndicator, FocusScope, useForwardPropsEmits, useFilter } from 'reka-ui'
|
||||||
import { defu } from 'defu'
|
import { defu } from 'defu'
|
||||||
@@ -154,7 +177,7 @@ import { useButtonGroup } from '../composables/useButtonGroup'
|
|||||||
import { useComponentIcons } from '../composables/useComponentIcons'
|
import { useComponentIcons } from '../composables/useComponentIcons'
|
||||||
import { useFormField } from '../composables/useFormField'
|
import { useFormField } from '../composables/useFormField'
|
||||||
import { useLocale } from '../composables/useLocale'
|
import { useLocale } from '../composables/useLocale'
|
||||||
import { get, compare } from '../utils'
|
import { compare, get, isArrayOfArray } from '../utils'
|
||||||
import UIcon from './Icon.vue'
|
import UIcon from './Icon.vue'
|
||||||
import UAvatar from './Avatar.vue'
|
import UAvatar from './Avatar.vue'
|
||||||
import UChip from './Chip.vue'
|
import UChip from './Chip.vue'
|
||||||
@@ -162,14 +185,14 @@ import UInput from './Input.vue'
|
|||||||
|
|
||||||
defineOptions({ inheritAttrs: false })
|
defineOptions({ inheritAttrs: false })
|
||||||
|
|
||||||
const props = withDefaults(defineProps<SelectMenuProps<T, I, V, M>>(), {
|
const props = withDefaults(defineProps<SelectMenuProps<T, VK, M>>(), {
|
||||||
portal: true,
|
portal: true,
|
||||||
searchInput: true,
|
searchInput: true,
|
||||||
labelKey: 'label' as never,
|
labelKey: 'label' as never,
|
||||||
resetSearchTermOnBlur: true
|
resetSearchTermOnBlur: true
|
||||||
})
|
})
|
||||||
const emits = defineEmits<SelectMenuEmits<T, V, M>>()
|
const emits = defineEmits<SelectMenuEmits<T, VK, M>>()
|
||||||
const slots = defineSlots<SelectMenuSlots<T, M>>()
|
const slots = defineSlots<SelectMenuSlots<T, VK, M>>()
|
||||||
|
|
||||||
const searchTerm = defineModel<string>('searchTerm', { default: '' })
|
const searchTerm = defineModel<string>('searchTerm', { default: '' })
|
||||||
|
|
||||||
@@ -201,7 +224,7 @@ const ui = computed(() => selectMenu({
|
|||||||
buttonGroup: orientation.value
|
buttonGroup: orientation.value
|
||||||
}))
|
}))
|
||||||
|
|
||||||
function displayValue(value: T | T[]): string {
|
function displayValue(value: GetItemValue<T, VK> | GetItemValue<T, VK>[]): string {
|
||||||
if (props.multiple && Array.isArray(value)) {
|
if (props.multiple && Array.isArray(value)) {
|
||||||
return value.map(v => displayValue(v)).filter(Boolean).join(', ')
|
return value.map(v => displayValue(v)).filter(Boolean).join(', ')
|
||||||
}
|
}
|
||||||
@@ -214,7 +237,13 @@ function displayValue(value: T | T[]): string {
|
|||||||
return item && (typeof item === 'object' ? get(item, props.labelKey as string) : item)
|
return item && (typeof item === 'object' ? get(item, props.labelKey as string) : item)
|
||||||
}
|
}
|
||||||
|
|
||||||
const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0]) ? props.items : [props.items]) as SelectMenuItem[][] : [])
|
const groups = computed<SelectMenuItem[][]>(() =>
|
||||||
|
props.items?.length
|
||||||
|
? isArrayOfArray(props.items)
|
||||||
|
? props.items
|
||||||
|
: [props.items]
|
||||||
|
: []
|
||||||
|
)
|
||||||
// eslint-disable-next-line vue/no-dupe-keys
|
// eslint-disable-next-line vue/no-dupe-keys
|
||||||
const items = computed(() => groups.value.flatMap(group => group) as T[])
|
const items = computed(() => groups.value.flatMap(group => group) as T[])
|
||||||
|
|
||||||
@@ -226,8 +255,8 @@ const filteredGroups = computed(() => {
|
|||||||
const fields = Array.isArray(props.filterFields) ? props.filterFields : [props.labelKey] as string[]
|
const fields = Array.isArray(props.filterFields) ? props.filterFields : [props.labelKey] as string[]
|
||||||
|
|
||||||
return groups.value.map(items => items.filter((item) => {
|
return groups.value.map(items => items.filter((item) => {
|
||||||
if (typeof item !== 'object') {
|
if (typeof item !== 'object' || item === null) {
|
||||||
return contains(item, searchTerm.value)
|
return contains(String(item), searchTerm.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.type && ['label', 'separator'].includes(item.type)) {
|
if (item.type && ['label', 'separator'].includes(item.type)) {
|
||||||
@@ -235,19 +264,21 @@ const filteredGroups = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return fields.some(field => contains(get(item, field), searchTerm.value))
|
return fields.some(field => contains(get(item, field), searchTerm.value))
|
||||||
})).filter(group => group.filter(item => !item.type || !['label', 'separator'].includes(item.type)).length > 0)
|
})).filter(group => group.filter(item =>
|
||||||
|
isSelectItem(item) && (!item.type || !['label', 'separator'].includes(item.type))
|
||||||
|
).length > 0)
|
||||||
})
|
})
|
||||||
const filteredItems = computed(() => filteredGroups.value.flatMap(group => group) as T[])
|
const filteredItems = computed(() => filteredGroups.value.flatMap(group => group))
|
||||||
|
|
||||||
const createItem = computed(() => {
|
const createItem = computed(() => {
|
||||||
if (!props.createItem || !searchTerm.value) {
|
if (!props.createItem || !searchTerm.value) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const newItem = props.valueKey ? { [props.valueKey]: searchTerm.value } as T : searchTerm.value
|
const newItem = props.valueKey ? { [props.valueKey]: searchTerm.value } as NestedItem<T> : searchTerm.value
|
||||||
|
|
||||||
if ((typeof props.createItem === 'object' && props.createItem.when === 'always') || props.createItem === 'always') {
|
if ((typeof props.createItem === 'object' && props.createItem.when === 'always') || props.createItem === 'always') {
|
||||||
return !filteredItems.value.find(item => compare(item, newItem, props.valueKey))
|
return !filteredItems.value.find(item => compare(item, newItem, String(props.valueKey)))
|
||||||
}
|
}
|
||||||
|
|
||||||
return !filteredItems.value.length
|
return !filteredItems.value.length
|
||||||
@@ -290,6 +321,10 @@ function onUpdateOpen(value: boolean) {
|
|||||||
clearTimeout(timeoutId)
|
clearTimeout(timeoutId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isSelectItem(item: SelectMenuItem): item is _SelectMenuItem {
|
||||||
|
return typeof item === 'object' && item !== null
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- eslint-disable vue/no-template-shadow -->
|
<!-- eslint-disable vue/no-template-shadow -->
|
||||||
@@ -324,14 +359,14 @@ function onUpdateOpen(value: boolean) {
|
|||||||
<ComboboxAnchor as-child>
|
<ComboboxAnchor as-child>
|
||||||
<ComboboxTrigger :class="ui.base({ class: [props.class, props.ui?.base] })" tabindex="0">
|
<ComboboxTrigger :class="ui.base({ class: [props.class, props.ui?.base] })" tabindex="0">
|
||||||
<span v-if="isLeading || !!avatar || !!slots.leading" :class="ui.leading({ class: props.ui?.leading })">
|
<span v-if="isLeading || !!avatar || !!slots.leading" :class="ui.leading({ class: props.ui?.leading })">
|
||||||
<slot name="leading" :model-value="(modelValue as M extends true ? T[] : T)" :open="open" :ui="ui">
|
<slot name="leading" :model-value="(modelValue as GetModelValue<T, VK, M>)" :open="open" :ui="ui">
|
||||||
<UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
|
<UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
|
||||||
<UAvatar v-else-if="!!avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
|
<UAvatar v-else-if="!!avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
|
||||||
</slot>
|
</slot>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<slot :model-value="(modelValue as M extends true ? T[] : T)" :open="open">
|
<slot :model-value="(modelValue as GetModelValue<T, VK, M>)" :open="open">
|
||||||
<template v-for="displayedModelValue in [displayValue(modelValue as M extends true ? T[] : T)]" :key="displayedModelValue">
|
<template v-for="displayedModelValue in [displayValue(modelValue as GetModelValue<T, VK, M>)]" :key="displayedModelValue">
|
||||||
<span v-if="displayedModelValue" :class="ui.value({ class: props.ui?.value })">
|
<span v-if="displayedModelValue" :class="ui.value({ class: props.ui?.value })">
|
||||||
{{ displayedModelValue }}
|
{{ displayedModelValue }}
|
||||||
</span>
|
</span>
|
||||||
@@ -342,7 +377,7 @@ function onUpdateOpen(value: boolean) {
|
|||||||
</slot>
|
</slot>
|
||||||
|
|
||||||
<span v-if="isTrailing || !!slots.trailing" :class="ui.trailing({ class: props.ui?.trailing })">
|
<span v-if="isTrailing || !!slots.trailing" :class="ui.trailing({ class: props.ui?.trailing })">
|
||||||
<slot name="trailing" :model-value="(modelValue as M extends true ? T[] : T)" :open="open" :ui="ui">
|
<slot name="trailing" :model-value="(modelValue as GetModelValue<T, VK, M>)" :open="open" :ui="ui">
|
||||||
<UIcon v-if="trailingIconName" :name="trailingIconName" :class="ui.trailingIcon({ class: props.ui?.trailingIcon })" />
|
<UIcon v-if="trailingIconName" :name="trailingIconName" :class="ui.trailingIcon({ class: props.ui?.trailingIcon })" />
|
||||||
</slot>
|
</slot>
|
||||||
</span>
|
</span>
|
||||||
@@ -367,25 +402,25 @@ function onUpdateOpen(value: boolean) {
|
|||||||
|
|
||||||
<ComboboxGroup v-for="(group, groupIndex) in filteredGroups" :key="`group-${groupIndex}`" :class="ui.group({ class: props.ui?.group })">
|
<ComboboxGroup v-for="(group, groupIndex) in filteredGroups" :key="`group-${groupIndex}`" :class="ui.group({ class: props.ui?.group })">
|
||||||
<template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
|
<template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
|
||||||
<ComboboxLabel v-if="item?.type === 'label'" :class="ui.label({ class: props.ui?.label })">
|
<ComboboxLabel v-if="isSelectItem(item) && item.type === 'label'" :class="ui.label({ class: props.ui?.label })">
|
||||||
{{ get(item, props.labelKey as string) }}
|
{{ get(item, props.labelKey as string) }}
|
||||||
</ComboboxLabel>
|
</ComboboxLabel>
|
||||||
|
|
||||||
<ComboboxSeparator v-else-if="item?.type === 'separator'" :class="ui.separator({ class: props.ui?.separator })" />
|
<ComboboxSeparator v-else-if="isSelectItem(item) && item.type === 'separator'" :class="ui.separator({ class: props.ui?.separator })" />
|
||||||
|
|
||||||
<ComboboxItem
|
<ComboboxItem
|
||||||
v-else
|
v-else
|
||||||
:class="ui.item({ class: props.ui?.item })"
|
:class="ui.item({ class: props.ui?.item })"
|
||||||
:disabled="item.disabled"
|
:disabled="isSelectItem(item) && item.disabled"
|
||||||
:value="valueKey && typeof item === 'object' ? get(item, props.valueKey as string) : item"
|
:value="props.valueKey && isSelectItem(item) ? get(item, props.valueKey as string) : item"
|
||||||
@select="item.onSelect"
|
@select="isSelectItem(item) && item.onSelect"
|
||||||
>
|
>
|
||||||
<slot name="item" :item="(item as T)" :index="index">
|
<slot name="item" :item="(item as NestedItem<T>)" :index="index">
|
||||||
<slot name="item-leading" :item="(item as T)" :index="index">
|
<slot name="item-leading" :item="(item as NestedItem<T>)" :index="index">
|
||||||
<UIcon v-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon })" />
|
<UIcon v-if="isSelectItem(item) && item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon })" />
|
||||||
<UAvatar v-else-if="item.avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
|
<UAvatar v-else-if="isSelectItem(item) && item.avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
|
||||||
<UChip
|
<UChip
|
||||||
v-else-if="item.chip"
|
v-else-if="isSelectItem(item) && item.chip"
|
||||||
:size="((props.ui?.itemLeadingChipSize || ui.itemLeadingChipSize()) as ChipProps['size'])"
|
:size="((props.ui?.itemLeadingChipSize || ui.itemLeadingChipSize()) as ChipProps['size'])"
|
||||||
inset
|
inset
|
||||||
standalone
|
standalone
|
||||||
@@ -395,13 +430,13 @@ function onUpdateOpen(value: boolean) {
|
|||||||
</slot>
|
</slot>
|
||||||
|
|
||||||
<span :class="ui.itemLabel({ class: props.ui?.itemLabel })">
|
<span :class="ui.itemLabel({ class: props.ui?.itemLabel })">
|
||||||
<slot name="item-label" :item="(item as T)" :index="index">
|
<slot name="item-label" :item="(item as NestedItem<T>)" :index="index">
|
||||||
{{ typeof item === 'object' ? get(item, props.labelKey as string) : item }}
|
{{ isSelectItem(item) ? get(item, props.labelKey as string) : item }}
|
||||||
</slot>
|
</slot>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span :class="ui.itemTrailing({ class: props.ui?.itemTrailing })">
|
<span :class="ui.itemTrailing({ class: props.ui?.itemTrailing })">
|
||||||
<slot name="item-trailing" :item="(item as T)" :index="index" />
|
<slot name="item-trailing" :item="(item as NestedItem<T>)" :index="index" />
|
||||||
|
|
||||||
<ComboboxItemIndicator as-child>
|
<ComboboxItemIndicator as-child>
|
||||||
<UIcon :name="selectedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: props.ui?.itemTrailingIcon })" />
|
<UIcon :name="selectedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: props.ui?.itemTrailingIcon })" />
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export interface SlideoverSlots {
|
|||||||
header(props?: {}): any
|
header(props?: {}): any
|
||||||
title(props?: {}): any
|
title(props?: {}): any
|
||||||
description(props?: {}): any
|
description(props?: {}): any
|
||||||
close(props: { ui: any }): any
|
close(props: { ui: ReturnType<typeof slideover> }): any
|
||||||
body(props?: {}): any
|
body(props?: {}): any
|
||||||
footer(props?: {}): any
|
footer(props?: {}): any
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,9 +25,10 @@ export interface StepperItem {
|
|||||||
icon?: string
|
icon?: string
|
||||||
content?: string
|
content?: string
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
|
[key: string]: any
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StepperProps<T extends StepperItem> extends Pick<StepperRootProps, 'linear'> {
|
export interface StepperProps<T extends StepperItem = StepperItem> extends Pick<StepperRootProps, 'linear'> {
|
||||||
/**
|
/**
|
||||||
* The element or component this component should render as.
|
* The element or component this component should render as.
|
||||||
* @defaultValue 'div'
|
* @defaultValue 'div'
|
||||||
@@ -56,19 +57,19 @@ export interface StepperProps<T extends StepperItem> extends Pick<StepperRootPro
|
|||||||
class?: any
|
class?: any
|
||||||
}
|
}
|
||||||
|
|
||||||
export type StepperEmits<T> = Omit<StepperRootEmits, 'update:modelValue'> & {
|
export type StepperEmits<T extends StepperItem = StepperItem> = Omit<StepperRootEmits, 'update:modelValue'> & {
|
||||||
next: [payload: T]
|
next: [payload: T]
|
||||||
prev: [payload: T]
|
prev: [payload: T]
|
||||||
}
|
}
|
||||||
|
|
||||||
type SlotProps<T extends StepperItem> = (props: { item: T }) => any
|
type SlotProps<T extends StepperItem> = (props: { item: T }) => any
|
||||||
|
|
||||||
export type StepperSlots<T extends StepperItem> = {
|
export type StepperSlots<T extends StepperItem = StepperItem> = {
|
||||||
indicator: SlotProps<T>
|
indicator: SlotProps<T>
|
||||||
title: SlotProps<T>
|
title: SlotProps<T>
|
||||||
description: SlotProps<T>
|
description: SlotProps<T>
|
||||||
content: SlotProps<T>
|
content: SlotProps<T>
|
||||||
} & DynamicSlots<T, SlotProps<T>>
|
} & DynamicSlots<T>
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -108,7 +109,7 @@ const currentStepIndex = computed({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const currentStep = computed(() => props.items?.[currentStepIndex.value] as T)
|
const currentStep = computed(() => props.items?.[currentStepIndex.value])
|
||||||
const hasNext = computed(() => currentStepIndex.value < props.items?.length - 1)
|
const hasNext = computed(() => currentStepIndex.value < props.items?.length - 1)
|
||||||
const hasPrev = computed(() => currentStepIndex.value > 0)
|
const hasPrev = computed(() => currentStepIndex.value > 0)
|
||||||
|
|
||||||
@@ -116,13 +117,13 @@ defineExpose({
|
|||||||
next() {
|
next() {
|
||||||
if (hasNext.value) {
|
if (hasNext.value) {
|
||||||
currentStepIndex.value += 1
|
currentStepIndex.value += 1
|
||||||
emits('next', currentStep.value)
|
emits('next', currentStep.value as T)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
prev() {
|
prev() {
|
||||||
if (hasPrev.value) {
|
if (hasPrev.value) {
|
||||||
currentStepIndex.value -= 1
|
currentStepIndex.value -= 1
|
||||||
emits('prev', currentStep.value)
|
emits('prev', currentStep.value as T)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
hasNext,
|
hasNext,
|
||||||
@@ -173,10 +174,10 @@ defineExpose({
|
|||||||
</StepperItem>
|
</StepperItem>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="currentStep?.content || !!slots.content || (currentStep?.slot && !!slots[currentStep.slot]) || (currentStep?.value && !!slots[currentStep.value])" :class="ui.content({ class: props.ui?.description })">
|
<div v-if="currentStep?.content || !!slots.content || currentStep?.slot" :class="ui.content({ class: props.ui?.description })">
|
||||||
<slot
|
<slot
|
||||||
:name="!!slots[currentStep?.slot ?? currentStep.value!] ? currentStep.slot ?? currentStep.value : 'content'"
|
:name="((currentStep?.slot || 'content') as keyof StepperSlots<T>)"
|
||||||
:item="currentStep"
|
:item="(currentStep as Extract<T, { slot: string }>)"
|
||||||
>
|
>
|
||||||
{{ currentStep?.content }}
|
{{ currentStep?.content }}
|
||||||
</slot>
|
</slot>
|
||||||
|
|||||||
@@ -25,11 +25,12 @@ export interface TabsItem {
|
|||||||
/** A unique value for the tab item. Defaults to the index. */
|
/** A unique value for the tab item. Defaults to the index. */
|
||||||
value?: string | number
|
value?: string | number
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
|
[key: string]: any
|
||||||
}
|
}
|
||||||
|
|
||||||
type TabsVariants = VariantProps<typeof tabs>
|
type TabsVariants = VariantProps<typeof tabs>
|
||||||
|
|
||||||
export interface TabsProps<T> extends Pick<TabsRootProps<string | number>, 'defaultValue' | 'modelValue' | 'activationMode' | 'unmountOnHide'> {
|
export interface TabsProps<T extends TabsItem = TabsItem> extends Pick<TabsRootProps<string | number>, 'defaultValue' | 'modelValue' | 'activationMode' | 'unmountOnHide'> {
|
||||||
/**
|
/**
|
||||||
* The element or component this component should render as.
|
* The element or component this component should render as.
|
||||||
* @defaultValue 'div'
|
* @defaultValue 'div'
|
||||||
@@ -69,14 +70,14 @@ export interface TabsProps<T> extends Pick<TabsRootProps<string | number>, 'defa
|
|||||||
|
|
||||||
export interface TabsEmits extends TabsRootEmits<string | number> {}
|
export interface TabsEmits extends TabsRootEmits<string | number> {}
|
||||||
|
|
||||||
type SlotProps<T> = (props: { item: T, index: number }) => any
|
type SlotProps<T extends TabsItem> = (props: { item: T, index: number }) => any
|
||||||
|
|
||||||
export type TabsSlots<T extends { slot?: string }> = {
|
export type TabsSlots<T extends TabsItem = TabsItem> = {
|
||||||
leading: SlotProps<T>
|
leading: SlotProps<T>
|
||||||
default: SlotProps<T>
|
default: SlotProps<T>
|
||||||
trailing: SlotProps<T>
|
trailing: SlotProps<T>
|
||||||
content: SlotProps<T>
|
content: SlotProps<T>
|
||||||
} & DynamicSlots<T, SlotProps<T>>
|
} & DynamicSlots<T, undefined, { index: number }>
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -129,7 +130,7 @@ const ui = computed(() => tabs({
|
|||||||
|
|
||||||
<template v-if="!!content">
|
<template v-if="!!content">
|
||||||
<TabsContent v-for="(item, index) of items" :key="index" :value="item.value || String(index)" :class="ui.content({ class: props.ui?.content })">
|
<TabsContent v-for="(item, index) of items" :key="index" :value="item.value || String(index)" :class="ui.content({ class: props.ui?.content })">
|
||||||
<slot :name="item.slot || 'content'" :item="item" :index="index">
|
<slot :name="((item.slot || 'content') as keyof TabsSlots<T>)" :item="(item as Extract<T, { slot: string; }>)" :index="index">
|
||||||
{{ item.content }}
|
{{ item.content }}
|
||||||
</slot>
|
</slot>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export interface ToastSlots {
|
|||||||
title(props?: {}): any
|
title(props?: {}): any
|
||||||
description(props?: {}): any
|
description(props?: {}): any
|
||||||
actions(props?: {}): any
|
actions(props?: {}): any
|
||||||
close(props: { ui: any }): any
|
close(props: { ui: ReturnType<typeof toast> }): any
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,14 @@ import type { AppConfig } from '@nuxt/schema'
|
|||||||
import _appConfig from '#build/app.config'
|
import _appConfig from '#build/app.config'
|
||||||
import theme from '#build/ui/tree'
|
import theme from '#build/ui/tree'
|
||||||
import { tv } from '../utils/tv'
|
import { tv } from '../utils/tv'
|
||||||
import type { PartialString, DynamicSlots, MaybeMultipleModelValue, SelectItemKey } from '../types/utils'
|
import type {
|
||||||
|
DynamicSlots,
|
||||||
|
GetItemKeys,
|
||||||
|
GetModelValue,
|
||||||
|
GetModelValueEmits,
|
||||||
|
NestedItem,
|
||||||
|
PartialString
|
||||||
|
} from '../types/utils'
|
||||||
|
|
||||||
const appConfig = _appConfig as AppConfig & { ui: { tree: Partial<typeof theme> } }
|
const appConfig = _appConfig as AppConfig & { ui: { tree: Partial<typeof theme> } }
|
||||||
|
|
||||||
@@ -31,9 +38,10 @@ export type TreeItem = {
|
|||||||
children?: TreeItem[]
|
children?: TreeItem[]
|
||||||
onToggle?(e: Event): void
|
onToggle?(e: Event): void
|
||||||
onSelect?(e?: Event): void
|
onSelect?(e?: Event): void
|
||||||
|
[key: string]: any
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TreeProps<T extends TreeItem, M extends boolean = false, K extends SelectItemKey<T> | undefined = undefined> extends Pick<TreeRootProps<T>, 'expanded' | 'defaultExpanded' | 'selectionBehavior' | 'propagateSelect' | 'disabled'> {
|
export interface TreeProps<T extends TreeItem[] = TreeItem[], VK extends GetItemKeys<T> = 'value', M extends boolean = false> extends Pick<TreeRootProps<T>, 'expanded' | 'defaultExpanded' | 'selectionBehavior' | 'propagateSelect' | 'disabled'> {
|
||||||
/**
|
/**
|
||||||
* The element or component this component should render as.
|
* The element or component this component should render as.
|
||||||
* @defaultValue 'ul'
|
* @defaultValue 'ul'
|
||||||
@@ -51,12 +59,12 @@ export interface TreeProps<T extends TreeItem, M extends boolean = false, K exte
|
|||||||
* The key used to get the value from the item.
|
* The key used to get the value from the item.
|
||||||
* @defaultValue 'value'
|
* @defaultValue 'value'
|
||||||
*/
|
*/
|
||||||
valueKey?: K
|
valueKey?: VK
|
||||||
/**
|
/**
|
||||||
* The key used to get the label from the item.
|
* The key used to get the label from the item.
|
||||||
* @defaultValue 'label'
|
* @defaultValue 'label'
|
||||||
*/
|
*/
|
||||||
labelKey?: K
|
labelKey?: keyof NestedItem<T>
|
||||||
/**
|
/**
|
||||||
* The icon displayed on the right side of a parent node.
|
* The icon displayed on the right side of a parent node.
|
||||||
* @defaultValue appConfig.ui.icons.chevronDown
|
* @defaultValue appConfig.ui.icons.chevronDown
|
||||||
@@ -75,33 +83,34 @@ export interface TreeProps<T extends TreeItem, M extends boolean = false, K exte
|
|||||||
* @IconifyIcon
|
* @IconifyIcon
|
||||||
*/
|
*/
|
||||||
collapsedIcon?: string
|
collapsedIcon?: string
|
||||||
items?: T[]
|
items?: T
|
||||||
/** The controlled value of the Tree. Can be bind as `v-model`. */
|
/** The controlled value of the Tree. Can be bind as `v-model`. */
|
||||||
modelValue?: MaybeMultipleModelValue<T, M>
|
modelValue?: GetModelValue<T, VK, M>
|
||||||
/** The value of the Tree when initially rendered. Use when you do not need to control the state of the Tree. */
|
/** The value of the Tree when initially rendered. Use when you do not need to control the state of the Tree. */
|
||||||
defaultValue?: MaybeMultipleModelValue<T, M>
|
defaultValue?: GetModelValue<T, VK, M>
|
||||||
/** Whether multiple options can be selected or not. */
|
/** Whether multiple options can be selected or not. */
|
||||||
multiple?: M & boolean
|
multiple?: M & boolean
|
||||||
class?: any
|
class?: any
|
||||||
ui?: PartialString<typeof tree.slots>
|
ui?: PartialString<typeof tree.slots>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TreeEmits<T, M extends boolean = false> = Omit<TreeRootEmits, 'update:modelValue'> & {
|
export type TreeEmits<A extends TreeItem[], VK extends GetItemKeys<A> | undefined, M extends boolean> = Omit<TreeRootEmits, 'update:modelValue'> & GetModelValueEmits<A, VK, M>
|
||||||
'update:modelValue': [payload: MaybeMultipleModelValue<T, M>]
|
|
||||||
}
|
|
||||||
|
|
||||||
type SlotProps<T> = (props: { item: T, index: number, level: number, expanded: boolean, selected: boolean }) => any
|
type SlotProps<T extends TreeItem> = (props: { item: T, index: number, level: number, expanded: boolean, selected: boolean }) => any
|
||||||
|
|
||||||
export type TreeSlots<T extends { slot?: string }> = {
|
export type TreeSlots<
|
||||||
|
A extends TreeItem[] = TreeItem[],
|
||||||
|
T extends NestedItem<A> = NestedItem<A>
|
||||||
|
> = {
|
||||||
'item': SlotProps<T>
|
'item': SlotProps<T>
|
||||||
'item-leading': SlotProps<T>
|
'item-leading': SlotProps<T>
|
||||||
'item-label': SlotProps<T>
|
'item-label': SlotProps<T>
|
||||||
'item-trailing': SlotProps<T>
|
'item-trailing': SlotProps<T>
|
||||||
} & DynamicSlots<T, SlotProps<T>>
|
} & DynamicSlots<T, undefined, { index: number, level: number, expanded: boolean, selected: boolean }>
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script setup lang="ts" generic="T extends TreeItem, M extends boolean = false, K extends SelectItemKey<T> | undefined = undefined">
|
<script setup lang="ts" generic="T extends TreeItem[], VK extends GetItemKeys<T> = 'value', M extends boolean = false">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import type { PropType } from 'vue'
|
import type { PropType } from 'vue'
|
||||||
import { TreeRoot, TreeItem, useForwardPropsEmits } from 'reka-ui'
|
import { TreeRoot, TreeItem, useForwardPropsEmits } from 'reka-ui'
|
||||||
@@ -109,18 +118,21 @@ import { reactivePick, createReusableTemplate } from '@vueuse/core'
|
|||||||
import { get } from '../utils'
|
import { get } from '../utils'
|
||||||
import UIcon from './Icon.vue'
|
import UIcon from './Icon.vue'
|
||||||
|
|
||||||
const props = withDefaults(defineProps<TreeProps<T, M, K>>(), {
|
const props = withDefaults(defineProps<TreeProps<T, VK, M>>(), {
|
||||||
labelKey: 'label' as never,
|
labelKey: 'label' as never,
|
||||||
valueKey: 'value' as never
|
valueKey: 'value' as never
|
||||||
})
|
})
|
||||||
const emits = defineEmits<TreeEmits<T, M>>()
|
const emits = defineEmits<TreeEmits<T, VK, M>>()
|
||||||
const slots = defineSlots<TreeSlots<T>>()
|
const slots = defineSlots<TreeSlots<T>>()
|
||||||
|
|
||||||
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'items', 'multiple', 'expanded', 'disabled', 'propagateSelect'), emits)
|
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'items', 'multiple', 'expanded', 'disabled', 'propagateSelect'), emits)
|
||||||
|
|
||||||
const [DefineTreeTemplate, ReuseTreeTemplate] = createReusableTemplate<{ items?: T[], level: number }>({
|
const [DefineTreeTemplate, ReuseTreeTemplate] = createReusableTemplate<
|
||||||
|
{ items?: NestedItem<T>[], level: number },
|
||||||
|
TreeSlots<T>
|
||||||
|
>({
|
||||||
props: {
|
props: {
|
||||||
items: Array as PropType<T[]>,
|
items: Array as PropType<NestedItem<T>[]>,
|
||||||
level: Number
|
level: Number
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -130,22 +142,24 @@ const ui = computed(() => tree({
|
|||||||
size: props.size
|
size: props.size
|
||||||
}))
|
}))
|
||||||
|
|
||||||
function getItemLabel(item: T) {
|
function getItemLabel(item: NestedItem<T>): string {
|
||||||
return get(item, props.labelKey as string)
|
return get(item, props.labelKey as string)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getItemValue(item?: T) {
|
function getItemValue(item: NestedItem<T>): string {
|
||||||
return get(item, props.valueKey as string) ?? get(item, props.labelKey as string)
|
return get(item, props.valueKey as string) ?? get(item, props.labelKey as string)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDefaultOpenedItems(item: T): string[] {
|
function getDefaultOpenedItems(item: NestedItem<T>): string[] {
|
||||||
const currentItem = item.defaultExpanded ? getItemValue(item) : null
|
const currentItem = item.defaultExpanded ? getItemValue(item) : null
|
||||||
const childItems = item.children?.flatMap(child => getDefaultOpenedItems(child as T)) ?? []
|
const childItems = item.children?.flatMap((child: TreeItem) => getDefaultOpenedItems(child as NestedItem<T>)) ?? []
|
||||||
|
|
||||||
return [currentItem, ...childItems].filter(Boolean) as string[]
|
return [currentItem, ...childItems].filter(Boolean) as string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultExpanded = computed(() => props.defaultExpanded ?? props.items?.flatMap(getDefaultOpenedItems))
|
const defaultExpanded = computed(() =>
|
||||||
|
props.defaultExpanded ?? props.items?.flatMap(item => getDefaultOpenedItems(item as NestedItem<T>))
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- eslint-disable vue/no-template-shadow -->
|
<!-- eslint-disable vue/no-template-shadow -->
|
||||||
@@ -165,8 +179,8 @@ const defaultExpanded = computed(() => props.defaultExpanded ?? props.items?.fla
|
|||||||
@select="item.onSelect"
|
@select="item.onSelect"
|
||||||
>
|
>
|
||||||
<button :disabled="item.disabled || disabled" :class="ui.link({ class: props.ui?.link, selected: isSelected, disabled: item.disabled || disabled })">
|
<button :disabled="item.disabled || disabled" :class="ui.link({ class: props.ui?.link, selected: isSelected, disabled: item.disabled || disabled })">
|
||||||
<slot :name="item.slot || 'item'" v-bind="{ item, index, level, expanded: isExpanded, selected: isSelected }">
|
<slot :name="((item.slot || 'item') as keyof TreeSlots<T>)" v-bind="{ index, level, expanded: isExpanded, selected: isSelected }" :item="(item as Extract<NestedItem<T>, { slot: string; }>)">
|
||||||
<slot :name="item.slot ? `${item.slot}-leading`: 'item-leading'" v-bind="{ item, index, level, expanded: isExpanded, selected: isSelected }">
|
<slot :name="((item.slot ? `${item.slot}-leading`: 'item-leading') as keyof TreeSlots<T>)" v-bind="{ index, level, expanded: isExpanded, selected: isSelected }" :item="(item as Extract<NestedItem<T>, { slot: string; }>)">
|
||||||
<UIcon
|
<UIcon
|
||||||
v-if="item.icon"
|
v-if="item.icon"
|
||||||
:name="item.icon"
|
:name="item.icon"
|
||||||
@@ -179,14 +193,14 @@ const defaultExpanded = computed(() => props.defaultExpanded ?? props.items?.fla
|
|||||||
/>
|
/>
|
||||||
</slot>
|
</slot>
|
||||||
|
|
||||||
<span v-if="getItemLabel(item) || !!slots[item.slot ? `${item.slot}-label`: 'item-label']" :class="ui.linkLabel({ class: props.ui?.linkLabel })">
|
<span v-if="getItemLabel(item) || !!slots[(item.slot ? `${item.slot}-label`: 'item-label') as keyof TreeSlots<T>]" :class="ui.linkLabel({ class: props.ui?.linkLabel })">
|
||||||
<slot :name="item.slot ? `${item.slot}-label`: 'item-label'" v-bind="{ item, index, level, expanded: isExpanded, selected: isSelected }">
|
<slot :name="((item.slot ? `${item.slot}-label`: 'item-label') as keyof TreeSlots<T>)" v-bind="{ item, index, level, expanded: isExpanded, selected: isSelected }" :item="(item as Extract<NestedItem<T>, { slot: string; }>)">
|
||||||
{{ getItemLabel(item) }}
|
{{ getItemLabel(item) }}
|
||||||
</slot>
|
</slot>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span v-if="item.trailingIcon || item.children?.length || !!slots[item.slot ? `${item.slot}-trailing`: 'item-trailing']" :class="ui.linkTrailing({ class: props.ui?.linkTrailing })">
|
<span v-if="item.trailingIcon || item.children?.length || !!slots[(item.slot ? `${item.slot}-trailing`: 'item-trailing') as keyof TreeSlots<T>]" :class="ui.linkTrailing({ class: props.ui?.linkTrailing })">
|
||||||
<slot :name="item.slot ? `${item.slot}-trailing`: 'item-trailing'" v-bind="{ item, index, level, expanded: isExpanded, selected: isSelected }">
|
<slot :name="((item.slot ? `${item.slot}-trailing`: 'item-trailing') as keyof TreeSlots<T>)" v-bind="{ item, index, level, expanded: isExpanded, selected: isSelected }" :item="(item as Extract<NestedItem<T>, { slot: string; }>)">
|
||||||
<UIcon v-if="item.trailingIcon" :name="item.trailingIcon" :class="ui.linkTrailingIcon({ class: props.ui?.linkTrailingIcon })" />
|
<UIcon v-if="item.trailingIcon" :name="item.trailingIcon" :class="ui.linkTrailingIcon({ class: props.ui?.linkTrailingIcon })" />
|
||||||
<UIcon v-else-if="item.children?.length" :name="trailingIcon ?? appConfig.ui.icons.chevronDown" :class="ui.linkTrailingIcon({ class: props.ui?.linkTrailingIcon })" />
|
<UIcon v-else-if="item.children?.length" :name="trailingIcon ?? appConfig.ui.icons.chevronDown" :class="ui.linkTrailingIcon({ class: props.ui?.linkTrailingIcon })" />
|
||||||
</slot>
|
</slot>
|
||||||
@@ -195,19 +209,19 @@ const defaultExpanded = computed(() => props.defaultExpanded ?? props.items?.fla
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<ul v-if="item.children?.length && isExpanded" :class="ui.listWithChildren({ class: props.ui?.listWithChildren })">
|
<ul v-if="item.children?.length && isExpanded" :class="ui.listWithChildren({ class: props.ui?.listWithChildren })">
|
||||||
<ReuseTreeTemplate :items="(item.children as T[])" :level="level + 1" />
|
<ReuseTreeTemplate :items="(item.children as NestedItem<T>[])" :level="level + 1" />
|
||||||
</ul>
|
</ul>
|
||||||
</TreeItem>
|
</TreeItem>
|
||||||
</li>
|
</li>
|
||||||
</DefineTreeTemplate>
|
</DefineTreeTemplate>
|
||||||
|
|
||||||
<TreeRoot
|
<TreeRoot
|
||||||
v-bind="rootProps"
|
v-bind="(rootProps as unknown as TreeRootProps<NestedItem<T>>)"
|
||||||
:class="ui.root({ class: [props.class, props.ui?.root] })"
|
:class="ui.root({ class: [props.class, props.ui?.root] })"
|
||||||
:get-key="getItemValue"
|
:get-key="getItemValue"
|
||||||
:default-expanded="defaultExpanded"
|
:default-expanded="defaultExpanded"
|
||||||
:selection-behavior="selectionBehavior"
|
:selection-behavior="selectionBehavior"
|
||||||
>
|
>
|
||||||
<ReuseTreeTemplate :items="items" :level="0" />
|
<ReuseTreeTemplate :items="(items as NestedItem<T>[] | undefined)" :level="0" />
|
||||||
</TreeRoot>
|
</TreeRoot>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { AcceptableValue as _AcceptableValue } from 'reka-ui'
|
||||||
import type { VNode } from 'vue'
|
import type { VNode } from 'vue'
|
||||||
|
|
||||||
export interface TightMap<O = any> {
|
export interface TightMap<O = any> {
|
||||||
@@ -14,8 +15,19 @@ export type DeepPartial<T, O = any> = {
|
|||||||
[key: string]: O | TightMap<O>
|
[key: string]: O | TightMap<O>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DynamicSlots<T extends { slot?: string }, SlotProps, Slot = T['slot']> =
|
export type DynamicSlots<
|
||||||
Record<string, SlotProps> & (Slot extends string ? Record<Slot, SlotProps> : Record<string, never>)
|
T extends { slot?: string },
|
||||||
|
S extends string | undefined = undefined,
|
||||||
|
D extends object = {}
|
||||||
|
> = {
|
||||||
|
[
|
||||||
|
K in T['slot'] as K extends string
|
||||||
|
? S extends string
|
||||||
|
? (K | `${K}-${S}`)
|
||||||
|
: K
|
||||||
|
: never
|
||||||
|
]?: (props: { item: Extract<T, { slot: K extends `${infer Base}-${S}` ? Base : K }> } & D) => any
|
||||||
|
}
|
||||||
|
|
||||||
export type GetObjectField<MaybeObject, Key extends string> = MaybeObject extends Record<string, any>
|
export type GetObjectField<MaybeObject, Key extends string> = MaybeObject extends Record<string, any>
|
||||||
? MaybeObject[Key]
|
? MaybeObject[Key]
|
||||||
@@ -25,18 +37,49 @@ export type PartialString<T> = {
|
|||||||
[K in keyof T]?: string
|
[K in keyof T]?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MaybeArrayOfArray<T> = T[] | T[][]
|
export type AcceptableValue = Exclude<_AcceptableValue, Record<string, any>>
|
||||||
export type MaybeArrayOfArrayItem<I> = I extends Array<infer T> ? T extends Array<infer U> ? U : T : never
|
export type ArrayOrNested<T> = T[] | T[][]
|
||||||
|
export type NestedItem<T> = T extends Array<infer I> ? NestedItem<I> : T
|
||||||
export type SelectModelValue<T, V, M extends boolean = false, DV = T> = (T extends Record<string, any> ? V extends keyof T ? T[V] : DV : T) extends infer U ? M extends true ? U[] : U : never
|
type AllKeys<T> = T extends any ? keyof T : never
|
||||||
|
type NonCommonKeys<T extends object> = Exclude<AllKeys<T>, keyof T>
|
||||||
export type SelectItemKey<T> = T extends Record<string, any> ? keyof T : string
|
type PickTypeOf<T, K extends string | number | symbol> = K extends AllKeys<T>
|
||||||
|
? T extends { [k in K]?: any }
|
||||||
export type SelectModelValueEmits<T, V, M extends boolean = false, DV = T> = {
|
? T[K]
|
||||||
'update:modelValue': [payload: SelectModelValue<T, V, M, DV>]
|
: undefined
|
||||||
|
: never
|
||||||
|
export type MergeTypes<T extends object> = {
|
||||||
|
[k in keyof T]: PickTypeOf<T, k>;
|
||||||
|
} & {
|
||||||
|
[k in NonCommonKeys<T>]?: PickTypeOf<T, k>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MaybeMultipleModelValue<T, M extends boolean = false> = (T extends infer U ? M extends true ? U[] : U : never)
|
export type GetItemKeys<I> = keyof Extract<NestedItem<I>, object>
|
||||||
|
|
||||||
|
export type GetItemValue<I, VK extends GetItemKeys<I> | undefined, T extends NestedItem<I> = NestedItem<I>> =
|
||||||
|
T extends object
|
||||||
|
? VK extends undefined
|
||||||
|
? T
|
||||||
|
: VK extends keyof T
|
||||||
|
? T[VK]
|
||||||
|
: never
|
||||||
|
: T
|
||||||
|
|
||||||
|
export type GetModelValue<
|
||||||
|
T,
|
||||||
|
VK extends GetItemKeys<T> | undefined,
|
||||||
|
M extends boolean
|
||||||
|
> = M extends true
|
||||||
|
? GetItemValue<T, VK>[]
|
||||||
|
: GetItemValue<T, VK>
|
||||||
|
|
||||||
|
export type GetModelValueEmits<
|
||||||
|
T,
|
||||||
|
VK extends GetItemKeys<T> | undefined,
|
||||||
|
M extends boolean
|
||||||
|
> = {
|
||||||
|
/** Event handler called when the value changes. */
|
||||||
|
'update:modelValue': [payload: GetModelValue<T, VK, M>]
|
||||||
|
}
|
||||||
|
|
||||||
export type StringOrVNode =
|
export type StringOrVNode =
|
||||||
| string
|
| string
|
||||||
|
|||||||
@@ -81,3 +81,7 @@ export function compare<T>(value?: T, currentValue?: T, comparator?: string | ((
|
|||||||
|
|
||||||
return isEqual(value, currentValue)
|
return isEqual(value, currentValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isArrayOfArray<A>(item: A[] | A[][]): item is A[][] {
|
||||||
|
return Array.isArray(item[0])
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ describe('Accordion', () => {
|
|||||||
icon: 'i-lucide-wrench',
|
icon: 'i-lucide-wrench',
|
||||||
trailingIcon: 'i-lucide-sun',
|
trailingIcon: 'i-lucide-sun',
|
||||||
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.',
|
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed neque elit, tristique placerat feugiat ac, facilisis vitae arcu. Proin eget egestas augue. Praesent ut sem nec arcu pellentesque aliquet. Duis dapibus diam vel metus tempus vulputate.',
|
||||||
slot: 'custom'
|
slot: 'custom' as const
|
||||||
}]
|
}]
|
||||||
|
|
||||||
const props = { items }
|
const props = { items }
|
||||||
@@ -57,7 +57,7 @@ describe('Accordion', () => {
|
|||||||
['with body slot', { props: { ...props, modelValue: '1' }, slots: { body: () => 'Body slot' } }],
|
['with body slot', { props: { ...props, modelValue: '1' }, slots: { body: () => 'Body slot' } }],
|
||||||
['with custom slot', { props: { ...props, modelValue: '5' }, slots: { custom: () => 'Custom slot' } }],
|
['with custom slot', { props: { ...props, modelValue: '5' }, slots: { custom: () => 'Custom slot' } }],
|
||||||
['with custom body slot', { props: { ...props, modelValue: '5' }, slots: { 'custom-body': () => 'Custom body slot' } }]
|
['with custom body slot', { props: { ...props, modelValue: '5' }, slots: { 'custom-body': () => 'Custom body slot' } }]
|
||||||
])('renders %s correctly', async (nameOrHtml: string, options: { props?: AccordionProps<typeof items[number]>, slots?: Partial<AccordionSlots<typeof items[number]>> }) => {
|
])('renders %s correctly', async (nameOrHtml: string, options: { props?: AccordionProps, slots?: Partial<AccordionSlots & { custom: () => 'Custom slot' } & { 'custom-body': () => 'Custom body slot' }> }) => {
|
||||||
const html = await ComponentRender(nameOrHtml, options, Accordion)
|
const html = await ComponentRender(nameOrHtml, options, Accordion)
|
||||||
expect(html).toMatchSnapshot()
|
expect(html).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ describe('Breadcrumb', () => {
|
|||||||
['with item-trailing slot', { props, slots: { 'item-trailing': () => 'Item trailing slot' } }],
|
['with item-trailing slot', { props, slots: { 'item-trailing': () => 'Item trailing slot' } }],
|
||||||
['with custom slot', { props, slots: { custom: () => 'Custom slot' } }],
|
['with custom slot', { props, slots: { custom: () => 'Custom slot' } }],
|
||||||
['with separator slot', { props, slots: { separator: () => '/' } }]
|
['with separator slot', { props, slots: { separator: () => '/' } }]
|
||||||
])('renders %s correctly', async (nameOrHtml: string, options: { props?: BreadcrumbProps<typeof items[number]>, slots?: Partial<BreadcrumbSlots<typeof items[number]>> }) => {
|
])('renders %s correctly', async (nameOrHtml: string, options: { props?: BreadcrumbProps, slots?: Partial<BreadcrumbSlots & { custom: () => 'Custom slot' }> }) => {
|
||||||
const html = await ComponentRender(nameOrHtml, options, Breadcrumb)
|
const html = await ComponentRender(nameOrHtml, options, Breadcrumb)
|
||||||
expect(html).toMatchSnapshot()
|
expect(html).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ describe('Carousel', () => {
|
|||||||
['with as', { props: { ...props, as: 'nav' } }],
|
['with as', { props: { ...props, as: 'nav' } }],
|
||||||
['with class', { props: { ...props, class: 'w-full max-w-xs' } }],
|
['with class', { props: { ...props, class: 'w-full max-w-xs' } }],
|
||||||
['with ui', { props: { ...props, ui: { viewport: 'h-[320px]' } } }]
|
['with ui', { props: { ...props, ui: { viewport: 'h-[320px]' } } }]
|
||||||
])('renders %s correctly', async (nameOrHtml: string, options: { props?: CarouselProps<typeof items[number]>, slots?: Partial<CarouselSlots<typeof items[number]>> }) => {
|
])('renders %s correctly', async (nameOrHtml: string, options: { props?: CarouselProps, slots?: Partial<CarouselSlots> }) => {
|
||||||
const html = await ComponentRender(nameOrHtml, options, CarouselWrapper)
|
const html = await ComponentRender(nameOrHtml, options, CarouselWrapper)
|
||||||
expect(html).toMatchSnapshot()
|
expect(html).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ describe('Chip', () => {
|
|||||||
// Slots
|
// Slots
|
||||||
['with default slot', { slots: { default: () => 'Default slot' } }],
|
['with default slot', { slots: { default: () => 'Default slot' } }],
|
||||||
['with content slot', { slots: { content: () => 'Content slot' } }]
|
['with content slot', { slots: { content: () => 'Content slot' } }]
|
||||||
])('renders %s correctly', async (nameOrHtml: string, options: { props?: ChipProps & { show?: boolean }, slots?: Partial<ChipSlots> }) => {
|
])('renders %s correctly', async (nameOrHtml: string, options: { props?: ChipProps, slots?: Partial<ChipSlots> }) => {
|
||||||
const html = await ComponentRender(nameOrHtml, options, Chip)
|
const html = await ComponentRender(nameOrHtml, options, Chip)
|
||||||
expect(html).toMatchSnapshot()
|
expect(html).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { h, defineComponent } from 'vue'
|
import { h, defineComponent } from 'vue'
|
||||||
import { describe, it, expect } from 'vitest'
|
import { describe, it, expect, test } from 'vitest'
|
||||||
import ContextMenu, { type ContextMenuProps, type ContextMenuSlots } from '../../src/runtime/components/ContextMenu.vue'
|
import ContextMenu, { type ContextMenuProps, type ContextMenuSlots } from '../../src/runtime/components/ContextMenu.vue'
|
||||||
import theme from '#build/ui/context-menu'
|
import theme from '#build/ui/context-menu'
|
||||||
import { mountSuspended } from '@nuxt/test-utils/runtime'
|
import { mountSuspended } from '@nuxt/test-utils/runtime'
|
||||||
|
import { expectSlotProps } from '../utils/types'
|
||||||
|
|
||||||
const ContextMenuWrapper = defineComponent({
|
const ContextMenuWrapper = defineComponent({
|
||||||
components: {
|
components: {
|
||||||
@@ -95,11 +96,33 @@ describe('ContextMenu', () => {
|
|||||||
['with item-label slot', { props, slots: { 'item-label': () => 'Item label slot' } }],
|
['with item-label slot', { props, slots: { 'item-label': () => 'Item label slot' } }],
|
||||||
['with item-trailing slot', { props, slots: { 'item-trailing': () => 'Item trailing slot' } }],
|
['with item-trailing slot', { props, slots: { 'item-trailing': () => 'Item trailing slot' } }],
|
||||||
['with custom slot', { props, slots: { custom: () => 'Custom slot' } }]
|
['with custom slot', { props, slots: { custom: () => 'Custom slot' } }]
|
||||||
])('renders %s correctly', async (nameOrHtml: string, options: { props?: ContextMenuProps<typeof items[number][number]>, slots?: Partial<ContextMenuSlots<any>> }) => {
|
])('renders %s correctly', async (nameOrHtml: string, options: { props?: ContextMenuProps, slots?: Partial<ContextMenuSlots> }) => {
|
||||||
const wrapper = await mountSuspended(ContextMenuWrapper, options as any)
|
const wrapper = await mountSuspended(ContextMenuWrapper, options as any)
|
||||||
|
|
||||||
await wrapper.find('span').trigger('click.right')
|
await wrapper.find('span').trigger('click.right')
|
||||||
|
|
||||||
expect(wrapper.html()).toMatchSnapshot()
|
expect(wrapper.html()).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('should have the correct types', () => {
|
||||||
|
// normal
|
||||||
|
expectSlotProps('item', () => ContextMenu({
|
||||||
|
items: [{ label: 'foo', value: 'bar' }]
|
||||||
|
})).toEqualTypeOf<{ item: { label: string, value: string }, index: number, active?: boolean }>()
|
||||||
|
|
||||||
|
// groups
|
||||||
|
expectSlotProps('item', () => ContextMenu({
|
||||||
|
items: [[{ label: 'foo', value: 'bar' }]]
|
||||||
|
})).toEqualTypeOf<{ item: { label: string, value: string }, index: number, active?: boolean }>()
|
||||||
|
|
||||||
|
// custom
|
||||||
|
expectSlotProps('item', () => ContextMenu({
|
||||||
|
items: [{ label: 'foo', value: 'bar', custom: 'nice' }]
|
||||||
|
})).toEqualTypeOf<{ item: { label: string, value: string, custom: string }, index: number, active?: boolean }>()
|
||||||
|
|
||||||
|
// custom + groups
|
||||||
|
expectSlotProps('item', () => ContextMenu({
|
||||||
|
items: [[{ label: 'foo', value: 'bar', custom: 'nice' }]]
|
||||||
|
})).toEqualTypeOf<{ item: { label: string, value: string, custom: string }, index: number, active?: boolean }>()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { describe, it, expect } from 'vitest'
|
import { describe, it, expect, test } from 'vitest'
|
||||||
import DropdownMenu, { type DropdownMenuProps, type DropdownMenuSlots } from '../../src/runtime/components/DropdownMenu.vue'
|
import DropdownMenu, { type DropdownMenuProps, type DropdownMenuSlots } from '../../src/runtime/components/DropdownMenu.vue'
|
||||||
import ComponentRender from '../component-render'
|
import ComponentRender from '../component-render'
|
||||||
import theme from '#build/ui/dropdown-menu'
|
import theme from '#build/ui/dropdown-menu'
|
||||||
|
import { expectSlotProps } from '../utils/types'
|
||||||
|
|
||||||
describe('DropdownMenu', () => {
|
describe('DropdownMenu', () => {
|
||||||
const sizes = Object.keys(theme.variants.size) as any
|
const sizes = Object.keys(theme.variants.size) as any
|
||||||
@@ -105,8 +106,30 @@ describe('DropdownMenu', () => {
|
|||||||
['with item-label slot', { props, slots: { 'item-label': () => 'Item label slot' } }],
|
['with item-label slot', { props, slots: { 'item-label': () => 'Item label slot' } }],
|
||||||
['with item-trailing slot', { props, slots: { 'item-trailing': () => 'Item trailing slot' } }],
|
['with item-trailing slot', { props, slots: { 'item-trailing': () => 'Item trailing slot' } }],
|
||||||
['with custom slot', { props, slots: { custom: () => 'Custom slot' } }]
|
['with custom slot', { props, slots: { custom: () => 'Custom slot' } }]
|
||||||
])('renders %s correctly', async (nameOrHtml: string, options: { props?: DropdownMenuProps<typeof items[number][number]>, slots?: Partial<DropdownMenuSlots<any>> }) => {
|
])('renders %s correctly', async (nameOrHtml: string, options: { props?: DropdownMenuProps, slots?: Partial<DropdownMenuSlots> }) => {
|
||||||
const html = await ComponentRender(nameOrHtml, options, DropdownMenu)
|
const html = await ComponentRender(nameOrHtml, options, DropdownMenu)
|
||||||
expect(html).toMatchSnapshot()
|
expect(html).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('should have the correct types', () => {
|
||||||
|
// normal
|
||||||
|
expectSlotProps('item', () => DropdownMenu({
|
||||||
|
items: [{ label: 'foo', value: 'bar' }]
|
||||||
|
})).toEqualTypeOf<{ item: { label: string, value: string }, index: number, active?: boolean }>()
|
||||||
|
|
||||||
|
// groups
|
||||||
|
expectSlotProps('item', () => DropdownMenu({
|
||||||
|
items: [[{ label: 'foo', value: 'bar' }]]
|
||||||
|
})).toEqualTypeOf<{ item: { label: string, value: string }, index: number, active?: boolean }>()
|
||||||
|
|
||||||
|
// custom
|
||||||
|
expectSlotProps('item', () => DropdownMenu({
|
||||||
|
items: [{ label: 'foo', value: 'bar', custom: 'nice' }]
|
||||||
|
})).toEqualTypeOf<{ item: { label: string, value: string, custom: string }, index: number, active?: boolean }>()
|
||||||
|
|
||||||
|
// custom + groups
|
||||||
|
expectSlotProps('item', () => DropdownMenu({
|
||||||
|
items: [[{ label: 'foo', value: 'bar', custom: 'nice' }]]
|
||||||
|
})).toEqualTypeOf<{ item: { label: string, value: string, custom: string }, index: number, active?: boolean }>()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -77,9 +77,9 @@ describe('InputMenu', () => {
|
|||||||
['with item slot', { props, slots: { item: () => 'Item slot' } }],
|
['with item slot', { props, slots: { item: () => 'Item slot' } }],
|
||||||
['with item-leading slot', { props, slots: { 'item-leading': () => 'Item leading slot' } }],
|
['with item-leading slot', { props, slots: { 'item-leading': () => 'Item leading slot' } }],
|
||||||
['with item-label slot', { props, slots: { 'item-label': () => 'Item label slot' } }],
|
['with item-label slot', { props, slots: { 'item-label': () => 'Item label slot' } }],
|
||||||
['with item-trailing slot', { props, slots: { 'item-trailing': () => 'Item trailing slot' } }]
|
['with item-trailing slot', { props, slots: { 'item-trailing': () => 'Item trailing slot' } }],
|
||||||
// ['with create-item-label slot', { props: { ...props, searchTerm: 'New value', createItem: true }, slots: { 'create-item-label': () => 'Create item slot' } }]
|
['with create-item-label slot', { props: { ...props, searchTerm: 'New value', createItem: true }, slots: { 'create-item-label': () => 'Create item slot' } }]
|
||||||
])('renders %s correctly', async (nameOrHtml: string, options: { props?: InputMenuProps<typeof items[number]>, slots?: Partial<InputMenuSlots<typeof items[number], false>> }) => {
|
])('renders %s correctly', async (nameOrHtml: string, options: { props?: InputMenuProps, slots?: Partial<InputMenuSlots> }) => {
|
||||||
const html = await ComponentRender(nameOrHtml, options, InputMenu)
|
const html = await ComponentRender(nameOrHtml, options, InputMenu)
|
||||||
expect(html).toMatchSnapshot()
|
expect(html).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ describe('NavigationMenu', () => {
|
|||||||
['with item-label slot', { props, slots: { 'item-label': () => 'Item label slot' } }],
|
['with item-label slot', { props, slots: { 'item-label': () => 'Item label slot' } }],
|
||||||
['with item-trailing slot', { props, slots: { 'item-trailing': () => 'Item trailing slot' } }],
|
['with item-trailing slot', { props, slots: { 'item-trailing': () => 'Item trailing slot' } }],
|
||||||
['with custom slot', { props, slots: { custom: () => 'Custom slot' } }]
|
['with custom slot', { props, slots: { custom: () => 'Custom slot' } }]
|
||||||
])('renders %s correctly', async (nameOrHtml: string, options: { props?: NavigationMenuProps<typeof items>, slots?: Partial<NavigationMenuSlots<any>> }) => {
|
])('renders %s correctly', async (nameOrHtml: string, options: { props?: NavigationMenuProps, slots?: Partial<NavigationMenuSlots> }) => {
|
||||||
const html = await ComponentRender(nameOrHtml, options, NavigationMenu)
|
const html = await ComponentRender(nameOrHtml, options, NavigationMenu)
|
||||||
expect(html).toMatchSnapshot()
|
expect(html).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ describe('RadioGroup', () => {
|
|||||||
['with legend slot', { props, slots: { label: () => 'Legend slot' } }],
|
['with legend slot', { props, slots: { label: () => 'Legend slot' } }],
|
||||||
['with label slot', { props, slots: { label: () => 'Label slot' } }],
|
['with label slot', { props, slots: { label: () => 'Label slot' } }],
|
||||||
['with description slot', { props, slots: { label: () => 'Description slot' } }]
|
['with description slot', { props, slots: { label: () => 'Description slot' } }]
|
||||||
])('renders %s correctly', async (nameOrHtml: string, options: { props?: RadioGroupProps<any>, slots?: Partial<RadioGroupSlots<any>> }) => {
|
])('renders %s correctly', async (nameOrHtml: string, options: { props?: RadioGroupProps, slots?: Partial<RadioGroupSlots> }) => {
|
||||||
const html = await ComponentRender(nameOrHtml, options, RadioGroup)
|
const html = await ComponentRender(nameOrHtml, options, RadioGroup)
|
||||||
expect(html).toMatchSnapshot()
|
expect(html).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ describe('Select', () => {
|
|||||||
['with item-leading slot', { props, slots: { 'item-leading': () => 'Item leading slot' } }],
|
['with item-leading slot', { props, slots: { 'item-leading': () => 'Item leading slot' } }],
|
||||||
['with item-label slot', { props, slots: { 'item-label': () => 'Item label slot' } }],
|
['with item-label slot', { props, slots: { 'item-label': () => 'Item label slot' } }],
|
||||||
['with item-trailing slot', { props, slots: { 'item-trailing': () => 'Item trailing slot' } }]
|
['with item-trailing slot', { props, slots: { 'item-trailing': () => 'Item trailing slot' } }]
|
||||||
])('renders %s correctly', async (nameOrHtml: string, options: { props?: SelectProps<typeof items[number]>, slots?: Partial<SelectSlots<typeof items[number], false>> }) => {
|
])('renders %s correctly', async (nameOrHtml: string, options: { props?: SelectProps, slots?: Partial<SelectSlots> }) => {
|
||||||
const html = await ComponentRender(nameOrHtml, options, Select)
|
const html = await ComponentRender(nameOrHtml, options, Select)
|
||||||
expect(html).toMatchSnapshot()
|
expect(html).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
@@ -192,10 +192,10 @@ describe('Select', () => {
|
|||||||
items: [['foo', { value: 1 }], [{ value: 'bar' }, 2]]
|
items: [['foo', { value: 1 }], [{ value: 'bar' }, 2]]
|
||||||
})).toEqualTypeOf<[string | number]>()
|
})).toEqualTypeOf<[string | number]>()
|
||||||
|
|
||||||
// with groups, mixed types and valueKey = undefined
|
// with groups, multiple, mixed types and valueKey
|
||||||
expectEmitPayloadType('update:modelValue', () => Select({
|
expectEmitPayloadType('update:modelValue', () => Select({
|
||||||
items: [['foo', { value: 1 }], [{ value: 'bar' }, 2]],
|
items: [['foo', { value: 1 }], [{ value: 'bar' }, 2]],
|
||||||
valueKey: undefined
|
valueKey: 'value' // TODO: value is already the default valueKey
|
||||||
})).toEqualTypeOf<[string | number]>()
|
})).toEqualTypeOf<[string | number]>()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -81,9 +81,9 @@ describe('SelectMenu', () => {
|
|||||||
['with item slot', { props, slots: { item: () => 'Item slot' } }],
|
['with item slot', { props, slots: { item: () => 'Item slot' } }],
|
||||||
['with item-leading slot', { props, slots: { 'item-leading': () => 'Item leading slot' } }],
|
['with item-leading slot', { props, slots: { 'item-leading': () => 'Item leading slot' } }],
|
||||||
['with item-label slot', { props, slots: { 'item-label': () => 'Item label slot' } }],
|
['with item-label slot', { props, slots: { 'item-label': () => 'Item label slot' } }],
|
||||||
['with item-trailing slot', { props, slots: { 'item-trailing': () => 'Item trailing slot' } }]
|
['with item-trailing slot', { props, slots: { 'item-trailing': () => 'Item trailing slot' } }],
|
||||||
// ['with create-item-label slot', { props: { ...props, searchTerm: 'New value', createItem: true }, slots: { 'create-item-label': () => 'Create item slot' } }]
|
['with create-item-label slot', { props: { ...props, searchTerm: 'New value', createItem: true }, slots: { 'create-item-label': () => 'Create item slot' } }]
|
||||||
])('renders %s correctly', async (nameOrHtml: string, options: { props?: SelectMenuProps<typeof items[number]>, slots?: Partial<SelectMenuSlots<typeof items[number], false>> }) => {
|
])('renders %s correctly', async (nameOrHtml: string, options: { props?: SelectMenuProps, slots?: Partial<SelectMenuSlots> }) => {
|
||||||
const html = await ComponentRender(nameOrHtml, options, SelectMenu)
|
const html = await ComponentRender(nameOrHtml, options, SelectMenu)
|
||||||
expect(html).toMatchSnapshot()
|
expect(html).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ describe('Stepper', () => {
|
|||||||
['with description slot', { props, slots: { description: () => 'Description slot' } }],
|
['with description slot', { props, slots: { description: () => 'Description slot' } }],
|
||||||
['with content slot', { props, slots: { content: () => 'Content slot' } }],
|
['with content slot', { props, slots: { content: () => 'Content slot' } }],
|
||||||
['with custom slot', { props, slots: { custom: () => 'Custom slot' } }]
|
['with custom slot', { props, slots: { custom: () => 'Custom slot' } }]
|
||||||
])('renders %s correctly', async (nameOrHtml: string, options: { props?: StepperProps<any>, slots?: Partial<StepperSlots<any>> }) => {
|
])('renders %s correctly', async (nameOrHtml: string, options: { props?: StepperProps, slots?: Partial<StepperSlots> }) => {
|
||||||
const html = await ComponentRender(nameOrHtml, options, Stepper)
|
const html = await ComponentRender(nameOrHtml, options, Stepper)
|
||||||
expect(html).toMatchSnapshot()
|
expect(html).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user