fix(components): improve generic types (#3331)

Co-authored-by: Benjamin Canac <canacb1@gmail.com>
This commit is contained in:
Sandro Circi
2025-03-24 21:38:13 +01:00
committed by GitHub
parent 370054b20c
commit b9983549a4
106 changed files with 1203 additions and 535 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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'

View File

@@ -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>

View File

@@ -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] })"

View File

@@ -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'

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 })" />

View File

@@ -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>

View File

@@ -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 })" />

View File

@@ -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
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 })" />

View File

@@ -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 })" />

View File

@@ -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
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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])
}