mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-14 12:14:41 +01:00
fix(components): improve generic types (#3331)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
This commit is contained in:
@@ -26,9 +26,10 @@ export interface AccordionItem {
|
||||
/** A unique value for the accordion item. Defaults to the index. */
|
||||
value?: string
|
||||
disabled?: boolean
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface AccordionProps<T> extends Pick<AccordionRootProps, 'collapsible' | 'defaultValue' | 'modelValue' | 'type' | 'disabled' | 'unmountOnHide'> {
|
||||
export interface AccordionProps<T extends AccordionItem = AccordionItem> extends Pick<AccordionRootProps, 'collapsible' | 'defaultValue' | 'modelValue' | 'type' | 'disabled' | 'unmountOnHide'> {
|
||||
/**
|
||||
* The element or component this component should render as.
|
||||
* @defaultValue 'div'
|
||||
@@ -52,15 +53,15 @@ export interface AccordionProps<T> extends Pick<AccordionRootProps, 'collapsible
|
||||
|
||||
export interface AccordionEmits extends AccordionRootEmits {}
|
||||
|
||||
type SlotProps<T> = (props: { item: T, index: number, open: boolean }) => any
|
||||
type SlotProps<T extends AccordionItem> = (props: { item: T, index: number, open: boolean }) => any
|
||||
|
||||
export type AccordionSlots<T extends { slot?: string }> = {
|
||||
export type AccordionSlots<T extends AccordionItem = AccordionItem> = {
|
||||
leading: SlotProps<T>
|
||||
default: SlotProps<T>
|
||||
trailing: SlotProps<T>
|
||||
content: SlotProps<T>
|
||||
body: SlotProps<T>
|
||||
} & DynamicSlots<T, SlotProps<T>>
|
||||
} & DynamicSlots<T, 'body', { index: number, open: boolean }>
|
||||
|
||||
</script>
|
||||
|
||||
@@ -92,7 +93,7 @@ const ui = computed(() => accordion({
|
||||
<template>
|
||||
<AccordionRoot v-bind="rootProps" :class="ui.root({ class: [props.class, props.ui?.root] })">
|
||||
<AccordionItem
|
||||
v-for="(item, index) in items"
|
||||
v-for="(item, index) in props.items"
|
||||
v-slot="{ open }"
|
||||
:key="index"
|
||||
:value="item.value || String(index)"
|
||||
@@ -115,10 +116,10 @@ const ui = computed(() => accordion({
|
||||
</AccordionTrigger>
|
||||
</AccordionHeader>
|
||||
|
||||
<AccordionContent v-if="item.content || !!slots.content || (item.slot && !!slots[item.slot]) || !!slots.body || (item.slot && !!slots[`${item.slot}-body`])" :class="ui.content({ class: props.ui?.content })">
|
||||
<slot :name="item.slot || 'content'" :item="item" :index="index" :open="open">
|
||||
<AccordionContent v-if="item.content || !!slots.content || (item.slot && !!slots[item.slot as keyof AccordionSlots<T>]) || !!slots.body || (item.slot && !!slots[`${item.slot}-body` as keyof AccordionSlots<T>])" :class="ui.content({ class: props.ui?.content })">
|
||||
<slot :name="((item.slot || 'content') as keyof AccordionSlots<T>)" :item="item" :index="index" :open="open">
|
||||
<div :class="ui.body({ class: props.ui?.body })">
|
||||
<slot :name="item.slot ? `${item.slot}-body`: 'body'" :item="item" :index="index" :open="open">
|
||||
<slot :name="((item.slot ? `${item.slot}-body`: 'body') as keyof AccordionSlots<T>)" :item="item" :index="index" :open="open">
|
||||
{{ item.content }}
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
@@ -71,7 +71,7 @@ export interface AlertSlots {
|
||||
title(props?: {}): any
|
||||
description(props?: {}): any
|
||||
actions(props?: {}): any
|
||||
close(props: { ui: any }): any
|
||||
close(props: { ui: ReturnType<typeof alert> }): any
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts" generic="T extends Messages = Messages">
|
||||
<script setup lang="ts" generic="T extends Messages">
|
||||
import { toRef, useId, provide } from 'vue'
|
||||
import { ConfigProvider, TooltipProvider, useForwardProps } from 'reka-ui'
|
||||
import { reactivePick } from '@vueuse/core'
|
||||
|
||||
@@ -19,9 +19,10 @@ export interface BreadcrumbItem extends Omit<LinkProps, 'raw' | 'custom'> {
|
||||
icon?: string
|
||||
avatar?: AvatarProps
|
||||
slot?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface BreadcrumbProps<T> {
|
||||
export interface BreadcrumbProps<T extends BreadcrumbItem = BreadcrumbItem> {
|
||||
/**
|
||||
* The element or component this component should render as.
|
||||
* @defaultValue 'nav'
|
||||
@@ -43,15 +44,15 @@ export interface BreadcrumbProps<T> {
|
||||
ui?: PartialString<typeof breadcrumb.slots>
|
||||
}
|
||||
|
||||
type SlotProps<T> = (props: { item: T, index: number, active?: boolean }) => any
|
||||
type SlotProps<T extends BreadcrumbItem> = (props: { item: T, index: number, active?: boolean }) => any
|
||||
|
||||
export type BreadcrumbSlots<T extends { slot?: string }> = {
|
||||
export type BreadcrumbSlots<T extends BreadcrumbItem = BreadcrumbItem> = {
|
||||
'item': SlotProps<T>
|
||||
'item-leading': SlotProps<T>
|
||||
'item-label': SlotProps<T>
|
||||
'item-trailing': SlotProps<T>
|
||||
'separator'(props?: {}): any
|
||||
} & DynamicSlots<T, SlotProps<T>>
|
||||
'separator': any
|
||||
} & DynamicSlots<T, 'leading' | 'label' | 'trailing', { index: number, active?: boolean }>
|
||||
|
||||
</script>
|
||||
|
||||
@@ -88,19 +89,19 @@ const ui = breadcrumb()
|
||||
<li :class="ui.item({ class: props.ui?.item })">
|
||||
<ULink v-slot="{ active, ...slotProps }" v-bind="pickLinkProps(item)" custom>
|
||||
<ULinkBase v-bind="slotProps" as="span" :aria-current="active && (index === items!.length - 1) ? 'page' : undefined" :class="ui.link({ class: [props.ui?.link, item.class], active: index === items!.length - 1, disabled: !!item.disabled, to: !!item.to })">
|
||||
<slot :name="item.slot || 'item'" :item="item" :index="index">
|
||||
<slot :name="item.slot ? `${item.slot}-leading`: 'item-leading'" :item="item" :active="index === items!.length - 1" :index="index">
|
||||
<slot :name="((item.slot || 'item') as keyof BreadcrumbSlots<T>)" :item="item" :index="index">
|
||||
<slot :name="((item.slot ? `${item.slot}-leading`: 'item-leading') as keyof BreadcrumbSlots<T>)" :item="item" :active="index === items!.length - 1" :index="index">
|
||||
<UIcon v-if="item.icon" :name="item.icon" :class="ui.linkLeadingIcon({ class: props.ui?.linkLeadingIcon, active: index === items!.length - 1 })" />
|
||||
<UAvatar v-else-if="item.avatar" :size="((props.ui?.linkLeadingAvatarSize || ui.linkLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.linkLeadingAvatar({ class: props.ui?.linkLeadingAvatar, active: index === items!.length - 1 })" />
|
||||
</slot>
|
||||
|
||||
<span v-if="get(item, props.labelKey as string) || !!slots[item.slot ? `${item.slot}-label`: 'item-label']" :class="ui.linkLabel({ class: props.ui?.linkLabel })">
|
||||
<slot :name="item.slot ? `${item.slot}-label`: 'item-label'" :item="item" :active="index === items!.length - 1" :index="index">
|
||||
<span v-if="get(item, props.labelKey as string) || !!slots[(item.slot ? `${item.slot}-label`: 'item-label') as keyof BreadcrumbSlots<T>]" :class="ui.linkLabel({ class: props.ui?.linkLabel })">
|
||||
<slot :name="((item.slot ? `${item.slot}-label`: 'item-label') as keyof BreadcrumbSlots<T>)" :item="item" :active="index === items!.length - 1" :index="index">
|
||||
{{ get(item, props.labelKey as string) }}
|
||||
</slot>
|
||||
</span>
|
||||
|
||||
<slot :name="item.slot ? `${item.slot}-trailing`: 'item-trailing'" :item="item" :active="index === items!.length - 1" :index="index" />
|
||||
<slot :name="((item.slot ? `${item.slot}-trailing`: 'item-trailing') as keyof BreadcrumbSlots<T>)" :item="item" :active="index === items!.length - 1" :index="index" />
|
||||
</slot>
|
||||
</ULinkBase>
|
||||
</ULink>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { VariantProps } from 'tailwind-variants'
|
||||
import type { CalendarRootProps, CalendarRootEmits, RangeCalendarRootEmits, DateRange, CalendarCellTriggerProps } from 'reka-ui'
|
||||
import type { CalendarRootProps, CalendarRootEmits, RangeCalendarRootProps, RangeCalendarRootEmits, DateRange, CalendarCellTriggerProps } from 'reka-ui'
|
||||
import type { DateValue } from '@internationalized/date'
|
||||
import type { AppConfig } from '@nuxt/schema'
|
||||
import type { ButtonProps } from '../types'
|
||||
@@ -15,13 +15,21 @@ const calendar = tv({ extend: tv(theme), ...(appConfigCalendar.ui?.calendar || {
|
||||
|
||||
type CalendarVariants = VariantProps<typeof calendar>
|
||||
|
||||
type CalendarModelValue<R extends boolean = false, M extends boolean = false> = R extends true
|
||||
type CalendarDefaultValue<R extends boolean = false, M extends boolean = false> = R extends true
|
||||
? DateRange
|
||||
: M extends true
|
||||
? DateValue[]
|
||||
: DateValue
|
||||
type CalendarModelValue<R extends boolean = false, M extends boolean = false> = R extends true
|
||||
? (DateRange | null)
|
||||
: M extends true
|
||||
? (DateValue[] | undefined)
|
||||
: (DateValue | undefined)
|
||||
|
||||
export interface CalendarProps<R extends boolean, M extends boolean> extends Omit<CalendarRootProps, 'as' | 'asChild' | 'modelValue' | 'defaultValue' | 'dir' | 'locale' | 'calendarLabel' | 'multiple'> {
|
||||
type _CalendarRootProps = Omit<CalendarRootProps, 'as' | 'asChild' | 'modelValue' | 'defaultValue' | 'dir' | 'locale' | 'calendarLabel' | 'multiple'>
|
||||
type _RangeCalendarRootProps = Omit<RangeCalendarRootProps, 'as' | 'asChild' | 'modelValue' | 'defaultValue' | 'dir' | 'locale' | 'calendarLabel' | 'multiple'>
|
||||
|
||||
export interface CalendarProps<R extends boolean = false, M extends boolean = false> extends _RangeCalendarRootProps, _CalendarRootProps {
|
||||
/**
|
||||
* The element or component this component should render as.
|
||||
* @defaultValue 'div'
|
||||
@@ -87,7 +95,7 @@ export interface CalendarProps<R extends boolean, M extends boolean> extends Omi
|
||||
monthControls?: boolean
|
||||
/** Show year controls */
|
||||
yearControls?: boolean
|
||||
defaultValue?: CalendarModelValue<R, M>
|
||||
defaultValue?: CalendarDefaultValue<R, M>
|
||||
modelValue?: CalendarModelValue<R, M>
|
||||
class?: any
|
||||
ui?: PartialString<typeof calendar.slots>
|
||||
@@ -104,7 +112,7 @@ export interface CalendarSlots {
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts" generic="R extends boolean = false, M extends boolean = false">
|
||||
<script setup lang="ts" generic="R extends boolean, M extends boolean">
|
||||
import { computed } from 'vue'
|
||||
import { useForwardPropsEmits } from 'reka-ui'
|
||||
import { Calendar as SingleCalendar, RangeCalendar } from 'reka-ui/namespaced'
|
||||
@@ -151,8 +159,8 @@ const Calendar = computed(() => props.range ? RangeCalendar : SingleCalendar)
|
||||
<Calendar.Root
|
||||
v-slot="{ weekDays, grid }"
|
||||
v-bind="rootProps"
|
||||
:model-value="(modelValue as CalendarModelValue<true & false>)"
|
||||
:default-value="(defaultValue as CalendarModelValue<true & false>)"
|
||||
:model-value="modelValue"
|
||||
:default-value="defaultValue"
|
||||
:locale="locale"
|
||||
:dir="dir"
|
||||
:class="ui.root({ class: [props.class, props.ui?.root] })"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<!-- eslint-disable vue/block-tag-newline -->
|
||||
<script lang="ts">
|
||||
import type { VariantProps } from 'tailwind-variants'
|
||||
import type { AppConfig } from '@nuxt/schema'
|
||||
@@ -21,7 +22,9 @@ const carousel = tv({ extend: tv(theme), ...(appConfigCarousel.ui?.carousel || {
|
||||
|
||||
type CarouselVariants = VariantProps<typeof carousel>
|
||||
|
||||
export interface CarouselProps<T> extends Omit<EmblaOptionsType, 'axis' | 'container' | 'slides' | 'direction'> {
|
||||
export type CarouselItem = AcceptableValue
|
||||
|
||||
export interface CarouselProps<T extends CarouselItem = CarouselItem> extends Omit<EmblaOptionsType, 'axis' | 'container' | 'slides' | 'direction'> {
|
||||
/**
|
||||
* The element or component this component should render as.
|
||||
* @defaultValue 'div'
|
||||
@@ -99,12 +102,13 @@ export interface CarouselProps<T> extends Omit<EmblaOptionsType, 'axis' | 'conta
|
||||
ui?: PartialString<typeof carousel.slots>
|
||||
}
|
||||
|
||||
export type CarouselSlots<T> = {
|
||||
export type CarouselSlots<T extends CarouselItem = CarouselItem> = {
|
||||
default(props: { item: T, index: number }): any
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<script setup lang="ts" generic="T extends AcceptableValue">
|
||||
<script setup lang="ts" generic="T extends CarouselItem">
|
||||
import { computed, ref, watch, onMounted } from 'vue'
|
||||
import useEmblaCarousel from 'embla-carousel-vue'
|
||||
import { Primitive, useForwardProps } from 'reka-ui'
|
||||
|
||||
@@ -9,7 +9,7 @@ import theme from '#build/ui/command-palette'
|
||||
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
|
||||
import { tv } from '../utils/tv'
|
||||
import type { AvatarProps, ButtonProps, ChipProps, KbdProps, InputProps, LinkProps } from '../types'
|
||||
import type { DynamicSlots, PartialString } from '../types/utils'
|
||||
import type { PartialString } from '../types/utils'
|
||||
|
||||
const appConfigCommandPalette = _appConfig as AppConfig & { ui: { commandPalette: Partial<typeof theme> } }
|
||||
|
||||
@@ -31,6 +31,7 @@ export interface CommandPaletteItem extends Omit<LinkProps, 'type' | 'raw' | 'cu
|
||||
disabled?: boolean
|
||||
slot?: string
|
||||
onSelect?(e?: Event): void
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface CommandPaletteGroup<T> {
|
||||
@@ -125,12 +126,12 @@ type SlotProps<T> = (props: { item: T, index: number }) => any
|
||||
|
||||
export type CommandPaletteSlots<G extends { slot?: string }, T extends { slot?: string }> = {
|
||||
'empty'(props: { searchTerm?: string }): any
|
||||
'close'(props: { ui: any }): any
|
||||
'close'(props: { ui: ReturnType<typeof commandPalette> }): any
|
||||
'item': SlotProps<T>
|
||||
'item-leading': SlotProps<T>
|
||||
'item-label': SlotProps<T>
|
||||
'item-trailing': SlotProps<T>
|
||||
} & DynamicSlots<G, SlotProps<T>> & DynamicSlots<T, SlotProps<T>>
|
||||
} & Record<string, SlotProps<G>> & Record<string, SlotProps<T>>
|
||||
|
||||
</script>
|
||||
|
||||
@@ -297,8 +298,8 @@ const groups = computed(() => {
|
||||
>
|
||||
<ULink v-slot="{ active, ...slotProps }" v-bind="pickLinkProps(item)" custom>
|
||||
<ULinkBase v-bind="slotProps" :class="ui.item({ class: props.ui?.item, active: active || item.active })">
|
||||
<slot :name="item.slot || group.slot || 'item'" :item="item" :index="index">
|
||||
<slot :name="item.slot ? `${item.slot}-leading` : group.slot ? `${group.slot}-leading` : `item-leading`" :item="item" :index="index">
|
||||
<slot :name="((item.slot || group.slot || 'item') as keyof CommandPaletteSlots<G, T>)" :item="(item as any)" :index="index">
|
||||
<slot :name="((item.slot ? `${item.slot}-leading` : group.slot ? `${group.slot}-leading` : `item-leading`) as keyof CommandPaletteSlots<G, T>)" :item="(item as any)" :index="index">
|
||||
<UIcon v-if="item.loading" :name="loadingIcon || appConfig.ui.icons.loading" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon, loading: true })" />
|
||||
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon, active: active || item.active })" />
|
||||
<UAvatar v-else-if="item.avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar, active: active || item.active })" />
|
||||
@@ -312,8 +313,8 @@ const groups = computed(() => {
|
||||
/>
|
||||
</slot>
|
||||
|
||||
<span v-if="item.labelHtml || get(item, props.labelKey as string) || !!slots[item.slot ? `${item.slot}-label` : group.slot ? `${group.slot}-label` : `item-label`]" :class="ui.itemLabel({ class: props.ui?.itemLabel, active: active || item.active })">
|
||||
<slot :name="item.slot ? `${item.slot}-label` : group.slot ? `${group.slot}-label` : `item-label`" :item="item" :index="index">
|
||||
<span v-if="item.labelHtml || get(item, props.labelKey as string) || !!slots[(item.slot ? `${item.slot}-label` : group.slot ? `${group.slot}-label` : `item-label`) as keyof CommandPaletteSlots<G, T>]" :class="ui.itemLabel({ class: props.ui?.itemLabel, active: active || item.active })">
|
||||
<slot :name="((item.slot ? `${item.slot}-label` : group.slot ? `${group.slot}-label` : `item-label`) as keyof CommandPaletteSlots<G, T>)" :item="(item as any)" :index="index">
|
||||
<span v-if="item.prefix" :class="ui.itemLabelPrefix({ class: props.ui?.itemLabelPrefix })">{{ item.prefix }}</span>
|
||||
|
||||
<span :class="ui.itemLabelBase({ class: props.ui?.itemLabelBase, active: active || item.active })" v-html="item.labelHtml || get(item, props.labelKey as string)" />
|
||||
@@ -323,7 +324,7 @@ const groups = computed(() => {
|
||||
</span>
|
||||
|
||||
<span :class="ui.itemTrailing({ class: props.ui?.itemTrailing })">
|
||||
<slot :name="item.slot ? `${item.slot}-trailing` : group.slot ? `${group.slot}-trailing` : `item-trailing`" :item="item" :index="index">
|
||||
<slot :name="((item.slot ? `${item.slot}-trailing` : group.slot ? `${group.slot}-trailing` : `item-trailing`) as keyof CommandPaletteSlots<G, T>)" :item="(item as any)" :index="index">
|
||||
<span v-if="item.kbds?.length" :class="ui.itemTrailingKbds({ class: props.ui?.itemTrailingKbds })">
|
||||
<UKbd v-for="(kbd, kbdIndex) in item.kbds" :key="kbdIndex" :size="((props.ui?.itemTrailingKbdsSize || ui.itemTrailingKbdsSize()) as KbdProps['size'])" v-bind="typeof kbd === 'string' ? { value: kbd } : kbd" />
|
||||
</span>
|
||||
|
||||
@@ -7,7 +7,14 @@ import _appConfig from '#build/app.config'
|
||||
import theme from '#build/ui/context-menu'
|
||||
import { tv } from '../utils/tv'
|
||||
import type { AvatarProps, KbdProps, LinkProps } from '../types'
|
||||
import type { DynamicSlots, PartialString, EmitsToProps } from '../types/utils'
|
||||
import type {
|
||||
ArrayOrNested,
|
||||
DynamicSlots,
|
||||
MergeTypes,
|
||||
NestedItem,
|
||||
PartialString,
|
||||
EmitsToProps
|
||||
} from '../types/utils'
|
||||
|
||||
const appConfigContextMenu = _appConfig as AppConfig & { ui: { contextMenu: Partial<typeof theme> } }
|
||||
|
||||
@@ -36,17 +43,18 @@ export interface ContextMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'custo
|
||||
checked?: boolean
|
||||
open?: boolean
|
||||
defaultOpen?: boolean
|
||||
children?: ContextMenuItem[] | ContextMenuItem[][]
|
||||
children?: ArrayOrNested<ContextMenuItem>
|
||||
onSelect?(e: Event): void
|
||||
onUpdateChecked?(checked: boolean): void
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface ContextMenuProps<T> extends Omit<ContextMenuRootProps, 'dir'> {
|
||||
export interface ContextMenuProps<T extends ArrayOrNested<ContextMenuItem> = ArrayOrNested<ContextMenuItem>> extends Omit<ContextMenuRootProps, 'dir'> {
|
||||
/**
|
||||
* @defaultValue 'md'
|
||||
*/
|
||||
size?: ContextMenuVariants['size']
|
||||
items?: T[] | T[][]
|
||||
items?: T
|
||||
/**
|
||||
* The icon displayed when an item is checked.
|
||||
* @defaultValue appConfig.ui.icons.check
|
||||
@@ -77,7 +85,7 @@ export interface ContextMenuProps<T> extends Omit<ContextMenuRootProps, 'dir'> {
|
||||
* The key used to get the label from the item.
|
||||
* @defaultValue 'label'
|
||||
*/
|
||||
labelKey?: string
|
||||
labelKey?: keyof NestedItem<T>
|
||||
disabled?: boolean
|
||||
class?: any
|
||||
ui?: PartialString<typeof contextMenu.slots>
|
||||
@@ -85,19 +93,22 @@ export interface ContextMenuProps<T> extends Omit<ContextMenuRootProps, 'dir'> {
|
||||
|
||||
export interface ContextMenuEmits extends ContextMenuRootEmits {}
|
||||
|
||||
type SlotProps<T> = (props: { item: T, active?: boolean, index: number }) => any
|
||||
type SlotProps<T extends ContextMenuItem> = (props: { item: T, active?: boolean, index: number }) => any
|
||||
|
||||
export type ContextMenuSlots<T extends { slot?: string }> = {
|
||||
export type ContextMenuSlots<
|
||||
A extends ArrayOrNested<ContextMenuItem> = ArrayOrNested<ContextMenuItem>,
|
||||
T extends NestedItem<A> = NestedItem<A>
|
||||
> = {
|
||||
'default'(props?: {}): any
|
||||
'item': SlotProps<T>
|
||||
'item-leading': SlotProps<T>
|
||||
'item-label': SlotProps<T>
|
||||
'item-trailing': SlotProps<T>
|
||||
} & DynamicSlots<T, SlotProps<T>>
|
||||
} & DynamicSlots<MergeTypes<T>, 'leading' | 'label' | 'trailing', { active?: boolean, index: number }>
|
||||
|
||||
</script>
|
||||
|
||||
<script setup lang="ts" generic="T extends ContextMenuItem">
|
||||
<script setup lang="ts" generic="T extends ArrayOrNested<ContextMenuItem>">
|
||||
import { computed, toRef } from 'vue'
|
||||
import { ContextMenuRoot, ContextMenuTrigger, useForwardPropsEmits } from 'reka-ui'
|
||||
import { reactivePick } from '@vueuse/core'
|
||||
@@ -114,8 +125,9 @@ const emits = defineEmits<ContextMenuEmits>()
|
||||
const slots = defineSlots<ContextMenuSlots<T>>()
|
||||
|
||||
const rootProps = useForwardPropsEmits(reactivePick(props, 'modal'), emits)
|
||||
|
||||
const contentProps = toRef(() => props.content)
|
||||
const proxySlots = omit(slots, ['default']) as Record<string, ContextMenuSlots<T>[string]>
|
||||
const proxySlots = omit(slots, ['default'])
|
||||
|
||||
const ui = computed(() => contextMenu({
|
||||
size: props.size
|
||||
@@ -135,13 +147,13 @@ const ui = computed(() => contextMenu({
|
||||
v-bind="contentProps"
|
||||
:items="items"
|
||||
:portal="portal"
|
||||
:label-key="labelKey"
|
||||
:label-key="(labelKey as keyof NestedItem<T>)"
|
||||
:checked-icon="checkedIcon"
|
||||
:loading-icon="loadingIcon"
|
||||
:external-icon="externalIcon"
|
||||
>
|
||||
<template v-for="(_, name) in proxySlots" #[name]="slotData">
|
||||
<slot :name="name" v-bind="slotData" />
|
||||
<slot :name="(name as keyof ContextMenuSlots<T>)" v-bind="slotData" />
|
||||
</template>
|
||||
</UContextMenuContent>
|
||||
</ContextMenuRoot>
|
||||
|
||||
@@ -2,15 +2,16 @@
|
||||
import type { ContextMenuContentProps as RekaContextMenuContentProps, ContextMenuContentEmits as RekaContextMenuContentEmits } from 'reka-ui'
|
||||
import theme from '#build/ui/context-menu'
|
||||
import { tv } from '../utils/tv'
|
||||
import type { KbdProps, AvatarProps, ContextMenuItem, ContextMenuSlots } from '../types'
|
||||
import type { AvatarProps, ContextMenuItem, ContextMenuSlots, KbdProps } from '../types'
|
||||
import type { ArrayOrNested, NestedItem } from '../types/utils'
|
||||
|
||||
const _contextMenu = tv(theme)()
|
||||
|
||||
interface ContextMenuContentProps<T> extends Omit<RekaContextMenuContentProps, 'as' | 'asChild' | 'forceMount'> {
|
||||
items?: T[] | T[][]
|
||||
interface ContextMenuContentProps<T extends ArrayOrNested<ContextMenuItem>> extends Omit<RekaContextMenuContentProps, 'as' | 'asChild' | 'forceMount'> {
|
||||
items?: T
|
||||
portal?: boolean
|
||||
sub?: boolean
|
||||
labelKey: string
|
||||
labelKey: keyof NestedItem<T>
|
||||
/**
|
||||
* @IconifyIcon
|
||||
*/
|
||||
@@ -31,13 +32,13 @@ interface ContextMenuContentProps<T> extends Omit<RekaContextMenuContentProps, '
|
||||
interface ContextMenuContentEmits extends RekaContextMenuContentEmits {}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts" generic="T extends ContextMenuItem">
|
||||
<script setup lang="ts" generic="T extends ArrayOrNested<ContextMenuItem>">
|
||||
import { computed } from 'vue'
|
||||
import { ContextMenu } from 'reka-ui/namespaced'
|
||||
import { useForwardPropsEmits } from 'reka-ui'
|
||||
import { reactiveOmit, createReusableTemplate } from '@vueuse/core'
|
||||
import { useAppConfig } from '#imports'
|
||||
import { omit, get } from '../utils'
|
||||
import { omit, get, isArrayOfArray } from '../utils'
|
||||
import { pickLinkProps } from '../utils/link'
|
||||
import ULinkBase from './LinkBase.vue'
|
||||
import ULink from './Link.vue'
|
||||
@@ -53,24 +54,30 @@ const slots = defineSlots<ContextMenuSlots<T>>()
|
||||
|
||||
const appConfig = useAppConfig()
|
||||
const contentProps = useForwardPropsEmits(reactiveOmit(props, 'sub', 'items', 'portal', 'labelKey', 'checkedIcon', 'loadingIcon', 'externalIcon', 'class', 'ui', 'uiOverride'), emits)
|
||||
const proxySlots = omit(slots, ['default']) as Record<string, ContextMenuSlots<T>[string]>
|
||||
const proxySlots = omit(slots, ['default'])
|
||||
|
||||
const [DefineItemTemplate, ReuseItemTemplate] = createReusableTemplate<{ item: ContextMenuItem, active?: boolean, index: number }>()
|
||||
|
||||
const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0]) ? props.items : [props.items]) as T[][] : [])
|
||||
const groups = computed<ContextMenuItem[][]>(() =>
|
||||
props.items?.length
|
||||
? isArrayOfArray(props.items)
|
||||
? props.items
|
||||
: [props.items]
|
||||
: []
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DefineItemTemplate v-slot="{ item, active, index }">
|
||||
<slot :name="item.slot || 'item'" :item="(item as T)" :index="index">
|
||||
<slot :name="item.slot ? `${item.slot}-leading`: 'item-leading'" :item="(item as T)" :active="active" :index="index">
|
||||
<slot :name="((item.slot || 'item') as keyof ContextMenuSlots<T>)" :item="item" :index="index">
|
||||
<slot :name="((item.slot ? `${item.slot}-leading`: 'item-leading') as keyof ContextMenuSlots<T>)" :item="item" :active="active" :index="index">
|
||||
<UIcon v-if="item.loading" :name="loadingIcon || appConfig.ui.icons.loading" :class="ui.itemLeadingIcon({ class: uiOverride?.itemLeadingIcon, color: item?.color, loading: true })" />
|
||||
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: uiOverride?.itemLeadingIcon, color: item?.color, active })" />
|
||||
<UAvatar v-else-if="item.avatar" :size="((props.uiOverride?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: uiOverride?.itemLeadingAvatar, active })" />
|
||||
</slot>
|
||||
|
||||
<span v-if="get(item, props.labelKey as string) || !!slots[item.slot ? `${item.slot}-label`: 'item-label']" :class="ui.itemLabel({ class: uiOverride?.itemLabel, active })">
|
||||
<slot :name="item.slot ? `${item.slot}-label`: 'item-label'" :item="(item as T)" :active="active" :index="index">
|
||||
<span v-if="get(item, props.labelKey as string) || !!slots[(item.slot ? `${item.slot}-label`: 'item-label') as keyof ContextMenuSlots<T>]" :class="ui.itemLabel({ class: uiOverride?.itemLabel, active })">
|
||||
<slot :name="((item.slot ? `${item.slot}-label`: 'item-label') as keyof ContextMenuSlots<T>)" :item="item" :active="active" :index="index">
|
||||
{{ get(item, props.labelKey as string) }}
|
||||
</slot>
|
||||
|
||||
@@ -78,7 +85,7 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
|
||||
</span>
|
||||
|
||||
<span :class="ui.itemTrailing({ class: uiOverride?.itemTrailing })">
|
||||
<slot :name="item.slot ? `${item.slot}-trailing`: 'item-trailing'" :item="(item as T)" :active="active" :index="index">
|
||||
<slot :name="((item.slot ? `${item.slot}-trailing`: 'item-trailing') as keyof ContextMenuSlots<T>)" :item="item" :active="active" :index="index">
|
||||
<UIcon v-if="item.children?.length" :name="appConfig.ui.icons.chevronRight" :class="ui.itemTrailingIcon({ class: uiOverride?.itemTrailingIcon, color: item?.color, active })" />
|
||||
<span v-else-if="item.kbds?.length" :class="ui.itemTrailingKbds({ class: uiOverride?.itemTrailingKbds })">
|
||||
<UKbd v-for="(kbd, kbdIndex) in item.kbds" :key="kbdIndex" :size="((props.uiOverride?.itemTrailingKbdsSize || ui.itemTrailingKbdsSize()) as KbdProps['size'])" v-bind="typeof kbd === 'string' ? { value: kbd } : kbd" />
|
||||
@@ -117,7 +124,7 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
|
||||
:ui="ui"
|
||||
:ui-override="uiOverride"
|
||||
:portal="portal"
|
||||
:items="item.children"
|
||||
:items="(item.children as T)"
|
||||
:align-offset="-4"
|
||||
:label-key="labelKey"
|
||||
:checked-icon="checkedIcon"
|
||||
@@ -126,7 +133,7 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
|
||||
v-bind="item.content"
|
||||
>
|
||||
<template v-for="(_, name) in proxySlots" #[name]="slotData: any">
|
||||
<slot :name="name" v-bind="slotData" />
|
||||
<slot :name="(name as keyof ContextMenuSlots<T>)" v-bind="slotData" />
|
||||
</template>
|
||||
</UContextMenuContent>
|
||||
</ContextMenu.Sub>
|
||||
|
||||
@@ -7,7 +7,14 @@ import _appConfig from '#build/app.config'
|
||||
import theme from '#build/ui/dropdown-menu'
|
||||
import { tv } from '../utils/tv'
|
||||
import type { AvatarProps, KbdProps, LinkProps } from '../types'
|
||||
import type { DynamicSlots, PartialString, EmitsToProps } from '../types/utils'
|
||||
import type {
|
||||
ArrayOrNested,
|
||||
DynamicSlots,
|
||||
MergeTypes,
|
||||
NestedItem,
|
||||
PartialString,
|
||||
EmitsToProps
|
||||
} from '../types/utils'
|
||||
|
||||
const appConfigDropdownMenu = _appConfig as AppConfig & { ui: { dropdownMenu: Partial<typeof theme> } }
|
||||
|
||||
@@ -36,17 +43,18 @@ export interface DropdownMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'cust
|
||||
checked?: boolean
|
||||
open?: boolean
|
||||
defaultOpen?: boolean
|
||||
children?: DropdownMenuItem[] | DropdownMenuItem[][]
|
||||
children?: ArrayOrNested<DropdownMenuItem>
|
||||
onSelect?(e: Event): void
|
||||
onUpdateChecked?(checked: boolean): void
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface DropdownMenuProps<T> extends Omit<DropdownMenuRootProps, 'dir'> {
|
||||
export interface DropdownMenuProps<T extends ArrayOrNested<DropdownMenuItem> = ArrayOrNested<DropdownMenuItem>> extends Omit<DropdownMenuRootProps, 'dir'> {
|
||||
/**
|
||||
* @defaultValue 'md'
|
||||
*/
|
||||
size?: DropdownMenuVariants['size']
|
||||
items?: T[] | T[][]
|
||||
items?: T
|
||||
/**
|
||||
* The icon displayed when an item is checked.
|
||||
* @defaultValue appConfig.ui.icons.check
|
||||
@@ -85,7 +93,7 @@ export interface DropdownMenuProps<T> extends Omit<DropdownMenuRootProps, 'dir'>
|
||||
* The key used to get the label from the item.
|
||||
* @defaultValue 'label'
|
||||
*/
|
||||
labelKey?: string
|
||||
labelKey?: keyof NestedItem<T>
|
||||
disabled?: boolean
|
||||
class?: any
|
||||
ui?: PartialString<typeof dropdownMenu.slots>
|
||||
@@ -93,19 +101,22 @@ export interface DropdownMenuProps<T> extends Omit<DropdownMenuRootProps, 'dir'>
|
||||
|
||||
export interface DropdownMenuEmits extends DropdownMenuRootEmits {}
|
||||
|
||||
type SlotProps<T> = (props: { item: T, active?: boolean, index: number }) => any
|
||||
type SlotProps<T extends DropdownMenuItem> = (props: { item: T, active?: boolean, index: number }) => any
|
||||
|
||||
export type DropdownMenuSlots<T extends { slot?: string }> = {
|
||||
export type DropdownMenuSlots<
|
||||
A extends ArrayOrNested<DropdownMenuItem> = ArrayOrNested<DropdownMenuItem>,
|
||||
T extends NestedItem<A> = NestedItem<A>
|
||||
> = {
|
||||
'default'(props: { open: boolean }): any
|
||||
'item': SlotProps<T>
|
||||
'item-leading': SlotProps<T>
|
||||
'item-label': SlotProps<T>
|
||||
'item-trailing': SlotProps<T>
|
||||
} & DynamicSlots<T, SlotProps<T>>
|
||||
} & DynamicSlots<MergeTypes<T>, 'leading' | 'label' | 'trailing', { active?: boolean, index: number }>
|
||||
|
||||
</script>
|
||||
|
||||
<script setup lang="ts" generic="T extends DropdownMenuItem">
|
||||
<script setup lang="ts" generic="T extends ArrayOrNested<DropdownMenuItem>">
|
||||
import { computed, toRef } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { DropdownMenuRoot, DropdownMenuTrigger, DropdownMenuArrow, useForwardPropsEmits } from 'reka-ui'
|
||||
@@ -125,7 +136,7 @@ const slots = defineSlots<DropdownMenuSlots<T>>()
|
||||
const rootProps = useForwardPropsEmits(reactivePick(props, 'defaultOpen', 'open', 'modal'), emits)
|
||||
const contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffset: 8, collisionPadding: 8 }) as DropdownMenuContentProps)
|
||||
const arrowProps = toRef(() => props.arrow as DropdownMenuArrowProps)
|
||||
const proxySlots = omit(slots, ['default']) as Record<string, DropdownMenuSlots<T>[string]>
|
||||
const proxySlots = omit(slots, ['default'])
|
||||
|
||||
const ui = computed(() => dropdownMenu({
|
||||
size: props.size
|
||||
@@ -145,13 +156,13 @@ const ui = computed(() => dropdownMenu({
|
||||
v-bind="contentProps"
|
||||
:items="items"
|
||||
:portal="portal"
|
||||
:label-key="labelKey"
|
||||
:label-key="(labelKey as keyof NestedItem<T>)"
|
||||
:checked-icon="checkedIcon"
|
||||
:loading-icon="loadingIcon"
|
||||
:external-icon="externalIcon"
|
||||
>
|
||||
<template v-for="(_, name) in proxySlots" #[name]="slotData">
|
||||
<slot :name="name" v-bind="slotData" />
|
||||
<slot :name="(name as keyof DropdownMenuSlots<T>)" v-bind="slotData" />
|
||||
</template>
|
||||
|
||||
<DropdownMenuArrow v-if="!!arrow" v-bind="arrowProps" :class="ui.arrow({ class: props.ui?.arrow })" />
|
||||
|
||||
@@ -4,14 +4,15 @@ import type { DropdownMenuContentProps as RekaDropdownMenuContentProps, Dropdown
|
||||
import theme from '#build/ui/dropdown-menu'
|
||||
import { tv } from '../utils/tv'
|
||||
import type { KbdProps, AvatarProps, DropdownMenuItem, DropdownMenuSlots } from '../types'
|
||||
import type { ArrayOrNested, NestedItem } from '../types/utils'
|
||||
|
||||
const _dropdownMenu = tv(theme)()
|
||||
|
||||
interface DropdownMenuContentProps<T> extends Omit<RekaDropdownMenuContentProps, 'as' | 'asChild' | 'forceMount'> {
|
||||
items?: T[] | T[][]
|
||||
interface DropdownMenuContentProps<T extends ArrayOrNested<DropdownMenuItem>> extends Omit<RekaDropdownMenuContentProps, 'as' | 'asChild' | 'forceMount'> {
|
||||
items?: T
|
||||
portal?: boolean
|
||||
sub?: boolean
|
||||
labelKey: string
|
||||
labelKey: keyof NestedItem<T>
|
||||
/**
|
||||
* @IconifyIcon
|
||||
*/
|
||||
@@ -31,19 +32,19 @@ interface DropdownMenuContentProps<T> extends Omit<RekaDropdownMenuContentProps,
|
||||
|
||||
interface DropdownMenuContentEmits extends RekaDropdownMenuContentEmits {}
|
||||
|
||||
type DropdownMenuContentSlots<T extends { slot?: string }> = Omit<DropdownMenuSlots<T>, 'default'> & {
|
||||
type DropdownMenuContentSlots<T extends ArrayOrNested<DropdownMenuItem>> = Omit<DropdownMenuSlots<T>, 'default'> & {
|
||||
default(props?: {}): any
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<script setup lang="ts" generic="T extends DropdownMenuItem">
|
||||
<script setup lang="ts" generic="T extends ArrayOrNested<DropdownMenuItem>">
|
||||
import { computed } from 'vue'
|
||||
import { DropdownMenu } from 'reka-ui/namespaced'
|
||||
import { useForwardPropsEmits } from 'reka-ui'
|
||||
import { reactiveOmit, createReusableTemplate } from '@vueuse/core'
|
||||
import { useAppConfig } from '#imports'
|
||||
import { omit, get } from '../utils'
|
||||
import { omit, get, isArrayOfArray } from '../utils'
|
||||
import { pickLinkProps } from '../utils/link'
|
||||
import ULinkBase from './LinkBase.vue'
|
||||
import ULink from './Link.vue'
|
||||
@@ -59,24 +60,30 @@ const slots = defineSlots<DropdownMenuContentSlots<T>>()
|
||||
|
||||
const appConfig = useAppConfig()
|
||||
const contentProps = useForwardPropsEmits(reactiveOmit(props, 'sub', 'items', 'portal', 'labelKey', 'checkedIcon', 'loadingIcon', 'externalIcon', 'class', 'ui', 'uiOverride'), emits)
|
||||
const proxySlots = omit(slots, ['default']) as Record<string, DropdownMenuContentSlots<T>[string]>
|
||||
const proxySlots = omit(slots, ['default'])
|
||||
|
||||
const [DefineItemTemplate, ReuseItemTemplate] = createReusableTemplate<{ item: DropdownMenuItem, active?: boolean, index: number }>()
|
||||
|
||||
const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0]) ? props.items : [props.items]) as T[][] : [])
|
||||
const groups = computed<DropdownMenuItem[][]>(() =>
|
||||
props.items?.length
|
||||
? isArrayOfArray(props.items)
|
||||
? props.items
|
||||
: [props.items]
|
||||
: []
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DefineItemTemplate v-slot="{ item, active, index }">
|
||||
<slot :name="item.slot || 'item'" :item="(item as T)" :index="index">
|
||||
<slot :name="item.slot ? `${item.slot}-leading`: 'item-leading'" :item="(item as T)" :active="active" :index="index">
|
||||
<slot :name="((item.slot || 'item') as keyof DropdownMenuContentSlots<T>)" :item="(item as Extract<NestedItem<T>, { slot: string; }>)" :index="index">
|
||||
<slot :name="((item.slot ? `${item.slot}-leading`: 'item-leading') as keyof DropdownMenuContentSlots<T>)" :item="(item as Extract<NestedItem<T>, { slot: string; }>)" :active="active" :index="index">
|
||||
<UIcon v-if="item.loading" :name="loadingIcon || appConfig.ui.icons.loading" :class="ui.itemLeadingIcon({ class: uiOverride?.itemLeadingIcon, color: item?.color, loading: true })" />
|
||||
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: uiOverride?.itemLeadingIcon, color: item?.color, active })" />
|
||||
<UAvatar v-else-if="item.avatar" :size="((props.uiOverride?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: uiOverride?.itemLeadingAvatar, active })" />
|
||||
</slot>
|
||||
|
||||
<span v-if="get(item, props.labelKey as string) || !!slots[item.slot ? `${item.slot}-label`: 'item-label']" :class="ui.itemLabel({ class: uiOverride?.itemLabel, active })">
|
||||
<slot :name="item.slot ? `${item.slot}-label`: 'item-label'" :item="(item as T)" :active="active" :index="index">
|
||||
<span v-if="get(item, props.labelKey as string) || !!slots[(item.slot ? `${item.slot}-label`: 'item-label') as keyof DropdownMenuContentSlots<T>]" :class="ui.itemLabel({ class: uiOverride?.itemLabel, active })">
|
||||
<slot :name="((item.slot ? `${item.slot}-label`: 'item-label') as keyof DropdownMenuContentSlots<T>)" :item="(item as Extract<NestedItem<T>, { slot: string; }>)" :active="active" :index="index">
|
||||
{{ get(item, props.labelKey as string) }}
|
||||
</slot>
|
||||
|
||||
@@ -84,7 +91,7 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
|
||||
</span>
|
||||
|
||||
<span :class="ui.itemTrailing({ class: uiOverride?.itemTrailing })">
|
||||
<slot :name="item.slot ? `${item.slot}-trailing`: 'item-trailing'" :item="(item as T)" :active="active" :index="index">
|
||||
<slot :name="((item.slot ? `${item.slot}-trailing`: 'item-trailing') as keyof DropdownMenuContentSlots<T>)" :item="(item as Extract<NestedItem<T>, { slot: string; }>)" :active="active" :index="index">
|
||||
<UIcon v-if="item.children?.length" :name="appConfig.ui.icons.chevronRight" :class="ui.itemTrailingIcon({ class: uiOverride?.itemTrailingIcon, color: item?.color, active })" />
|
||||
<span v-else-if="item.kbds?.length" :class="ui.itemTrailingKbds({ class: uiOverride?.itemTrailingKbds })">
|
||||
<UKbd v-for="(kbd, kbdIndex) in item.kbds" :key="kbdIndex" :size="((props.uiOverride?.itemTrailingKbdsSize || ui.itemTrailingKbdsSize()) as KbdProps['size'])" v-bind="typeof kbd === 'string' ? { value: kbd } : kbd" />
|
||||
@@ -123,7 +130,7 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
|
||||
:ui="ui"
|
||||
:ui-override="uiOverride"
|
||||
:portal="portal"
|
||||
:items="item.children"
|
||||
:items="(item.children as T)"
|
||||
side="right"
|
||||
align="start"
|
||||
:align-offset="-4"
|
||||
@@ -135,7 +142,7 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
|
||||
v-bind="item.content"
|
||||
>
|
||||
<template v-for="(_, name) in proxySlots" #[name]="slotData: any">
|
||||
<slot :name="name" v-bind="slotData" />
|
||||
<slot :name="(name as keyof DropdownMenuContentSlots<T>)" v-bind="slotData" />
|
||||
</template>
|
||||
</UDropdownMenuContent>
|
||||
</DropdownMenu.Sub>
|
||||
|
||||
@@ -1,20 +1,29 @@
|
||||
<script lang="ts">
|
||||
import type { InputHTMLAttributes } from 'vue'
|
||||
import type { VariantProps } from 'tailwind-variants'
|
||||
import type { ComboboxRootProps, ComboboxRootEmits, ComboboxContentProps, ComboboxContentEmits, ComboboxArrowProps, AcceptableValue } from 'reka-ui'
|
||||
import type { ComboboxRootProps, ComboboxRootEmits, ComboboxContentProps, ComboboxContentEmits, ComboboxArrowProps } from 'reka-ui'
|
||||
import type { AppConfig } from '@nuxt/schema'
|
||||
import _appConfig from '#build/app.config'
|
||||
import theme from '#build/ui/input-menu'
|
||||
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
|
||||
import { tv } from '../utils/tv'
|
||||
import type { AvatarProps, ChipProps, InputProps } from '../types'
|
||||
import type { PartialString, MaybeArrayOfArray, MaybeArrayOfArrayItem, SelectModelValue, SelectModelValueEmits, SelectItemKey, EmitsToProps } from '../types/utils'
|
||||
import type {
|
||||
AcceptableValue,
|
||||
ArrayOrNested,
|
||||
GetItemKeys,
|
||||
GetModelValue,
|
||||
GetModelValueEmits,
|
||||
NestedItem,
|
||||
PartialString,
|
||||
EmitsToProps
|
||||
} from '../types/utils'
|
||||
|
||||
const appConfigInputMenu = _appConfig as AppConfig & { ui: { inputMenu: Partial<typeof theme> } }
|
||||
|
||||
const inputMenu = tv({ extend: tv(theme), ...(appConfigInputMenu.ui?.inputMenu || {}) })
|
||||
|
||||
export interface InputMenuItem {
|
||||
interface _InputMenuItem {
|
||||
label?: string
|
||||
/**
|
||||
* @IconifyIcon
|
||||
@@ -27,13 +36,16 @@ export interface InputMenuItem {
|
||||
* @defaultValue 'item'
|
||||
*/
|
||||
type?: 'label' | 'separator' | 'item'
|
||||
value?: string | number
|
||||
disabled?: boolean
|
||||
onSelect?(e?: Event): void
|
||||
[key: string]: any
|
||||
}
|
||||
export type InputMenuItem = _InputMenuItem | AcceptableValue | boolean
|
||||
|
||||
type InputMenuVariants = VariantProps<typeof inputMenu>
|
||||
|
||||
export interface InputMenuProps<T extends MaybeArrayOfArrayItem<I>, I extends MaybeArrayOfArray<InputMenuItem | AcceptableValue | boolean> = MaybeArrayOfArray<InputMenuItem | AcceptableValue | boolean>, V extends SelectItemKey<T> | undefined = undefined, M extends boolean = false> extends Pick<ComboboxRootProps<T>, 'open' | 'defaultOpen' | 'disabled' | 'name' | 'resetSearchTermOnBlur' | 'highlightOnHover'>, UseComponentIconsProps {
|
||||
export interface InputMenuProps<T extends ArrayOrNested<InputMenuItem> = ArrayOrNested<InputMenuItem>, VK extends GetItemKeys<T> | undefined = undefined, M extends boolean = false> extends Pick<ComboboxRootProps<T>, 'open' | 'defaultOpen' | 'disabled' | 'name' | 'resetSearchTermOnBlur' | 'highlightOnHover'>, UseComponentIconsProps {
|
||||
/**
|
||||
* The element or component this component should render as.
|
||||
* @defaultValue 'div'
|
||||
@@ -96,17 +108,17 @@ export interface InputMenuProps<T extends MaybeArrayOfArrayItem<I>, I extends Ma
|
||||
* When `items` is an array of objects, select the field to use as the value instead of the object itself.
|
||||
* @defaultValue undefined
|
||||
*/
|
||||
valueKey?: V
|
||||
valueKey?: VK
|
||||
/**
|
||||
* When `items` is an array of objects, select the field to use as the label.
|
||||
* @defaultValue 'label'
|
||||
*/
|
||||
labelKey?: V
|
||||
items?: I
|
||||
labelKey?: keyof NestedItem<T>
|
||||
items?: T
|
||||
/** The value of the InputMenu when initially rendered. Use when you do not need to control the state of the InputMenu. */
|
||||
defaultValue?: SelectModelValue<T, V, M>
|
||||
defaultValue?: GetModelValue<T, VK, M>
|
||||
/** The controlled value of the InputMenu. Can be binded-with with `v-model`. */
|
||||
modelValue?: SelectModelValue<T, V, M>
|
||||
modelValue?: GetModelValue<T, VK, M>
|
||||
/** Whether multiple options can be selected or not. */
|
||||
multiple?: M & boolean
|
||||
/** Highlight the ring color like a focus state. */
|
||||
@@ -130,18 +142,28 @@ export interface InputMenuProps<T extends MaybeArrayOfArrayItem<I>, I extends Ma
|
||||
ui?: PartialString<typeof inputMenu.slots>
|
||||
}
|
||||
|
||||
export type InputMenuEmits<T, V, M extends boolean> = Omit<ComboboxRootEmits<T>, 'update:modelValue'> & {
|
||||
export type InputMenuEmits<A extends ArrayOrNested<InputMenuItem>, VK extends GetItemKeys<A> | undefined, M extends boolean> = Pick<ComboboxRootEmits, 'update:open'> & {
|
||||
change: [payload: Event]
|
||||
blur: [payload: FocusEvent]
|
||||
focus: [payload: FocusEvent]
|
||||
create: [item: string]
|
||||
} & SelectModelValueEmits<T, V, M>
|
||||
/** Event handler when highlighted element changes. */
|
||||
highlight: [payload: {
|
||||
ref: HTMLElement
|
||||
value: GetModelValue<A, VK, M>
|
||||
} | undefined]
|
||||
} & GetModelValueEmits<A, VK, M>
|
||||
|
||||
type SlotProps<T> = (props: { item: T, index: number }) => any
|
||||
type SlotProps<T extends InputMenuItem> = (props: { item: T, index: number }) => any
|
||||
|
||||
export interface InputMenuSlots<T, M extends boolean> {
|
||||
'leading'(props: { modelValue?: M extends true ? T[] : T, open: boolean, ui: any }): any
|
||||
'trailing'(props: { modelValue?: M extends true ? T[] : T, open: boolean, ui: any }): any
|
||||
export interface InputMenuSlots<
|
||||
A extends ArrayOrNested<InputMenuItem> = ArrayOrNested<InputMenuItem>,
|
||||
VK extends GetItemKeys<A> | undefined = undefined,
|
||||
M extends boolean = false,
|
||||
T extends NestedItem<A> = NestedItem<A>
|
||||
> {
|
||||
'leading'(props: { modelValue?: GetModelValue<A, VK, M>, open: boolean, ui: ReturnType<typeof inputMenu> }): any
|
||||
'trailing'(props: { modelValue?: GetModelValue<A, VK, M>, open: boolean, ui: ReturnType<typeof inputMenu> }): any
|
||||
'empty'(props: { searchTerm?: string }): any
|
||||
'item': SlotProps<T>
|
||||
'item-leading': SlotProps<T>
|
||||
@@ -153,7 +175,7 @@ export interface InputMenuSlots<T, M extends boolean> {
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts" generic="T extends MaybeArrayOfArrayItem<I>, I extends MaybeArrayOfArray<InputMenuItem | AcceptableValue | boolean> = MaybeArrayOfArray<InputMenuItem | AcceptableValue | boolean>, V extends SelectItemKey<T> | undefined = undefined, M extends boolean = false">
|
||||
<script setup lang="ts" generic="T extends ArrayOrNested<InputMenuItem>, VK extends GetItemKeys<T> | undefined = undefined, M extends boolean = false">
|
||||
import { computed, ref, toRef, onMounted, toRaw } from 'vue'
|
||||
import { ComboboxRoot, ComboboxArrow, ComboboxAnchor, ComboboxInput, ComboboxTrigger, ComboboxPortal, ComboboxContent, ComboboxViewport, ComboboxEmpty, ComboboxGroup, ComboboxLabel, ComboboxSeparator, ComboboxItem, ComboboxItemIndicator, TagsInputRoot, TagsInputItem, TagsInputItemText, TagsInputItemDelete, TagsInputInput, useForwardPropsEmits, useFilter } from 'reka-ui'
|
||||
import { defu } from 'defu'
|
||||
@@ -164,21 +186,21 @@ import { useButtonGroup } from '../composables/useButtonGroup'
|
||||
import { useComponentIcons } from '../composables/useComponentIcons'
|
||||
import { useFormField } from '../composables/useFormField'
|
||||
import { useLocale } from '../composables/useLocale'
|
||||
import { get, compare } from '../utils'
|
||||
import { compare, get, isArrayOfArray } from '../utils'
|
||||
import UIcon from './Icon.vue'
|
||||
import UAvatar from './Avatar.vue'
|
||||
import UChip from './Chip.vue'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
const props = withDefaults(defineProps<InputMenuProps<T, I, V, M>>(), {
|
||||
const props = withDefaults(defineProps<InputMenuProps<T, VK, M>>(), {
|
||||
type: 'text',
|
||||
autofocusDelay: 0,
|
||||
portal: true,
|
||||
labelKey: 'label' as never
|
||||
})
|
||||
const emits = defineEmits<InputMenuEmits<T, V, M>>()
|
||||
const slots = defineSlots<InputMenuSlots<T, M>>()
|
||||
const emits = defineEmits<InputMenuEmits<T, VK, M>>()
|
||||
const slots = defineSlots<InputMenuSlots<T, VK, M>>()
|
||||
|
||||
const searchTerm = defineModel<string>('searchTerm', { default: '' })
|
||||
|
||||
@@ -219,9 +241,15 @@ function displayValue(value: T): string {
|
||||
return item && (typeof item === 'object' ? get(item, props.labelKey as string) : item)
|
||||
}
|
||||
|
||||
const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0]) ? props.items : [props.items]) as InputMenuItem[][] : [])
|
||||
const groups = computed<InputMenuItem[][]>(() =>
|
||||
props.items?.length
|
||||
? isArrayOfArray(props.items)
|
||||
? props.items
|
||||
: [props.items]
|
||||
: []
|
||||
)
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
const items = computed(() => groups.value.flatMap(group => group) as T[])
|
||||
const items = computed(() => groups.value.flatMap(group => group))
|
||||
|
||||
const filteredGroups = computed(() => {
|
||||
if (props.ignoreFilter || !searchTerm.value) {
|
||||
@@ -230,9 +258,9 @@ const filteredGroups = computed(() => {
|
||||
|
||||
const fields = Array.isArray(props.filterFields) ? props.filterFields : [props.labelKey] as string[]
|
||||
|
||||
return groups.value.map(items => items.filter((item) => {
|
||||
if (typeof item !== 'object') {
|
||||
return contains(item, searchTerm.value)
|
||||
return groups.value.map(group => group.filter((item) => {
|
||||
if (typeof item !== 'object' || item === null) {
|
||||
return contains(String(item), searchTerm.value)
|
||||
}
|
||||
|
||||
if (item.type && ['label', 'separator'].includes(item.type)) {
|
||||
@@ -240,19 +268,21 @@ const filteredGroups = computed(() => {
|
||||
}
|
||||
|
||||
return fields.some(field => contains(get(item, field), searchTerm.value))
|
||||
})).filter(group => group.filter(item => !item.type || !['label', 'separator'].includes(item.type)).length > 0)
|
||||
})).filter(group => group.filter(item =>
|
||||
isInputItem(item) && (!item.type || !['label', 'separator'].includes(item.type))
|
||||
).length > 0)
|
||||
})
|
||||
const filteredItems = computed(() => filteredGroups.value.flatMap(group => group) as T[])
|
||||
const filteredItems = computed(() => filteredGroups.value.flatMap(group => group))
|
||||
|
||||
const createItem = computed(() => {
|
||||
if (!props.createItem || !searchTerm.value) {
|
||||
return false
|
||||
}
|
||||
|
||||
const newItem = props.valueKey ? { [props.valueKey]: searchTerm.value } as T : searchTerm.value
|
||||
const newItem = props.valueKey ? { [props.valueKey]: searchTerm.value } as NestedItem<T> : searchTerm.value
|
||||
|
||||
if ((typeof props.createItem === 'object' && props.createItem.when === 'always') || props.createItem === 'always') {
|
||||
return !filteredItems.value.find(item => compare(item, newItem, props.valueKey))
|
||||
return !filteredItems.value.find(item => compare(item, newItem, String(props.valueKey)))
|
||||
}
|
||||
|
||||
return !filteredItems.value.length
|
||||
@@ -307,12 +337,16 @@ function onUpdateOpen(value: boolean) {
|
||||
|
||||
function onRemoveTag(event: any) {
|
||||
if (props.multiple) {
|
||||
const modelValue = props.modelValue as SelectModelValue<T, V, true>
|
||||
const modelValue = props.modelValue as GetModelValue<T, VK, true>
|
||||
const filteredValue = modelValue.filter(value => !isEqual(value, event))
|
||||
emits('update:modelValue', filteredValue as SelectModelValue<T, V, M>)
|
||||
emits('update:modelValue', filteredValue as GetModelValue<T, VK, M>)
|
||||
}
|
||||
}
|
||||
|
||||
function isInputItem(item: InputMenuItem): item is _InputMenuItem {
|
||||
return typeof item === 'object' && item !== null
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
inputRef
|
||||
})
|
||||
@@ -362,15 +396,15 @@ defineExpose({
|
||||
@focus="onFocus"
|
||||
@remove-tag="onRemoveTag"
|
||||
>
|
||||
<TagsInputItem v-for="(item, index) in tags" :key="index" :value="(item as string)" :class="ui.tagsItem({ class: props.ui?.tagsItem })">
|
||||
<TagsInputItem v-for="(item, index) in tags" :key="index" :value="item" :class="ui.tagsItem({ class: props.ui?.tagsItem })">
|
||||
<TagsInputItemText :class="ui.tagsItemText({ class: props.ui?.tagsItemText })">
|
||||
<slot name="tags-item-text" :item="(item as T)" :index="index">
|
||||
<slot name="tags-item-text" :item="(item as NestedItem<T>)" :index="index">
|
||||
{{ displayValue(item as T) }}
|
||||
</slot>
|
||||
</TagsInputItemText>
|
||||
|
||||
<TagsInputItemDelete :class="ui.tagsItemDelete({ class: props.ui?.tagsItemDelete })" :disabled="disabled">
|
||||
<slot name="tags-item-delete" :item="(item as T)" :index="index">
|
||||
<slot name="tags-item-delete" :item="(item as NestedItem<T>)" :index="index">
|
||||
<UIcon :name="deleteIcon || appConfig.ui.icons.close" :class="ui.tagsItemDeleteIcon({ class: props.ui?.tagsItemDeleteIcon })" />
|
||||
</slot>
|
||||
</TagsInputItemDelete>
|
||||
@@ -401,14 +435,14 @@ defineExpose({
|
||||
/>
|
||||
|
||||
<span v-if="isLeading || !!avatar || !!slots.leading" :class="ui.leading({ class: props.ui?.leading })">
|
||||
<slot name="leading" :model-value="(modelValue as M extends true ? T[] : T)" :open="open" :ui="ui">
|
||||
<slot name="leading" :model-value="(modelValue as GetModelValue<T, VK, M>)" :open="open" :ui="ui">
|
||||
<UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
|
||||
<UAvatar v-else-if="!!avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
|
||||
</slot>
|
||||
</span>
|
||||
|
||||
<ComboboxTrigger v-if="isTrailing || !!slots.trailing" :class="ui.trailing({ class: props.ui?.trailing })">
|
||||
<slot name="trailing" :model-value="(modelValue as M extends true ? T[] : T)" :open="open" :ui="ui">
|
||||
<slot name="trailing" :model-value="(modelValue as GetModelValue<T, VK, M>)" :open="open" :ui="ui">
|
||||
<UIcon v-if="trailingIconName" :name="trailingIconName" :class="ui.trailingIcon({ class: props.ui?.trailingIcon })" />
|
||||
</slot>
|
||||
</ComboboxTrigger>
|
||||
@@ -427,25 +461,25 @@ defineExpose({
|
||||
|
||||
<ComboboxGroup v-for="(group, groupIndex) in filteredGroups" :key="`group-${groupIndex}`" :class="ui.group({ class: props.ui?.group })">
|
||||
<template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
|
||||
<ComboboxLabel v-if="item?.type === 'label'" :class="ui.label({ class: props.ui?.label })">
|
||||
<ComboboxLabel v-if="isInputItem(item) && item.type === 'label'" :class="ui.label({ class: props.ui?.label })">
|
||||
{{ get(item, props.labelKey as string) }}
|
||||
</ComboboxLabel>
|
||||
|
||||
<ComboboxSeparator v-else-if="item?.type === 'separator'" :class="ui.separator({ class: props.ui?.separator })" />
|
||||
<ComboboxSeparator v-else-if="isInputItem(item) && item.type === 'separator'" :class="ui.separator({ class: props.ui?.separator })" />
|
||||
|
||||
<ComboboxItem
|
||||
v-else
|
||||
:class="ui.item({ class: props.ui?.item })"
|
||||
:disabled="item.disabled"
|
||||
:value="valueKey && typeof item === 'object' ? get(item, props.valueKey as string) : item"
|
||||
@select="item.onSelect"
|
||||
:disabled="isInputItem(item) && item.disabled"
|
||||
:value="props.valueKey && isInputItem(item) ? get(item, String(props.valueKey)) : item"
|
||||
@select="isInputItem(item) && item.onSelect"
|
||||
>
|
||||
<slot name="item" :item="(item as T)" :index="index">
|
||||
<slot name="item-leading" :item="(item as T)" :index="index">
|
||||
<UIcon v-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon })" />
|
||||
<UAvatar v-else-if="item.avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
|
||||
<slot name="item" :item="(item as NestedItem<T>)" :index="index">
|
||||
<slot name="item-leading" :item="(item as NestedItem<T>)" :index="index">
|
||||
<UIcon v-if="isInputItem(item) && item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon })" />
|
||||
<UAvatar v-else-if="isInputItem(item) && item.avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
|
||||
<UChip
|
||||
v-else-if="item.chip"
|
||||
v-else-if="isInputItem(item) && item.chip"
|
||||
:size="((props.ui?.itemLeadingChipSize || ui.itemLeadingChipSize()) as ChipProps['size'])"
|
||||
inset
|
||||
standalone
|
||||
@@ -455,13 +489,13 @@ defineExpose({
|
||||
</slot>
|
||||
|
||||
<span :class="ui.itemLabel({ class: props.ui?.itemLabel })">
|
||||
<slot name="item-label" :item="(item as T)" :index="index">
|
||||
{{ typeof item === 'object' ? get(item, props.labelKey as string) : item }}
|
||||
<slot name="item-label" :item="(item as NestedItem<T>)" :index="index">
|
||||
{{ isInputItem(item) ? get(item, props.labelKey as string) : item }}
|
||||
</slot>
|
||||
</span>
|
||||
|
||||
<span :class="ui.itemTrailing({ class: props.ui?.itemTrailing })">
|
||||
<slot name="item-trailing" :item="(item as T)" :index="index" />
|
||||
<slot name="item-trailing" :item="(item as NestedItem<T>)" :index="index" />
|
||||
|
||||
<ComboboxItemIndicator as-child>
|
||||
<UIcon :name="selectedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: props.ui?.itemTrailingIcon })" />
|
||||
|
||||
@@ -67,7 +67,7 @@ export interface ModalSlots {
|
||||
header(props?: {}): any
|
||||
title(props?: {}): any
|
||||
description(props?: {}): any
|
||||
close(props: { ui: any }): any
|
||||
close(props: { ui: ReturnType<typeof modal> }): any
|
||||
body(props?: {}): any
|
||||
footer(props?: {}): any
|
||||
}
|
||||
|
||||
@@ -7,15 +7,23 @@ import _appConfig from '#build/app.config'
|
||||
import theme from '#build/ui/navigation-menu'
|
||||
import { tv } from '../utils/tv'
|
||||
import type { AvatarProps, BadgeProps, LinkProps } from '../types'
|
||||
import type { DynamicSlots, MaybeArrayOfArray, MaybeArrayOfArrayItem, PartialString, EmitsToProps } from '../types/utils'
|
||||
import type {
|
||||
ArrayOrNested,
|
||||
DynamicSlots,
|
||||
MergeTypes,
|
||||
NestedItem,
|
||||
PartialString,
|
||||
EmitsToProps
|
||||
} from '../types/utils'
|
||||
|
||||
const appConfigNavigationMenu = _appConfig as AppConfig & { ui: { navigationMenu: Partial<typeof theme> } }
|
||||
|
||||
const navigationMenu = tv({ extend: tv(theme), ...(appConfigNavigationMenu.ui?.navigationMenu || {}) })
|
||||
|
||||
export interface NavigationMenuChildItem extends Omit<NavigationMenuItem, 'children' | 'type'> {
|
||||
export interface NavigationMenuChildItem extends Omit<NavigationMenuItem, 'type'> {
|
||||
/** Description is only used when `orientation` is `horizontal`. */
|
||||
description?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface NavigationMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'custom'>, Pick<CollapsibleRootProps, 'defaultOpen' | 'open'> {
|
||||
@@ -44,11 +52,12 @@ export interface NavigationMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'cu
|
||||
value?: string
|
||||
children?: NavigationMenuChildItem[]
|
||||
onSelect?(e: Event): void
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
type NavigationMenuVariants = VariantProps<typeof navigationMenu>
|
||||
|
||||
export interface NavigationMenuProps<T> extends Pick<NavigationMenuRootProps, 'modelValue' | 'defaultValue' | 'delayDuration' | 'disableClickTrigger' | 'disableHoverTrigger' | 'skipDelayDuration' | 'disablePointerLeaveClose' | 'unmountOnHide'> {
|
||||
export interface NavigationMenuProps<T extends ArrayOrNested<NavigationMenuItem> = ArrayOrNested<NavigationMenuItem>> extends Pick<NavigationMenuRootProps, 'modelValue' | 'defaultValue' | 'delayDuration' | 'disableClickTrigger' | 'disableHoverTrigger' | 'skipDelayDuration' | 'disablePointerLeaveClose' | 'unmountOnHide'> {
|
||||
/**
|
||||
* The element or component this component should render as.
|
||||
* @defaultValue 'div'
|
||||
@@ -110,31 +119,34 @@ export interface NavigationMenuProps<T> extends Pick<NavigationMenuRootProps, 'm
|
||||
* The key used to get the label from the item.
|
||||
* @defaultValue 'label'
|
||||
*/
|
||||
labelKey?: string
|
||||
labelKey?: keyof NestedItem<T>
|
||||
class?: any
|
||||
ui?: PartialString<typeof navigationMenu.slots>
|
||||
}
|
||||
|
||||
export interface NavigationMenuEmits extends NavigationMenuRootEmits {}
|
||||
|
||||
type SlotProps<T> = (props: { item: T, index: number, active?: boolean }) => any
|
||||
type SlotProps<T extends NavigationMenuItem> = (props: { item: T, index: number, active?: boolean }) => any
|
||||
|
||||
export type NavigationMenuSlots<T extends { slot?: string }> = {
|
||||
export type NavigationMenuSlots<
|
||||
A extends ArrayOrNested<NavigationMenuItem> = ArrayOrNested<NavigationMenuItem>,
|
||||
T extends NestedItem<A> = NestedItem<A>
|
||||
> = {
|
||||
'item': SlotProps<T>
|
||||
'item-leading': SlotProps<T>
|
||||
'item-label': SlotProps<T>
|
||||
'item-trailing': SlotProps<T>
|
||||
'item-content': SlotProps<T>
|
||||
} & DynamicSlots<T, SlotProps<T>>
|
||||
} & DynamicSlots<MergeTypes<T>, 'leading' | 'label' | 'trailing' | 'content', { index: number, active?: boolean }>
|
||||
|
||||
</script>
|
||||
|
||||
<script setup lang="ts" generic="T extends MaybeArrayOfArrayItem<I>, I extends MaybeArrayOfArray<NavigationMenuItem>">
|
||||
<script setup lang="ts" generic="T extends ArrayOrNested<NavigationMenuItem>">
|
||||
import { computed, toRef } from 'vue'
|
||||
import { NavigationMenuRoot, NavigationMenuList, NavigationMenuItem, NavigationMenuTrigger, NavigationMenuContent, NavigationMenuLink, NavigationMenuIndicator, NavigationMenuViewport, useForwardPropsEmits } from 'reka-ui'
|
||||
import { createReusableTemplate } from '@vueuse/core'
|
||||
import { useAppConfig } from '#imports'
|
||||
import { get } from '../utils'
|
||||
import { get, isArrayOfArray } from '../utils'
|
||||
import { pickLinkProps } from '../utils/link'
|
||||
import ULinkBase from './LinkBase.vue'
|
||||
import ULink from './Link.vue'
|
||||
@@ -143,7 +155,7 @@ import UIcon from './Icon.vue'
|
||||
import UBadge from './Badge.vue'
|
||||
import UCollapsible from './Collapsible.vue'
|
||||
|
||||
const props = withDefaults(defineProps<NavigationMenuProps<I>>(), {
|
||||
const props = withDefaults(defineProps<NavigationMenuProps<T>>(), {
|
||||
orientation: 'horizontal',
|
||||
contentOrientation: 'horizontal',
|
||||
externalIcon: true,
|
||||
@@ -170,8 +182,14 @@ const rootProps = useForwardPropsEmits(computed(() => ({
|
||||
const contentProps = toRef(() => props.content)
|
||||
|
||||
const appConfig = useAppConfig()
|
||||
const [DefineLinkTemplate, ReuseLinkTemplate] = createReusableTemplate<{ item: NavigationMenuItem, index: number, active?: boolean }>()
|
||||
const [DefineItemTemplate, ReuseItemTemplate] = createReusableTemplate<{ item: NavigationMenuItem, index: number, level?: number }>({
|
||||
const [DefineLinkTemplate, ReuseLinkTemplate] = createReusableTemplate<
|
||||
{ item: NavigationMenuItem, index: number, active?: boolean },
|
||||
NavigationMenuSlots<T>
|
||||
>()
|
||||
const [DefineItemTemplate, ReuseItemTemplate] = createReusableTemplate<
|
||||
{ item: NavigationMenuItem, index: number, level?: number },
|
||||
NavigationMenuSlots<T>
|
||||
>({
|
||||
props: {
|
||||
item: Object,
|
||||
index: Number,
|
||||
@@ -189,30 +207,36 @@ const ui = computed(() => navigationMenu({
|
||||
highlightColor: props.highlightColor || props.color
|
||||
}))
|
||||
|
||||
const lists = computed(() => props.items?.length ? (Array.isArray(props.items[0]) ? props.items : [props.items]) as T[][] : [])
|
||||
const lists = computed<NavigationMenuItem[][]>(() =>
|
||||
props.items?.length
|
||||
? isArrayOfArray(props.items)
|
||||
? props.items
|
||||
: [props.items]
|
||||
: []
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DefineLinkTemplate v-slot="{ item, active, index }">
|
||||
<slot :name="item.slot || 'item'" :item="(item as T)" :index="index">
|
||||
<slot :name="item.slot ? `${item.slot}-leading` : 'item-leading'" :item="(item as T)" :active="active" :index="index">
|
||||
<slot :name="((item.slot || 'item') as keyof NavigationMenuSlots<T>)" :item="item" :index="index">
|
||||
<slot :name="((item.slot ? `${item.slot}-leading` : 'item-leading') as keyof NavigationMenuSlots<T>)" :item="item" :active="active" :index="index">
|
||||
<UAvatar v-if="item.avatar" :size="((props.ui?.linkLeadingAvatarSize || ui.linkLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.linkLeadingAvatar({ class: props.ui?.linkLeadingAvatar, active, disabled: !!item.disabled })" />
|
||||
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.linkLeadingIcon({ class: props.ui?.linkLeadingIcon, active, disabled: !!item.disabled })" />
|
||||
</slot>
|
||||
|
||||
<span
|
||||
v-if="(!collapsed || orientation !== 'vertical') && (get(item, props.labelKey as string) || !!slots[item.slot ? `${item.slot}-label` : 'item-label'])"
|
||||
v-if="(!collapsed || orientation !== 'vertical') && (get(item, props.labelKey as string) || !!slots[(item.slot ? `${item.slot}-label` : 'item-label') as keyof NavigationMenuSlots<T>])"
|
||||
:class="ui.linkLabel({ class: props.ui?.linkLabel })"
|
||||
>
|
||||
<slot :name="item.slot ? `${item.slot}-label` : 'item-label'" :item="(item as T)" :active="active" :index="index">
|
||||
<slot :name="((item.slot ? `${item.slot}-label` : 'item-label') as keyof NavigationMenuSlots<T>)" :item="item" :active="active" :index="index">
|
||||
{{ get(item, props.labelKey as string) }}
|
||||
</slot>
|
||||
|
||||
<UIcon v-if="item.target === '_blank' && externalIcon !== false" :name="typeof externalIcon === 'string' ? externalIcon : appConfig.ui.icons.external" :class="ui.linkLabelExternalIcon({ class: props.ui?.linkLabelExternalIcon, active })" />
|
||||
</span>
|
||||
|
||||
<span v-if="(!collapsed || orientation !== 'vertical') && (item.badge || (orientation === 'horizontal' && (item.children?.length || !!slots[item.slot ? `${item.slot}-content` : 'item-content'])) || (orientation === 'vertical' && item.children?.length) || item.trailingIcon || !!slots[item.slot ? `${item.slot}-trailing` : 'item-trailing'])" :class="ui.linkTrailing({ class: props.ui?.linkTrailing })">
|
||||
<slot :name="item.slot ? `${item.slot}-trailing` : 'item-trailing'" :item="(item as T)" :active="active" :index="index">
|
||||
<span v-if="(!collapsed || orientation !== 'vertical') && (item.badge || (orientation === 'horizontal' && (item.children?.length || !!slots[(item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>])) || (orientation === 'vertical' && item.children?.length) || item.trailingIcon || !!slots[(item.slot ? `${item.slot}-trailing` : 'item-trailing') as keyof NavigationMenuSlots<T>])" :class="ui.linkTrailing({ class: props.ui?.linkTrailing })">
|
||||
<slot :name="((item.slot ? `${item.slot}-trailing` : 'item-trailing') as keyof NavigationMenuSlots<T>)" :item="item" :active="active" :index="index">
|
||||
<UBadge
|
||||
v-if="item.badge"
|
||||
color="neutral"
|
||||
@@ -222,7 +246,7 @@ const lists = computed(() => props.items?.length ? (Array.isArray(props.items[0]
|
||||
:class="ui.linkTrailingBadge({ class: props.ui?.linkTrailingBadge })"
|
||||
/>
|
||||
|
||||
<UIcon v-if="(orientation === 'horizontal' && (item.children?.length || !!slots[item.slot ? `${item.slot}-content` : 'item-content'])) || (orientation === 'vertical' && item.children?.length)" :name="item.trailingIcon || trailingIcon || appConfig.ui.icons.chevronDown" :class="ui.linkTrailingIcon({ class: props.ui?.linkTrailingIcon, active })" />
|
||||
<UIcon v-if="(orientation === 'horizontal' && (item.children?.length || !!slots[(item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>])) || (orientation === 'vertical' && item.children?.length)" :name="item.trailingIcon || trailingIcon || appConfig.ui.icons.chevronDown" :class="ui.linkTrailingIcon({ class: props.ui?.linkTrailingIcon, active })" />
|
||||
<UIcon v-else-if="item.trailingIcon" :name="item.trailingIcon" :class="ui.linkTrailingIcon({ class: props.ui?.linkTrailingIcon, active })" />
|
||||
</slot>
|
||||
</span>
|
||||
@@ -239,23 +263,23 @@ const lists = computed(() => props.items?.length ? (Array.isArray(props.items[0]
|
||||
:open="item.open"
|
||||
>
|
||||
<div v-if="orientation === 'vertical' && item.type === 'label'" :class="ui.label({ class: props.ui?.label })">
|
||||
<ReuseLinkTemplate :item="(item as T)" :index="index" />
|
||||
<ReuseLinkTemplate :item="item" :index="index" />
|
||||
</div>
|
||||
<ULink v-else-if="item.type !== 'label'" v-slot="{ active, ...slotProps }" v-bind="(orientation === 'vertical' && item.children?.length && !collapsed) ? {} : pickLinkProps(item as Omit<NavigationMenuItem, 'type'>)" custom>
|
||||
<component
|
||||
:is="(orientation === 'horizontal' && (item.children?.length || !!slots[item.slot ? `${item.slot}-content` : 'item-content'])) ? NavigationMenuTrigger : NavigationMenuLink"
|
||||
:is="(orientation === 'horizontal' && (item.children?.length || !!slots[(item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>])) ? NavigationMenuTrigger : NavigationMenuLink"
|
||||
as-child
|
||||
:active="active || item.active"
|
||||
:disabled="item.disabled"
|
||||
@select="item.onSelect"
|
||||
>
|
||||
<ULinkBase v-bind="slotProps" :class="ui.link({ class: [props.ui?.link, item.class], active: active || item.active, disabled: !!item.disabled, level: orientation === 'horizontal' || level > 0 })">
|
||||
<ReuseLinkTemplate :item="(item as T)" :active="active || item.active" :index="index" />
|
||||
<ReuseLinkTemplate :item="item" :active="active || item.active" :index="index" />
|
||||
</ULinkBase>
|
||||
</component>
|
||||
|
||||
<NavigationMenuContent v-if="orientation === 'horizontal' && (item.children?.length || !!slots[item.slot ? `${item.slot}-content` : 'item-content'])" v-bind="contentProps" :class="ui.content({ class: props.ui?.content })">
|
||||
<slot :name="item.slot ? `${item.slot}-content` : 'item-content'" :item="(item as T)" :active="active" :index="index">
|
||||
<NavigationMenuContent v-if="orientation === 'horizontal' && (item.children?.length || !!slots[(item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>])" v-bind="contentProps" :class="ui.content({ class: props.ui?.content })">
|
||||
<slot :name="((item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>)" :item="item" :active="active" :index="index">
|
||||
<ul :class="ui.childList({ class: props.ui?.childList })">
|
||||
<li v-for="(childItem, childIndex) in item.children" :key="childIndex" :class="ui.childItem({ class: props.ui?.childItem })">
|
||||
<ULink v-slot="{ active: childActive, ...childSlotProps }" v-bind="pickLinkProps(childItem)" custom>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script lang="ts">
|
||||
import type { VariantProps } from 'tailwind-variants'
|
||||
import type { RadioGroupRootProps, RadioGroupRootEmits, AcceptableValue } from 'reka-ui'
|
||||
import type { RadioGroupRootProps, RadioGroupRootEmits } from 'reka-ui'
|
||||
import type { AppConfig } from '@nuxt/schema'
|
||||
import _appConfig from '#build/app.config'
|
||||
import theme from '#build/ui/radio-group'
|
||||
import { tv } from '../utils/tv'
|
||||
import type { AcceptableValue } from '../types/utils'
|
||||
|
||||
const appConfigRadioGroup = _appConfig as AppConfig & { ui: { radioGroup: Partial<typeof theme> } }
|
||||
|
||||
@@ -12,14 +13,16 @@ const radioGroup = tv({ extend: tv(theme), ...(appConfigRadioGroup.ui?.radioGrou
|
||||
|
||||
type RadioGroupVariants = VariantProps<typeof radioGroup>
|
||||
|
||||
export interface RadioGroupItem {
|
||||
export type RadioGroupValue = AcceptableValue
|
||||
export type RadioGroupItem = {
|
||||
label?: string
|
||||
description?: string
|
||||
disabled?: boolean
|
||||
value?: string
|
||||
}
|
||||
[key: string]: any
|
||||
} | RadioGroupValue
|
||||
|
||||
export interface RadioGroupProps<T> extends Pick<RadioGroupRootProps, 'defaultValue' | 'disabled' | 'loop' | 'modelValue' | 'name' | 'required'> {
|
||||
export interface RadioGroupProps<T extends RadioGroupItem = RadioGroupItem> extends Pick<RadioGroupRootProps, 'defaultValue' | 'disabled' | 'loop' | 'modelValue' | 'name' | 'required'> {
|
||||
/**
|
||||
* The element or component this component should render as.
|
||||
* @defaultValue 'div'
|
||||
@@ -63,16 +66,16 @@ export type RadioGroupEmits = RadioGroupRootEmits & {
|
||||
change: [payload: Event]
|
||||
}
|
||||
|
||||
type SlotProps<T> = (props: { item: T, modelValue?: AcceptableValue }) => any
|
||||
type SlotProps<T extends RadioGroupItem> = (props: { item: T & { id: string }, modelValue?: RadioGroupValue }) => any
|
||||
|
||||
export interface RadioGroupSlots<T> {
|
||||
export interface RadioGroupSlots<T extends RadioGroupItem = RadioGroupItem> {
|
||||
legend(props?: {}): any
|
||||
label: SlotProps<T>
|
||||
description: SlotProps<T>
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts" generic="T extends RadioGroupItem | AcceptableValue">
|
||||
<script setup lang="ts" generic="T extends RadioGroupItem">
|
||||
import { computed, useId } from 'vue'
|
||||
import { RadioGroupRoot, RadioGroupItem, RadioGroupIndicator, Label, useForwardPropsEmits } from 'reka-ui'
|
||||
import { reactivePick } from '@vueuse/core'
|
||||
@@ -102,11 +105,19 @@ const ui = computed(() => radioGroup({
|
||||
}))
|
||||
|
||||
function normalizeItem(item: any) {
|
||||
if (['string', 'number', 'boolean'].includes(typeof item)) {
|
||||
if (item === null) {
|
||||
return {
|
||||
id: `${id}:null`,
|
||||
value: undefined,
|
||||
label: undefined
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof item === 'string' || typeof item === 'number') {
|
||||
return {
|
||||
id: `${id}:${item}`,
|
||||
value: item,
|
||||
label: item
|
||||
value: String(item),
|
||||
label: String(item)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,10 +181,10 @@ function onUpdate(value: any) {
|
||||
|
||||
<div :class="ui.wrapper({ class: props.ui?.wrapper })">
|
||||
<Label :class="ui.label({ class: props.ui?.label })" :for="item.id">
|
||||
<slot name="label" :item="item" :model-value="modelValue">{{ item.label }}</slot>
|
||||
<slot name="label" :item="item" :model-value="(modelValue as RadioGroupValue)">{{ item.label }}</slot>
|
||||
</Label>
|
||||
<p v-if="item.description || !!slots.description" :class="ui.description({ class: props.ui?.description })">
|
||||
<slot name="description" :item="item" :model-value="modelValue">
|
||||
<slot name="description" :item="item" :model-value="(modelValue as RadioGroupValue)">
|
||||
{{ item.description }}
|
||||
</slot>
|
||||
</p>
|
||||
|
||||
@@ -1,19 +1,29 @@
|
||||
<script lang="ts">
|
||||
import type { VariantProps } from 'tailwind-variants'
|
||||
import type { SelectRootProps, SelectRootEmits, SelectContentProps, SelectContentEmits, SelectArrowProps, AcceptableValue } from 'reka-ui'
|
||||
import type { SelectRootProps, SelectRootEmits, SelectContentProps, SelectContentEmits, SelectArrowProps } from 'reka-ui'
|
||||
import type { AppConfig } from '@nuxt/schema'
|
||||
import _appConfig from '#build/app.config'
|
||||
import theme from '#build/ui/select'
|
||||
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
|
||||
import { tv } from '../utils/tv'
|
||||
import type { AvatarProps, ChipProps, InputProps } from '../types'
|
||||
import type { PartialString, MaybeArrayOfArray, MaybeArrayOfArrayItem, SelectModelValue, SelectModelValueEmits, SelectItemKey, EmitsToProps } from '../types/utils'
|
||||
import type {
|
||||
AcceptableValue,
|
||||
ArrayOrNested,
|
||||
GetItemKeys,
|
||||
GetItemValue,
|
||||
GetModelValue,
|
||||
GetModelValueEmits,
|
||||
NestedItem,
|
||||
PartialString,
|
||||
EmitsToProps
|
||||
} from '../types/utils'
|
||||
|
||||
const appConfigSelect = _appConfig as AppConfig & { ui: { select: Partial<typeof theme> } }
|
||||
|
||||
const select = tv({ extend: tv(theme), ...(appConfigSelect.ui?.select || {}) })
|
||||
|
||||
export interface SelectItem {
|
||||
interface SelectItemBase {
|
||||
label?: string
|
||||
/**
|
||||
* @IconifyIcon
|
||||
@@ -26,13 +36,15 @@ export interface SelectItem {
|
||||
* @defaultValue 'item'
|
||||
*/
|
||||
type?: 'label' | 'separator' | 'item'
|
||||
value?: string
|
||||
value?: string | number
|
||||
disabled?: boolean
|
||||
[key: string]: any
|
||||
}
|
||||
export type SelectItem = SelectItemBase | AcceptableValue | boolean
|
||||
|
||||
type SelectVariants = VariantProps<typeof select>
|
||||
|
||||
export interface SelectProps<T extends MaybeArrayOfArrayItem<I>, I extends MaybeArrayOfArray<SelectItem | AcceptableValue | boolean> = MaybeArrayOfArray<SelectItem | AcceptableValue | boolean>, V extends SelectItemKey<T> | undefined = undefined, M extends boolean = false> extends Omit<SelectRootProps<T>, 'dir' | 'multiple' | 'modelValue' | 'defaultValue' | 'by'>, UseComponentIconsProps {
|
||||
export interface SelectProps<T extends ArrayOrNested<SelectItem> = ArrayOrNested<SelectItem>, VK extends GetItemKeys<T> = 'value', M extends boolean = false> extends Omit<SelectRootProps<T>, 'dir' | 'multiple' | 'modelValue' | 'defaultValue' | 'by'>, UseComponentIconsProps {
|
||||
id?: string
|
||||
/** The placeholder text when the select is empty. */
|
||||
placeholder?: string
|
||||
@@ -79,17 +91,17 @@ export interface SelectProps<T extends MaybeArrayOfArrayItem<I>, I extends Maybe
|
||||
* When `items` is an array of objects, select the field to use as the value.
|
||||
* @defaultValue 'value'
|
||||
*/
|
||||
valueKey?: V
|
||||
valueKey?: VK
|
||||
/**
|
||||
* When `items` is an array of objects, select the field to use as the label.
|
||||
* @defaultValue 'label'
|
||||
*/
|
||||
labelKey?: V
|
||||
items?: I
|
||||
labelKey?: keyof NestedItem<T>
|
||||
items?: T
|
||||
/** The value of the Select when initially rendered. Use when you do not need to control the state of the Select. */
|
||||
defaultValue?: SelectModelValue<T, V, M, T extends { value: infer U } ? U : never>
|
||||
defaultValue?: GetModelValue<T, VK, M>
|
||||
/** The controlled value of the Select. Can be bind as `v-model`. */
|
||||
modelValue?: SelectModelValue<T, V, M, T extends { value: infer U } ? U : never>
|
||||
modelValue?: GetModelValue<T, VK, M>
|
||||
/** Whether multiple options can be selected or not. */
|
||||
multiple?: M & boolean
|
||||
/** Highlight the ring color like a focus state. */
|
||||
@@ -98,18 +110,23 @@ export interface SelectProps<T extends MaybeArrayOfArrayItem<I>, I extends Maybe
|
||||
ui?: PartialString<typeof select.slots>
|
||||
}
|
||||
|
||||
export type SelectEmits<T, V, M extends boolean> = Omit<SelectRootEmits<T>, 'update:modelValue'> & {
|
||||
export type SelectEmits<A extends ArrayOrNested<SelectItem>, VK extends GetItemKeys<A> | undefined, M extends boolean> = Omit<SelectRootEmits, 'update:modelValue'> & {
|
||||
change: [payload: Event]
|
||||
blur: [payload: FocusEvent]
|
||||
focus: [payload: FocusEvent]
|
||||
} & SelectModelValueEmits<T, V, M, T extends { value: infer U } ? U : never>
|
||||
} & GetModelValueEmits<A, VK, M>
|
||||
|
||||
type SlotProps<T> = (props: { item: T, index: number }) => any
|
||||
type SlotProps<T extends SelectItem> = (props: { item: T, index: number }) => any
|
||||
|
||||
export interface SelectSlots<T, M extends boolean> {
|
||||
'leading'(props: { modelValue?: M extends true ? AcceptableValue[] : AcceptableValue, open: boolean, ui: any }): any
|
||||
'default'(props: { modelValue?: M extends true ? AcceptableValue[] : AcceptableValue, open: boolean }): any
|
||||
'trailing'(props: { modelValue?: M extends true ? AcceptableValue[] : AcceptableValue, open: boolean, ui: any }): any
|
||||
export interface SelectSlots<
|
||||
A extends ArrayOrNested<SelectItem> = ArrayOrNested<SelectItem>,
|
||||
VK extends GetItemKeys<A> | undefined = undefined,
|
||||
M extends boolean = false,
|
||||
T extends NestedItem<A> = NestedItem<A>
|
||||
> {
|
||||
'leading'(props: { modelValue?: GetModelValue<A, VK, M>, open: boolean, ui: ReturnType<typeof select> }): any
|
||||
'default'(props: { modelValue?: GetModelValue<A, VK, M>, open: boolean }): any
|
||||
'trailing'(props: { modelValue?: GetModelValue<A, VK, M>, open: boolean, ui: ReturnType<typeof select> }): any
|
||||
'item': SlotProps<T>
|
||||
'item-leading': SlotProps<T>
|
||||
'item-label': SlotProps<T>
|
||||
@@ -117,7 +134,7 @@ export interface SelectSlots<T, M extends boolean> {
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts" generic="T extends MaybeArrayOfArrayItem<I>, I extends MaybeArrayOfArray<SelectItem | AcceptableValue | boolean> = MaybeArrayOfArray<SelectItem | AcceptableValue | boolean>, V extends SelectItemKey<T> | undefined = undefined, M extends boolean = false">
|
||||
<script setup lang="ts" generic="T extends ArrayOrNested<SelectItem>, VK extends GetItemKeys<T> = 'value', M extends boolean = false">
|
||||
import { computed, toRef } from 'vue'
|
||||
import { SelectRoot, SelectArrow, SelectTrigger, SelectPortal, SelectContent, SelectViewport, SelectLabel, SelectGroup, SelectItem, SelectItemIndicator, SelectItemText, SelectSeparator, useForwardPropsEmits } from 'reka-ui'
|
||||
import { defu } from 'defu'
|
||||
@@ -126,20 +143,20 @@ import { useAppConfig } from '#imports'
|
||||
import { useButtonGroup } from '../composables/useButtonGroup'
|
||||
import { useComponentIcons } from '../composables/useComponentIcons'
|
||||
import { useFormField } from '../composables/useFormField'
|
||||
import { get, compare } from '../utils'
|
||||
import { compare, get, isArrayOfArray } from '../utils'
|
||||
import UIcon from './Icon.vue'
|
||||
import UAvatar from './Avatar.vue'
|
||||
import UChip from './Chip.vue'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
const props = withDefaults(defineProps<SelectProps<T, I, V, M>>(), {
|
||||
const props = withDefaults(defineProps<SelectProps<T, VK, M>>(), {
|
||||
valueKey: 'value' as never,
|
||||
labelKey: 'label' as never,
|
||||
portal: true
|
||||
})
|
||||
const emits = defineEmits<SelectEmits<T, V, M>>()
|
||||
const slots = defineSlots<SelectSlots<T, M>>()
|
||||
const emits = defineEmits<SelectEmits<T, VK, M>>()
|
||||
const slots = defineSlots<SelectSlots<T, VK, M>>()
|
||||
|
||||
const appConfig = useAppConfig()
|
||||
const rootProps = useForwardPropsEmits(reactivePick(props, 'open', 'defaultOpen', 'disabled', 'autocomplete', 'required', 'multiple'), emits)
|
||||
@@ -163,11 +180,17 @@ const ui = computed(() => select({
|
||||
buttonGroup: orientation.value
|
||||
}))
|
||||
|
||||
const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0]) ? props.items : [props.items]) as SelectItem[][] : [])
|
||||
const groups = computed<SelectItem[][]>(() =>
|
||||
props.items?.length
|
||||
? isArrayOfArray(props.items)
|
||||
? props.items
|
||||
: [props.items]
|
||||
: []
|
||||
)
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
const items = computed(() => groups.value.flatMap(group => group) as T[])
|
||||
|
||||
function displayValue(value?: AcceptableValue | AcceptableValue[]): string | undefined {
|
||||
function displayValue(value?: GetItemValue<T, VK> | GetItemValue<T, VK>[]): string {
|
||||
if (props.multiple && Array.isArray(value)) {
|
||||
return value.map(v => displayValue(v)).filter(Boolean).join(', ')
|
||||
}
|
||||
@@ -195,6 +218,10 @@ function onUpdateOpen(value: boolean) {
|
||||
emitFormFocus()
|
||||
}
|
||||
}
|
||||
|
||||
function isSelectItem(item: SelectItem): item is SelectItemBase {
|
||||
return typeof item === 'object' && item !== null
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable vue/no-template-shadow -->
|
||||
@@ -205,21 +232,21 @@ function onUpdateOpen(value: boolean) {
|
||||
v-bind="rootProps"
|
||||
:autocomplete="autocomplete"
|
||||
:disabled="disabled"
|
||||
:default-value="(defaultValue as (AcceptableValue | AcceptableValue[] | undefined))"
|
||||
:model-value="(modelValue as (AcceptableValue | AcceptableValue[] | undefined))"
|
||||
:default-value="(defaultValue as (AcceptableValue | AcceptableValue[]))"
|
||||
:model-value="(modelValue as (AcceptableValue | AcceptableValue[]))"
|
||||
@update:model-value="onUpdate"
|
||||
@update:open="onUpdateOpen"
|
||||
>
|
||||
<SelectTrigger :id="id" :class="ui.base({ class: [props.class, props.ui?.base] })" v-bind="{ ...$attrs, ...ariaAttrs }">
|
||||
<span v-if="isLeading || !!avatar || !!slots.leading" :class="ui.leading({ class: props.ui?.leading })">
|
||||
<slot name="leading" :model-value="(modelValue as M extends true ? AcceptableValue[] : AcceptableValue)" :open="open" :ui="ui">
|
||||
<slot name="leading" :model-value="(modelValue as GetModelValue<T, VK, M>)" :open="open" :ui="ui">
|
||||
<UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
|
||||
<UAvatar v-else-if="!!avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
|
||||
</slot>
|
||||
</span>
|
||||
|
||||
<slot :model-value="(modelValue as M extends true ? AcceptableValue[] : AcceptableValue)" :open="open">
|
||||
<template v-for="displayedModelValue in [displayValue(modelValue)]" :key="displayedModelValue">
|
||||
<slot :model-value="(modelValue as GetModelValue<T, VK, M>)" :open="open">
|
||||
<template v-for="displayedModelValue in [displayValue(modelValue as GetModelValue<T, VK, M>)]" :key="displayedModelValue">
|
||||
<span v-if="displayedModelValue" :class="ui.value({ class: props.ui?.value })">
|
||||
{{ displayedModelValue }}
|
||||
</span>
|
||||
@@ -230,7 +257,7 @@ function onUpdateOpen(value: boolean) {
|
||||
</slot>
|
||||
|
||||
<span v-if="isTrailing || !!slots.trailing" :class="ui.trailing({ class: props.ui?.trailing })">
|
||||
<slot name="trailing" :model-value="(modelValue as M extends true ? AcceptableValue[] : AcceptableValue)" :open="open" :ui="ui">
|
||||
<slot name="trailing" :model-value="(modelValue as GetModelValue<T, VK, M>)" :open="open" :ui="ui">
|
||||
<UIcon v-if="trailingIconName" :name="trailingIconName" :class="ui.trailingIcon({ class: props.ui?.trailingIcon })" />
|
||||
</slot>
|
||||
</span>
|
||||
@@ -241,24 +268,24 @@ function onUpdateOpen(value: boolean) {
|
||||
<SelectViewport :class="ui.viewport({ class: props.ui?.viewport })">
|
||||
<SelectGroup v-for="(group, groupIndex) in groups" :key="`group-${groupIndex}`" :class="ui.group({ class: props.ui?.group })">
|
||||
<template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
|
||||
<SelectLabel v-if="item?.type === 'label'" :class="ui.label({ class: props.ui?.label })">
|
||||
<SelectLabel v-if="isSelectItem(item) && item.type === 'label'" :class="ui.label({ class: props.ui?.label })">
|
||||
{{ get(item, props.labelKey as string) }}
|
||||
</SelectLabel>
|
||||
|
||||
<SelectSeparator v-else-if="item?.type === 'separator'" :class="ui.separator({ class: props.ui?.separator })" />
|
||||
<SelectSeparator v-else-if="isSelectItem(item) && item.type === 'separator'" :class="ui.separator({ class: props.ui?.separator })" />
|
||||
|
||||
<SelectItem
|
||||
v-else
|
||||
:class="ui.item({ class: props.ui?.item })"
|
||||
:disabled="item.disabled"
|
||||
:value="typeof item === 'object' ? get(item, props.valueKey as string) : item"
|
||||
:disabled="isSelectItem(item) && item.disabled"
|
||||
:value="isSelectItem(item) ? get(item, props.valueKey as string) : item"
|
||||
>
|
||||
<slot name="item" :item="(item as T)" :index="index">
|
||||
<slot name="item-leading" :item="(item as T)" :index="index">
|
||||
<UIcon v-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon })" />
|
||||
<UAvatar v-else-if="item.avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
|
||||
<slot name="item" :item="(item as NestedItem<T>)" :index="index">
|
||||
<slot name="item-leading" :item="(item as NestedItem<T>)" :index="index">
|
||||
<UIcon v-if="isSelectItem(item) && item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon })" />
|
||||
<UAvatar v-else-if="isSelectItem(item) && item.avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
|
||||
<UChip
|
||||
v-else-if="item.chip"
|
||||
v-else-if="isSelectItem(item) && item.chip"
|
||||
:size="((props.ui?.itemLeadingChipSize || ui.itemLeadingChipSize()) as ChipProps['size'])"
|
||||
inset
|
||||
standalone
|
||||
@@ -268,13 +295,13 @@ function onUpdateOpen(value: boolean) {
|
||||
</slot>
|
||||
|
||||
<SelectItemText :class="ui.itemLabel({ class: props.ui?.itemLabel })">
|
||||
<slot name="item-label" :item="(item as T)" :index="index">
|
||||
{{ typeof item === 'object' ? get(item, props.labelKey as string) : item }}
|
||||
<slot name="item-label" :item="(item as NestedItem<T>)" :index="index">
|
||||
{{ isSelectItem(item) ? get(item, props.labelKey as string) : item }}
|
||||
</slot>
|
||||
</SelectItemText>
|
||||
|
||||
<span :class="ui.itemTrailing({ class: props.ui?.itemTrailing })">
|
||||
<slot name="item-trailing" :item="(item as T)" :index="index" />
|
||||
<slot name="item-trailing" :item="(item as NestedItem<T>)" :index="index" />
|
||||
|
||||
<SelectItemIndicator as-child>
|
||||
<UIcon :name="selectedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: props.ui?.itemTrailingIcon })" />
|
||||
|
||||
@@ -1,19 +1,29 @@
|
||||
<script lang="ts">
|
||||
import type { VariantProps } from 'tailwind-variants'
|
||||
import type { ComboboxRootProps, ComboboxRootEmits, ComboboxContentProps, ComboboxContentEmits, ComboboxArrowProps, AcceptableValue } from 'reka-ui'
|
||||
import type { ComboboxRootProps, ComboboxRootEmits, ComboboxContentProps, ComboboxContentEmits, ComboboxArrowProps } from 'reka-ui'
|
||||
import type { AppConfig } from '@nuxt/schema'
|
||||
import _appConfig from '#build/app.config'
|
||||
import theme from '#build/ui/select-menu'
|
||||
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
|
||||
import { tv } from '../utils/tv'
|
||||
import type { AvatarProps, ChipProps, InputProps } from '../types'
|
||||
import type { PartialString, MaybeArrayOfArray, MaybeArrayOfArrayItem, SelectModelValue, SelectModelValueEmits, SelectItemKey, EmitsToProps } from '../types/utils'
|
||||
import type {
|
||||
AcceptableValue,
|
||||
ArrayOrNested,
|
||||
GetItemKeys,
|
||||
GetItemValue,
|
||||
GetModelValue,
|
||||
GetModelValueEmits,
|
||||
NestedItem,
|
||||
PartialString,
|
||||
EmitsToProps
|
||||
} from '../types/utils'
|
||||
|
||||
const appConfigSelectMenu = _appConfig as AppConfig & { ui: { selectMenu: Partial<typeof theme> } }
|
||||
|
||||
const selectMenu = tv({ extend: tv(theme), ...(appConfigSelectMenu.ui?.selectMenu || {}) })
|
||||
|
||||
export interface SelectMenuItem {
|
||||
interface _SelectMenuItem {
|
||||
label?: string
|
||||
/**
|
||||
* @IconifyIcon
|
||||
@@ -26,13 +36,16 @@ export interface SelectMenuItem {
|
||||
* @defaultValue 'item'
|
||||
*/
|
||||
type?: 'label' | 'separator' | 'item'
|
||||
value?: string | number
|
||||
disabled?: boolean
|
||||
onSelect?(e?: Event): void
|
||||
[key: string]: any
|
||||
}
|
||||
export type SelectMenuItem = _SelectMenuItem | AcceptableValue | boolean
|
||||
|
||||
type SelectMenuVariants = VariantProps<typeof selectMenu>
|
||||
|
||||
export interface SelectMenuProps<T extends MaybeArrayOfArrayItem<I>, I extends MaybeArrayOfArray<SelectMenuItem | AcceptableValue | boolean> = MaybeArrayOfArray<SelectMenuItem | AcceptableValue | boolean>, V extends SelectItemKey<T> | undefined = undefined, M extends boolean = false> extends Pick<ComboboxRootProps<T>, 'open' | 'defaultOpen' | 'disabled' | 'name' | 'resetSearchTermOnBlur' | 'highlightOnHover'>, UseComponentIconsProps {
|
||||
export interface SelectMenuProps<T extends ArrayOrNested<SelectMenuItem> = ArrayOrNested<SelectMenuItem>, VK extends GetItemKeys<T> | undefined = undefined, M extends boolean = false> extends Pick<ComboboxRootProps<T>, 'open' | 'defaultOpen' | 'disabled' | 'name' | 'resetSearchTermOnBlur' | 'highlightOnHover'>, UseComponentIconsProps {
|
||||
id?: string
|
||||
/** The placeholder text when the select is empty. */
|
||||
placeholder?: string
|
||||
@@ -88,17 +101,17 @@ export interface SelectMenuProps<T extends MaybeArrayOfArrayItem<I>, I extends M
|
||||
* When `items` is an array of objects, select the field to use as the value instead of the object itself.
|
||||
* @defaultValue undefined
|
||||
*/
|
||||
valueKey?: V
|
||||
valueKey?: VK
|
||||
/**
|
||||
* When `items` is an array of objects, select the field to use as the label.
|
||||
* @defaultValue 'label'
|
||||
*/
|
||||
labelKey?: V
|
||||
items?: I
|
||||
labelKey?: keyof NestedItem<T>
|
||||
items?: T
|
||||
/** The value of the SelectMenu when initially rendered. Use when you do not need to control the state of the SelectMenu. */
|
||||
defaultValue?: SelectModelValue<T, V, M>
|
||||
defaultValue?: GetModelValue<T, VK, M>
|
||||
/** The controlled value of the SelectMenu. Can be binded-with with `v-model`. */
|
||||
modelValue?: SelectModelValue<T, V, M>
|
||||
modelValue?: GetModelValue<T, VK, M>
|
||||
/** Whether multiple options can be selected or not. */
|
||||
multiple?: M & boolean
|
||||
/** Highlight the ring color like a focus state. */
|
||||
@@ -122,19 +135,29 @@ export interface SelectMenuProps<T extends MaybeArrayOfArrayItem<I>, I extends M
|
||||
ui?: PartialString<typeof selectMenu.slots>
|
||||
}
|
||||
|
||||
export type SelectMenuEmits<T, V, M extends boolean> = Omit<ComboboxRootEmits<T>, 'update:modelValue'> & {
|
||||
export type SelectMenuEmits<A extends ArrayOrNested<SelectMenuItem>, VK extends GetItemKeys<A> | undefined, M extends boolean> = Pick<ComboboxRootEmits, 'update:open'> & {
|
||||
change: [payload: Event]
|
||||
blur: [payload: FocusEvent]
|
||||
focus: [payload: FocusEvent]
|
||||
create: [item: string]
|
||||
} & SelectModelValueEmits<T, V, M>
|
||||
/** Event handler when highlighted element changes. */
|
||||
highlight: [payload: {
|
||||
ref: HTMLElement
|
||||
value: GetModelValue<A, VK, M>
|
||||
} | undefined]
|
||||
} & GetModelValueEmits<A, VK, M>
|
||||
|
||||
type SlotProps<T> = (props: { item: T, index: number }) => any
|
||||
type SlotProps<T extends SelectMenuItem> = (props: { item: T, index: number }) => any
|
||||
|
||||
export interface SelectMenuSlots<T, M extends boolean> {
|
||||
'leading'(props: { modelValue?: M extends true ? T[] : T, open: boolean, ui: any }): any
|
||||
'default'(props: { modelValue?: M extends true ? T[] : T, open: boolean }): any
|
||||
'trailing'(props: { modelValue?: M extends true ? T[] : T, open: boolean, ui: any }): any
|
||||
export interface SelectMenuSlots<
|
||||
A extends ArrayOrNested<SelectMenuItem> = ArrayOrNested<SelectMenuItem>,
|
||||
VK extends GetItemKeys<A> | undefined = undefined,
|
||||
M extends boolean = false,
|
||||
T extends NestedItem<A> = NestedItem<A>
|
||||
> {
|
||||
'leading'(props: { modelValue?: GetModelValue<A, VK, M>, open: boolean, ui: ReturnType<typeof selectMenu> }): any
|
||||
'default'(props: { modelValue?: GetModelValue<A, VK, M>, open: boolean }): any
|
||||
'trailing'(props: { modelValue?: GetModelValue<A, VK, M>, open: boolean, ui: ReturnType<typeof selectMenu> }): any
|
||||
'empty'(props: { searchTerm?: string }): any
|
||||
'item': SlotProps<T>
|
||||
'item-leading': SlotProps<T>
|
||||
@@ -144,7 +167,7 @@ export interface SelectMenuSlots<T, M extends boolean> {
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts" generic="T extends MaybeArrayOfArrayItem<I>, I extends MaybeArrayOfArray<SelectMenuItem | AcceptableValue | boolean> = MaybeArrayOfArray<SelectMenuItem | AcceptableValue | boolean>, V extends SelectItemKey<T> | undefined = undefined, M extends boolean = false">
|
||||
<script setup lang="ts" generic="T extends ArrayOrNested<SelectMenuItem>, VK extends GetItemKeys<T> | undefined = undefined, M extends boolean = false">
|
||||
import { computed, toRef, toRaw } from 'vue'
|
||||
import { ComboboxRoot, ComboboxArrow, ComboboxAnchor, ComboboxInput, ComboboxTrigger, ComboboxPortal, ComboboxContent, ComboboxViewport, ComboboxEmpty, ComboboxGroup, ComboboxLabel, ComboboxSeparator, ComboboxItem, ComboboxItemIndicator, FocusScope, useForwardPropsEmits, useFilter } from 'reka-ui'
|
||||
import { defu } from 'defu'
|
||||
@@ -154,7 +177,7 @@ import { useButtonGroup } from '../composables/useButtonGroup'
|
||||
import { useComponentIcons } from '../composables/useComponentIcons'
|
||||
import { useFormField } from '../composables/useFormField'
|
||||
import { useLocale } from '../composables/useLocale'
|
||||
import { get, compare } from '../utils'
|
||||
import { compare, get, isArrayOfArray } from '../utils'
|
||||
import UIcon from './Icon.vue'
|
||||
import UAvatar from './Avatar.vue'
|
||||
import UChip from './Chip.vue'
|
||||
@@ -162,14 +185,14 @@ import UInput from './Input.vue'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
const props = withDefaults(defineProps<SelectMenuProps<T, I, V, M>>(), {
|
||||
const props = withDefaults(defineProps<SelectMenuProps<T, VK, M>>(), {
|
||||
portal: true,
|
||||
searchInput: true,
|
||||
labelKey: 'label' as never,
|
||||
resetSearchTermOnBlur: true
|
||||
})
|
||||
const emits = defineEmits<SelectMenuEmits<T, V, M>>()
|
||||
const slots = defineSlots<SelectMenuSlots<T, M>>()
|
||||
const emits = defineEmits<SelectMenuEmits<T, VK, M>>()
|
||||
const slots = defineSlots<SelectMenuSlots<T, VK, M>>()
|
||||
|
||||
const searchTerm = defineModel<string>('searchTerm', { default: '' })
|
||||
|
||||
@@ -201,7 +224,7 @@ const ui = computed(() => selectMenu({
|
||||
buttonGroup: orientation.value
|
||||
}))
|
||||
|
||||
function displayValue(value: T | T[]): string {
|
||||
function displayValue(value: GetItemValue<T, VK> | GetItemValue<T, VK>[]): string {
|
||||
if (props.multiple && Array.isArray(value)) {
|
||||
return value.map(v => displayValue(v)).filter(Boolean).join(', ')
|
||||
}
|
||||
@@ -214,7 +237,13 @@ function displayValue(value: T | T[]): string {
|
||||
return item && (typeof item === 'object' ? get(item, props.labelKey as string) : item)
|
||||
}
|
||||
|
||||
const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0]) ? props.items : [props.items]) as SelectMenuItem[][] : [])
|
||||
const groups = computed<SelectMenuItem[][]>(() =>
|
||||
props.items?.length
|
||||
? isArrayOfArray(props.items)
|
||||
? props.items
|
||||
: [props.items]
|
||||
: []
|
||||
)
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
const items = computed(() => groups.value.flatMap(group => group) as T[])
|
||||
|
||||
@@ -226,8 +255,8 @@ const filteredGroups = computed(() => {
|
||||
const fields = Array.isArray(props.filterFields) ? props.filterFields : [props.labelKey] as string[]
|
||||
|
||||
return groups.value.map(items => items.filter((item) => {
|
||||
if (typeof item !== 'object') {
|
||||
return contains(item, searchTerm.value)
|
||||
if (typeof item !== 'object' || item === null) {
|
||||
return contains(String(item), searchTerm.value)
|
||||
}
|
||||
|
||||
if (item.type && ['label', 'separator'].includes(item.type)) {
|
||||
@@ -235,19 +264,21 @@ const filteredGroups = computed(() => {
|
||||
}
|
||||
|
||||
return fields.some(field => contains(get(item, field), searchTerm.value))
|
||||
})).filter(group => group.filter(item => !item.type || !['label', 'separator'].includes(item.type)).length > 0)
|
||||
})).filter(group => group.filter(item =>
|
||||
isSelectItem(item) && (!item.type || !['label', 'separator'].includes(item.type))
|
||||
).length > 0)
|
||||
})
|
||||
const filteredItems = computed(() => filteredGroups.value.flatMap(group => group) as T[])
|
||||
const filteredItems = computed(() => filteredGroups.value.flatMap(group => group))
|
||||
|
||||
const createItem = computed(() => {
|
||||
if (!props.createItem || !searchTerm.value) {
|
||||
return false
|
||||
}
|
||||
|
||||
const newItem = props.valueKey ? { [props.valueKey]: searchTerm.value } as T : searchTerm.value
|
||||
const newItem = props.valueKey ? { [props.valueKey]: searchTerm.value } as NestedItem<T> : searchTerm.value
|
||||
|
||||
if ((typeof props.createItem === 'object' && props.createItem.when === 'always') || props.createItem === 'always') {
|
||||
return !filteredItems.value.find(item => compare(item, newItem, props.valueKey))
|
||||
return !filteredItems.value.find(item => compare(item, newItem, String(props.valueKey)))
|
||||
}
|
||||
|
||||
return !filteredItems.value.length
|
||||
@@ -290,6 +321,10 @@ function onUpdateOpen(value: boolean) {
|
||||
clearTimeout(timeoutId)
|
||||
}
|
||||
}
|
||||
|
||||
function isSelectItem(item: SelectMenuItem): item is _SelectMenuItem {
|
||||
return typeof item === 'object' && item !== null
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable vue/no-template-shadow -->
|
||||
@@ -324,14 +359,14 @@ function onUpdateOpen(value: boolean) {
|
||||
<ComboboxAnchor as-child>
|
||||
<ComboboxTrigger :class="ui.base({ class: [props.class, props.ui?.base] })" tabindex="0">
|
||||
<span v-if="isLeading || !!avatar || !!slots.leading" :class="ui.leading({ class: props.ui?.leading })">
|
||||
<slot name="leading" :model-value="(modelValue as M extends true ? T[] : T)" :open="open" :ui="ui">
|
||||
<slot name="leading" :model-value="(modelValue as GetModelValue<T, VK, M>)" :open="open" :ui="ui">
|
||||
<UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
|
||||
<UAvatar v-else-if="!!avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
|
||||
</slot>
|
||||
</span>
|
||||
|
||||
<slot :model-value="(modelValue as M extends true ? T[] : T)" :open="open">
|
||||
<template v-for="displayedModelValue in [displayValue(modelValue as M extends true ? T[] : T)]" :key="displayedModelValue">
|
||||
<slot :model-value="(modelValue as GetModelValue<T, VK, M>)" :open="open">
|
||||
<template v-for="displayedModelValue in [displayValue(modelValue as GetModelValue<T, VK, M>)]" :key="displayedModelValue">
|
||||
<span v-if="displayedModelValue" :class="ui.value({ class: props.ui?.value })">
|
||||
{{ displayedModelValue }}
|
||||
</span>
|
||||
@@ -342,7 +377,7 @@ function onUpdateOpen(value: boolean) {
|
||||
</slot>
|
||||
|
||||
<span v-if="isTrailing || !!slots.trailing" :class="ui.trailing({ class: props.ui?.trailing })">
|
||||
<slot name="trailing" :model-value="(modelValue as M extends true ? T[] : T)" :open="open" :ui="ui">
|
||||
<slot name="trailing" :model-value="(modelValue as GetModelValue<T, VK, M>)" :open="open" :ui="ui">
|
||||
<UIcon v-if="trailingIconName" :name="trailingIconName" :class="ui.trailingIcon({ class: props.ui?.trailingIcon })" />
|
||||
</slot>
|
||||
</span>
|
||||
@@ -367,25 +402,25 @@ function onUpdateOpen(value: boolean) {
|
||||
|
||||
<ComboboxGroup v-for="(group, groupIndex) in filteredGroups" :key="`group-${groupIndex}`" :class="ui.group({ class: props.ui?.group })">
|
||||
<template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
|
||||
<ComboboxLabel v-if="item?.type === 'label'" :class="ui.label({ class: props.ui?.label })">
|
||||
<ComboboxLabel v-if="isSelectItem(item) && item.type === 'label'" :class="ui.label({ class: props.ui?.label })">
|
||||
{{ get(item, props.labelKey as string) }}
|
||||
</ComboboxLabel>
|
||||
|
||||
<ComboboxSeparator v-else-if="item?.type === 'separator'" :class="ui.separator({ class: props.ui?.separator })" />
|
||||
<ComboboxSeparator v-else-if="isSelectItem(item) && item.type === 'separator'" :class="ui.separator({ class: props.ui?.separator })" />
|
||||
|
||||
<ComboboxItem
|
||||
v-else
|
||||
:class="ui.item({ class: props.ui?.item })"
|
||||
:disabled="item.disabled"
|
||||
:value="valueKey && typeof item === 'object' ? get(item, props.valueKey as string) : item"
|
||||
@select="item.onSelect"
|
||||
:disabled="isSelectItem(item) && item.disabled"
|
||||
:value="props.valueKey && isSelectItem(item) ? get(item, props.valueKey as string) : item"
|
||||
@select="isSelectItem(item) && item.onSelect"
|
||||
>
|
||||
<slot name="item" :item="(item as T)" :index="index">
|
||||
<slot name="item-leading" :item="(item as T)" :index="index">
|
||||
<UIcon v-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon })" />
|
||||
<UAvatar v-else-if="item.avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
|
||||
<slot name="item" :item="(item as NestedItem<T>)" :index="index">
|
||||
<slot name="item-leading" :item="(item as NestedItem<T>)" :index="index">
|
||||
<UIcon v-if="isSelectItem(item) && item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon })" />
|
||||
<UAvatar v-else-if="isSelectItem(item) && item.avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
|
||||
<UChip
|
||||
v-else-if="item.chip"
|
||||
v-else-if="isSelectItem(item) && item.chip"
|
||||
:size="((props.ui?.itemLeadingChipSize || ui.itemLeadingChipSize()) as ChipProps['size'])"
|
||||
inset
|
||||
standalone
|
||||
@@ -395,13 +430,13 @@ function onUpdateOpen(value: boolean) {
|
||||
</slot>
|
||||
|
||||
<span :class="ui.itemLabel({ class: props.ui?.itemLabel })">
|
||||
<slot name="item-label" :item="(item as T)" :index="index">
|
||||
{{ typeof item === 'object' ? get(item, props.labelKey as string) : item }}
|
||||
<slot name="item-label" :item="(item as NestedItem<T>)" :index="index">
|
||||
{{ isSelectItem(item) ? get(item, props.labelKey as string) : item }}
|
||||
</slot>
|
||||
</span>
|
||||
|
||||
<span :class="ui.itemTrailing({ class: props.ui?.itemTrailing })">
|
||||
<slot name="item-trailing" :item="(item as T)" :index="index" />
|
||||
<slot name="item-trailing" :item="(item as NestedItem<T>)" :index="index" />
|
||||
|
||||
<ComboboxItemIndicator as-child>
|
||||
<UIcon :name="selectedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: props.ui?.itemTrailingIcon })" />
|
||||
|
||||
@@ -70,7 +70,7 @@ export interface SlideoverSlots {
|
||||
header(props?: {}): any
|
||||
title(props?: {}): any
|
||||
description(props?: {}): any
|
||||
close(props: { ui: any }): any
|
||||
close(props: { ui: ReturnType<typeof slideover> }): any
|
||||
body(props?: {}): any
|
||||
footer(props?: {}): any
|
||||
}
|
||||
|
||||
@@ -25,9 +25,10 @@ export interface StepperItem {
|
||||
icon?: string
|
||||
content?: string
|
||||
disabled?: boolean
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface StepperProps<T extends StepperItem> extends Pick<StepperRootProps, 'linear'> {
|
||||
export interface StepperProps<T extends StepperItem = StepperItem> extends Pick<StepperRootProps, 'linear'> {
|
||||
/**
|
||||
* The element or component this component should render as.
|
||||
* @defaultValue 'div'
|
||||
@@ -56,19 +57,19 @@ export interface StepperProps<T extends StepperItem> extends Pick<StepperRootPro
|
||||
class?: any
|
||||
}
|
||||
|
||||
export type StepperEmits<T> = Omit<StepperRootEmits, 'update:modelValue'> & {
|
||||
export type StepperEmits<T extends StepperItem = StepperItem> = Omit<StepperRootEmits, 'update:modelValue'> & {
|
||||
next: [payload: T]
|
||||
prev: [payload: T]
|
||||
}
|
||||
|
||||
type SlotProps<T extends StepperItem> = (props: { item: T }) => any
|
||||
|
||||
export type StepperSlots<T extends StepperItem> = {
|
||||
export type StepperSlots<T extends StepperItem = StepperItem> = {
|
||||
indicator: SlotProps<T>
|
||||
title: SlotProps<T>
|
||||
description: SlotProps<T>
|
||||
content: SlotProps<T>
|
||||
} & DynamicSlots<T, SlotProps<T>>
|
||||
} & DynamicSlots<T>
|
||||
|
||||
</script>
|
||||
|
||||
@@ -108,7 +109,7 @@ const currentStepIndex = computed({
|
||||
}
|
||||
})
|
||||
|
||||
const currentStep = computed(() => props.items?.[currentStepIndex.value] as T)
|
||||
const currentStep = computed(() => props.items?.[currentStepIndex.value])
|
||||
const hasNext = computed(() => currentStepIndex.value < props.items?.length - 1)
|
||||
const hasPrev = computed(() => currentStepIndex.value > 0)
|
||||
|
||||
@@ -116,13 +117,13 @@ defineExpose({
|
||||
next() {
|
||||
if (hasNext.value) {
|
||||
currentStepIndex.value += 1
|
||||
emits('next', currentStep.value)
|
||||
emits('next', currentStep.value as T)
|
||||
}
|
||||
},
|
||||
prev() {
|
||||
if (hasPrev.value) {
|
||||
currentStepIndex.value -= 1
|
||||
emits('prev', currentStep.value)
|
||||
emits('prev', currentStep.value as T)
|
||||
}
|
||||
},
|
||||
hasNext,
|
||||
@@ -173,10 +174,10 @@ defineExpose({
|
||||
</StepperItem>
|
||||
</div>
|
||||
|
||||
<div v-if="currentStep?.content || !!slots.content || (currentStep?.slot && !!slots[currentStep.slot]) || (currentStep?.value && !!slots[currentStep.value])" :class="ui.content({ class: props.ui?.description })">
|
||||
<div v-if="currentStep?.content || !!slots.content || currentStep?.slot" :class="ui.content({ class: props.ui?.description })">
|
||||
<slot
|
||||
:name="!!slots[currentStep?.slot ?? currentStep.value!] ? currentStep.slot ?? currentStep.value : 'content'"
|
||||
:item="currentStep"
|
||||
:name="((currentStep?.slot || 'content') as keyof StepperSlots<T>)"
|
||||
:item="(currentStep as Extract<T, { slot: string }>)"
|
||||
>
|
||||
{{ currentStep?.content }}
|
||||
</slot>
|
||||
|
||||
@@ -25,11 +25,12 @@ export interface TabsItem {
|
||||
/** A unique value for the tab item. Defaults to the index. */
|
||||
value?: string | number
|
||||
disabled?: boolean
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
type TabsVariants = VariantProps<typeof tabs>
|
||||
|
||||
export interface TabsProps<T> extends Pick<TabsRootProps<string | number>, 'defaultValue' | 'modelValue' | 'activationMode' | 'unmountOnHide'> {
|
||||
export interface TabsProps<T extends TabsItem = TabsItem> extends Pick<TabsRootProps<string | number>, 'defaultValue' | 'modelValue' | 'activationMode' | 'unmountOnHide'> {
|
||||
/**
|
||||
* The element or component this component should render as.
|
||||
* @defaultValue 'div'
|
||||
@@ -69,14 +70,14 @@ export interface TabsProps<T> extends Pick<TabsRootProps<string | number>, 'defa
|
||||
|
||||
export interface TabsEmits extends TabsRootEmits<string | number> {}
|
||||
|
||||
type SlotProps<T> = (props: { item: T, index: number }) => any
|
||||
type SlotProps<T extends TabsItem> = (props: { item: T, index: number }) => any
|
||||
|
||||
export type TabsSlots<T extends { slot?: string }> = {
|
||||
export type TabsSlots<T extends TabsItem = TabsItem> = {
|
||||
leading: SlotProps<T>
|
||||
default: SlotProps<T>
|
||||
trailing: SlotProps<T>
|
||||
content: SlotProps<T>
|
||||
} & DynamicSlots<T, SlotProps<T>>
|
||||
} & DynamicSlots<T, undefined, { index: number }>
|
||||
|
||||
</script>
|
||||
|
||||
@@ -129,7 +130,7 @@ const ui = computed(() => tabs({
|
||||
|
||||
<template v-if="!!content">
|
||||
<TabsContent v-for="(item, index) of items" :key="index" :value="item.value || String(index)" :class="ui.content({ class: props.ui?.content })">
|
||||
<slot :name="item.slot || 'content'" :item="item" :index="index">
|
||||
<slot :name="((item.slot || 'content') as keyof TabsSlots<T>)" :item="(item as Extract<T, { slot: string; }>)" :index="index">
|
||||
{{ item.content }}
|
||||
</slot>
|
||||
</TabsContent>
|
||||
|
||||
@@ -66,7 +66,7 @@ export interface ToastSlots {
|
||||
title(props?: {}): any
|
||||
description(props?: {}): any
|
||||
actions(props?: {}): any
|
||||
close(props: { ui: any }): any
|
||||
close(props: { ui: ReturnType<typeof toast> }): any
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -6,7 +6,14 @@ import type { AppConfig } from '@nuxt/schema'
|
||||
import _appConfig from '#build/app.config'
|
||||
import theme from '#build/ui/tree'
|
||||
import { tv } from '../utils/tv'
|
||||
import type { PartialString, DynamicSlots, MaybeMultipleModelValue, SelectItemKey } from '../types/utils'
|
||||
import type {
|
||||
DynamicSlots,
|
||||
GetItemKeys,
|
||||
GetModelValue,
|
||||
GetModelValueEmits,
|
||||
NestedItem,
|
||||
PartialString
|
||||
} from '../types/utils'
|
||||
|
||||
const appConfig = _appConfig as AppConfig & { ui: { tree: Partial<typeof theme> } }
|
||||
|
||||
@@ -31,9 +38,10 @@ export type TreeItem = {
|
||||
children?: TreeItem[]
|
||||
onToggle?(e: Event): void
|
||||
onSelect?(e?: Event): void
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface TreeProps<T extends TreeItem, M extends boolean = false, K extends SelectItemKey<T> | undefined = undefined> extends Pick<TreeRootProps<T>, 'expanded' | 'defaultExpanded' | 'selectionBehavior' | 'propagateSelect' | 'disabled'> {
|
||||
export interface TreeProps<T extends TreeItem[] = TreeItem[], VK extends GetItemKeys<T> = 'value', M extends boolean = false> extends Pick<TreeRootProps<T>, 'expanded' | 'defaultExpanded' | 'selectionBehavior' | 'propagateSelect' | 'disabled'> {
|
||||
/**
|
||||
* The element or component this component should render as.
|
||||
* @defaultValue 'ul'
|
||||
@@ -51,12 +59,12 @@ export interface TreeProps<T extends TreeItem, M extends boolean = false, K exte
|
||||
* The key used to get the value from the item.
|
||||
* @defaultValue 'value'
|
||||
*/
|
||||
valueKey?: K
|
||||
valueKey?: VK
|
||||
/**
|
||||
* The key used to get the label from the item.
|
||||
* @defaultValue 'label'
|
||||
*/
|
||||
labelKey?: K
|
||||
labelKey?: keyof NestedItem<T>
|
||||
/**
|
||||
* The icon displayed on the right side of a parent node.
|
||||
* @defaultValue appConfig.ui.icons.chevronDown
|
||||
@@ -75,33 +83,34 @@ export interface TreeProps<T extends TreeItem, M extends boolean = false, K exte
|
||||
* @IconifyIcon
|
||||
*/
|
||||
collapsedIcon?: string
|
||||
items?: T[]
|
||||
items?: T
|
||||
/** The controlled value of the Tree. Can be bind as `v-model`. */
|
||||
modelValue?: MaybeMultipleModelValue<T, M>
|
||||
modelValue?: GetModelValue<T, VK, M>
|
||||
/** The value of the Tree when initially rendered. Use when you do not need to control the state of the Tree. */
|
||||
defaultValue?: MaybeMultipleModelValue<T, M>
|
||||
defaultValue?: GetModelValue<T, VK, M>
|
||||
/** Whether multiple options can be selected or not. */
|
||||
multiple?: M & boolean
|
||||
class?: any
|
||||
ui?: PartialString<typeof tree.slots>
|
||||
}
|
||||
|
||||
export type TreeEmits<T, M extends boolean = false> = Omit<TreeRootEmits, 'update:modelValue'> & {
|
||||
'update:modelValue': [payload: MaybeMultipleModelValue<T, M>]
|
||||
}
|
||||
export type TreeEmits<A extends TreeItem[], VK extends GetItemKeys<A> | undefined, M extends boolean> = Omit<TreeRootEmits, 'update:modelValue'> & GetModelValueEmits<A, VK, M>
|
||||
|
||||
type SlotProps<T> = (props: { item: T, index: number, level: number, expanded: boolean, selected: boolean }) => any
|
||||
type SlotProps<T extends TreeItem> = (props: { item: T, index: number, level: number, expanded: boolean, selected: boolean }) => any
|
||||
|
||||
export type TreeSlots<T extends { slot?: string }> = {
|
||||
export type TreeSlots<
|
||||
A extends TreeItem[] = TreeItem[],
|
||||
T extends NestedItem<A> = NestedItem<A>
|
||||
> = {
|
||||
'item': SlotProps<T>
|
||||
'item-leading': SlotProps<T>
|
||||
'item-label': SlotProps<T>
|
||||
'item-trailing': SlotProps<T>
|
||||
} & DynamicSlots<T, SlotProps<T>>
|
||||
} & DynamicSlots<T, undefined, { index: number, level: number, expanded: boolean, selected: boolean }>
|
||||
|
||||
</script>
|
||||
|
||||
<script setup lang="ts" generic="T extends TreeItem, M extends boolean = false, K extends SelectItemKey<T> | undefined = undefined">
|
||||
<script setup lang="ts" generic="T extends TreeItem[], VK extends GetItemKeys<T> = 'value', M extends boolean = false">
|
||||
import { computed } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { TreeRoot, TreeItem, useForwardPropsEmits } from 'reka-ui'
|
||||
@@ -109,18 +118,21 @@ import { reactivePick, createReusableTemplate } from '@vueuse/core'
|
||||
import { get } from '../utils'
|
||||
import UIcon from './Icon.vue'
|
||||
|
||||
const props = withDefaults(defineProps<TreeProps<T, M, K>>(), {
|
||||
const props = withDefaults(defineProps<TreeProps<T, VK, M>>(), {
|
||||
labelKey: 'label' as never,
|
||||
valueKey: 'value' as never
|
||||
})
|
||||
const emits = defineEmits<TreeEmits<T, M>>()
|
||||
const emits = defineEmits<TreeEmits<T, VK, M>>()
|
||||
const slots = defineSlots<TreeSlots<T>>()
|
||||
|
||||
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'items', 'multiple', 'expanded', 'disabled', 'propagateSelect'), emits)
|
||||
|
||||
const [DefineTreeTemplate, ReuseTreeTemplate] = createReusableTemplate<{ items?: T[], level: number }>({
|
||||
const [DefineTreeTemplate, ReuseTreeTemplate] = createReusableTemplate<
|
||||
{ items?: NestedItem<T>[], level: number },
|
||||
TreeSlots<T>
|
||||
>({
|
||||
props: {
|
||||
items: Array as PropType<T[]>,
|
||||
items: Array as PropType<NestedItem<T>[]>,
|
||||
level: Number
|
||||
}
|
||||
})
|
||||
@@ -130,22 +142,24 @@ const ui = computed(() => tree({
|
||||
size: props.size
|
||||
}))
|
||||
|
||||
function getItemLabel(item: T) {
|
||||
function getItemLabel(item: NestedItem<T>): string {
|
||||
return get(item, props.labelKey as string)
|
||||
}
|
||||
|
||||
function getItemValue(item?: T) {
|
||||
function getItemValue(item: NestedItem<T>): string {
|
||||
return get(item, props.valueKey as string) ?? get(item, props.labelKey as string)
|
||||
}
|
||||
|
||||
function getDefaultOpenedItems(item: T): string[] {
|
||||
function getDefaultOpenedItems(item: NestedItem<T>): string[] {
|
||||
const currentItem = item.defaultExpanded ? getItemValue(item) : null
|
||||
const childItems = item.children?.flatMap(child => getDefaultOpenedItems(child as T)) ?? []
|
||||
const childItems = item.children?.flatMap((child: TreeItem) => getDefaultOpenedItems(child as NestedItem<T>)) ?? []
|
||||
|
||||
return [currentItem, ...childItems].filter(Boolean) as string[]
|
||||
}
|
||||
|
||||
const defaultExpanded = computed(() => props.defaultExpanded ?? props.items?.flatMap(getDefaultOpenedItems))
|
||||
const defaultExpanded = computed(() =>
|
||||
props.defaultExpanded ?? props.items?.flatMap(item => getDefaultOpenedItems(item as NestedItem<T>))
|
||||
)
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable vue/no-template-shadow -->
|
||||
@@ -165,8 +179,8 @@ const defaultExpanded = computed(() => props.defaultExpanded ?? props.items?.fla
|
||||
@select="item.onSelect"
|
||||
>
|
||||
<button :disabled="item.disabled || disabled" :class="ui.link({ class: props.ui?.link, selected: isSelected, disabled: item.disabled || disabled })">
|
||||
<slot :name="item.slot || 'item'" v-bind="{ item, index, level, expanded: isExpanded, selected: isSelected }">
|
||||
<slot :name="item.slot ? `${item.slot}-leading`: 'item-leading'" v-bind="{ item, index, level, expanded: isExpanded, selected: isSelected }">
|
||||
<slot :name="((item.slot || 'item') as keyof TreeSlots<T>)" v-bind="{ index, level, expanded: isExpanded, selected: isSelected }" :item="(item as Extract<NestedItem<T>, { slot: string; }>)">
|
||||
<slot :name="((item.slot ? `${item.slot}-leading`: 'item-leading') as keyof TreeSlots<T>)" v-bind="{ index, level, expanded: isExpanded, selected: isSelected }" :item="(item as Extract<NestedItem<T>, { slot: string; }>)">
|
||||
<UIcon
|
||||
v-if="item.icon"
|
||||
:name="item.icon"
|
||||
@@ -179,14 +193,14 @@ const defaultExpanded = computed(() => props.defaultExpanded ?? props.items?.fla
|
||||
/>
|
||||
</slot>
|
||||
|
||||
<span v-if="getItemLabel(item) || !!slots[item.slot ? `${item.slot}-label`: 'item-label']" :class="ui.linkLabel({ class: props.ui?.linkLabel })">
|
||||
<slot :name="item.slot ? `${item.slot}-label`: 'item-label'" v-bind="{ item, index, level, expanded: isExpanded, selected: isSelected }">
|
||||
<span v-if="getItemLabel(item) || !!slots[(item.slot ? `${item.slot}-label`: 'item-label') as keyof TreeSlots<T>]" :class="ui.linkLabel({ class: props.ui?.linkLabel })">
|
||||
<slot :name="((item.slot ? `${item.slot}-label`: 'item-label') as keyof TreeSlots<T>)" v-bind="{ item, index, level, expanded: isExpanded, selected: isSelected }" :item="(item as Extract<NestedItem<T>, { slot: string; }>)">
|
||||
{{ getItemLabel(item) }}
|
||||
</slot>
|
||||
</span>
|
||||
|
||||
<span v-if="item.trailingIcon || item.children?.length || !!slots[item.slot ? `${item.slot}-trailing`: 'item-trailing']" :class="ui.linkTrailing({ class: props.ui?.linkTrailing })">
|
||||
<slot :name="item.slot ? `${item.slot}-trailing`: 'item-trailing'" v-bind="{ item, index, level, expanded: isExpanded, selected: isSelected }">
|
||||
<span v-if="item.trailingIcon || item.children?.length || !!slots[(item.slot ? `${item.slot}-trailing`: 'item-trailing') as keyof TreeSlots<T>]" :class="ui.linkTrailing({ class: props.ui?.linkTrailing })">
|
||||
<slot :name="((item.slot ? `${item.slot}-trailing`: 'item-trailing') as keyof TreeSlots<T>)" v-bind="{ item, index, level, expanded: isExpanded, selected: isSelected }" :item="(item as Extract<NestedItem<T>, { slot: string; }>)">
|
||||
<UIcon v-if="item.trailingIcon" :name="item.trailingIcon" :class="ui.linkTrailingIcon({ class: props.ui?.linkTrailingIcon })" />
|
||||
<UIcon v-else-if="item.children?.length" :name="trailingIcon ?? appConfig.ui.icons.chevronDown" :class="ui.linkTrailingIcon({ class: props.ui?.linkTrailingIcon })" />
|
||||
</slot>
|
||||
@@ -195,19 +209,19 @@ const defaultExpanded = computed(() => props.defaultExpanded ?? props.items?.fla
|
||||
</button>
|
||||
|
||||
<ul v-if="item.children?.length && isExpanded" :class="ui.listWithChildren({ class: props.ui?.listWithChildren })">
|
||||
<ReuseTreeTemplate :items="(item.children as T[])" :level="level + 1" />
|
||||
<ReuseTreeTemplate :items="(item.children as NestedItem<T>[])" :level="level + 1" />
|
||||
</ul>
|
||||
</TreeItem>
|
||||
</li>
|
||||
</DefineTreeTemplate>
|
||||
|
||||
<TreeRoot
|
||||
v-bind="rootProps"
|
||||
v-bind="(rootProps as unknown as TreeRootProps<NestedItem<T>>)"
|
||||
:class="ui.root({ class: [props.class, props.ui?.root] })"
|
||||
:get-key="getItemValue"
|
||||
:default-expanded="defaultExpanded"
|
||||
:selection-behavior="selectionBehavior"
|
||||
>
|
||||
<ReuseTreeTemplate :items="items" :level="0" />
|
||||
<ReuseTreeTemplate :items="(items as NestedItem<T>[] | undefined)" :level="0" />
|
||||
</TreeRoot>
|
||||
</template>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AcceptableValue as _AcceptableValue } from 'reka-ui'
|
||||
import type { VNode } from 'vue'
|
||||
|
||||
export interface TightMap<O = any> {
|
||||
@@ -14,8 +15,19 @@ export type DeepPartial<T, O = any> = {
|
||||
[key: string]: O | TightMap<O>
|
||||
}
|
||||
|
||||
export type DynamicSlots<T extends { slot?: string }, SlotProps, Slot = T['slot']> =
|
||||
Record<string, SlotProps> & (Slot extends string ? Record<Slot, SlotProps> : Record<string, never>)
|
||||
export type DynamicSlots<
|
||||
T extends { slot?: string },
|
||||
S extends string | undefined = undefined,
|
||||
D extends object = {}
|
||||
> = {
|
||||
[
|
||||
K in T['slot'] as K extends string
|
||||
? S extends string
|
||||
? (K | `${K}-${S}`)
|
||||
: K
|
||||
: never
|
||||
]?: (props: { item: Extract<T, { slot: K extends `${infer Base}-${S}` ? Base : K }> } & D) => any
|
||||
}
|
||||
|
||||
export type GetObjectField<MaybeObject, Key extends string> = MaybeObject extends Record<string, any>
|
||||
? MaybeObject[Key]
|
||||
@@ -25,18 +37,49 @@ export type PartialString<T> = {
|
||||
[K in keyof T]?: string
|
||||
}
|
||||
|
||||
export type MaybeArrayOfArray<T> = T[] | T[][]
|
||||
export type MaybeArrayOfArrayItem<I> = I extends Array<infer T> ? T extends Array<infer U> ? U : T : never
|
||||
|
||||
export type SelectModelValue<T, V, M extends boolean = false, DV = T> = (T extends Record<string, any> ? V extends keyof T ? T[V] : DV : T) extends infer U ? M extends true ? U[] : U : never
|
||||
|
||||
export type SelectItemKey<T> = T extends Record<string, any> ? keyof T : string
|
||||
|
||||
export type SelectModelValueEmits<T, V, M extends boolean = false, DV = T> = {
|
||||
'update:modelValue': [payload: SelectModelValue<T, V, M, DV>]
|
||||
export type AcceptableValue = Exclude<_AcceptableValue, Record<string, any>>
|
||||
export type ArrayOrNested<T> = T[] | T[][]
|
||||
export type NestedItem<T> = T extends Array<infer I> ? NestedItem<I> : T
|
||||
type AllKeys<T> = T extends any ? keyof T : never
|
||||
type NonCommonKeys<T extends object> = Exclude<AllKeys<T>, keyof T>
|
||||
type PickTypeOf<T, K extends string | number | symbol> = K extends AllKeys<T>
|
||||
? T extends { [k in K]?: any }
|
||||
? T[K]
|
||||
: undefined
|
||||
: never
|
||||
export type MergeTypes<T extends object> = {
|
||||
[k in keyof T]: PickTypeOf<T, k>;
|
||||
} & {
|
||||
[k in NonCommonKeys<T>]?: PickTypeOf<T, k>;
|
||||
}
|
||||
|
||||
export type MaybeMultipleModelValue<T, M extends boolean = false> = (T extends infer U ? M extends true ? U[] : U : never)
|
||||
export type GetItemKeys<I> = keyof Extract<NestedItem<I>, object>
|
||||
|
||||
export type GetItemValue<I, VK extends GetItemKeys<I> | undefined, T extends NestedItem<I> = NestedItem<I>> =
|
||||
T extends object
|
||||
? VK extends undefined
|
||||
? T
|
||||
: VK extends keyof T
|
||||
? T[VK]
|
||||
: never
|
||||
: T
|
||||
|
||||
export type GetModelValue<
|
||||
T,
|
||||
VK extends GetItemKeys<T> | undefined,
|
||||
M extends boolean
|
||||
> = M extends true
|
||||
? GetItemValue<T, VK>[]
|
||||
: GetItemValue<T, VK>
|
||||
|
||||
export type GetModelValueEmits<
|
||||
T,
|
||||
VK extends GetItemKeys<T> | undefined,
|
||||
M extends boolean
|
||||
> = {
|
||||
/** Event handler called when the value changes. */
|
||||
'update:modelValue': [payload: GetModelValue<T, VK, M>]
|
||||
}
|
||||
|
||||
export type StringOrVNode =
|
||||
| string
|
||||
|
||||
@@ -81,3 +81,7 @@ export function compare<T>(value?: T, currentValue?: T, comparator?: string | ((
|
||||
|
||||
return isEqual(value, currentValue)
|
||||
}
|
||||
|
||||
export function isArrayOfArray<A>(item: A[] | A[][]): item is A[][] {
|
||||
return Array.isArray(item[0])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user