mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-14 12:14:41 +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 -->
|
||||
<script setup lang="ts">
|
||||
import type { ChipProps } from '@nuxt/ui'
|
||||
import json5 from 'json5'
|
||||
import { upperFirst, camelCase, kebabCase } from 'scule'
|
||||
import { hash } from 'ohash'
|
||||
@@ -53,6 +54,8 @@ const props = defineProps<{
|
||||
hide?: string[]
|
||||
/** List of props to externalize in script setup */
|
||||
external?: string[]
|
||||
/** The types of the externalized props */
|
||||
externalTypes?: string[]
|
||||
/** List of props to use with `v-model` */
|
||||
model?: string[]
|
||||
/** List of props to cast from code and selection */
|
||||
@@ -209,11 +212,21 @@ ${props.slots?.default}
|
||||
code += `
|
||||
<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 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>
|
||||
@@ -346,7 +359,7 @@ const { data: ast } = await useAsyncData(`component-code-${name}-${hash({ props:
|
||||
inset
|
||||
standalone
|
||||
:color="(modelValue as any)"
|
||||
:size="ui.itemLeadingChipSize()"
|
||||
:size="(ui.itemLeadingChipSize() as ChipProps['size'])"
|
||||
class="size-2"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { ChipProps } from '@nuxt/ui'
|
||||
import { camelCase } from 'scule'
|
||||
import { useElementSize } from '@vueuse/core'
|
||||
import { get, set } from '#ui/utils'
|
||||
@@ -185,7 +186,7 @@ const urlSearchParams = computed(() => {
|
||||
inset
|
||||
standalone
|
||||
:color="(modelValue as any)"
|
||||
:size="ui.itemLeadingChipSize()"
|
||||
:size="(ui.itemLeadingChipSize() as ChipProps['size'])"
|
||||
class="size-2"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
const items = [
|
||||
import type { AccordionItem } from '@nuxt/ui'
|
||||
|
||||
const items: AccordionItem[] = [
|
||||
{
|
||||
label: 'Icons',
|
||||
icon: 'i-lucide-smile'
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
const items = [
|
||||
import type { AccordionItem } from '@nuxt/ui'
|
||||
|
||||
const items: AccordionItem[] = [
|
||||
{
|
||||
label: 'Icons',
|
||||
icon: 'i-lucide-smile'
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { AccordionItem } from '@nuxt/ui'
|
||||
|
||||
const items = [
|
||||
{
|
||||
label: 'Icons',
|
||||
@@ -8,7 +10,7 @@ const items = [
|
||||
{
|
||||
label: 'Colors',
|
||||
icon: 'i-lucide-swatch-book',
|
||||
slot: 'colors',
|
||||
slot: 'colors' as const,
|
||||
content: 'Choose a primary and a neutral color from your Tailwind CSS theme.'
|
||||
},
|
||||
{
|
||||
@@ -16,7 +18,7 @@ const items = [
|
||||
icon: 'i-lucide-box',
|
||||
content: 'You can customize components by using the `class` / `ui` props or in your app.config.ts.'
|
||||
}
|
||||
]
|
||||
] satisfies AccordionItem[]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
const items = [
|
||||
import type { AccordionItem } from '@nuxt/ui'
|
||||
|
||||
const items: AccordionItem[] = [
|
||||
{
|
||||
label: 'Icons',
|
||||
icon: 'i-lucide-smile',
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import type { BreadcrumbItem } from '@nuxt/ui'
|
||||
|
||||
const items = [{
|
||||
label: 'Home',
|
||||
to: '/'
|
||||
}, {
|
||||
slot: 'dropdown',
|
||||
slot: 'dropdown' as const,
|
||||
icon: 'i-lucide-ellipsis',
|
||||
children: [{
|
||||
label: 'Documentation'
|
||||
@@ -18,7 +20,7 @@ const items = [{
|
||||
}, {
|
||||
label: 'Breadcrumb',
|
||||
to: '/components/breadcrumb'
|
||||
}]
|
||||
}] satisfies BreadcrumbItem[]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
const items = [{
|
||||
import type { BreadcrumbItem } from '@nuxt/ui'
|
||||
|
||||
const items: BreadcrumbItem[] = [{
|
||||
label: 'Home',
|
||||
to: '/'
|
||||
}, {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
const items = [{
|
||||
import type { DropdownMenuItem } from '@nuxt/ui'
|
||||
|
||||
const items: DropdownMenuItem[] = [{
|
||||
label: 'Team',
|
||||
icon: 'i-lucide-users'
|
||||
}, {
|
||||
|
||||
@@ -11,7 +11,7 @@ const groups = [{
|
||||
label: 'Billing',
|
||||
icon: 'i-lucide-credit-card',
|
||||
kbds: ['meta', 'B'],
|
||||
slot: 'billing'
|
||||
slot: 'billing' as const
|
||||
},
|
||||
{
|
||||
label: 'Notifications',
|
||||
@@ -25,7 +25,7 @@ const groups = [{
|
||||
}, {
|
||||
id: 'users',
|
||||
label: 'Users',
|
||||
slot: 'users',
|
||||
slot: 'users' as const,
|
||||
items: [
|
||||
{
|
||||
label: 'Benjamin Canac',
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import type { ContextMenuItem } from '@nuxt/ui'
|
||||
|
||||
const showSidebar = ref(true)
|
||||
const showToolbar = ref(false)
|
||||
|
||||
const items = computed(() => [{
|
||||
const items = computed<ContextMenuItem[]>(() => [{
|
||||
label: 'View',
|
||||
type: 'label' as const
|
||||
}, {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
const items = [
|
||||
import type { ContextMenuItem } from '@nuxt/ui'
|
||||
|
||||
const items: ContextMenuItem[][] = [
|
||||
[
|
||||
{
|
||||
label: 'View',
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import type { ContextMenuItem } from '@nuxt/ui'
|
||||
|
||||
const loading = ref(true)
|
||||
|
||||
const items = [{
|
||||
const items: ContextMenuItem[] = [{
|
||||
label: 'Refresh the Page',
|
||||
slot: 'refresh'
|
||||
}, {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuItem } from '@nuxt/ui'
|
||||
|
||||
const showBookmarks = ref(true)
|
||||
const showHistory = ref(false)
|
||||
const showDownloads = ref(false)
|
||||
@@ -36,7 +38,7 @@ const items = computed(() => [{
|
||||
onUpdateChecked(checked: boolean) {
|
||||
showDownloads.value = checked
|
||||
}
|
||||
}])
|
||||
}] satisfies DropdownMenuItem[])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
const items = [
|
||||
import type { DropdownMenuItem } from '@nuxt/ui'
|
||||
|
||||
const items: DropdownMenuItem[][] = [
|
||||
[
|
||||
{
|
||||
label: 'View',
|
||||
@@ -17,7 +19,7 @@ const items = [
|
||||
[
|
||||
{
|
||||
label: 'Delete',
|
||||
color: 'error' as const,
|
||||
color: 'error',
|
||||
icon: 'i-lucide-trash'
|
||||
}
|
||||
]
|
||||
@@ -27,9 +29,5 @@ const items = [
|
||||
<template>
|
||||
<UDropdownMenu :items="items" :ui="{ content: 'w-48' }">
|
||||
<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>
|
||||
</template>
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
const items = [{
|
||||
label: 'Profile',
|
||||
icon: 'i-lucide-user',
|
||||
slot: 'profile'
|
||||
}, {
|
||||
label: 'Billing',
|
||||
icon: 'i-lucide-credit-card'
|
||||
}, {
|
||||
label: 'Settings',
|
||||
icon: 'i-lucide-cog'
|
||||
}]
|
||||
import type { DropdownMenuItem } from '@nuxt/ui'
|
||||
|
||||
const items = [
|
||||
{
|
||||
label: 'Profile',
|
||||
icon: 'i-lucide-user',
|
||||
slot: 'profile' as const
|
||||
}, {
|
||||
label: 'Billing',
|
||||
icon: 'i-lucide-credit-card'
|
||||
}, {
|
||||
label: 'Settings',
|
||||
icon: 'i-lucide-cog'
|
||||
}
|
||||
] satisfies DropdownMenuItem[]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuItem } from '@nuxt/ui'
|
||||
|
||||
const open = ref(false)
|
||||
|
||||
defineShortcuts({
|
||||
o: () => open.value = !open.value
|
||||
})
|
||||
|
||||
const items = [{
|
||||
label: 'Profile',
|
||||
icon: 'i-lucide-user'
|
||||
}, {
|
||||
label: 'Billing',
|
||||
icon: 'i-lucide-credit-card'
|
||||
}, {
|
||||
label: 'Settings',
|
||||
icon: 'i-lucide-cog'
|
||||
}]
|
||||
const items: DropdownMenuItem[] = [
|
||||
{
|
||||
label: 'Profile',
|
||||
icon: 'i-lucide-user'
|
||||
}, {
|
||||
label: 'Billing',
|
||||
icon: 'i-lucide-credit-card'
|
||||
}, {
|
||||
label: 'Settings',
|
||||
icon: 'i-lucide-cog'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -16,7 +16,7 @@ function onOpen() {
|
||||
|
||||
<template>
|
||||
<UInputMenu
|
||||
:items="countries || []"
|
||||
:items="countries"
|
||||
:loading="status === 'pending'"
|
||||
label-key="name"
|
||||
:search-input="{ icon: 'i-lucide-search' }"
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { AvatarProps } from '@nuxt/ui'
|
||||
|
||||
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
|
||||
key: 'typicode-users',
|
||||
transform: (data: { id: number, name: string }[]) => {
|
||||
@@ -6,7 +8,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
||||
label: user.name,
|
||||
value: String(user.id),
|
||||
avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` }
|
||||
})) || []
|
||||
}))
|
||||
},
|
||||
lazy: true
|
||||
})
|
||||
@@ -14,7 +16,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
||||
|
||||
<template>
|
||||
<UInputMenu
|
||||
:items="users || []"
|
||||
:items="users"
|
||||
:loading="status === 'pending'"
|
||||
icon="i-lucide-user"
|
||||
placeholder="Select user"
|
||||
@@ -23,7 +25,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
||||
<UAvatar
|
||||
v-if="modelValue"
|
||||
v-bind="modelValue.avatar"
|
||||
:size="ui.leadingAvatarSize()"
|
||||
:size="(ui.leadingAvatarSize() as AvatarProps['size'])"
|
||||
:class="ui.leadingAvatar()"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { AvatarProps } from '@nuxt/ui'
|
||||
|
||||
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
|
||||
key: 'typicode-users-email',
|
||||
transform: (data: { id: number, name: string, email: string }[]) => {
|
||||
@@ -7,7 +9,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
||||
email: user.email,
|
||||
value: String(user.id),
|
||||
avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` }
|
||||
})) || []
|
||||
}))
|
||||
},
|
||||
lazy: true
|
||||
})
|
||||
@@ -15,7 +17,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
||||
|
||||
<template>
|
||||
<UInputMenu
|
||||
:items="users || []"
|
||||
:items="users"
|
||||
:loading="status === 'pending'"
|
||||
:filter-fields="['label', 'email']"
|
||||
icon="i-lucide-user"
|
||||
@@ -26,7 +28,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
||||
<UAvatar
|
||||
v-if="modelValue"
|
||||
v-bind="modelValue.avatar"
|
||||
:size="ui.leadingAvatarSize()"
|
||||
:size="(ui.leadingAvatarSize() as AvatarProps['size'])"
|
||||
:class="ui.leadingAvatar()"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { AvatarProps } from '@nuxt/ui'
|
||||
|
||||
const searchTerm = ref('')
|
||||
const searchTermDebounced = refDebounced(searchTerm, 200)
|
||||
|
||||
@@ -10,7 +12,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
||||
label: user.name,
|
||||
value: String(user.id),
|
||||
avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` }
|
||||
})) || []
|
||||
}))
|
||||
},
|
||||
lazy: true
|
||||
})
|
||||
@@ -19,7 +21,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
||||
<template>
|
||||
<UInputMenu
|
||||
v-model:search-term="searchTerm"
|
||||
:items="users || []"
|
||||
:items="users"
|
||||
:loading="status === 'pending'"
|
||||
ignore-filter
|
||||
icon="i-lucide-user"
|
||||
@@ -29,7 +31,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
||||
<UAvatar
|
||||
v-if="modelValue"
|
||||
v-bind="modelValue.avatar"
|
||||
:size="ui.leadingAvatarSize()"
|
||||
:size="(ui.leadingAvatarSize() as AvatarProps['size'])"
|
||||
:class="ui.leadingAvatar()"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { InputMenuItem } from '@nuxt/ui'
|
||||
|
||||
const items = ref([
|
||||
{
|
||||
label: 'benjamincanac',
|
||||
@@ -23,8 +25,16 @@ const items = ref([
|
||||
src: 'https://github.com/noook.png',
|
||||
alt: 'noook'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'sandros94',
|
||||
value: 'sandros94',
|
||||
avatar: {
|
||||
src: 'https://github.com/sandros94.png',
|
||||
alt: 'sandros94'
|
||||
}
|
||||
}
|
||||
])
|
||||
] satisfies InputMenuItem[])
|
||||
const value = ref(items.value[0])
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,27 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import type { InputMenuItem, ChipProps } from '@nuxt/ui'
|
||||
|
||||
const items = ref([
|
||||
{
|
||||
label: 'bug',
|
||||
value: 'bug',
|
||||
chip: {
|
||||
color: 'error' as const
|
||||
color: 'error'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'feature',
|
||||
value: 'feature',
|
||||
chip: {
|
||||
color: 'success' as const
|
||||
color: 'success'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'enhancement',
|
||||
value: 'enhancement',
|
||||
chip: {
|
||||
color: 'info' as const
|
||||
color: 'info'
|
||||
}
|
||||
}
|
||||
])
|
||||
] satisfies InputMenuItem[])
|
||||
|
||||
const value = ref(items.value[0])
|
||||
</script>
|
||||
|
||||
@@ -33,7 +36,7 @@ const value = ref(items.value[0])
|
||||
v-bind="modelValue.chip"
|
||||
inset
|
||||
standalone
|
||||
:size="ui.itemLeadingChipSize()"
|
||||
:size="(ui.itemLeadingChipSize() as ChipProps['size'])"
|
||||
:class="ui.itemLeadingChip()"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { InputMenuItem } from '@nuxt/ui'
|
||||
|
||||
const items = ref([
|
||||
{
|
||||
label: 'Backlog',
|
||||
@@ -20,7 +22,8 @@ const items = ref([
|
||||
value: 'done',
|
||||
icon: 'i-lucide-circle-check'
|
||||
}
|
||||
])
|
||||
] satisfies InputMenuItem[])
|
||||
|
||||
const value = ref(items.value[0])
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import type { NavigationMenuItem } from '@nuxt/ui'
|
||||
|
||||
const items = [
|
||||
{
|
||||
label: 'Docs',
|
||||
icon: 'i-lucide-book-open',
|
||||
slot: 'docs',
|
||||
slot: 'docs' as const,
|
||||
children: [
|
||||
{
|
||||
label: 'Icons',
|
||||
@@ -22,7 +24,7 @@ const items = [
|
||||
{
|
||||
label: 'Components',
|
||||
icon: 'i-lucide-box',
|
||||
slot: 'components',
|
||||
slot: 'components' as const,
|
||||
children: [
|
||||
{
|
||||
label: 'Link',
|
||||
@@ -54,7 +56,7 @@ const items = [
|
||||
label: 'GitHub',
|
||||
icon: 'i-simple-icons-github'
|
||||
}
|
||||
]
|
||||
] satisfies NavigationMenuItem[]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
const items = [
|
||||
import type { NavigationMenuItem } from '@nuxt/ui'
|
||||
|
||||
const items: NavigationMenuItem[] = [
|
||||
{
|
||||
label: 'Guide',
|
||||
icon: 'i-lucide-book-open'
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
const items = [
|
||||
import type { NavigationMenuItem } from '@nuxt/ui'
|
||||
|
||||
const items: NavigationMenuItem[] = [
|
||||
{
|
||||
label: 'Guide',
|
||||
icon: 'i-lucide-book-open',
|
||||
|
||||
@@ -4,8 +4,7 @@ const { data: countries, status, execute } = await useLazyFetch<{
|
||||
code: string
|
||||
emoji: string
|
||||
}[]>('/api/countries.json', {
|
||||
immediate: false,
|
||||
default: () => []
|
||||
immediate: false
|
||||
})
|
||||
|
||||
function onOpen() {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { AvatarProps } from '@nuxt/ui'
|
||||
|
||||
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
|
||||
key: 'typicode-users',
|
||||
transform: (data: { id: number, name: string }[]) => {
|
||||
@@ -6,7 +8,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
||||
label: user.name,
|
||||
value: String(user.id),
|
||||
avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` }
|
||||
})) || []
|
||||
}))
|
||||
},
|
||||
lazy: true
|
||||
})
|
||||
@@ -14,7 +16,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
||||
|
||||
<template>
|
||||
<USelectMenu
|
||||
:items="users || []"
|
||||
:items="users"
|
||||
:loading="status === 'pending'"
|
||||
icon="i-lucide-user"
|
||||
placeholder="Select user"
|
||||
@@ -24,7 +26,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
||||
<UAvatar
|
||||
v-if="modelValue"
|
||||
v-bind="modelValue.avatar"
|
||||
:size="ui.leadingAvatarSize()"
|
||||
:size="(ui.leadingAvatarSize() as AvatarProps['size'])"
|
||||
:class="ui.leadingAvatar()"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { AvatarProps } from '@nuxt/ui'
|
||||
|
||||
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
|
||||
key: 'typicode-users-email',
|
||||
transform: (data: { id: number, name: string, email: string }[]) => {
|
||||
@@ -7,7 +9,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
||||
email: user.email,
|
||||
value: String(user.id),
|
||||
avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` }
|
||||
})) || []
|
||||
}))
|
||||
},
|
||||
lazy: true
|
||||
})
|
||||
@@ -15,7 +17,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
||||
|
||||
<template>
|
||||
<USelectMenu
|
||||
:items="users || []"
|
||||
:items="users"
|
||||
:loading="status === 'pending'"
|
||||
:filter-fields="['label', 'email']"
|
||||
icon="i-lucide-user"
|
||||
@@ -26,7 +28,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
||||
<UAvatar
|
||||
v-if="modelValue"
|
||||
v-bind="modelValue.avatar"
|
||||
:size="ui.leadingAvatarSize()"
|
||||
:size="(ui.leadingAvatarSize() as AvatarProps['size'])"
|
||||
:class="ui.leadingAvatar()"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { AvatarProps } from '@nuxt/ui'
|
||||
|
||||
const searchTerm = ref('')
|
||||
const searchTermDebounced = refDebounced(searchTerm, 200)
|
||||
|
||||
@@ -10,7 +12,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
||||
label: user.name,
|
||||
value: String(user.id),
|
||||
avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` }
|
||||
})) || []
|
||||
}))
|
||||
},
|
||||
lazy: true
|
||||
})
|
||||
@@ -19,7 +21,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
||||
<template>
|
||||
<USelectMenu
|
||||
v-model:search-term="searchTerm"
|
||||
:items="users || []"
|
||||
:items="users"
|
||||
:loading="status === 'pending'"
|
||||
ignore-filter
|
||||
icon="i-lucide-user"
|
||||
@@ -30,7 +32,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
||||
<UAvatar
|
||||
v-if="modelValue"
|
||||
v-bind="modelValue.avatar"
|
||||
:size="ui.leadingAvatarSize()"
|
||||
:size="(ui.leadingAvatarSize() as AvatarProps['size'])"
|
||||
:class="ui.leadingAvatar()"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectMenuItem } from '@nuxt/ui'
|
||||
|
||||
const items = ref([
|
||||
{
|
||||
label: 'benjamincanac',
|
||||
@@ -23,8 +25,16 @@ const items = ref([
|
||||
src: 'https://github.com/noook.png',
|
||||
alt: 'noook'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'sandros94',
|
||||
value: 'sandros94',
|
||||
avatar: {
|
||||
src: 'https://github.com/sandros94.png',
|
||||
alt: 'sandros94'
|
||||
}
|
||||
}
|
||||
])
|
||||
] satisfies SelectMenuItem[])
|
||||
const value = ref(items.value[0])
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,27 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectMenuItem, ChipProps } from '@nuxt/ui'
|
||||
|
||||
const items = ref([
|
||||
{
|
||||
label: 'bug',
|
||||
value: 'bug',
|
||||
chip: {
|
||||
color: 'error' as const
|
||||
color: 'error'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'feature',
|
||||
value: 'feature',
|
||||
chip: {
|
||||
color: 'success' as const
|
||||
color: 'success'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'enhancement',
|
||||
value: 'enhancement',
|
||||
chip: {
|
||||
color: 'info' as const
|
||||
color: 'info'
|
||||
}
|
||||
}
|
||||
])
|
||||
] satisfies SelectMenuItem[])
|
||||
const value = ref(items.value[0])
|
||||
</script>
|
||||
|
||||
@@ -33,7 +35,7 @@ const value = ref(items.value[0])
|
||||
v-bind="modelValue.chip"
|
||||
inset
|
||||
standalone
|
||||
:size="ui.itemLeadingChipSize()"
|
||||
:size="(ui.itemLeadingChipSize() as ChipProps['size'])"
|
||||
:class="ui.itemLeadingChip()"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectMenuItem } from '@nuxt/ui'
|
||||
|
||||
const items = ref([
|
||||
{
|
||||
label: 'Backlog',
|
||||
@@ -20,7 +22,7 @@ const items = ref([
|
||||
value: 'done',
|
||||
icon: 'i-lucide-circle-check'
|
||||
}
|
||||
])
|
||||
] satisfies SelectMenuItem[])
|
||||
const value = ref(items.value[0])
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { AvatarProps } from '@nuxt/ui'
|
||||
|
||||
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
|
||||
key: 'typicode-users',
|
||||
transform: (data: { id: number, name: string }[]) => {
|
||||
@@ -6,7 +8,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
||||
label: user.name,
|
||||
value: String(user.id),
|
||||
avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` }
|
||||
})) || []
|
||||
}))
|
||||
},
|
||||
lazy: true
|
||||
})
|
||||
@@ -18,17 +20,18 @@ function getUserAvatar(value: string) {
|
||||
|
||||
<template>
|
||||
<USelect
|
||||
:items="users || []"
|
||||
:items="users"
|
||||
:loading="status === 'pending'"
|
||||
icon="i-lucide-user"
|
||||
placeholder="Select user"
|
||||
class="w-48"
|
||||
value-key="value"
|
||||
>
|
||||
<template #leading="{ modelValue, ui }">
|
||||
<UAvatar
|
||||
v-if="modelValue"
|
||||
v-bind="getUserAvatar(modelValue as string)"
|
||||
:size="ui.leadingAvatarSize()"
|
||||
v-bind="getUserAvatar(modelValue)"
|
||||
:size="(ui.leadingAvatarSize() as AvatarProps['size'])"
|
||||
:class="ui.leadingAvatar()"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectItem } from '@nuxt/ui'
|
||||
|
||||
const items = ref([
|
||||
{
|
||||
label: 'benjamincanac',
|
||||
@@ -23,13 +25,21 @@ const items = ref([
|
||||
src: 'https://github.com/noook.png',
|
||||
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 avatar = computed(() => items.value.find(item => item.value === value.value)?.avatar)
|
||||
</script>
|
||||
|
||||
<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>
|
||||
|
||||
@@ -1,27 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectItem, ChipProps } from '@nuxt/ui'
|
||||
|
||||
const items = ref([
|
||||
{
|
||||
label: 'bug',
|
||||
value: 'bug',
|
||||
chip: {
|
||||
color: 'error' as const
|
||||
color: 'error'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'feature',
|
||||
value: 'feature',
|
||||
chip: {
|
||||
color: 'success' as const
|
||||
color: 'success'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'enhancement',
|
||||
value: 'enhancement',
|
||||
chip: {
|
||||
color: 'info' as const
|
||||
color: 'info'
|
||||
}
|
||||
}
|
||||
])
|
||||
] satisfies SelectItem[])
|
||||
|
||||
const value = ref(items.value[0]?.value)
|
||||
|
||||
function getChip(value: string) {
|
||||
@@ -30,14 +33,14 @@ function getChip(value: string) {
|
||||
</script>
|
||||
|
||||
<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 }">
|
||||
<UChip
|
||||
v-if="modelValue"
|
||||
v-bind="getChip(modelValue as string)"
|
||||
v-bind="getChip(modelValue)"
|
||||
inset
|
||||
standalone
|
||||
:size="ui.itemLeadingChipSize()"
|
||||
:size="(ui.itemLeadingChipSize() as ChipProps['size'])"
|
||||
:class="ui.itemLeadingChip()"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectItem } from '@nuxt/ui'
|
||||
|
||||
const items = ref([
|
||||
{
|
||||
label: 'Backlog',
|
||||
@@ -20,12 +22,12 @@ const items = ref([
|
||||
value: 'done',
|
||||
icon: 'i-lucide-circle-check'
|
||||
}
|
||||
])
|
||||
] satisfies SelectItem[])
|
||||
const value = ref(items.value[0]?.value)
|
||||
|
||||
const icon = computed(() => items.value.find(item => item.value === value.value)?.icon)
|
||||
</script>
|
||||
|
||||
<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>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
const items = [
|
||||
import type { StepperItem } from '@nuxt/ui'
|
||||
|
||||
const items: StepperItem[] = [
|
||||
{
|
||||
title: 'Address',
|
||||
description: 'Add your address here',
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
const items = [
|
||||
import type { StepperItem } from '@nuxt/ui'
|
||||
|
||||
const items: StepperItem[] = [
|
||||
{
|
||||
slot: 'address',
|
||||
title: 'Address',
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import type { StepperItem } from '@nuxt/ui'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
const items = [
|
||||
const items: StepperItem[] = [
|
||||
{
|
||||
title: 'Address',
|
||||
description: 'Add your address here',
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
const items = [
|
||||
import type { StepperItem } from '@nuxt/ui'
|
||||
|
||||
const items: StepperItem[] = [
|
||||
{
|
||||
slot: 'address',
|
||||
title: 'Address',
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
const items = [
|
||||
import type { TabsItem } from '@nuxt/ui'
|
||||
|
||||
const items: TabsItem[] = [
|
||||
{
|
||||
label: 'Account',
|
||||
icon: 'i-lucide-user'
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import type { TabsItem } from '@nuxt/ui'
|
||||
|
||||
const items = [
|
||||
{
|
||||
label: 'Account',
|
||||
description: 'Make changes to your account here. Click save when you\'re done.',
|
||||
icon: 'i-lucide-user',
|
||||
slot: 'account'
|
||||
slot: 'account' as const
|
||||
},
|
||||
{
|
||||
label: 'Password',
|
||||
description: 'Change your password here. After saving, you\'ll be logged out.',
|
||||
icon: 'i-lucide-lock',
|
||||
slot: 'password'
|
||||
slot: 'password' as const
|
||||
}
|
||||
]
|
||||
] satisfies TabsItem[]
|
||||
|
||||
const state = reactive({
|
||||
name: 'Benjamin Canac',
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
const items = [
|
||||
import type { TabsItem } from '@nuxt/ui'
|
||||
|
||||
const items: TabsItem[] = [
|
||||
{
|
||||
label: 'Account'
|
||||
},
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import type { TreeItem } from '@nuxt/ui'
|
||||
|
||||
const items: TreeItem[] = [
|
||||
const items = [
|
||||
{
|
||||
label: 'app/',
|
||||
slot: 'app',
|
||||
slot: 'app' as const,
|
||||
defaultExpanded: true,
|
||||
children: [{
|
||||
label: 'composables/',
|
||||
@@ -24,7 +24,7 @@ const items: TreeItem[] = [
|
||||
},
|
||||
{ label: 'app.vue', icon: 'i-vscode-icons-file-type-vue' },
|
||||
{ label: 'nuxt.config.ts', icon: 'i-vscode-icons-file-type-nuxt' }
|
||||
]
|
||||
] satisfies TreeItem[]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -25,7 +25,7 @@ const items: TreeItem[] = [
|
||||
{ label: 'nuxt.config.ts', icon: 'i-vscode-icons-file-type-nuxt' }
|
||||
]
|
||||
|
||||
const value = ref(items[items.length - 1])
|
||||
const value = ref()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -30,6 +30,8 @@ ignore:
|
||||
- items
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- AccordionItem[]
|
||||
hide:
|
||||
- class
|
||||
props:
|
||||
@@ -58,6 +60,8 @@ ignore:
|
||||
- items
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- AccordionItem[]
|
||||
hide:
|
||||
- class
|
||||
props:
|
||||
@@ -87,6 +91,8 @@ ignore:
|
||||
- items
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- AccordionItem[]
|
||||
hide:
|
||||
- class
|
||||
props:
|
||||
@@ -115,6 +121,8 @@ ignore:
|
||||
- items
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- AccordionItem[]
|
||||
hide:
|
||||
- class
|
||||
props:
|
||||
@@ -149,6 +157,8 @@ ignore:
|
||||
- items
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- AccordionItem[]
|
||||
hide:
|
||||
- class
|
||||
props:
|
||||
@@ -182,6 +192,8 @@ ignore:
|
||||
- items
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- AccordionItem[]
|
||||
hide:
|
||||
- class
|
||||
props:
|
||||
|
||||
@@ -27,6 +27,8 @@ ignore:
|
||||
- items
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- BreadcrumbItem[]
|
||||
props:
|
||||
items:
|
||||
- label: 'Home'
|
||||
@@ -54,6 +56,8 @@ ignore:
|
||||
- items
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- BreadcrumbItem[]
|
||||
props:
|
||||
separatorIcon: 'i-lucide-arrow-right'
|
||||
items:
|
||||
|
||||
@@ -44,6 +44,8 @@ ignore:
|
||||
- ui.content
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- ContextMenuItem[][]
|
||||
props:
|
||||
items:
|
||||
- - label: Appearance
|
||||
@@ -124,6 +126,8 @@ ignore:
|
||||
- ui.content
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- ContextMenuItem[]
|
||||
props:
|
||||
size: xl
|
||||
items:
|
||||
@@ -158,6 +162,8 @@ ignore:
|
||||
- ui.content
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- ContextMenuItem[]
|
||||
props:
|
||||
disabled: true
|
||||
items:
|
||||
|
||||
@@ -44,6 +44,8 @@ ignore:
|
||||
- ui.content
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- DropdownMenuItem[][]
|
||||
props:
|
||||
items:
|
||||
- - label: Benjamin
|
||||
@@ -123,6 +125,8 @@ ignore:
|
||||
- ui.content
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- DropdownMenuItem[]
|
||||
items:
|
||||
content.align:
|
||||
- start
|
||||
@@ -169,6 +173,8 @@ ignore:
|
||||
- ui.content
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- DropdownMenuItem[]
|
||||
props:
|
||||
arrow: true
|
||||
items:
|
||||
@@ -202,6 +208,8 @@ ignore:
|
||||
- ui.content
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- DropdownMenuItem[]
|
||||
props:
|
||||
size: xl
|
||||
items:
|
||||
@@ -244,6 +252,8 @@ ignore:
|
||||
- ui.content
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- DropdownMenuItem[]
|
||||
props:
|
||||
disabled: true
|
||||
items:
|
||||
@@ -334,7 +344,9 @@ Inside the `defineShortcuts` composable, there is an `extractShortcuts` utility
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const items = [{
|
||||
import type { DropdownMenuItem } from '@nuxt/ui'
|
||||
|
||||
const items: DropdownMenuItem[] = [{
|
||||
label: 'Invite users',
|
||||
icon: 'i-lucide-user-plus',
|
||||
children: [{
|
||||
|
||||
@@ -39,6 +39,8 @@ ignore:
|
||||
- class
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- NavigationMenuItem[]
|
||||
props:
|
||||
items:
|
||||
- label: Guide
|
||||
@@ -148,6 +150,8 @@ ignore:
|
||||
- class
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- NavigationMenuItem[][]
|
||||
props:
|
||||
orientation: 'vertical'
|
||||
items:
|
||||
@@ -247,6 +251,8 @@ ignore:
|
||||
- class
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- NavigationMenuItem[][]
|
||||
props:
|
||||
highlight: true
|
||||
highlightColor: 'primary'
|
||||
@@ -346,6 +352,8 @@ ignore:
|
||||
- class
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- NavigationMenuItem[][]
|
||||
props:
|
||||
color: neutral
|
||||
items:
|
||||
@@ -379,6 +387,8 @@ ignore:
|
||||
- class
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- NavigationMenuItem[][]
|
||||
props:
|
||||
color: neutral
|
||||
variant: link
|
||||
@@ -423,6 +433,8 @@ ignore:
|
||||
- class
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- NavigationMenuItem[]
|
||||
props:
|
||||
trailingIcon: 'i-lucide-arrow-down'
|
||||
items:
|
||||
@@ -519,6 +531,8 @@ ignore:
|
||||
- class
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- NavigationMenuItem[]
|
||||
props:
|
||||
arrow: true
|
||||
items:
|
||||
@@ -611,6 +625,8 @@ ignore:
|
||||
- class
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- NavigationMenuItem[]
|
||||
props:
|
||||
arrow: true
|
||||
contentOrientation: 'vertical'
|
||||
@@ -682,6 +698,8 @@ ignore:
|
||||
- class
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- NavigationMenuItem[]
|
||||
props:
|
||||
unmountOnHide: false
|
||||
items:
|
||||
|
||||
@@ -28,6 +28,9 @@ ignore:
|
||||
external:
|
||||
- items
|
||||
- modelValue
|
||||
externalTypes:
|
||||
- RadioGroupItem[]
|
||||
- RadioGroupValue
|
||||
props:
|
||||
modelValue: 'System'
|
||||
items:
|
||||
@@ -52,6 +55,9 @@ ignore:
|
||||
external:
|
||||
- items
|
||||
- modelValue
|
||||
externalTypes:
|
||||
- RadioGroupItem[]
|
||||
- RadioGroupValue
|
||||
props:
|
||||
modelValue: 'system'
|
||||
items:
|
||||
@@ -84,6 +90,9 @@ ignore:
|
||||
external:
|
||||
- items
|
||||
- modelValue
|
||||
externalTypes:
|
||||
- RadioGroupItem[]
|
||||
- RadioGroupValue
|
||||
props:
|
||||
modelValue: 'light'
|
||||
valueKey: 'id'
|
||||
@@ -112,6 +121,8 @@ ignore:
|
||||
- items
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- RadioGroupItem[]
|
||||
props:
|
||||
legend: 'Theme'
|
||||
defaultValue: 'System'
|
||||
@@ -134,6 +145,8 @@ ignore:
|
||||
- items
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- RadioGroupItem[]
|
||||
props:
|
||||
orientation: 'horizontal'
|
||||
defaultValue: 'System'
|
||||
@@ -156,6 +169,8 @@ ignore:
|
||||
- items
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- RadioGroupItem[]
|
||||
props:
|
||||
color: neutral
|
||||
defaultValue: 'System'
|
||||
@@ -178,6 +193,8 @@ ignore:
|
||||
- items
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- RadioGroupItem[]
|
||||
props:
|
||||
size: 'xl'
|
||||
defaultValue: 'System'
|
||||
@@ -200,6 +217,8 @@ ignore:
|
||||
- items
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- RadioGroupItem[]
|
||||
props:
|
||||
disabled: true
|
||||
defaultValue: 'System'
|
||||
|
||||
@@ -31,6 +31,8 @@ ignore:
|
||||
- class
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- StepperItem[]
|
||||
props:
|
||||
items:
|
||||
- title: 'Address'
|
||||
@@ -61,6 +63,8 @@ ignore:
|
||||
- class
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- StepperItem[]
|
||||
props:
|
||||
color: neutral
|
||||
items:
|
||||
@@ -88,6 +92,8 @@ ignore:
|
||||
- class
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- StepperItem[]
|
||||
props:
|
||||
size: xl
|
||||
items:
|
||||
@@ -115,6 +121,8 @@ ignore:
|
||||
- class
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- StepperItem[]
|
||||
props:
|
||||
orientation: vertical
|
||||
items:
|
||||
@@ -142,6 +150,8 @@ ignore:
|
||||
- class
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- StepperItem[]
|
||||
props:
|
||||
disabled: true
|
||||
items:
|
||||
|
||||
@@ -31,6 +31,8 @@ ignore:
|
||||
- class
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- TabsItem[]
|
||||
props:
|
||||
items:
|
||||
- label: Account
|
||||
@@ -55,6 +57,8 @@ ignore:
|
||||
- class
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- TabsItem[]
|
||||
props:
|
||||
content: false
|
||||
items:
|
||||
@@ -80,6 +84,8 @@ ignore:
|
||||
- class
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- TabsItem[]
|
||||
props:
|
||||
unmountOnHide: false
|
||||
items:
|
||||
@@ -109,6 +115,8 @@ ignore:
|
||||
- class
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- TabsItem[]
|
||||
props:
|
||||
color: neutral
|
||||
content: false
|
||||
@@ -131,6 +139,8 @@ ignore:
|
||||
- class
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- TabsItem[]
|
||||
props:
|
||||
color: neutral
|
||||
variant: link
|
||||
@@ -154,6 +164,8 @@ ignore:
|
||||
- class
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- TabsItem[]
|
||||
props:
|
||||
size: md
|
||||
variant: pill
|
||||
@@ -177,6 +189,8 @@ ignore:
|
||||
- class
|
||||
external:
|
||||
- items
|
||||
externalTypes:
|
||||
- TabsItem[]
|
||||
props:
|
||||
orientation: vertical
|
||||
variant: pill
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"@nuxt/content": "^3.4.0",
|
||||
"@nuxt/image": "^1.10.0",
|
||||
"@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",
|
||||
"@nuxtjs/plausible": "^1.2.0",
|
||||
"@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.'
|
||||
}, {
|
||||
label: 'Components',
|
||||
slot: 'test' as const,
|
||||
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.'
|
||||
}, {
|
||||
@@ -37,6 +38,11 @@ const items = [{
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<template #custom="{ item }">
|
||||
<p class="text-(--ui-text-muted)">
|
||||
Custom: {{ item.content }}
|
||||
</p>
|
||||
</template>
|
||||
<template #custom-body="{ item }">
|
||||
<p class="text-(--ui-text-muted)">
|
||||
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' }">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { InputMenuItem, AvatarProps } from '@nuxt/ui'
|
||||
|
||||
import { upperFirst } from 'scule'
|
||||
import { refDebounced } from '@vueuse/core'
|
||||
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 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 statuses = [{
|
||||
@@ -28,7 +30,7 @@ const statuses = [{
|
||||
}, {
|
||||
label: 'Canceled',
|
||||
icon: 'i-lucide-circle-x'
|
||||
}]
|
||||
}] satisfies InputMenuItem[]
|
||||
|
||||
const searchTerm = ref('')
|
||||
const searchTermDebounced = refDebounced(searchTerm, 200)
|
||||
@@ -126,7 +128,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
||||
class="w-48"
|
||||
>
|
||||
<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>
|
||||
</UInputMenu>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectMenuItem, AvatarProps } from '@nuxt/ui'
|
||||
|
||||
import { upperFirst } from 'scule'
|
||||
import { refDebounced } from '@vueuse/core'
|
||||
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 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 statuses = [{
|
||||
@@ -33,7 +35,7 @@ const statuses = [{
|
||||
label: 'Canceled',
|
||||
value: 'canceled',
|
||||
icon: 'i-lucide-circle-x'
|
||||
}]
|
||||
}] satisfies SelectMenuItem[]
|
||||
|
||||
const searchTerm = ref('')
|
||||
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', {
|
||||
params: { q: searchTermDebounced },
|
||||
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
|
||||
})
|
||||
@@ -122,7 +124,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
||||
v-for="size in sizes"
|
||||
:key="size"
|
||||
v-model:search-term="searchTerm"
|
||||
:items="users || []"
|
||||
:items="users"
|
||||
:loading="status === 'pending'"
|
||||
ignore-filter
|
||||
icon="i-lucide-user"
|
||||
@@ -132,7 +134,7 @@ const { data: users, status } = await useFetch('https://jsonplaceholder.typicode
|
||||
@update:open="searchTerm = ''"
|
||||
>
|
||||
<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>
|
||||
</USelectMenu>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectItem, AvatarProps } from '@nuxt/ui'
|
||||
import { upperFirst } from 'scule'
|
||||
import theme from '#build/ui/select'
|
||||
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 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 statuses = [{
|
||||
@@ -32,7 +33,7 @@ const statuses = [{
|
||||
label: 'Canceled',
|
||||
value: 'canceled',
|
||||
icon: 'i-lucide-circle-x'
|
||||
}]
|
||||
}] satisfies SelectItem[]
|
||||
|
||||
const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
|
||||
transform: (data: User[]) => {
|
||||
@@ -114,9 +115,10 @@ function getUserAvatar(value: string) {
|
||||
trailing-icon="i-lucide-chevrons-up-down"
|
||||
:size="size"
|
||||
class="w-48"
|
||||
value-key="value"
|
||||
>
|
||||
<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>
|
||||
</USelect>
|
||||
</div>
|
||||
@@ -130,9 +132,10 @@ function getUserAvatar(value: string) {
|
||||
placeholder="Search users..."
|
||||
:size="size"
|
||||
class="w-48"
|
||||
value-key="value"
|
||||
>
|
||||
<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>
|
||||
</USelect>
|
||||
</div>
|
||||
|
||||
@@ -11,22 +11,22 @@ const size = ref('md' as const)
|
||||
|
||||
const items = [
|
||||
{
|
||||
slot: 'address',
|
||||
slot: 'address' as const,
|
||||
title: 'Address',
|
||||
description: 'Add your address here',
|
||||
icon: 'i-lucide-house'
|
||||
}, {
|
||||
slot: 'shipping',
|
||||
slot: 'shipping' as const,
|
||||
title: 'Shipping',
|
||||
description: 'Set your preferred shipping method',
|
||||
icon: 'i-lucide-truck'
|
||||
}, {
|
||||
slot: 'payment',
|
||||
slot: 'payment' as const,
|
||||
title: 'Payment',
|
||||
description: 'Select your payment method',
|
||||
icon: 'i-lucide-credit-card'
|
||||
}, {
|
||||
slot: 'checkout',
|
||||
slot: 'checkout' as const,
|
||||
title: 'Checkout',
|
||||
description: 'Confirm your order'
|
||||
}
|
||||
@@ -50,27 +50,27 @@ const stepper = useTemplateRef('stepper')
|
||||
:orientation="orientation"
|
||||
:size="size"
|
||||
>
|
||||
<template #address>
|
||||
<template #address="{ item }">
|
||||
<Placeholder class="size-full min-h-60 min-w-60">
|
||||
Address
|
||||
{{ item.title }}
|
||||
</Placeholder>
|
||||
</template>
|
||||
|
||||
<template #shipping>
|
||||
<template #shipping="{ item }">
|
||||
<Placeholder class="size-full min-h-60 min-w-60">
|
||||
Shipping
|
||||
{{ item.title }}
|
||||
</Placeholder>
|
||||
</template>
|
||||
|
||||
<template #payment>
|
||||
<template #payment="{ item }">
|
||||
<Placeholder class="size-full min-h-60 min-w-60">
|
||||
Payment
|
||||
{{ item.title }}
|
||||
</Placeholder>
|
||||
</template>
|
||||
|
||||
<template #checkout>
|
||||
<template #checkout="{ item }">
|
||||
<Placeholder class="size-full min-h-60 min-w-60">
|
||||
Checkout
|
||||
{{ item.title }}
|
||||
</Placeholder>
|
||||
</template>
|
||||
</UStepper>
|
||||
|
||||
@@ -41,8 +41,8 @@ const itemsWithMappedId = [
|
||||
{ id: 'id3', title: 'obiwan kenobi' }
|
||||
]
|
||||
|
||||
const modelValue = ref<TreeItem>()
|
||||
const modelValues = ref<TreeItem[]>([])
|
||||
const modelValue = ref<string>()
|
||||
const modelValues = ref<string[]>([])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -64,22 +64,14 @@ const modelValues = ref<TreeItem[]>([])
|
||||
|
||||
<!-- Typescript tests -->
|
||||
<template v-if="false">
|
||||
<!-- @vue-expect-error - multiple props should type modelValue to array. -->
|
||||
<UTree :model-value="modelValue" :items="items" multiple />
|
||||
<!-- @vue-expect-error - multiple props should type defaultValue to array. -->
|
||||
<UTree :default-value="modelValue" :items="items" multiple />
|
||||
<!-- @vue-expect-error - multiple props should type @update:modelValue to array. -->
|
||||
<UTree :items="items" multiple @update:model-value="(payload: TreeItem) => 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" />
|
||||
<UTree :model-value="modelValues" :items="items" multiple />
|
||||
<UTree :default-value="modelValues" :items="items" multiple />
|
||||
<UTree :items="items" multiple @update:model-value="(payload) => payload" />
|
||||
<UTree :model-value="modelValue" :items="items" />
|
||||
<UTree :default-value="modelValue" :items="items" />
|
||||
<UTree :items="items" @update:model-value="(payload) => payload" />
|
||||
|
||||
<!-- @vue-expect-error - value key should type v-model. -->
|
||||
<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" />
|
||||
</template>
|
||||
</div>
|
||||
|
||||
30
pnpm-lock.yaml
generated
30
pnpm-lock.yaml
generated
@@ -240,8 +240,8 @@ importers:
|
||||
specifier: workspace:*
|
||||
version: link:..
|
||||
'@nuxt/ui-pro':
|
||||
specifier: https://pkg.pr.new/@nuxt/ui-pro@d96a086
|
||||
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))
|
||||
specifier: https://pkg.pr.new/@nuxt/ui-pro@e524f08
|
||||
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':
|
||||
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))
|
||||
@@ -1555,9 +1555,9 @@ packages:
|
||||
vitest:
|
||||
optional: true
|
||||
|
||||
'@nuxt/ui-pro@https://pkg.pr.new/@nuxt/ui-pro@d96a086':
|
||||
resolution: {tarball: https://pkg.pr.new/@nuxt/ui-pro@d96a086}
|
||||
version: 3.0.0
|
||||
'@nuxt/ui-pro@https://pkg.pr.new/@nuxt/ui-pro@e524f08':
|
||||
resolution: {tarball: https://pkg.pr.new/@nuxt/ui-pro@e524f08}
|
||||
version: 3.0.1
|
||||
peerDependencies:
|
||||
typescript: ^5.6.3
|
||||
|
||||
@@ -4046,10 +4046,6 @@ packages:
|
||||
resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==}
|
||||
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:
|
||||
resolution: {integrity: sha512-I2ZXrXeOc0KrCvC7swqtIFXFN+rbjnC7b2T943tvemIOVNl+XP8YnA9UVwqFhzzLClnSA60KR/qEjLpXzs73Qg==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -5330,10 +5326,6 @@ packages:
|
||||
parse-entities@4.0.2:
|
||||
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:
|
||||
resolution: {integrity: sha512-OL/zLggRp8mFhKL0rNORUTR4yBYujK/uU+xZL+/0Rgm2QE4nLO9v8PzEweSJEbMGKmDRjJE4R3IMJlL2di4JeQ==}
|
||||
engines: {node: '>= 18'}
|
||||
@@ -8415,7 +8407,7 @@ snapshots:
|
||||
- typescript
|
||||
- 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:
|
||||
'@nuxt/kit': 3.16.1(magicast@0.3.5)
|
||||
'@nuxt/schema': 3.16.1
|
||||
@@ -8427,9 +8419,8 @@ snapshots:
|
||||
git-url-parse: 16.0.1
|
||||
ofetch: 1.4.1
|
||||
ohash: 2.0.11
|
||||
parse-git-config: 3.0.0
|
||||
pathe: 2.0.3
|
||||
pkg-types: 1.3.1
|
||||
pkg-types: 2.1.0
|
||||
scule: 1.3.0
|
||||
tinyglobby: 0.2.12
|
||||
typescript: 5.8.2
|
||||
@@ -11147,8 +11138,6 @@ snapshots:
|
||||
nypm: 0.6.0
|
||||
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):
|
||||
dependencies:
|
||||
'@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-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:
|
||||
dependencies:
|
||||
es-module-lexer: 1.6.0
|
||||
|
||||
@@ -26,9 +26,10 @@ export interface AccordionItem {
|
||||
/** A unique value for the accordion item. Defaults to the index. */
|
||||
value?: string
|
||||
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.
|
||||
* @defaultValue 'div'
|
||||
@@ -52,15 +53,15 @@ export interface AccordionProps<T> extends Pick<AccordionRootProps, 'collapsible
|
||||
|
||||
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>
|
||||
default: SlotProps<T>
|
||||
trailing: SlotProps<T>
|
||||
content: SlotProps<T>
|
||||
body: SlotProps<T>
|
||||
} & DynamicSlots<T, SlotProps<T>>
|
||||
} & DynamicSlots<T, 'body', { index: number, open: boolean }>
|
||||
|
||||
</script>
|
||||
|
||||
@@ -92,7 +93,7 @@ const ui = computed(() => accordion({
|
||||
<template>
|
||||
<AccordionRoot v-bind="rootProps" :class="ui.root({ class: [props.class, props.ui?.root] })">
|
||||
<AccordionItem
|
||||
v-for="(item, index) in items"
|
||||
v-for="(item, index) in props.items"
|
||||
v-slot="{ open }"
|
||||
:key="index"
|
||||
:value="item.value || String(index)"
|
||||
@@ -115,10 +116,10 @@ const ui = computed(() => accordion({
|
||||
</AccordionTrigger>
|
||||
</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 })">
|
||||
<slot :name="item.slot || 'content'" :item="item" :index="index" :open="open">
|
||||
<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') as keyof AccordionSlots<T>)" :item="item" :index="index" :open="open">
|
||||
<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 }}
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
@@ -71,7 +71,7 @@ export interface AlertSlots {
|
||||
title(props?: {}): any
|
||||
description(props?: {}): any
|
||||
actions(props?: {}): any
|
||||
close(props: { ui: any }): any
|
||||
close(props: { ui: ReturnType<typeof alert> }): any
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ export default {
|
||||
}
|
||||
</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 { ConfigProvider, TooltipProvider, useForwardProps } from 'reka-ui'
|
||||
import { reactivePick } from '@vueuse/core'
|
||||
|
||||
@@ -19,9 +19,10 @@ export interface BreadcrumbItem extends Omit<LinkProps, 'raw' | 'custom'> {
|
||||
icon?: string
|
||||
avatar?: AvatarProps
|
||||
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.
|
||||
* @defaultValue 'nav'
|
||||
@@ -43,15 +44,15 @@ export interface BreadcrumbProps<T> {
|
||||
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-leading': SlotProps<T>
|
||||
'item-label': SlotProps<T>
|
||||
'item-trailing': SlotProps<T>
|
||||
'separator'(props?: {}): any
|
||||
} & DynamicSlots<T, SlotProps<T>>
|
||||
'separator': any
|
||||
} & DynamicSlots<T, 'leading' | 'label' | 'trailing', { index: number, active?: boolean }>
|
||||
|
||||
</script>
|
||||
|
||||
@@ -88,19 +89,19 @@ const ui = breadcrumb()
|
||||
<li :class="ui.item({ class: props.ui?.item })">
|
||||
<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 })">
|
||||
<slot :name="item.slot || 'item'" :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') as keyof BreadcrumbSlots<T>)" :item="item" :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 })" />
|
||||
<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>
|
||||
|
||||
<span v-if="get(item, props.labelKey as string) || !!slots[item.slot ? `${item.slot}-label`: 'item-label']" :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">
|
||||
<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') as keyof BreadcrumbSlots<T>)" :item="item" :active="index === items!.length - 1" :index="index">
|
||||
{{ get(item, props.labelKey as string) }}
|
||||
</slot>
|
||||
</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>
|
||||
</ULinkBase>
|
||||
</ULink>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
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 { AppConfig } from '@nuxt/schema'
|
||||
import type { ButtonProps } from '../types'
|
||||
@@ -15,13 +15,21 @@ const calendar = tv({ extend: tv(theme), ...(appConfigCalendar.ui?.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
|
||||
: M extends true
|
||||
? 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.
|
||||
* @defaultValue 'div'
|
||||
@@ -87,7 +95,7 @@ export interface CalendarProps<R extends boolean, M extends boolean> extends Omi
|
||||
monthControls?: boolean
|
||||
/** Show year controls */
|
||||
yearControls?: boolean
|
||||
defaultValue?: CalendarModelValue<R, M>
|
||||
defaultValue?: CalendarDefaultValue<R, M>
|
||||
modelValue?: CalendarModelValue<R, M>
|
||||
class?: any
|
||||
ui?: PartialString<typeof calendar.slots>
|
||||
@@ -104,7 +112,7 @@ export interface CalendarSlots {
|
||||
}
|
||||
</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 { useForwardPropsEmits } from 'reka-ui'
|
||||
import { Calendar as SingleCalendar, RangeCalendar } from 'reka-ui/namespaced'
|
||||
@@ -151,8 +159,8 @@ const Calendar = computed(() => props.range ? RangeCalendar : SingleCalendar)
|
||||
<Calendar.Root
|
||||
v-slot="{ weekDays, grid }"
|
||||
v-bind="rootProps"
|
||||
:model-value="(modelValue as CalendarModelValue<true & false>)"
|
||||
:default-value="(defaultValue as CalendarModelValue<true & false>)"
|
||||
:model-value="modelValue"
|
||||
:default-value="defaultValue"
|
||||
:locale="locale"
|
||||
:dir="dir"
|
||||
:class="ui.root({ class: [props.class, props.ui?.root] })"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<!-- eslint-disable vue/block-tag-newline -->
|
||||
<script lang="ts">
|
||||
import type { VariantProps } from 'tailwind-variants'
|
||||
import type { AppConfig } from '@nuxt/schema'
|
||||
@@ -21,7 +22,9 @@ const carousel = tv({ extend: tv(theme), ...(appConfigCarousel.ui?.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.
|
||||
* @defaultValue 'div'
|
||||
@@ -99,12 +102,13 @@ export interface CarouselProps<T> extends Omit<EmblaOptionsType, 'axis' | 'conta
|
||||
ui?: PartialString<typeof carousel.slots>
|
||||
}
|
||||
|
||||
export type CarouselSlots<T> = {
|
||||
export type CarouselSlots<T extends CarouselItem = CarouselItem> = {
|
||||
default(props: { item: T, index: number }): any
|
||||
}
|
||||
|
||||
</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 useEmblaCarousel from 'embla-carousel-vue'
|
||||
import { Primitive, useForwardProps } from 'reka-ui'
|
||||
|
||||
@@ -9,7 +9,7 @@ import theme from '#build/ui/command-palette'
|
||||
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
|
||||
import { tv } from '../utils/tv'
|
||||
import type { AvatarProps, ButtonProps, ChipProps, KbdProps, InputProps, LinkProps } from '../types'
|
||||
import type { DynamicSlots, PartialString } from '../types/utils'
|
||||
import type { PartialString } from '../types/utils'
|
||||
|
||||
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
|
||||
slot?: string
|
||||
onSelect?(e?: Event): void
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
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 }> = {
|
||||
'empty'(props: { searchTerm?: string }): any
|
||||
'close'(props: { ui: any }): any
|
||||
'close'(props: { ui: ReturnType<typeof commandPalette> }): any
|
||||
'item': SlotProps<T>
|
||||
'item-leading': SlotProps<T>
|
||||
'item-label': SlotProps<T>
|
||||
'item-trailing': SlotProps<T>
|
||||
} & DynamicSlots<G, SlotProps<T>> & DynamicSlots<T, SlotProps<T>>
|
||||
} & Record<string, SlotProps<G>> & Record<string, SlotProps<T>>
|
||||
|
||||
</script>
|
||||
|
||||
@@ -297,8 +298,8 @@ const groups = computed(() => {
|
||||
>
|
||||
<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 })">
|
||||
<slot :name="item.slot || group.slot || 'item'" :item="item" :index="index">
|
||||
<slot :name="item.slot ? `${item.slot}-leading` : group.slot ? `${group.slot}-leading` : `item-leading`" :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`) 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-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 })" />
|
||||
@@ -312,8 +313,8 @@ const groups = computed(() => {
|
||||
/>
|
||||
</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 })">
|
||||
<slot :name="item.slot ? `${item.slot}-label` : group.slot ? `${group.slot}-label` : `item-label`" :item="item" :index="index">
|
||||
<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`) 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 :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 :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 })">
|
||||
<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>
|
||||
|
||||
@@ -7,7 +7,14 @@ import _appConfig from '#build/app.config'
|
||||
import theme from '#build/ui/context-menu'
|
||||
import { tv } from '../utils/tv'
|
||||
import type { AvatarProps, KbdProps, LinkProps } from '../types'
|
||||
import type { 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> } }
|
||||
|
||||
@@ -36,17 +43,18 @@ export interface ContextMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'custo
|
||||
checked?: boolean
|
||||
open?: boolean
|
||||
defaultOpen?: boolean
|
||||
children?: ContextMenuItem[] | ContextMenuItem[][]
|
||||
children?: ArrayOrNested<ContextMenuItem>
|
||||
onSelect?(e: Event): 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'
|
||||
*/
|
||||
size?: ContextMenuVariants['size']
|
||||
items?: T[] | T[][]
|
||||
items?: T
|
||||
/**
|
||||
* The icon displayed when an item is checked.
|
||||
* @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.
|
||||
* @defaultValue 'label'
|
||||
*/
|
||||
labelKey?: string
|
||||
labelKey?: keyof NestedItem<T>
|
||||
disabled?: boolean
|
||||
class?: any
|
||||
ui?: PartialString<typeof contextMenu.slots>
|
||||
@@ -85,19 +93,22 @@ export interface ContextMenuProps<T> extends Omit<ContextMenuRootProps, 'dir'> {
|
||||
|
||||
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
|
||||
'item': SlotProps<T>
|
||||
'item-leading': SlotProps<T>
|
||||
'item-label': SlotProps<T>
|
||||
'item-trailing': SlotProps<T>
|
||||
} & DynamicSlots<T, SlotProps<T>>
|
||||
} & DynamicSlots<MergeTypes<T>, 'leading' | 'label' | 'trailing', { active?: boolean, index: number }>
|
||||
|
||||
</script>
|
||||
|
||||
<script setup lang="ts" generic="T extends ContextMenuItem">
|
||||
<script setup lang="ts" generic="T extends ArrayOrNested<ContextMenuItem>">
|
||||
import { computed, toRef } from 'vue'
|
||||
import { ContextMenuRoot, ContextMenuTrigger, useForwardPropsEmits } from 'reka-ui'
|
||||
import { reactivePick } from '@vueuse/core'
|
||||
@@ -114,8 +125,9 @@ const emits = defineEmits<ContextMenuEmits>()
|
||||
const slots = defineSlots<ContextMenuSlots<T>>()
|
||||
|
||||
const rootProps = useForwardPropsEmits(reactivePick(props, 'modal'), emits)
|
||||
|
||||
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({
|
||||
size: props.size
|
||||
@@ -135,13 +147,13 @@ const ui = computed(() => contextMenu({
|
||||
v-bind="contentProps"
|
||||
:items="items"
|
||||
:portal="portal"
|
||||
:label-key="labelKey"
|
||||
:label-key="(labelKey as keyof NestedItem<T>)"
|
||||
:checked-icon="checkedIcon"
|
||||
:loading-icon="loadingIcon"
|
||||
:external-icon="externalIcon"
|
||||
>
|
||||
<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>
|
||||
</UContextMenuContent>
|
||||
</ContextMenuRoot>
|
||||
|
||||
@@ -2,15 +2,16 @@
|
||||
import type { ContextMenuContentProps as RekaContextMenuContentProps, ContextMenuContentEmits as RekaContextMenuContentEmits } from 'reka-ui'
|
||||
import theme from '#build/ui/context-menu'
|
||||
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)()
|
||||
|
||||
interface ContextMenuContentProps<T> extends Omit<RekaContextMenuContentProps, 'as' | 'asChild' | 'forceMount'> {
|
||||
items?: T[] | T[][]
|
||||
interface ContextMenuContentProps<T extends ArrayOrNested<ContextMenuItem>> extends Omit<RekaContextMenuContentProps, 'as' | 'asChild' | 'forceMount'> {
|
||||
items?: T
|
||||
portal?: boolean
|
||||
sub?: boolean
|
||||
labelKey: string
|
||||
labelKey: keyof NestedItem<T>
|
||||
/**
|
||||
* @IconifyIcon
|
||||
*/
|
||||
@@ -31,13 +32,13 @@ interface ContextMenuContentProps<T> extends Omit<RekaContextMenuContentProps, '
|
||||
interface ContextMenuContentEmits extends RekaContextMenuContentEmits {}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts" generic="T extends ContextMenuItem">
|
||||
<script setup lang="ts" generic="T extends ArrayOrNested<ContextMenuItem>">
|
||||
import { computed } from 'vue'
|
||||
import { ContextMenu } from 'reka-ui/namespaced'
|
||||
import { useForwardPropsEmits } from 'reka-ui'
|
||||
import { reactiveOmit, createReusableTemplate } from '@vueuse/core'
|
||||
import { useAppConfig } from '#imports'
|
||||
import { omit, get } from '../utils'
|
||||
import { omit, get, isArrayOfArray } from '../utils'
|
||||
import { pickLinkProps } from '../utils/link'
|
||||
import ULinkBase from './LinkBase.vue'
|
||||
import ULink from './Link.vue'
|
||||
@@ -53,24 +54,30 @@ const slots = defineSlots<ContextMenuSlots<T>>()
|
||||
|
||||
const appConfig = useAppConfig()
|
||||
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 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>
|
||||
|
||||
<template>
|
||||
<DefineItemTemplate v-slot="{ item, active, index }">
|
||||
<slot :name="item.slot || 'item'" :item="(item as T)" :index="index">
|
||||
<slot :name="item.slot ? `${item.slot}-leading`: 'item-leading'" :item="(item as T)" :active="active" :index="index">
|
||||
<slot :name="((item.slot || 'item') as keyof ContextMenuSlots<T>)" :item="item" :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-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 })" />
|
||||
</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 })">
|
||||
<slot :name="item.slot ? `${item.slot}-label`: 'item-label'" :item="(item as T)" :active="active" :index="index">
|
||||
<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') as keyof ContextMenuSlots<T>)" :item="item" :active="active" :index="index">
|
||||
{{ get(item, props.labelKey as string) }}
|
||||
</slot>
|
||||
|
||||
@@ -78,7 +85,7 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
|
||||
</span>
|
||||
|
||||
<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 })" />
|
||||
<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" />
|
||||
@@ -117,7 +124,7 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
|
||||
:ui="ui"
|
||||
:ui-override="uiOverride"
|
||||
:portal="portal"
|
||||
:items="item.children"
|
||||
:items="(item.children as T)"
|
||||
:align-offset="-4"
|
||||
:label-key="labelKey"
|
||||
:checked-icon="checkedIcon"
|
||||
@@ -126,7 +133,7 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
|
||||
v-bind="item.content"
|
||||
>
|
||||
<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>
|
||||
</UContextMenuContent>
|
||||
</ContextMenu.Sub>
|
||||
|
||||
@@ -7,7 +7,14 @@ import _appConfig from '#build/app.config'
|
||||
import theme from '#build/ui/dropdown-menu'
|
||||
import { tv } from '../utils/tv'
|
||||
import type { AvatarProps, KbdProps, LinkProps } from '../types'
|
||||
import type { 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> } }
|
||||
|
||||
@@ -36,17 +43,18 @@ export interface DropdownMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'cust
|
||||
checked?: boolean
|
||||
open?: boolean
|
||||
defaultOpen?: boolean
|
||||
children?: DropdownMenuItem[] | DropdownMenuItem[][]
|
||||
children?: ArrayOrNested<DropdownMenuItem>
|
||||
onSelect?(e: Event): 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'
|
||||
*/
|
||||
size?: DropdownMenuVariants['size']
|
||||
items?: T[] | T[][]
|
||||
items?: T
|
||||
/**
|
||||
* The icon displayed when an item is checked.
|
||||
* @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.
|
||||
* @defaultValue 'label'
|
||||
*/
|
||||
labelKey?: string
|
||||
labelKey?: keyof NestedItem<T>
|
||||
disabled?: boolean
|
||||
class?: any
|
||||
ui?: PartialString<typeof dropdownMenu.slots>
|
||||
@@ -93,19 +101,22 @@ export interface DropdownMenuProps<T> extends Omit<DropdownMenuRootProps, 'dir'>
|
||||
|
||||
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
|
||||
'item': SlotProps<T>
|
||||
'item-leading': SlotProps<T>
|
||||
'item-label': SlotProps<T>
|
||||
'item-trailing': SlotProps<T>
|
||||
} & DynamicSlots<T, SlotProps<T>>
|
||||
} & DynamicSlots<MergeTypes<T>, 'leading' | 'label' | 'trailing', { active?: boolean, index: number }>
|
||||
|
||||
</script>
|
||||
|
||||
<script setup lang="ts" generic="T extends DropdownMenuItem">
|
||||
<script setup lang="ts" generic="T extends ArrayOrNested<DropdownMenuItem>">
|
||||
import { computed, toRef } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
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 contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffset: 8, collisionPadding: 8 }) as DropdownMenuContentProps)
|
||||
const arrowProps = toRef(() => props.arrow as DropdownMenuArrowProps)
|
||||
const proxySlots = omit(slots, ['default']) as Record<string, DropdownMenuSlots<T>[string]>
|
||||
const proxySlots = omit(slots, ['default'])
|
||||
|
||||
const ui = computed(() => dropdownMenu({
|
||||
size: props.size
|
||||
@@ -145,13 +156,13 @@ const ui = computed(() => dropdownMenu({
|
||||
v-bind="contentProps"
|
||||
:items="items"
|
||||
:portal="portal"
|
||||
:label-key="labelKey"
|
||||
:label-key="(labelKey as keyof NestedItem<T>)"
|
||||
:checked-icon="checkedIcon"
|
||||
:loading-icon="loadingIcon"
|
||||
:external-icon="externalIcon"
|
||||
>
|
||||
<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>
|
||||
|
||||
<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 { tv } from '../utils/tv'
|
||||
import type { KbdProps, AvatarProps, DropdownMenuItem, DropdownMenuSlots } from '../types'
|
||||
import type { ArrayOrNested, NestedItem } from '../types/utils'
|
||||
|
||||
const _dropdownMenu = tv(theme)()
|
||||
|
||||
interface DropdownMenuContentProps<T> extends Omit<RekaDropdownMenuContentProps, 'as' | 'asChild' | 'forceMount'> {
|
||||
items?: T[] | T[][]
|
||||
interface DropdownMenuContentProps<T extends ArrayOrNested<DropdownMenuItem>> extends Omit<RekaDropdownMenuContentProps, 'as' | 'asChild' | 'forceMount'> {
|
||||
items?: T
|
||||
portal?: boolean
|
||||
sub?: boolean
|
||||
labelKey: string
|
||||
labelKey: keyof NestedItem<T>
|
||||
/**
|
||||
* @IconifyIcon
|
||||
*/
|
||||
@@ -31,19 +32,19 @@ interface DropdownMenuContentProps<T> extends Omit<RekaDropdownMenuContentProps,
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<script setup lang="ts" generic="T extends DropdownMenuItem">
|
||||
<script setup lang="ts" generic="T extends ArrayOrNested<DropdownMenuItem>">
|
||||
import { computed } from 'vue'
|
||||
import { DropdownMenu } from 'reka-ui/namespaced'
|
||||
import { useForwardPropsEmits } from 'reka-ui'
|
||||
import { reactiveOmit, createReusableTemplate } from '@vueuse/core'
|
||||
import { useAppConfig } from '#imports'
|
||||
import { omit, get } from '../utils'
|
||||
import { omit, get, isArrayOfArray } from '../utils'
|
||||
import { pickLinkProps } from '../utils/link'
|
||||
import ULinkBase from './LinkBase.vue'
|
||||
import ULink from './Link.vue'
|
||||
@@ -59,24 +60,30 @@ const slots = defineSlots<DropdownMenuContentSlots<T>>()
|
||||
|
||||
const appConfig = useAppConfig()
|
||||
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 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>
|
||||
|
||||
<template>
|
||||
<DefineItemTemplate v-slot="{ item, active, index }">
|
||||
<slot :name="item.slot || 'item'" :item="(item as T)" :index="index">
|
||||
<slot :name="item.slot ? `${item.slot}-leading`: 'item-leading'" :item="(item as T)" :active="active" :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') 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-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 })" />
|
||||
</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 })">
|
||||
<slot :name="item.slot ? `${item.slot}-label`: 'item-label'" :item="(item as T)" :active="active" :index="index">
|
||||
<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') as keyof DropdownMenuContentSlots<T>)" :item="(item as Extract<NestedItem<T>, { slot: string; }>)" :active="active" :index="index">
|
||||
{{ get(item, props.labelKey as string) }}
|
||||
</slot>
|
||||
|
||||
@@ -84,7 +91,7 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
|
||||
</span>
|
||||
|
||||
<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 })" />
|
||||
<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" />
|
||||
@@ -123,7 +130,7 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
|
||||
:ui="ui"
|
||||
:ui-override="uiOverride"
|
||||
:portal="portal"
|
||||
:items="item.children"
|
||||
:items="(item.children as T)"
|
||||
side="right"
|
||||
align="start"
|
||||
:align-offset="-4"
|
||||
@@ -135,7 +142,7 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
|
||||
v-bind="item.content"
|
||||
>
|
||||
<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>
|
||||
</UDropdownMenuContent>
|
||||
</DropdownMenu.Sub>
|
||||
|
||||
@@ -1,20 +1,29 @@
|
||||
<script lang="ts">
|
||||
import type { InputHTMLAttributes } from 'vue'
|
||||
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 _appConfig from '#build/app.config'
|
||||
import theme from '#build/ui/input-menu'
|
||||
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
|
||||
import { tv } from '../utils/tv'
|
||||
import type { AvatarProps, ChipProps, InputProps } from '../types'
|
||||
import type { 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 inputMenu = tv({ extend: tv(theme), ...(appConfigInputMenu.ui?.inputMenu || {}) })
|
||||
|
||||
export interface InputMenuItem {
|
||||
interface _InputMenuItem {
|
||||
label?: string
|
||||
/**
|
||||
* @IconifyIcon
|
||||
@@ -27,13 +36,16 @@ export interface InputMenuItem {
|
||||
* @defaultValue 'item'
|
||||
*/
|
||||
type?: 'label' | 'separator' | 'item'
|
||||
value?: string | number
|
||||
disabled?: boolean
|
||||
onSelect?(e?: Event): void
|
||||
[key: string]: any
|
||||
}
|
||||
export type InputMenuItem = _InputMenuItem | AcceptableValue | boolean
|
||||
|
||||
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.
|
||||
* @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.
|
||||
* @defaultValue undefined
|
||||
*/
|
||||
valueKey?: V
|
||||
valueKey?: VK
|
||||
/**
|
||||
* When `items` is an array of objects, select the field to use as the label.
|
||||
* @defaultValue 'label'
|
||||
*/
|
||||
labelKey?: V
|
||||
items?: I
|
||||
labelKey?: keyof NestedItem<T>
|
||||
items?: T
|
||||
/** 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`. */
|
||||
modelValue?: SelectModelValue<T, V, M>
|
||||
modelValue?: GetModelValue<T, VK, M>
|
||||
/** Whether multiple options can be selected or not. */
|
||||
multiple?: M & boolean
|
||||
/** 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>
|
||||
}
|
||||
|
||||
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]
|
||||
blur: [payload: FocusEvent]
|
||||
focus: [payload: FocusEvent]
|
||||
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> {
|
||||
'leading'(props: { modelValue?: M extends true ? T[] : T, open: boolean, ui: any }): any
|
||||
'trailing'(props: { modelValue?: M extends true ? T[] : T, open: boolean, ui: any }): any
|
||||
export interface InputMenuSlots<
|
||||
A extends ArrayOrNested<InputMenuItem> = ArrayOrNested<InputMenuItem>,
|
||||
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
|
||||
'item': SlotProps<T>
|
||||
'item-leading': SlotProps<T>
|
||||
@@ -153,7 +175,7 @@ export interface InputMenuSlots<T, M extends boolean> {
|
||||
}
|
||||
</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 { 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'
|
||||
@@ -164,21 +186,21 @@ import { useButtonGroup } from '../composables/useButtonGroup'
|
||||
import { useComponentIcons } from '../composables/useComponentIcons'
|
||||
import { useFormField } from '../composables/useFormField'
|
||||
import { useLocale } from '../composables/useLocale'
|
||||
import { get, compare } from '../utils'
|
||||
import { compare, get, isArrayOfArray } from '../utils'
|
||||
import UIcon from './Icon.vue'
|
||||
import UAvatar from './Avatar.vue'
|
||||
import UChip from './Chip.vue'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
const props = withDefaults(defineProps<InputMenuProps<T, I, V, M>>(), {
|
||||
const props = withDefaults(defineProps<InputMenuProps<T, VK, M>>(), {
|
||||
type: 'text',
|
||||
autofocusDelay: 0,
|
||||
portal: true,
|
||||
labelKey: 'label' as never
|
||||
})
|
||||
const emits = defineEmits<InputMenuEmits<T, V, M>>()
|
||||
const slots = defineSlots<InputMenuSlots<T, M>>()
|
||||
const emits = defineEmits<InputMenuEmits<T, VK, M>>()
|
||||
const slots = defineSlots<InputMenuSlots<T, VK, M>>()
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
const items = computed(() => groups.value.flatMap(group => group) as T[])
|
||||
const items = computed(() => groups.value.flatMap(group => group))
|
||||
|
||||
const filteredGroups = computed(() => {
|
||||
if (props.ignoreFilter || !searchTerm.value) {
|
||||
@@ -230,9 +258,9 @@ const filteredGroups = computed(() => {
|
||||
|
||||
const fields = Array.isArray(props.filterFields) ? props.filterFields : [props.labelKey] as string[]
|
||||
|
||||
return groups.value.map(items => items.filter((item) => {
|
||||
if (typeof item !== 'object') {
|
||||
return contains(item, searchTerm.value)
|
||||
return groups.value.map(group => group.filter((item) => {
|
||||
if (typeof item !== 'object' || item === null) {
|
||||
return contains(String(item), searchTerm.value)
|
||||
}
|
||||
|
||||
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))
|
||||
})).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(() => {
|
||||
if (!props.createItem || !searchTerm.value) {
|
||||
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') {
|
||||
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
|
||||
@@ -307,12 +337,16 @@ function onUpdateOpen(value: boolean) {
|
||||
|
||||
function onRemoveTag(event: any) {
|
||||
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))
|
||||
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({
|
||||
inputRef
|
||||
})
|
||||
@@ -362,15 +396,15 @@ defineExpose({
|
||||
@focus="onFocus"
|
||||
@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 })">
|
||||
<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) }}
|
||||
</slot>
|
||||
</TagsInputItemText>
|
||||
|
||||
<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 })" />
|
||||
</slot>
|
||||
</TagsInputItemDelete>
|
||||
@@ -401,14 +435,14 @@ defineExpose({
|
||||
/>
|
||||
|
||||
<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 })" />
|
||||
<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>
|
||||
</span>
|
||||
|
||||
<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 })" />
|
||||
</slot>
|
||||
</ComboboxTrigger>
|
||||
@@ -427,25 +461,25 @@ defineExpose({
|
||||
|
||||
<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}`">
|
||||
<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) }}
|
||||
</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
|
||||
v-else
|
||||
:class="ui.item({ class: props.ui?.item })"
|
||||
:disabled="item.disabled"
|
||||
:value="valueKey && typeof item === 'object' ? get(item, props.valueKey as string) : item"
|
||||
@select="item.onSelect"
|
||||
:disabled="isInputItem(item) && item.disabled"
|
||||
:value="props.valueKey && isInputItem(item) ? get(item, String(props.valueKey)) : item"
|
||||
@select="isInputItem(item) && item.onSelect"
|
||||
>
|
||||
<slot name="item" :item="(item as T)" :index="index">
|
||||
<slot name="item-leading" :item="(item as T)" :index="index">
|
||||
<UIcon v-if="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 })" />
|
||||
<slot name="item" :item="(item as NestedItem<T>)" :index="index">
|
||||
<slot name="item-leading" :item="(item as NestedItem<T>)" :index="index">
|
||||
<UIcon v-if="isInputItem(item) && item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon })" />
|
||||
<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
|
||||
v-else-if="item.chip"
|
||||
v-else-if="isInputItem(item) && item.chip"
|
||||
:size="((props.ui?.itemLeadingChipSize || ui.itemLeadingChipSize()) as ChipProps['size'])"
|
||||
inset
|
||||
standalone
|
||||
@@ -455,13 +489,13 @@ defineExpose({
|
||||
</slot>
|
||||
|
||||
<span :class="ui.itemLabel({ class: props.ui?.itemLabel })">
|
||||
<slot name="item-label" :item="(item as T)" :index="index">
|
||||
{{ typeof item === 'object' ? get(item, props.labelKey as string) : item }}
|
||||
<slot name="item-label" :item="(item as NestedItem<T>)" :index="index">
|
||||
{{ isInputItem(item) ? get(item, props.labelKey as string) : item }}
|
||||
</slot>
|
||||
</span>
|
||||
|
||||
<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>
|
||||
<UIcon :name="selectedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: props.ui?.itemTrailingIcon })" />
|
||||
|
||||
@@ -67,7 +67,7 @@ export interface ModalSlots {
|
||||
header(props?: {}): any
|
||||
title(props?: {}): any
|
||||
description(props?: {}): any
|
||||
close(props: { ui: any }): any
|
||||
close(props: { ui: ReturnType<typeof modal> }): any
|
||||
body(props?: {}): any
|
||||
footer(props?: {}): any
|
||||
}
|
||||
|
||||
@@ -7,15 +7,23 @@ import _appConfig from '#build/app.config'
|
||||
import theme from '#build/ui/navigation-menu'
|
||||
import { tv } from '../utils/tv'
|
||||
import type { AvatarProps, BadgeProps, LinkProps } from '../types'
|
||||
import type { 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 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?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
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
|
||||
children?: NavigationMenuChildItem[]
|
||||
onSelect?(e: Event): void
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
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.
|
||||
* @defaultValue 'div'
|
||||
@@ -110,31 +119,34 @@ export interface NavigationMenuProps<T> extends Pick<NavigationMenuRootProps, 'm
|
||||
* The key used to get the label from the item.
|
||||
* @defaultValue 'label'
|
||||
*/
|
||||
labelKey?: string
|
||||
labelKey?: keyof NestedItem<T>
|
||||
class?: any
|
||||
ui?: PartialString<typeof navigationMenu.slots>
|
||||
}
|
||||
|
||||
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-leading': SlotProps<T>
|
||||
'item-label': SlotProps<T>
|
||||
'item-trailing': SlotProps<T>
|
||||
'item-content': SlotProps<T>
|
||||
} & DynamicSlots<T, SlotProps<T>>
|
||||
} & DynamicSlots<MergeTypes<T>, 'leading' | 'label' | 'trailing' | 'content', { index: number, active?: boolean }>
|
||||
|
||||
</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 { NavigationMenuRoot, NavigationMenuList, NavigationMenuItem, NavigationMenuTrigger, NavigationMenuContent, NavigationMenuLink, NavigationMenuIndicator, NavigationMenuViewport, useForwardPropsEmits } from 'reka-ui'
|
||||
import { createReusableTemplate } from '@vueuse/core'
|
||||
import { useAppConfig } from '#imports'
|
||||
import { get } from '../utils'
|
||||
import { get, isArrayOfArray } from '../utils'
|
||||
import { pickLinkProps } from '../utils/link'
|
||||
import ULinkBase from './LinkBase.vue'
|
||||
import ULink from './Link.vue'
|
||||
@@ -143,7 +155,7 @@ import UIcon from './Icon.vue'
|
||||
import UBadge from './Badge.vue'
|
||||
import UCollapsible from './Collapsible.vue'
|
||||
|
||||
const props = withDefaults(defineProps<NavigationMenuProps<I>>(), {
|
||||
const props = withDefaults(defineProps<NavigationMenuProps<T>>(), {
|
||||
orientation: 'horizontal',
|
||||
contentOrientation: 'horizontal',
|
||||
externalIcon: true,
|
||||
@@ -170,8 +182,14 @@ const rootProps = useForwardPropsEmits(computed(() => ({
|
||||
const contentProps = toRef(() => props.content)
|
||||
|
||||
const appConfig = useAppConfig()
|
||||
const [DefineLinkTemplate, ReuseLinkTemplate] = createReusableTemplate<{ item: NavigationMenuItem, index: number, active?: boolean }>()
|
||||
const [DefineItemTemplate, ReuseItemTemplate] = createReusableTemplate<{ item: NavigationMenuItem, index: number, level?: number }>({
|
||||
const [DefineLinkTemplate, ReuseLinkTemplate] = createReusableTemplate<
|
||||
{ item: NavigationMenuItem, index: number, active?: boolean },
|
||||
NavigationMenuSlots<T>
|
||||
>()
|
||||
const [DefineItemTemplate, ReuseItemTemplate] = createReusableTemplate<
|
||||
{ item: NavigationMenuItem, index: number, level?: number },
|
||||
NavigationMenuSlots<T>
|
||||
>({
|
||||
props: {
|
||||
item: Object,
|
||||
index: Number,
|
||||
@@ -189,30 +207,36 @@ const ui = computed(() => navigationMenu({
|
||||
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>
|
||||
|
||||
<template>
|
||||
<DefineLinkTemplate v-slot="{ item, active, index }">
|
||||
<slot :name="item.slot || 'item'" :item="(item as T)" :index="index">
|
||||
<slot :name="item.slot ? `${item.slot}-leading` : 'item-leading'" :item="(item as T)" :active="active" :index="index">
|
||||
<slot :name="((item.slot || 'item') as keyof NavigationMenuSlots<T>)" :item="item" :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 })" />
|
||||
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.linkLeadingIcon({ class: props.ui?.linkLeadingIcon, active, disabled: !!item.disabled })" />
|
||||
</slot>
|
||||
|
||||
<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 })"
|
||||
>
|
||||
<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) }}
|
||||
</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 })" />
|
||||
</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 })">
|
||||
<slot :name="item.slot ? `${item.slot}-trailing` : 'item-trailing'" :item="(item as T)" :active="active" :index="index">
|
||||
<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') as keyof NavigationMenuSlots<T>)" :item="item" :active="active" :index="index">
|
||||
<UBadge
|
||||
v-if="item.badge"
|
||||
color="neutral"
|
||||
@@ -222,7 +246,7 @@ const lists = computed(() => props.items?.length ? (Array.isArray(props.items[0]
|
||||
: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 })" />
|
||||
</slot>
|
||||
</span>
|
||||
@@ -239,23 +263,23 @@ const lists = computed(() => props.items?.length ? (Array.isArray(props.items[0]
|
||||
:open="item.open"
|
||||
>
|
||||
<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>
|
||||
<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
|
||||
: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
|
||||
:active="active || item.active"
|
||||
:disabled="item.disabled"
|
||||
@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 })">
|
||||
<ReuseLinkTemplate :item="(item as T)" :active="active || item.active" :index="index" />
|
||||
<ReuseLinkTemplate :item="item" :active="active || item.active" :index="index" />
|
||||
</ULinkBase>
|
||||
</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 })">
|
||||
<slot :name="item.slot ? `${item.slot}-content` : 'item-content'" :item="(item as T)" :active="active" :index="index">
|
||||
<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') as keyof NavigationMenuSlots<T>)" :item="item" :active="active" :index="index">
|
||||
<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 })">
|
||||
<ULink v-slot="{ active: childActive, ...childSlotProps }" v-bind="pickLinkProps(childItem)" custom>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script lang="ts">
|
||||
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 _appConfig from '#build/app.config'
|
||||
import theme from '#build/ui/radio-group'
|
||||
import { tv } from '../utils/tv'
|
||||
import type { AcceptableValue } from '../types/utils'
|
||||
|
||||
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>
|
||||
|
||||
export interface RadioGroupItem {
|
||||
export type RadioGroupValue = AcceptableValue
|
||||
export type RadioGroupItem = {
|
||||
label?: string
|
||||
description?: string
|
||||
disabled?: boolean
|
||||
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.
|
||||
* @defaultValue 'div'
|
||||
@@ -63,16 +66,16 @@ export type RadioGroupEmits = RadioGroupRootEmits & {
|
||||
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
|
||||
label: SlotProps<T>
|
||||
description: SlotProps<T>
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts" generic="T extends RadioGroupItem | AcceptableValue">
|
||||
<script setup lang="ts" generic="T extends RadioGroupItem">
|
||||
import { computed, useId } from 'vue'
|
||||
import { RadioGroupRoot, RadioGroupItem, RadioGroupIndicator, Label, useForwardPropsEmits } from 'reka-ui'
|
||||
import { reactivePick } from '@vueuse/core'
|
||||
@@ -102,11 +105,19 @@ const ui = computed(() => radioGroup({
|
||||
}))
|
||||
|
||||
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 {
|
||||
id: `${id}:${item}`,
|
||||
value: item,
|
||||
label: item
|
||||
value: String(item),
|
||||
label: String(item)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,10 +181,10 @@ function onUpdate(value: any) {
|
||||
|
||||
<div :class="ui.wrapper({ class: props.ui?.wrapper })">
|
||||
<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>
|
||||
<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 }}
|
||||
</slot>
|
||||
</p>
|
||||
|
||||
@@ -1,19 +1,29 @@
|
||||
<script lang="ts">
|
||||
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 _appConfig from '#build/app.config'
|
||||
import theme from '#build/ui/select'
|
||||
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
|
||||
import { tv } from '../utils/tv'
|
||||
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 select = tv({ extend: tv(theme), ...(appConfigSelect.ui?.select || {}) })
|
||||
|
||||
export interface SelectItem {
|
||||
interface SelectItemBase {
|
||||
label?: string
|
||||
/**
|
||||
* @IconifyIcon
|
||||
@@ -26,13 +36,15 @@ export interface SelectItem {
|
||||
* @defaultValue 'item'
|
||||
*/
|
||||
type?: 'label' | 'separator' | 'item'
|
||||
value?: string
|
||||
value?: string | number
|
||||
disabled?: boolean
|
||||
[key: string]: any
|
||||
}
|
||||
export type SelectItem = SelectItemBase | AcceptableValue | boolean
|
||||
|
||||
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
|
||||
/** The placeholder text when the select is empty. */
|
||||
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.
|
||||
* @defaultValue 'value'
|
||||
*/
|
||||
valueKey?: V
|
||||
valueKey?: VK
|
||||
/**
|
||||
* When `items` is an array of objects, select the field to use as the label.
|
||||
* @defaultValue 'label'
|
||||
*/
|
||||
labelKey?: V
|
||||
items?: I
|
||||
labelKey?: keyof NestedItem<T>
|
||||
items?: T
|
||||
/** 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`. */
|
||||
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. */
|
||||
multiple?: M & boolean
|
||||
/** 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>
|
||||
}
|
||||
|
||||
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]
|
||||
blur: [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> {
|
||||
'leading'(props: { modelValue?: M extends true ? AcceptableValue[] : AcceptableValue, open: boolean, ui: any }): any
|
||||
'default'(props: { modelValue?: M extends true ? AcceptableValue[] : AcceptableValue, open: boolean }): any
|
||||
'trailing'(props: { modelValue?: M extends true ? AcceptableValue[] : AcceptableValue, open: boolean, ui: any }): any
|
||||
export interface SelectSlots<
|
||||
A extends ArrayOrNested<SelectItem> = ArrayOrNested<SelectItem>,
|
||||
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 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-leading': SlotProps<T>
|
||||
'item-label': SlotProps<T>
|
||||
@@ -117,7 +134,7 @@ export interface SelectSlots<T, M extends boolean> {
|
||||
}
|
||||
</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 { SelectRoot, SelectArrow, SelectTrigger, SelectPortal, SelectContent, SelectViewport, SelectLabel, SelectGroup, SelectItem, SelectItemIndicator, SelectItemText, SelectSeparator, useForwardPropsEmits } from 'reka-ui'
|
||||
import { defu } from 'defu'
|
||||
@@ -126,20 +143,20 @@ import { useAppConfig } from '#imports'
|
||||
import { useButtonGroup } from '../composables/useButtonGroup'
|
||||
import { useComponentIcons } from '../composables/useComponentIcons'
|
||||
import { useFormField } from '../composables/useFormField'
|
||||
import { get, compare } from '../utils'
|
||||
import { compare, get, isArrayOfArray } from '../utils'
|
||||
import UIcon from './Icon.vue'
|
||||
import UAvatar from './Avatar.vue'
|
||||
import UChip from './Chip.vue'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
const props = withDefaults(defineProps<SelectProps<T, I, V, M>>(), {
|
||||
const props = withDefaults(defineProps<SelectProps<T, VK, M>>(), {
|
||||
valueKey: 'value' as never,
|
||||
labelKey: 'label' as never,
|
||||
portal: true
|
||||
})
|
||||
const emits = defineEmits<SelectEmits<T, V, M>>()
|
||||
const slots = defineSlots<SelectSlots<T, M>>()
|
||||
const emits = defineEmits<SelectEmits<T, VK, M>>()
|
||||
const slots = defineSlots<SelectSlots<T, VK, M>>()
|
||||
|
||||
const appConfig = useAppConfig()
|
||||
const rootProps = useForwardPropsEmits(reactivePick(props, 'open', 'defaultOpen', 'disabled', 'autocomplete', 'required', 'multiple'), emits)
|
||||
@@ -163,11 +180,17 @@ const ui = computed(() => select({
|
||||
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
|
||||
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)) {
|
||||
return value.map(v => displayValue(v)).filter(Boolean).join(', ')
|
||||
}
|
||||
@@ -195,6 +218,10 @@ function onUpdateOpen(value: boolean) {
|
||||
emitFormFocus()
|
||||
}
|
||||
}
|
||||
|
||||
function isSelectItem(item: SelectItem): item is SelectItemBase {
|
||||
return typeof item === 'object' && item !== null
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable vue/no-template-shadow -->
|
||||
@@ -205,21 +232,21 @@ function onUpdateOpen(value: boolean) {
|
||||
v-bind="rootProps"
|
||||
:autocomplete="autocomplete"
|
||||
:disabled="disabled"
|
||||
:default-value="(defaultValue as (AcceptableValue | AcceptableValue[] | undefined))"
|
||||
:model-value="(modelValue as (AcceptableValue | AcceptableValue[] | undefined))"
|
||||
:default-value="(defaultValue as (AcceptableValue | AcceptableValue[]))"
|
||||
:model-value="(modelValue as (AcceptableValue | AcceptableValue[]))"
|
||||
@update:model-value="onUpdate"
|
||||
@update:open="onUpdateOpen"
|
||||
>
|
||||
<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 })">
|
||||
<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 })" />
|
||||
<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>
|
||||
</span>
|
||||
|
||||
<slot :model-value="(modelValue as M extends true ? AcceptableValue[] : AcceptableValue)" :open="open">
|
||||
<template v-for="displayedModelValue in [displayValue(modelValue)]" :key="displayedModelValue">
|
||||
<slot :model-value="(modelValue as GetModelValue<T, VK, M>)" :open="open">
|
||||
<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 })">
|
||||
{{ displayedModelValue }}
|
||||
</span>
|
||||
@@ -230,7 +257,7 @@ function onUpdateOpen(value: boolean) {
|
||||
</slot>
|
||||
|
||||
<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 })" />
|
||||
</slot>
|
||||
</span>
|
||||
@@ -241,24 +268,24 @@ function onUpdateOpen(value: boolean) {
|
||||
<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 })">
|
||||
<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) }}
|
||||
</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
|
||||
v-else
|
||||
:class="ui.item({ class: props.ui?.item })"
|
||||
:disabled="item.disabled"
|
||||
:value="typeof item === 'object' ? get(item, props.valueKey as string) : item"
|
||||
:disabled="isSelectItem(item) && item.disabled"
|
||||
:value="isSelectItem(item) ? get(item, props.valueKey as string) : item"
|
||||
>
|
||||
<slot name="item" :item="(item as T)" :index="index">
|
||||
<slot name="item-leading" :item="(item as T)" :index="index">
|
||||
<UIcon v-if="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 })" />
|
||||
<slot name="item" :item="(item as NestedItem<T>)" :index="index">
|
||||
<slot name="item-leading" :item="(item as NestedItem<T>)" :index="index">
|
||||
<UIcon v-if="isSelectItem(item) && item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon })" />
|
||||
<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
|
||||
v-else-if="item.chip"
|
||||
v-else-if="isSelectItem(item) && item.chip"
|
||||
:size="((props.ui?.itemLeadingChipSize || ui.itemLeadingChipSize()) as ChipProps['size'])"
|
||||
inset
|
||||
standalone
|
||||
@@ -268,13 +295,13 @@ function onUpdateOpen(value: boolean) {
|
||||
</slot>
|
||||
|
||||
<SelectItemText :class="ui.itemLabel({ class: props.ui?.itemLabel })">
|
||||
<slot name="item-label" :item="(item as T)" :index="index">
|
||||
{{ typeof item === 'object' ? get(item, props.labelKey as string) : item }}
|
||||
<slot name="item-label" :item="(item as NestedItem<T>)" :index="index">
|
||||
{{ isSelectItem(item) ? get(item, props.labelKey as string) : item }}
|
||||
</slot>
|
||||
</SelectItemText>
|
||||
|
||||
<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>
|
||||
<UIcon :name="selectedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: props.ui?.itemTrailingIcon })" />
|
||||
|
||||
@@ -1,19 +1,29 @@
|
||||
<script lang="ts">
|
||||
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 _appConfig from '#build/app.config'
|
||||
import theme from '#build/ui/select-menu'
|
||||
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
|
||||
import { tv } from '../utils/tv'
|
||||
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 selectMenu = tv({ extend: tv(theme), ...(appConfigSelectMenu.ui?.selectMenu || {}) })
|
||||
|
||||
export interface SelectMenuItem {
|
||||
interface _SelectMenuItem {
|
||||
label?: string
|
||||
/**
|
||||
* @IconifyIcon
|
||||
@@ -26,13 +36,16 @@ export interface SelectMenuItem {
|
||||
* @defaultValue 'item'
|
||||
*/
|
||||
type?: 'label' | 'separator' | 'item'
|
||||
value?: string | number
|
||||
disabled?: boolean
|
||||
onSelect?(e?: Event): void
|
||||
[key: string]: any
|
||||
}
|
||||
export type SelectMenuItem = _SelectMenuItem | AcceptableValue | boolean
|
||||
|
||||
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
|
||||
/** The placeholder text when the select is empty. */
|
||||
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.
|
||||
* @defaultValue undefined
|
||||
*/
|
||||
valueKey?: V
|
||||
valueKey?: VK
|
||||
/**
|
||||
* When `items` is an array of objects, select the field to use as the label.
|
||||
* @defaultValue 'label'
|
||||
*/
|
||||
labelKey?: V
|
||||
items?: I
|
||||
labelKey?: keyof NestedItem<T>
|
||||
items?: T
|
||||
/** 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`. */
|
||||
modelValue?: SelectModelValue<T, V, M>
|
||||
modelValue?: GetModelValue<T, VK, M>
|
||||
/** Whether multiple options can be selected or not. */
|
||||
multiple?: M & boolean
|
||||
/** 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>
|
||||
}
|
||||
|
||||
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]
|
||||
blur: [payload: FocusEvent]
|
||||
focus: [payload: FocusEvent]
|
||||
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> {
|
||||
'leading'(props: { modelValue?: M extends true ? T[] : T, open: boolean, ui: any }): any
|
||||
'default'(props: { modelValue?: M extends true ? T[] : T, open: boolean }): any
|
||||
'trailing'(props: { modelValue?: M extends true ? T[] : T, open: boolean, ui: any }): any
|
||||
export interface SelectMenuSlots<
|
||||
A extends ArrayOrNested<SelectMenuItem> = ArrayOrNested<SelectMenuItem>,
|
||||
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 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
|
||||
'item': SlotProps<T>
|
||||
'item-leading': SlotProps<T>
|
||||
@@ -144,7 +167,7 @@ export interface SelectMenuSlots<T, M extends boolean> {
|
||||
}
|
||||
</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 { ComboboxRoot, ComboboxArrow, ComboboxAnchor, ComboboxInput, ComboboxTrigger, ComboboxPortal, ComboboxContent, ComboboxViewport, ComboboxEmpty, ComboboxGroup, ComboboxLabel, ComboboxSeparator, ComboboxItem, ComboboxItemIndicator, FocusScope, useForwardPropsEmits, useFilter } from 'reka-ui'
|
||||
import { defu } from 'defu'
|
||||
@@ -154,7 +177,7 @@ import { useButtonGroup } from '../composables/useButtonGroup'
|
||||
import { useComponentIcons } from '../composables/useComponentIcons'
|
||||
import { useFormField } from '../composables/useFormField'
|
||||
import { useLocale } from '../composables/useLocale'
|
||||
import { get, compare } from '../utils'
|
||||
import { compare, get, isArrayOfArray } from '../utils'
|
||||
import UIcon from './Icon.vue'
|
||||
import UAvatar from './Avatar.vue'
|
||||
import UChip from './Chip.vue'
|
||||
@@ -162,14 +185,14 @@ import UInput from './Input.vue'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
const props = withDefaults(defineProps<SelectMenuProps<T, I, V, M>>(), {
|
||||
const props = withDefaults(defineProps<SelectMenuProps<T, VK, M>>(), {
|
||||
portal: true,
|
||||
searchInput: true,
|
||||
labelKey: 'label' as never,
|
||||
resetSearchTermOnBlur: true
|
||||
})
|
||||
const emits = defineEmits<SelectMenuEmits<T, V, M>>()
|
||||
const slots = defineSlots<SelectMenuSlots<T, M>>()
|
||||
const emits = defineEmits<SelectMenuEmits<T, VK, M>>()
|
||||
const slots = defineSlots<SelectMenuSlots<T, VK, M>>()
|
||||
|
||||
const searchTerm = defineModel<string>('searchTerm', { default: '' })
|
||||
|
||||
@@ -201,7 +224,7 @@ const ui = computed(() => selectMenu({
|
||||
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)) {
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
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[]
|
||||
|
||||
return groups.value.map(items => items.filter((item) => {
|
||||
if (typeof item !== 'object') {
|
||||
return contains(item, searchTerm.value)
|
||||
if (typeof item !== 'object' || item === null) {
|
||||
return contains(String(item), searchTerm.value)
|
||||
}
|
||||
|
||||
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))
|
||||
})).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(() => {
|
||||
if (!props.createItem || !searchTerm.value) {
|
||||
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') {
|
||||
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
|
||||
@@ -290,6 +321,10 @@ function onUpdateOpen(value: boolean) {
|
||||
clearTimeout(timeoutId)
|
||||
}
|
||||
}
|
||||
|
||||
function isSelectItem(item: SelectMenuItem): item is _SelectMenuItem {
|
||||
return typeof item === 'object' && item !== null
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable vue/no-template-shadow -->
|
||||
@@ -324,14 +359,14 @@ function onUpdateOpen(value: boolean) {
|
||||
<ComboboxAnchor as-child>
|
||||
<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 })">
|
||||
<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 })" />
|
||||
<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>
|
||||
</span>
|
||||
|
||||
<slot :model-value="(modelValue as M extends true ? T[] : T)" :open="open">
|
||||
<template v-for="displayedModelValue in [displayValue(modelValue as M extends true ? T[] : T)]" :key="displayedModelValue">
|
||||
<slot :model-value="(modelValue as GetModelValue<T, VK, M>)" :open="open">
|
||||
<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 })">
|
||||
{{ displayedModelValue }}
|
||||
</span>
|
||||
@@ -342,7 +377,7 @@ function onUpdateOpen(value: boolean) {
|
||||
</slot>
|
||||
|
||||
<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 })" />
|
||||
</slot>
|
||||
</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 })">
|
||||
<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) }}
|
||||
</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
|
||||
v-else
|
||||
:class="ui.item({ class: props.ui?.item })"
|
||||
:disabled="item.disabled"
|
||||
:value="valueKey && typeof item === 'object' ? get(item, props.valueKey as string) : item"
|
||||
@select="item.onSelect"
|
||||
:disabled="isSelectItem(item) && item.disabled"
|
||||
:value="props.valueKey && isSelectItem(item) ? get(item, props.valueKey as string) : item"
|
||||
@select="isSelectItem(item) && item.onSelect"
|
||||
>
|
||||
<slot name="item" :item="(item as T)" :index="index">
|
||||
<slot name="item-leading" :item="(item as T)" :index="index">
|
||||
<UIcon v-if="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 })" />
|
||||
<slot name="item" :item="(item as NestedItem<T>)" :index="index">
|
||||
<slot name="item-leading" :item="(item as NestedItem<T>)" :index="index">
|
||||
<UIcon v-if="isSelectItem(item) && item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon })" />
|
||||
<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
|
||||
v-else-if="item.chip"
|
||||
v-else-if="isSelectItem(item) && item.chip"
|
||||
:size="((props.ui?.itemLeadingChipSize || ui.itemLeadingChipSize()) as ChipProps['size'])"
|
||||
inset
|
||||
standalone
|
||||
@@ -395,13 +430,13 @@ function onUpdateOpen(value: boolean) {
|
||||
</slot>
|
||||
|
||||
<span :class="ui.itemLabel({ class: props.ui?.itemLabel })">
|
||||
<slot name="item-label" :item="(item as T)" :index="index">
|
||||
{{ typeof item === 'object' ? get(item, props.labelKey as string) : item }}
|
||||
<slot name="item-label" :item="(item as NestedItem<T>)" :index="index">
|
||||
{{ isSelectItem(item) ? get(item, props.labelKey as string) : item }}
|
||||
</slot>
|
||||
</span>
|
||||
|
||||
<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>
|
||||
<UIcon :name="selectedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: props.ui?.itemTrailingIcon })" />
|
||||
|
||||
@@ -70,7 +70,7 @@ export interface SlideoverSlots {
|
||||
header(props?: {}): any
|
||||
title(props?: {}): any
|
||||
description(props?: {}): any
|
||||
close(props: { ui: any }): any
|
||||
close(props: { ui: ReturnType<typeof slideover> }): any
|
||||
body(props?: {}): any
|
||||
footer(props?: {}): any
|
||||
}
|
||||
|
||||
@@ -25,9 +25,10 @@ export interface StepperItem {
|
||||
icon?: string
|
||||
content?: string
|
||||
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.
|
||||
* @defaultValue 'div'
|
||||
@@ -56,19 +57,19 @@ export interface StepperProps<T extends StepperItem> extends Pick<StepperRootPro
|
||||
class?: any
|
||||
}
|
||||
|
||||
export type StepperEmits<T> = Omit<StepperRootEmits, 'update:modelValue'> & {
|
||||
export type StepperEmits<T extends StepperItem = StepperItem> = Omit<StepperRootEmits, 'update:modelValue'> & {
|
||||
next: [payload: T]
|
||||
prev: [payload: T]
|
||||
}
|
||||
|
||||
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>
|
||||
title: SlotProps<T>
|
||||
description: SlotProps<T>
|
||||
content: SlotProps<T>
|
||||
} & DynamicSlots<T, SlotProps<T>>
|
||||
} & DynamicSlots<T>
|
||||
|
||||
</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 hasPrev = computed(() => currentStepIndex.value > 0)
|
||||
|
||||
@@ -116,13 +117,13 @@ defineExpose({
|
||||
next() {
|
||||
if (hasNext.value) {
|
||||
currentStepIndex.value += 1
|
||||
emits('next', currentStep.value)
|
||||
emits('next', currentStep.value as T)
|
||||
}
|
||||
},
|
||||
prev() {
|
||||
if (hasPrev.value) {
|
||||
currentStepIndex.value -= 1
|
||||
emits('prev', currentStep.value)
|
||||
emits('prev', currentStep.value as T)
|
||||
}
|
||||
},
|
||||
hasNext,
|
||||
@@ -173,10 +174,10 @@ defineExpose({
|
||||
</StepperItem>
|
||||
</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
|
||||
:name="!!slots[currentStep?.slot ?? currentStep.value!] ? currentStep.slot ?? currentStep.value : 'content'"
|
||||
:item="currentStep"
|
||||
:name="((currentStep?.slot || 'content') as keyof StepperSlots<T>)"
|
||||
:item="(currentStep as Extract<T, { slot: string }>)"
|
||||
>
|
||||
{{ currentStep?.content }}
|
||||
</slot>
|
||||
|
||||
@@ -25,11 +25,12 @@ export interface TabsItem {
|
||||
/** A unique value for the tab item. Defaults to the index. */
|
||||
value?: string | number
|
||||
disabled?: boolean
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
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.
|
||||
* @defaultValue 'div'
|
||||
@@ -69,14 +70,14 @@ export interface TabsProps<T> extends Pick<TabsRootProps<string | number>, 'defa
|
||||
|
||||
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>
|
||||
default: SlotProps<T>
|
||||
trailing: SlotProps<T>
|
||||
content: SlotProps<T>
|
||||
} & DynamicSlots<T, SlotProps<T>>
|
||||
} & DynamicSlots<T, undefined, { index: number }>
|
||||
|
||||
</script>
|
||||
|
||||
@@ -129,7 +130,7 @@ const ui = computed(() => tabs({
|
||||
|
||||
<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 })">
|
||||
<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 }}
|
||||
</slot>
|
||||
</TabsContent>
|
||||
|
||||
@@ -66,7 +66,7 @@ export interface ToastSlots {
|
||||
title(props?: {}): any
|
||||
description(props?: {}): any
|
||||
actions(props?: {}): any
|
||||
close(props: { ui: any }): any
|
||||
close(props: { ui: ReturnType<typeof toast> }): any
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -6,7 +6,14 @@ import type { AppConfig } from '@nuxt/schema'
|
||||
import _appConfig from '#build/app.config'
|
||||
import theme from '#build/ui/tree'
|
||||
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> } }
|
||||
|
||||
@@ -31,9 +38,10 @@ export type TreeItem = {
|
||||
children?: TreeItem[]
|
||||
onToggle?(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.
|
||||
* @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.
|
||||
* @defaultValue 'value'
|
||||
*/
|
||||
valueKey?: K
|
||||
valueKey?: VK
|
||||
/**
|
||||
* The key used to get the label from the item.
|
||||
* @defaultValue 'label'
|
||||
*/
|
||||
labelKey?: K
|
||||
labelKey?: keyof NestedItem<T>
|
||||
/**
|
||||
* The icon displayed on the right side of a parent node.
|
||||
* @defaultValue appConfig.ui.icons.chevronDown
|
||||
@@ -75,33 +83,34 @@ export interface TreeProps<T extends TreeItem, M extends boolean = false, K exte
|
||||
* @IconifyIcon
|
||||
*/
|
||||
collapsedIcon?: string
|
||||
items?: T[]
|
||||
items?: T
|
||||
/** 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. */
|
||||
defaultValue?: MaybeMultipleModelValue<T, M>
|
||||
defaultValue?: GetModelValue<T, VK, M>
|
||||
/** Whether multiple options can be selected or not. */
|
||||
multiple?: M & boolean
|
||||
class?: any
|
||||
ui?: PartialString<typeof tree.slots>
|
||||
}
|
||||
|
||||
export type TreeEmits<T, M extends boolean = false> = Omit<TreeRootEmits, 'update:modelValue'> & {
|
||||
'update:modelValue': [payload: MaybeMultipleModelValue<T, M>]
|
||||
}
|
||||
export type TreeEmits<A extends TreeItem[], VK extends GetItemKeys<A> | undefined, M extends boolean> = Omit<TreeRootEmits, 'update:modelValue'> & GetModelValueEmits<A, VK, 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-leading': SlotProps<T>
|
||||
'item-label': SlotProps<T>
|
||||
'item-trailing': SlotProps<T>
|
||||
} & DynamicSlots<T, SlotProps<T>>
|
||||
} & DynamicSlots<T, undefined, { index: number, level: number, expanded: boolean, selected: boolean }>
|
||||
|
||||
</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 type { PropType } from 'vue'
|
||||
import { TreeRoot, TreeItem, useForwardPropsEmits } from 'reka-ui'
|
||||
@@ -109,18 +118,21 @@ import { reactivePick, createReusableTemplate } from '@vueuse/core'
|
||||
import { get } from '../utils'
|
||||
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,
|
||||
valueKey: 'value' as never
|
||||
})
|
||||
const emits = defineEmits<TreeEmits<T, M>>()
|
||||
const emits = defineEmits<TreeEmits<T, VK, M>>()
|
||||
const slots = defineSlots<TreeSlots<T>>()
|
||||
|
||||
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: {
|
||||
items: Array as PropType<T[]>,
|
||||
items: Array as PropType<NestedItem<T>[]>,
|
||||
level: Number
|
||||
}
|
||||
})
|
||||
@@ -130,22 +142,24 @@ const ui = computed(() => tree({
|
||||
size: props.size
|
||||
}))
|
||||
|
||||
function getItemLabel(item: T) {
|
||||
function getItemLabel(item: NestedItem<T>): 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)
|
||||
}
|
||||
|
||||
function getDefaultOpenedItems(item: T): string[] {
|
||||
function getDefaultOpenedItems(item: NestedItem<T>): string[] {
|
||||
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[]
|
||||
}
|
||||
|
||||
const defaultExpanded = computed(() => props.defaultExpanded ?? props.items?.flatMap(getDefaultOpenedItems))
|
||||
const defaultExpanded = computed(() =>
|
||||
props.defaultExpanded ?? props.items?.flatMap(item => getDefaultOpenedItems(item as NestedItem<T>))
|
||||
)
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable vue/no-template-shadow -->
|
||||
@@ -165,8 +179,8 @@ const defaultExpanded = computed(() => props.defaultExpanded ?? props.items?.fla
|
||||
@select="item.onSelect"
|
||||
>
|
||||
<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.slot}-leading`: 'item-leading'" 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') as keyof TreeSlots<T>)" v-bind="{ index, level, expanded: isExpanded, selected: isSelected }" :item="(item as Extract<NestedItem<T>, { slot: string; }>)">
|
||||
<UIcon
|
||||
v-if="item.icon"
|
||||
:name="item.icon"
|
||||
@@ -179,14 +193,14 @@ const defaultExpanded = computed(() => props.defaultExpanded ?? props.items?.fla
|
||||
/>
|
||||
</slot>
|
||||
|
||||
<span v-if="getItemLabel(item) || !!slots[item.slot ? `${item.slot}-label`: 'item-label']" :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 }">
|
||||
<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') as keyof TreeSlots<T>)" v-bind="{ item, index, level, expanded: isExpanded, selected: isSelected }" :item="(item as Extract<NestedItem<T>, { slot: string; }>)">
|
||||
{{ getItemLabel(item) }}
|
||||
</slot>
|
||||
</span>
|
||||
|
||||
<span v-if="item.trailingIcon || item.children?.length || !!slots[item.slot ? `${item.slot}-trailing`: 'item-trailing']" :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 }">
|
||||
<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') 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-else-if="item.children?.length" :name="trailingIcon ?? appConfig.ui.icons.chevronDown" :class="ui.linkTrailingIcon({ class: props.ui?.linkTrailingIcon })" />
|
||||
</slot>
|
||||
@@ -195,19 +209,19 @@ const defaultExpanded = computed(() => props.defaultExpanded ?? props.items?.fla
|
||||
</button>
|
||||
|
||||
<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>
|
||||
</TreeItem>
|
||||
</li>
|
||||
</DefineTreeTemplate>
|
||||
|
||||
<TreeRoot
|
||||
v-bind="rootProps"
|
||||
v-bind="(rootProps as unknown as TreeRootProps<NestedItem<T>>)"
|
||||
:class="ui.root({ class: [props.class, props.ui?.root] })"
|
||||
:get-key="getItemValue"
|
||||
:default-expanded="defaultExpanded"
|
||||
:selection-behavior="selectionBehavior"
|
||||
>
|
||||
<ReuseTreeTemplate :items="items" :level="0" />
|
||||
<ReuseTreeTemplate :items="(items as NestedItem<T>[] | undefined)" :level="0" />
|
||||
</TreeRoot>
|
||||
</template>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AcceptableValue as _AcceptableValue } from 'reka-ui'
|
||||
import type { VNode } from 'vue'
|
||||
|
||||
export interface TightMap<O = any> {
|
||||
@@ -14,8 +15,19 @@ export type DeepPartial<T, O = any> = {
|
||||
[key: string]: O | TightMap<O>
|
||||
}
|
||||
|
||||
export type DynamicSlots<T extends { slot?: string }, SlotProps, Slot = T['slot']> =
|
||||
Record<string, SlotProps> & (Slot extends string ? Record<Slot, SlotProps> : Record<string, never>)
|
||||
export type DynamicSlots<
|
||||
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>
|
||||
? MaybeObject[Key]
|
||||
@@ -25,18 +37,49 @@ export type PartialString<T> = {
|
||||
[K in keyof T]?: string
|
||||
}
|
||||
|
||||
export type MaybeArrayOfArray<T> = T[] | T[][]
|
||||
export type MaybeArrayOfArrayItem<I> = I extends Array<infer T> ? T extends Array<infer U> ? U : T : never
|
||||
|
||||
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
|
||||
|
||||
export type SelectItemKey<T> = T extends Record<string, any> ? keyof T : string
|
||||
|
||||
export type SelectModelValueEmits<T, V, M extends boolean = false, DV = T> = {
|
||||
'update:modelValue': [payload: SelectModelValue<T, V, M, DV>]
|
||||
export type AcceptableValue = Exclude<_AcceptableValue, Record<string, any>>
|
||||
export type ArrayOrNested<T> = T[] | T[][]
|
||||
export type NestedItem<T> = T extends Array<infer I> ? NestedItem<I> : T
|
||||
type AllKeys<T> = T extends any ? keyof T : never
|
||||
type NonCommonKeys<T extends object> = Exclude<AllKeys<T>, keyof T>
|
||||
type PickTypeOf<T, K extends string | number | symbol> = K extends AllKeys<T>
|
||||
? T extends { [k in K]?: any }
|
||||
? T[K]
|
||||
: 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 =
|
||||
| string
|
||||
|
||||
@@ -81,3 +81,7 @@ export function compare<T>(value?: T, currentValue?: T, comparator?: string | ((
|
||||
|
||||
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',
|
||||
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.',
|
||||
slot: 'custom'
|
||||
slot: 'custom' as const
|
||||
}]
|
||||
|
||||
const props = { items }
|
||||
@@ -57,7 +57,7 @@ describe('Accordion', () => {
|
||||
['with body slot', { props: { ...props, modelValue: '1' }, slots: { body: () => 'Body 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' } }]
|
||||
])('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)
|
||||
expect(html).toMatchSnapshot()
|
||||
})
|
||||
|
||||
@@ -37,7 +37,7 @@ describe('Breadcrumb', () => {
|
||||
['with item-trailing slot', { props, slots: { 'item-trailing': () => 'Item trailing slot' } }],
|
||||
['with custom slot', { props, slots: { custom: () => 'Custom slot' } }],
|
||||
['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)
|
||||
expect(html).toMatchSnapshot()
|
||||
})
|
||||
|
||||
@@ -37,7 +37,7 @@ describe('Carousel', () => {
|
||||
['with as', { props: { ...props, as: 'nav' } }],
|
||||
['with class', { props: { ...props, class: 'w-full max-w-xs' } }],
|
||||
['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)
|
||||
expect(html).toMatchSnapshot()
|
||||
})
|
||||
|
||||
@@ -21,7 +21,7 @@ describe('Chip', () => {
|
||||
// Slots
|
||||
['with default slot', { slots: { default: () => 'Default 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)
|
||||
expect(html).toMatchSnapshot()
|
||||
})
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
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 theme from '#build/ui/context-menu'
|
||||
import { mountSuspended } from '@nuxt/test-utils/runtime'
|
||||
import { expectSlotProps } from '../utils/types'
|
||||
|
||||
const ContextMenuWrapper = defineComponent({
|
||||
components: {
|
||||
@@ -95,11 +96,33 @@ describe('ContextMenu', () => {
|
||||
['with item-label slot', { props, slots: { 'item-label': () => 'Item label slot' } }],
|
||||
['with item-trailing slot', { props, slots: { 'item-trailing': () => 'Item trailing 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)
|
||||
|
||||
await wrapper.find('span').trigger('click.right')
|
||||
|
||||
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 ComponentRender from '../component-render'
|
||||
import theme from '#build/ui/dropdown-menu'
|
||||
import { expectSlotProps } from '../utils/types'
|
||||
|
||||
describe('DropdownMenu', () => {
|
||||
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-trailing slot', { props, slots: { 'item-trailing': () => 'Item trailing 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)
|
||||
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-leading slot', { props, slots: { 'item-leading': () => 'Item leading slot' } }],
|
||||
['with item-label slot', { props, slots: { 'item-label': () => 'Item label 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' } }]
|
||||
])('renders %s correctly', async (nameOrHtml: string, options: { props?: InputMenuProps<typeof items[number]>, slots?: Partial<InputMenuSlots<typeof items[number], false>> }) => {
|
||||
['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' } }]
|
||||
])('renders %s correctly', async (nameOrHtml: string, options: { props?: InputMenuProps, slots?: Partial<InputMenuSlots> }) => {
|
||||
const html = await ComponentRender(nameOrHtml, options, InputMenu)
|
||||
expect(html).toMatchSnapshot()
|
||||
})
|
||||
|
||||
@@ -108,7 +108,7 @@ describe('NavigationMenu', () => {
|
||||
['with item-label slot', { props, slots: { 'item-label': () => 'Item label slot' } }],
|
||||
['with item-trailing slot', { props, slots: { 'item-trailing': () => 'Item trailing 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)
|
||||
expect(html).toMatchSnapshot()
|
||||
})
|
||||
|
||||
@@ -38,7 +38,7 @@ describe('RadioGroup', () => {
|
||||
['with legend slot', { props, slots: { label: () => 'Legend slot' } }],
|
||||
['with label slot', { props, slots: { label: () => 'Label 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)
|
||||
expect(html).toMatchSnapshot()
|
||||
})
|
||||
|
||||
@@ -78,7 +78,7 @@ describe('Select', () => {
|
||||
['with item-leading slot', { props, slots: { 'item-leading': () => 'Item leading slot' } }],
|
||||
['with item-label slot', { props, slots: { 'item-label': () => 'Item label 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)
|
||||
expect(html).toMatchSnapshot()
|
||||
})
|
||||
@@ -192,10 +192,10 @@ describe('Select', () => {
|
||||
items: [['foo', { value: 1 }], [{ value: 'bar' }, 2]]
|
||||
})).toEqualTypeOf<[string | number]>()
|
||||
|
||||
// with groups, mixed types and valueKey = undefined
|
||||
// with groups, multiple, mixed types and valueKey
|
||||
expectEmitPayloadType('update:modelValue', () => Select({
|
||||
items: [['foo', { value: 1 }], [{ value: 'bar' }, 2]],
|
||||
valueKey: undefined
|
||||
valueKey: 'value' // TODO: value is already the default valueKey
|
||||
})).toEqualTypeOf<[string | number]>()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -81,9 +81,9 @@ describe('SelectMenu', () => {
|
||||
['with item slot', { props, slots: { item: () => 'Item 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-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' } }]
|
||||
])('renders %s correctly', async (nameOrHtml: string, options: { props?: SelectMenuProps<typeof items[number]>, slots?: Partial<SelectMenuSlots<typeof items[number], false>> }) => {
|
||||
['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' } }]
|
||||
])('renders %s correctly', async (nameOrHtml: string, options: { props?: SelectMenuProps, slots?: Partial<SelectMenuSlots> }) => {
|
||||
const html = await ComponentRender(nameOrHtml, options, SelectMenu)
|
||||
expect(html).toMatchSnapshot()
|
||||
})
|
||||
|
||||
@@ -43,7 +43,7 @@ describe('Stepper', () => {
|
||||
['with description slot', { props, slots: { description: () => 'Description slot' } }],
|
||||
['with content slot', { props, slots: { content: () => 'Content 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)
|
||||
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