mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-25 09:20:36 +01:00
feat(Select): new component (#92)
This commit is contained in:
@@ -49,7 +49,7 @@ const props = withDefaults(defineProps<AccordionProps<T>>(), {
|
||||
collapsible: true
|
||||
})
|
||||
const emits = defineEmits<AccordionEmits>()
|
||||
defineSlots<AccordionSlots<T>>()
|
||||
const slots = defineSlots<AccordionSlots<T>>()
|
||||
|
||||
const appConfig = useAppConfig()
|
||||
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'collapsible', 'defaultValue', 'disabled', 'modelValue', 'type'), emits)
|
||||
@@ -67,7 +67,7 @@ const ui = computed(() => tv({ extend: accordion, slots: props.ui })({ disabled:
|
||||
<UIcon v-if="item.icon" :name="item.icon" :class="ui.leadingIcon()" />
|
||||
</slot>
|
||||
|
||||
<span v-if="item.label || $slots.default" :class="ui.label()">
|
||||
<span v-if="item.label || !!slots.default" :class="ui.label()">
|
||||
<slot :item="item" :index="index">{{ item.label }}</slot>
|
||||
</span>
|
||||
|
||||
@@ -77,7 +77,7 @@ const ui = computed(() => tv({ extend: accordion, slots: props.ui })({ disabled:
|
||||
</AccordionTrigger>
|
||||
</AccordionHeader>
|
||||
|
||||
<AccordionContent v-if="item.content || $slots.content || (item.slot && $slots[item.slot])" v-bind="contentProps" :class="ui.content()">
|
||||
<AccordionContent v-if="item.content || !!slots.content || (item.slot && !!slots[item.slot])" v-bind="contentProps" :class="ui.content()">
|
||||
<slot :name="item.slot || 'content'" :item="item" :index="index">
|
||||
{{ item.content }}
|
||||
</slot>
|
||||
|
||||
@@ -47,7 +47,7 @@ import { UIcon, UAvatar } from '#components'
|
||||
|
||||
const props = withDefaults(defineProps<AlertProps>(), { as: 'div' })
|
||||
const emits = defineEmits<AlertEmits>()
|
||||
defineSlots<AlertSlots>()
|
||||
const slots = defineSlots<AlertSlots>()
|
||||
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
@@ -67,12 +67,12 @@ const ui = computed(() => tv({ extend: alert, slots: props.ui })({
|
||||
</slot>
|
||||
|
||||
<div :class="ui.wrapper()">
|
||||
<div v-if="title || $slots.title" :class="ui.title()">
|
||||
<div v-if="title || !!slots.title" :class="ui.title()">
|
||||
<slot name="title">
|
||||
{{ title }}
|
||||
</slot>
|
||||
</div>
|
||||
<template v-if="description || $slots.description">
|
||||
<template v-if="description || !!slots.description">
|
||||
<component :is="description" v-if="description && isVNode(description)" />
|
||||
<div v-else :class="ui.description()">
|
||||
<slot name="description">
|
||||
|
||||
@@ -44,7 +44,7 @@ import { ULink, UIcon, UAvatar } from '#components'
|
||||
import { omit } from '#ui/utils'
|
||||
|
||||
const props = defineProps<BreadcrumbProps<T>>()
|
||||
defineSlots<BreadcrumbSlots<T>>()
|
||||
const slots = defineSlots<BreadcrumbSlots<T>>()
|
||||
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
@@ -63,7 +63,7 @@ const ui = computed(() => tv({ extend: breadcrumb, slots: props.ui })())
|
||||
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ active: index === items!.length - 1 })" />
|
||||
</slot>
|
||||
|
||||
<span v-if="item.label || $slots[item.slot ? `${item.slot}-label`: 'item-label']" :class="ui.itemLabel()">
|
||||
<span v-if="item.label || !!slots[item.slot ? `${item.slot}-label`: 'item-label']" :class="ui.itemLabel()">
|
||||
<slot :name="item.slot ? `${item.slot}-label`: 'item-label'" :item="item" :active="index === items!.length - 1" :index="index">
|
||||
{{ item.label }}
|
||||
</slot>
|
||||
|
||||
@@ -36,7 +36,7 @@ import { computed } from 'vue'
|
||||
import { useForwardProps } from 'radix-vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { useComponentIcons, useButtonGroup } from '#imports'
|
||||
import { UIcon, UAvatar, ULink } from '#components'
|
||||
import { UIcon, ULink } from '#components'
|
||||
|
||||
const props = defineProps<ButtonProps>()
|
||||
const slots = defineSlots<ButtonSlots>()
|
||||
@@ -44,7 +44,7 @@ const slots = defineSlots<ButtonSlots>()
|
||||
const linkProps = useForwardProps(reactiveOmit(props, 'type', 'label', 'color', 'variant', 'size', 'icon', 'leading', 'leadingIcon', 'trailing', 'trailingIcon', 'loading', 'loadingIcon', 'square', 'block', 'disabled', 'truncate', 'class', 'ui'))
|
||||
|
||||
const { orientation, size: buttonSize } = useButtonGroup<ButtonProps>(props)
|
||||
const { isLeading, isTrailing, leadingIconName, trailingIconName, avatarSize } = useComponentIcons<ButtonProps>(props)
|
||||
const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons<ButtonProps>(props)
|
||||
|
||||
const ui = computed(() => tv({ extend: button, slots: props.ui })({
|
||||
color: props.color,
|
||||
@@ -64,10 +64,9 @@ const ui = computed(() => tv({ extend: button, slots: props.ui })({
|
||||
<ULink :type="type" :disabled="disabled || loading" :class="ui.base({ class: props.class })" v-bind="linkProps" raw>
|
||||
<slot name="leading">
|
||||
<UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="ui.leadingIcon()" />
|
||||
<UAvatar v-else-if="avatar" :size="avatarSize" v-bind="avatar" :class="ui.leadingAvatar()" />
|
||||
</slot>
|
||||
|
||||
<span v-if="label || $slots.default" :class="ui.label()">
|
||||
<span v-if="label || !!slots.default" :class="ui.label()">
|
||||
<slot>
|
||||
{{ label }}
|
||||
</slot>
|
||||
|
||||
@@ -26,22 +26,22 @@ import { computed } from 'vue'
|
||||
import { Primitive } from 'radix-vue'
|
||||
|
||||
const props = withDefaults(defineProps<CardProps>(), { as: 'div' })
|
||||
defineSlots<CardSlots>()
|
||||
const slots = defineSlots<CardSlots>()
|
||||
|
||||
const ui = computed(() => tv({ extend: card, slots: props.ui })())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive :as="as" :class="ui.root({ class: props.class })">
|
||||
<div v-if="$slots.header" :class="ui.header()">
|
||||
<div v-if="!!slots.header" :class="ui.header()">
|
||||
<slot name="header" />
|
||||
</div>
|
||||
|
||||
<div v-if="$slots.default" :class="ui.body()">
|
||||
<div v-if="!!slots.default" :class="ui.body()">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<div v-if="$slots.footer" :class="ui.footer()">
|
||||
<div v-if="!!slots.footer" :class="ui.footer()">
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</Primitive>
|
||||
|
||||
@@ -46,7 +46,7 @@ import { reactivePick } from '@vueuse/core'
|
||||
import { useId, useAppConfig, useFormField } from '#imports'
|
||||
|
||||
const props = defineProps<CheckboxProps>()
|
||||
defineSlots<CheckboxSlots>()
|
||||
const slots = defineSlots<CheckboxSlots>()
|
||||
|
||||
const modelValue = defineModel<boolean | undefined>({ default: undefined })
|
||||
|
||||
@@ -104,13 +104,13 @@ function onChecked() {
|
||||
</CheckboxRoot>
|
||||
</div>
|
||||
|
||||
<div v-if="(label || $slots.label) || (description || $slots.description)" :class="ui.wrapper()">
|
||||
<Label v-if="label || $slots.label" :for="id" :class="ui.label()">
|
||||
<div v-if="(label || !!slots.label) || (description || !!slots.description)" :class="ui.wrapper()">
|
||||
<Label v-if="label || !!slots.label" :for="id" :class="ui.label()">
|
||||
<slot name="label" :label="label">
|
||||
{{ label }}
|
||||
</slot>
|
||||
</Label>
|
||||
<p v-if="description || $slots.description" :class="ui.description()">
|
||||
<p v-if="description || !!slots.description" :class="ui.description()">
|
||||
<slot name="description" :description="description">
|
||||
{{ description }}
|
||||
</slot>
|
||||
|
||||
@@ -29,7 +29,7 @@ import { reactivePick } from '@vueuse/core'
|
||||
|
||||
const props = defineProps<CollapsibleProps>()
|
||||
const emits = defineEmits<CollapsibleEmits>()
|
||||
defineSlots<CollapsibleSlots>()
|
||||
const slots = defineSlots<CollapsibleSlots>()
|
||||
|
||||
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'defaultOpen', 'open', 'disabled'), emits)
|
||||
|
||||
@@ -38,7 +38,7 @@ const ui = computed(() => tv({ extend: collapsible, slots: props.ui })())
|
||||
|
||||
<template>
|
||||
<CollapsibleRoot v-bind="rootProps" :class="ui.root({ class: props.class })">
|
||||
<CollapsibleTrigger v-if="$slots.default" as-child>
|
||||
<CollapsibleTrigger v-if="!!slots.default" as-child>
|
||||
<slot />
|
||||
</CollapsibleTrigger>
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ export interface CommandPaletteGroup<T> {
|
||||
highlightedIcon?: string
|
||||
}
|
||||
|
||||
export interface CommandPaletteProps<G, T> extends Pick<ComboboxRootProps, 'as' | 'multiple' | 'disabled' | 'modelValue' | 'defaultValue'>, Omit<UseComponentIconsProps, 'leading' | 'trailing' | 'icon' | 'avatar'> {
|
||||
export interface CommandPaletteProps<G, T> extends Pick<ComboboxRootProps, 'as' | 'multiple' | 'disabled' | 'modelValue' | 'defaultValue'>, Pick<UseComponentIconsProps, 'loading' | 'loadingIcon'> {
|
||||
/**
|
||||
* The icon displayed in the input.
|
||||
* @defaultValue `appConfig.ui.icons.search`
|
||||
@@ -55,9 +55,7 @@ export interface CommandPaletteProps<G, T> extends Pick<ComboboxRootProps, 'as'
|
||||
ui?: Partial<typeof commandPalette.slots>
|
||||
}
|
||||
|
||||
export type CommandPaletteEmits<T> = {
|
||||
close: []
|
||||
} & Omit<ComboboxRootEmits<T>, 'update:open'>
|
||||
export type CommandPaletteEmits<T> = ComboboxRootEmits<T>
|
||||
|
||||
type SlotProps<T> = (props: { item: T, index: number }) => any
|
||||
|
||||
@@ -87,7 +85,7 @@ const props = withDefaults(defineProps<CommandPaletteProps<G, T>>(), {
|
||||
placeholder: 'Type a command or search...'
|
||||
})
|
||||
const emits = defineEmits<CommandPaletteEmits<T>>()
|
||||
defineSlots<CommandPaletteSlots<G, T>>()
|
||||
const slots = defineSlots<CommandPaletteSlots<G, T>>()
|
||||
|
||||
const searchTerm = defineModel<string>('searchTerm', { default: '' })
|
||||
|
||||
@@ -150,7 +148,7 @@ const groups = computed(() => {
|
||||
:icon="icon || appConfig.ui.icons.search"
|
||||
:class="ui.input()"
|
||||
>
|
||||
<template v-if="close || $slots.close" #trailing>
|
||||
<template v-if="close || !!slots.close" #trailing>
|
||||
<slot name="close" :class="ui.close()">
|
||||
<UButton
|
||||
v-if="close"
|
||||
@@ -161,7 +159,7 @@ const groups = computed(() => {
|
||||
aria-label="Close"
|
||||
v-bind="typeof close === 'object' ? close : {}"
|
||||
:class="ui.close()"
|
||||
@click="emits('close')"
|
||||
@click="emits('update:open', false)"
|
||||
/>
|
||||
</slot>
|
||||
</template>
|
||||
@@ -204,7 +202,7 @@ const groups = computed(() => {
|
||||
/>
|
||||
</slot>
|
||||
|
||||
<span v-if="item.label || $slots[item.slot ? `${item.slot}-label` : group.slot ? `${group.slot}-label` : `item-label`]" :class="ui.itemLabel()">
|
||||
<span v-if="item.label || !!slots[item.slot ? `${item.slot}-label` : group.slot ? `${group.slot}-label` : `item-label`]" :class="ui.itemLabel()">
|
||||
<slot :name="item.slot ? `${item.slot}-label` : group.slot ? `${group.slot}-label` : `item-label`" :item="item" :index="index">
|
||||
<span v-if="item.prefix" :class="ui.itemLabelPrefix()">{{ item.prefix }}</span>
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ const ui = computed(() => tv({ extend: contextMenu, slots: props.ui })())
|
||||
|
||||
<template>
|
||||
<ContextMenuRoot v-bind="rootProps">
|
||||
<ContextMenuTrigger v-if="$slots.default" as-child :disabled="disabled">
|
||||
<ContextMenuTrigger v-if="!!slots.default" as-child :disabled="disabled">
|
||||
<slot />
|
||||
</ContextMenuTrigger>
|
||||
|
||||
|
||||
@@ -49,13 +49,13 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
|
||||
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ active })" />
|
||||
</slot>
|
||||
|
||||
<span v-if="item.label || $slots[item.slot ? `${item.slot}-label`: 'item-label']" :class="ui.itemLabel()">
|
||||
<span v-if="item.label || !!slots[item.slot ? `${item.slot}-label`: 'item-label']" :class="ui.itemLabel()">
|
||||
<slot :name="item.slot ? `${item.slot}-label`: 'item-label'" :item="item" :active="active" :index="index">
|
||||
{{ item.label }}
|
||||
</slot>
|
||||
</span>
|
||||
|
||||
<span v-if="item.children?.length || item.kbds?.length || $slots[item.slot ? `${item.slot}-trailing`: 'item-trailing']" :class="ui.itemTrailing()">
|
||||
<span v-if="item.children?.length || item.kbds?.length || !!slots[item.slot ? `${item.slot}-trailing`: 'item-trailing']" :class="ui.itemTrailing()">
|
||||
<slot :name="item.slot ? `${item.slot}-trailing`: 'item-trailing'" :item="item" :active="active" :index="index">
|
||||
<UIcon v-if="item.children?.length" :name="appConfig.ui.icons.chevronRight" :class="ui.itemTrailingIcon()" />
|
||||
<span v-else-if="item.kbds?.length" :class="ui.itemTrailingKbds()">
|
||||
|
||||
@@ -45,7 +45,7 @@ const props = withDefaults(defineProps<DrawerProps>(), {
|
||||
overlay: true
|
||||
})
|
||||
const emits = defineEmits<DrawerEmits>()
|
||||
defineSlots<DrawerSlots>()
|
||||
const slots = defineSlots<DrawerSlots>()
|
||||
|
||||
const rootProps = useForwardPropsEmits(reactivePick(props, 'activeSnapPoint', 'closeThreshold', 'defaultOpen', 'dismissible', 'fadeFromIndex', 'fixed', 'modal', 'nested', 'open', 'scrollLockTimeout', 'shouldScaleBackground', 'snapPoints'), emits)
|
||||
const contentProps = toRef(() => props.content)
|
||||
@@ -55,7 +55,7 @@ const ui = computed(() => tv({ extend: drawer, slots: props.ui })())
|
||||
|
||||
<template>
|
||||
<DrawerRoot v-bind="rootProps">
|
||||
<DrawerTrigger v-if="$slots.default" as-child>
|
||||
<DrawerTrigger v-if="!!slots.default" as-child>
|
||||
<slot />
|
||||
</DrawerTrigger>
|
||||
|
||||
@@ -69,15 +69,15 @@ const ui = computed(() => tv({ extend: drawer, slots: props.ui })())
|
||||
|
||||
<slot name="content">
|
||||
<div :class="ui.container()">
|
||||
<div v-if="$slots.header || (title || $slots.title) || (description || $slots.description)" :class="ui.header()">
|
||||
<div v-if="!!slots.header || (title || !!slots.title) || (description || !!slots.description)" :class="ui.header()">
|
||||
<slot name="header">
|
||||
<DrawerTitle v-if="title || $slots.title" :class="ui.title()">
|
||||
<DrawerTitle v-if="title || !!slots.title" :class="ui.title()">
|
||||
<slot name="title">
|
||||
{{ title }}
|
||||
</slot>
|
||||
</DrawerTitle>
|
||||
|
||||
<DrawerDescription v-if="description || $slots.description" :class="ui.description()">
|
||||
<DrawerDescription v-if="description || !!slots.description" :class="ui.description()">
|
||||
<slot name="description">
|
||||
{{ description }}
|
||||
</slot>
|
||||
@@ -85,11 +85,11 @@ const ui = computed(() => tv({ extend: drawer, slots: props.ui })())
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<div v-if="$slots.body" :class="ui.body()">
|
||||
<div v-if="!!slots.body" :class="ui.body()">
|
||||
<slot name="body" />
|
||||
</div>
|
||||
|
||||
<div v-if="$slots.footer" :class="ui.footer()">
|
||||
<div v-if="!!slots.footer" :class="ui.footer()">
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -76,7 +76,7 @@ const ui = computed(() => tv({ extend: dropdownMenu, slots: props.ui })())
|
||||
|
||||
<template>
|
||||
<DropdownMenuRoot v-bind="rootProps">
|
||||
<DropdownMenuTrigger v-if="$slots.default" as-child :disabled="disabled">
|
||||
<DropdownMenuTrigger v-if="!!slots.default" as-child :disabled="disabled">
|
||||
<slot />
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
|
||||
@@ -49,13 +49,13 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
|
||||
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ active })" />
|
||||
</slot>
|
||||
|
||||
<span v-if="item.label || $slots[item.slot ? `${item.slot}-label`: 'item-label']" :class="ui.itemLabel()">
|
||||
<span v-if="item.label || !!slots[item.slot ? `${item.slot}-label`: 'item-label']" :class="ui.itemLabel()">
|
||||
<slot :name="item.slot ? `${item.slot}-label`: 'item-label'" :item="item" :active="active" :index="index">
|
||||
{{ item.label }}
|
||||
</slot>
|
||||
</span>
|
||||
|
||||
<span v-if="item.children?.length || item.kbds?.length || $slots[item.slot ? `${item.slot}-trailing`: 'item-trailing']" :class="ui.itemTrailing()">
|
||||
<span v-if="item.children?.length || item.kbds?.length || !!slots[item.slot ? `${item.slot}-trailing`: 'item-trailing']" :class="ui.itemTrailing()">
|
||||
<slot :name="item.slot ? `${item.slot}-trailing`: 'item-trailing'" :item="item" :active="active" :index="index">
|
||||
<UIcon v-if="item.children?.length" :name="appConfig.ui.icons.chevronRight" :class="ui.itemTrailingIcon()" />
|
||||
<span v-else-if="item.kbds?.length" :class="ui.itemTrailingKbds()">
|
||||
|
||||
@@ -42,7 +42,7 @@ import type { FormError } from '#ui/types/form'
|
||||
import { useId, formFieldInjectionKey } from '#imports'
|
||||
|
||||
const props = defineProps<FormFieldProps>()
|
||||
defineSlots<FormFieldSlots>()
|
||||
const slots = defineSlots<FormFieldSlots>()
|
||||
|
||||
const ui = computed(() => tv({ extend: formField, slots: props.ui })({
|
||||
size: props.size,
|
||||
@@ -73,20 +73,20 @@ provide(formFieldInjectionKey, computed(() => ({
|
||||
<template>
|
||||
<div :class="ui.root({ class: props.class })">
|
||||
<div :class="ui.wrapper()">
|
||||
<div v-if="label || $slots.label" :class="ui.labelWrapper()">
|
||||
<div v-if="label || !!slots.label" :class="ui.labelWrapper()">
|
||||
<Label :for="id" :class="ui.label()">
|
||||
<slot name="label" :label="label">
|
||||
{{ label }}
|
||||
</slot>
|
||||
</Label>
|
||||
<span v-if="hint || $slots.hint" :class="ui.hint()">
|
||||
<span v-if="hint || !!slots.hint" :class="ui.hint()">
|
||||
<slot name="hint" :hint="hint">
|
||||
{{ hint }}
|
||||
</slot>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p v-if="description || $slots.description" :class="ui.description()">
|
||||
<p v-if="description || !!slots.description" :class="ui.description()">
|
||||
<slot name="description" :description="description">
|
||||
{{ description }}
|
||||
</slot>
|
||||
@@ -96,12 +96,12 @@ provide(formFieldInjectionKey, computed(() => ({
|
||||
<div :class="[label && ui.container()]">
|
||||
<slot :error="error" />
|
||||
|
||||
<p v-if="(typeof error === 'string' && error) || $slots.error" :class="ui.error()">
|
||||
<p v-if="(typeof error === 'string' && error) || !!slots.error" :class="ui.error()">
|
||||
<slot name="error" :error="error">
|
||||
{{ error }}
|
||||
</slot>
|
||||
</p>
|
||||
<p v-else-if="help || $slots.help" :class="ui.help()">
|
||||
<p v-else-if="help || !!slots.help" :class="ui.help()">
|
||||
<slot name="help" :help="help">
|
||||
{{ help }}
|
||||
</slot>
|
||||
|
||||
@@ -16,10 +16,7 @@ export interface InputProps extends UseComponentIconsProps {
|
||||
id?: string
|
||||
name?: string
|
||||
type?: InputHTMLAttributes['type']
|
||||
/**
|
||||
* The placeholder text when the input is empty.
|
||||
* @defaultValue `'Type a command or search...'`
|
||||
*/
|
||||
/** The placeholder text when the input is empty. */
|
||||
placeholder?: string
|
||||
color?: InputVariants['color']
|
||||
variant?: InputVariants['variant']
|
||||
@@ -39,21 +36,21 @@ export interface InputEmits {
|
||||
export interface InputSlots {
|
||||
leading(): any
|
||||
default(): any
|
||||
trailing(props: { iconClass: string }): any
|
||||
trailing(): any
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useComponentIcons, useFormField, useButtonGroup } from '#imports'
|
||||
import { UIcon, UAvatar } from '#components'
|
||||
import { UIcon } from '#components'
|
||||
import { looseToNumber } from '#ui/utils'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
const props = withDefaults(defineProps<InputProps>(), {
|
||||
type: 'text',
|
||||
autofocusDelay: 100
|
||||
autofocusDelay: 0
|
||||
})
|
||||
const emits = defineEmits<InputEmits>()
|
||||
const slots = defineSlots<InputSlots>()
|
||||
@@ -62,7 +59,7 @@ const [modelValue, modelModifiers] = defineModel<string | number>()
|
||||
|
||||
const { emitFormBlur, emitFormInput, size: formGroupSize, color, id, name, disabled } = useFormField<InputProps>(props)
|
||||
const { orientation, size: buttonGroupSize } = useButtonGroup<InputProps>(props)
|
||||
const { isLeading, isTrailing, leadingIconName, trailingIconName, avatarSize } = useComponentIcons<InputProps>(props)
|
||||
const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons<InputProps>(props)
|
||||
|
||||
const inputSize = computed(() => buttonGroupSize.value || formGroupSize.value)
|
||||
|
||||
@@ -150,15 +147,14 @@ onMounted(() => {
|
||||
|
||||
<slot />
|
||||
|
||||
<span v-if="isLeading || $slots.leading" :class="ui.leading()">
|
||||
<span v-if="isLeading || !!slots.leading" :class="ui.leading()">
|
||||
<slot name="leading">
|
||||
<UAvatar v-if="avatar" :size="avatarSize" v-bind="avatar" :class="ui.leadingAvatar()" />
|
||||
<UIcon v-else-if="leadingIconName" :name="leadingIconName" :class="ui.leadingIcon()" />
|
||||
<UIcon v-if="leadingIconName" :name="leadingIconName" :class="ui.leadingIcon()" />
|
||||
</slot>
|
||||
</span>
|
||||
|
||||
<span v-if="isTrailing || $slots.trailing" :class="ui.trailing()">
|
||||
<slot name="trailing" :icon-class="ui.trailingIcon()">
|
||||
<span v-if="isTrailing || !!slots.trailing" :class="ui.trailing()">
|
||||
<slot name="trailing">
|
||||
<UIcon v-if="trailingIconName" :name="trailingIconName" :class="ui.trailingIcon()" />
|
||||
</slot>
|
||||
</span>
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { tv } from 'tailwind-variants'
|
||||
import type { InputHTMLAttributes } from 'vue'
|
||||
import { tv, type VariantProps } from 'tailwind-variants'
|
||||
import type { ComboboxRootProps, ComboboxRootEmits, ComboboxContentProps, ComboboxItemProps, ComboboxArrowProps } from 'radix-vue'
|
||||
import type { AppConfig } from '@nuxt/schema'
|
||||
import _appConfig from '#build/app.config'
|
||||
import theme from '#build/ui/input-menu'
|
||||
import type { UseComponentIconsProps } from '#ui/composables/useComponentIcons'
|
||||
import type { AvatarProps, ChipProps, InputProps } from '#ui/types'
|
||||
|
||||
type AcceptableValue = string | number | boolean | Record<string, any>
|
||||
type ArrayOrWrapped<T> = T extends any[] ? T : Array<T>
|
||||
import type { AcceptableValue, ArrayOrWrapped } from '#ui/types/utils'
|
||||
|
||||
const appConfig = _appConfig as AppConfig & { ui: { inputMenu: Partial<typeof theme> } }
|
||||
|
||||
@@ -26,9 +25,21 @@ export interface InputMenuItem extends Pick<ComboboxItemProps, 'disabled'> {
|
||||
type?: 'label' | 'separator' | 'item'
|
||||
}
|
||||
|
||||
export interface InputMenuProps<T> extends Omit<ComboboxRootProps, 'asChild' | 'dir' | 'filterFunction' | 'displayValue' | 'multiple'>, Omit<UseComponentIconsProps, 'leading' | 'trailing' | 'trailingIcon'> {
|
||||
type InputMenuVariants = VariantProps<typeof inputMenu>
|
||||
|
||||
export interface InputMenuProps<T> extends Omit<ComboboxRootProps, 'asChild' | 'dir' | 'filterFunction' | 'displayValue' | 'multiple'>, UseComponentIconsProps {
|
||||
id?: string
|
||||
type?: InputHTMLAttributes['type']
|
||||
/** The placeholder text when the input is empty. */
|
||||
placeholder?: string
|
||||
color?: InputMenuVariants['color']
|
||||
variant?: InputMenuVariants['variant']
|
||||
size?: InputMenuVariants['size']
|
||||
required?: boolean
|
||||
autofocus?: boolean
|
||||
autofocusDelay?: number
|
||||
/**
|
||||
* The icon displayed in the input.
|
||||
* The icon displayed to open the menu.
|
||||
* @defaultValue `appConfig.ui.icons.chevronDown`
|
||||
*/
|
||||
trailingIcon?: string
|
||||
@@ -37,12 +48,6 @@ export interface InputMenuProps<T> extends Omit<ComboboxRootProps, 'asChild' | '
|
||||
* @defaultValue `appConfig.ui.icons.check`
|
||||
*/
|
||||
selectedIcon?: string
|
||||
placeholder?: InputProps['placeholder']
|
||||
required?: InputProps['required']
|
||||
avatar?: InputProps['avatar']
|
||||
color?: InputProps['color']
|
||||
variant?: InputProps['variant']
|
||||
size?: InputProps['size']
|
||||
content?: Omit<ComboboxContentProps, 'asChild' | 'forceMount'>
|
||||
arrow?: boolean | Omit<ComboboxArrowProps, 'asChild'>
|
||||
portal?: boolean
|
||||
@@ -63,6 +68,8 @@ type SlotProps<T> = (props: { item: T, index: number }) => any
|
||||
|
||||
export type InputMenuSlots<T> = {
|
||||
'leading'(): any
|
||||
'default'(): any
|
||||
'trailing'(): any
|
||||
'empty'(props: { searchTerm?: string }): any
|
||||
'item': SlotProps<T>
|
||||
'item-leading': SlotProps<T>
|
||||
@@ -72,29 +79,44 @@ export type InputMenuSlots<T> = {
|
||||
</script>
|
||||
|
||||
<script setup lang="ts" generic="T extends InputMenuItem | AcceptableValue">
|
||||
import { computed, toRef } from 'vue'
|
||||
import { ComboboxRoot, ComboboxAnchor, ComboboxInput, ComboboxTrigger, ComboboxPortal, ComboboxContent, ComboboxViewport, ComboboxEmpty, ComboboxGroup, ComboboxLabel, ComboboxSeparator, ComboboxItem, ComboboxItemIndicator, useForwardProps, useForwardPropsEmits } from 'radix-vue'
|
||||
import { computed, ref, toRef, onMounted } from 'vue'
|
||||
import { ComboboxRoot, ComboboxAnchor, ComboboxInput, ComboboxTrigger, ComboboxPortal, ComboboxContent, ComboboxViewport, ComboboxEmpty, ComboboxGroup, ComboboxLabel, ComboboxSeparator, ComboboxItem, ComboboxItemIndicator, useForwardPropsEmits } from 'radix-vue'
|
||||
import { defu } from 'defu'
|
||||
import { reactivePick } from '@vueuse/core'
|
||||
import { useAppConfig } from '#imports'
|
||||
import { useAppConfig, useFormField, useButtonGroup, useComponentIcons } from '#imports'
|
||||
import { UIcon, UChip, UAvatar } from '#components'
|
||||
import { get } from '#ui/utils'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
const props = withDefaults(defineProps<InputMenuProps<T>>(), {
|
||||
type: 'text',
|
||||
autofocusDelay: 0,
|
||||
portal: true,
|
||||
filter: () => ['label']
|
||||
})
|
||||
const emits = defineEmits<InputMenuEmits<T>>()
|
||||
defineSlots<InputMenuSlots<T>>()
|
||||
const slots = defineSlots<InputMenuSlots<T>>()
|
||||
|
||||
const appConfig = useAppConfig()
|
||||
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'open', 'defaultOpen', 'disabled', 'name'), emits)
|
||||
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'open', 'defaultOpen'), emits)
|
||||
const contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffset: 8, position: 'popper' }) as ComboboxContentProps)
|
||||
const inputProps = useForwardProps(reactivePick(props, 'name', 'loading', 'loadingIcon', 'placeholder', 'required', 'color', 'variant', 'size'))
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { emitFormBlur, emitFormInput, size: formGroupSize, color, id, name, disabled } = useFormField<InputProps>(props)
|
||||
const { orientation, size: buttonGroupSize } = useButtonGroup<InputProps>(props)
|
||||
const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons<InputProps>(defu(props, { trailingIcon: appConfig.ui.icons.chevronDown }))
|
||||
|
||||
const ui = computed(() => tv({ extend: inputMenu, slots: props.ui })())
|
||||
const inputSize = computed(() => buttonGroupSize.value || formGroupSize.value)
|
||||
|
||||
const ui = computed(() => tv({ extend: inputMenu, slots: props.ui })({
|
||||
color: color.value,
|
||||
variant: props.variant,
|
||||
size: inputSize?.value,
|
||||
loading: props.loading,
|
||||
leading: isLeading.value || !!slots.leading,
|
||||
trailing: isTrailing.value || !!slots.trailing,
|
||||
buttonGroup: orientation.value
|
||||
}))
|
||||
|
||||
function displayValue(val: AcceptableValue) {
|
||||
if (typeof val === 'object') {
|
||||
@@ -125,24 +147,53 @@ function filterFunction(items: ArrayOrWrapped<AcceptableValue>, searchTerm: stri
|
||||
}
|
||||
|
||||
const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0]) ? props.items : [props.items]) as InputMenuItem[][] : [])
|
||||
|
||||
const inputRef = ref<InstanceType<typeof ComboboxInput> | null>(null)
|
||||
|
||||
function autoFocus() {
|
||||
if (props.autofocus) {
|
||||
inputRef.value?.$el?.focus()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
autoFocus()
|
||||
}, props.autofocusDelay)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ComboboxRoot v-slot="{ modelValue }" v-bind="rootProps" :display-value="displayValue" :filter-function="filterFunction" :class="ui.root({ class: props.class })">
|
||||
<ComboboxRoot
|
||||
:id="id"
|
||||
:name="name"
|
||||
:disabled="disabled"
|
||||
v-bind="rootProps"
|
||||
:display-value="displayValue"
|
||||
:filter-function="filterFunction"
|
||||
:class="ui.root({ class: props.class })"
|
||||
>
|
||||
<ComboboxAnchor as-child>
|
||||
<ComboboxInput as-child>
|
||||
<UInput v-bind="{ ...inputProps, ...$attrs }" :icon="(modelValue as InputMenuItem)?.icon || icon" :avatar="(modelValue as InputMenuItem)?.avatar || avatar" :class="ui.input()">
|
||||
<template v-if="$slots.leading" #leading>
|
||||
<slot name="leading" />
|
||||
</template>
|
||||
<ComboboxInput
|
||||
ref="inputRef"
|
||||
v-bind="$attrs"
|
||||
:type="type"
|
||||
:placeholder="placeholder"
|
||||
:required="required"
|
||||
:class="ui.base()"
|
||||
/>
|
||||
|
||||
<template #trailing="{ iconClass }">
|
||||
<ComboboxTrigger :class="ui.trigger()">
|
||||
<UIcon :name="trailingIcon || appConfig.ui.icons.chevronDown" :class="iconClass" />
|
||||
</ComboboxTrigger>
|
||||
</template>
|
||||
</UInput>
|
||||
</ComboboxInput>
|
||||
<span v-if="isLeading || !!slots.leading" :class="ui.leading()">
|
||||
<slot name="leading">
|
||||
<UIcon v-if="leadingIconName" :name="leadingIconName" :class="ui.leadingIcon()" />
|
||||
</slot>
|
||||
</span>
|
||||
|
||||
<ComboboxTrigger v-if="isTrailing || !!slots.trailing" :class="ui.trailing()">
|
||||
<slot name="trailing">
|
||||
<UIcon v-if="trailingIconName" :name="trailingIconName" :class="ui.trailingIcon()" />
|
||||
</slot>
|
||||
</ComboboxTrigger>
|
||||
</ComboboxAnchor>
|
||||
|
||||
<ComboboxPortal :disabled="!portal">
|
||||
|
||||
@@ -51,7 +51,7 @@ const props = withDefaults(defineProps<ModalProps>(), {
|
||||
transition: true
|
||||
})
|
||||
const emits = defineEmits<ModalEmits>()
|
||||
defineSlots<ModalSlots>()
|
||||
const slots = defineSlots<ModalSlots>()
|
||||
|
||||
const rootProps = useForwardPropsEmits(reactivePick(props, 'open', 'defaultOpen', 'modal'), emits)
|
||||
const contentProps = toRef(() => props.content)
|
||||
@@ -76,7 +76,7 @@ const ui = computed(() => tv({ extend: modal, slots: props.ui })({
|
||||
|
||||
<template>
|
||||
<DialogRoot v-bind="rootProps">
|
||||
<DialogTrigger v-if="$slots.default" as-child>
|
||||
<DialogTrigger v-if="!!slots.default" as-child>
|
||||
<slot />
|
||||
</DialogTrigger>
|
||||
|
||||
@@ -85,15 +85,15 @@ const ui = computed(() => tv({ extend: modal, slots: props.ui })({
|
||||
|
||||
<DialogContent :class="ui.content({ class: props.class })" v-bind="contentProps" v-on="contentEvents">
|
||||
<slot name="content">
|
||||
<div v-if="$slots.header || (title || $slots.title) || (description || $slots.description) || (close !== null || $slots.close)" :class="ui.header()">
|
||||
<div v-if="!!slots.header || (title || !!slots.title) || (description || !!slots.description) || (close !== null || !!slots.close)" :class="ui.header()">
|
||||
<slot name="header">
|
||||
<DialogTitle v-if="title || $slots.title" :class="ui.title()">
|
||||
<DialogTitle v-if="title || !!slots.title" :class="ui.title()">
|
||||
<slot name="title">
|
||||
{{ title }}
|
||||
</slot>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogDescription v-if="description || $slots.description" :class="ui.description()">
|
||||
<DialogDescription v-if="description || !!slots.description" :class="ui.description()">
|
||||
<slot name="description">
|
||||
{{ description }}
|
||||
</slot>
|
||||
@@ -116,11 +116,11 @@ const ui = computed(() => tv({ extend: modal, slots: props.ui })({
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<div v-if="$slots.body" :class="ui.body()">
|
||||
<div v-if="!!slots.body" :class="ui.body()">
|
||||
<slot name="body" />
|
||||
</div>
|
||||
|
||||
<div v-if="$slots.footer" :class="ui.footer()">
|
||||
<div v-if="!!slots.footer" :class="ui.footer()">
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</slot>
|
||||
|
||||
@@ -48,7 +48,7 @@ import { omit } from '#ui/utils'
|
||||
|
||||
const props = withDefaults(defineProps<NavigationMenuProps<T>>(), { orientation: 'horizontal' })
|
||||
const emits = defineEmits<NavigationMenuEmits>()
|
||||
defineSlots<NavigationMenuSlots<T>>()
|
||||
const slots = defineSlots<NavigationMenuSlots<T>>()
|
||||
|
||||
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'delayDuration', 'skipDelayDuration', 'orientation'), emits)
|
||||
|
||||
@@ -71,13 +71,13 @@ const lists = computed(() => props.items?.length ? (Array.isArray(props.items[0]
|
||||
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ active, disabled: !!item.disabled })" />
|
||||
</slot>
|
||||
|
||||
<span v-if="item.label || $slots[item.slot ? `${item.slot}-label`: 'item-label']" :class="ui.itemLabel()">
|
||||
<span v-if="item.label || !!slots[item.slot ? `${item.slot}-label`: 'item-label']" :class="ui.itemLabel()">
|
||||
<slot :name="item.slot ? `${item.slot}-label`: 'item-label'" :item="item" :active="active" :index="index">
|
||||
{{ item.label }}
|
||||
</slot>
|
||||
</span>
|
||||
|
||||
<span v-if="item.badge || $slots[item.slot ? `${item.slot}-trailing`: 'item-trailing']" :class="ui.itemTrailing()">
|
||||
<span v-if="item.badge || !!slots[item.slot ? `${item.slot}-trailing`: 'item-trailing']" :class="ui.itemTrailing()">
|
||||
<slot :name="item.slot ? `${item.slot}-trailing`: 'item-trailing'" :item="item" :active="active" :index="index">
|
||||
<UBadge
|
||||
v-if="item.badge"
|
||||
|
||||
@@ -62,7 +62,7 @@ const props = withDefaults(defineProps<PaginationProps>(), {
|
||||
showControls: true
|
||||
})
|
||||
const emits = defineEmits<PaginationEmits>()
|
||||
defineSlots<PaginationSlots>()
|
||||
const slots = defineSlots<PaginationSlots>()
|
||||
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
@@ -74,12 +74,12 @@ const ui = computed(() => tv({ extend: pagination, slots: props.ui })())
|
||||
<template>
|
||||
<PaginationRoot v-slot="{ page, pageCount }" v-bind="rootProps" :class="ui.root({ class: props.class })">
|
||||
<PaginationList v-slot="{ items }" :class="ui.list()">
|
||||
<PaginationFirst v-if="showControls || $slots.first" as-child>
|
||||
<PaginationFirst v-if="showControls || !!slots.first" as-child>
|
||||
<slot name="first">
|
||||
<UButton :color="color" :variant="variant" :size="size" :icon="firstIcon || appConfig.ui.icons.chevronDoubleLeft" />
|
||||
</slot>
|
||||
</PaginationFirst>
|
||||
<PaginationPrev v-if="showControls || $slots.prev" as-child>
|
||||
<PaginationPrev v-if="showControls || !!slots.prev" as-child>
|
||||
<slot name="prev">
|
||||
<UButton :color="color" :variant="variant" :size="size" :icon="prevIcon || appConfig.ui.icons.chevronLeft" />
|
||||
</slot>
|
||||
@@ -106,12 +106,12 @@ const ui = computed(() => tv({ extend: pagination, slots: props.ui })())
|
||||
</PaginationEllipsis>
|
||||
</template>
|
||||
|
||||
<PaginationNext v-if="showControls || $slots.next" as-child>
|
||||
<PaginationNext v-if="showControls || !!slots.next" as-child>
|
||||
<slot name="next">
|
||||
<UButton :color="color" :variant="variant" :size="size" :icon="nextIcon || appConfig.ui.icons.chevronRight" />
|
||||
</slot>
|
||||
</PaginationNext>
|
||||
<PaginationLast v-if="showControls || $slots.last" as-child>
|
||||
<PaginationLast v-if="showControls || !!slots.last" as-child>
|
||||
<slot name="last">
|
||||
<UButton :color="color" :variant="variant" :size="size" :icon="lastIcon || appConfig.ui.icons.chevronDoubleRight" />
|
||||
</slot>
|
||||
|
||||
@@ -44,7 +44,7 @@ const props = withDefaults(defineProps<PopoverProps>(), {
|
||||
closeDelay: 0
|
||||
})
|
||||
const emits = defineEmits<PopoverEmits>()
|
||||
defineSlots<PopoverSlots>()
|
||||
const slots = defineSlots<PopoverSlots>()
|
||||
|
||||
const pick = props.mode === 'hover' ? reactivePick(props, 'defaultOpen', 'open', 'openDelay', 'closeDelay') : reactivePick(props, 'defaultOpen', 'open', 'modal')
|
||||
const rootProps = useForwardPropsEmits(pick, emits)
|
||||
@@ -58,7 +58,7 @@ const Component = computed(() => props.mode === 'hover' ? HoverCard : Popover)
|
||||
|
||||
<template>
|
||||
<Component.Root v-bind="rootProps">
|
||||
<Component.Trigger v-if="$slots.default" as-child>
|
||||
<Component.Trigger v-if="!!slots.default" as-child>
|
||||
<slot />
|
||||
</Component.Trigger>
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ import { useId, useFormField } from '#imports'
|
||||
|
||||
const props = withDefaults(defineProps<RadioGroupProps<T>>(), { orientation: 'vertical' })
|
||||
const emits = defineEmits<RadioGroupEmits>()
|
||||
defineSlots<RadioGroupSlots<T>>()
|
||||
const slots = defineSlots<RadioGroupSlots<T>>()
|
||||
|
||||
const modelValue = defineModel<T>({
|
||||
set(value) {
|
||||
@@ -106,7 +106,7 @@ function onUpdate() {
|
||||
@update:model-value="onUpdate"
|
||||
>
|
||||
<fieldset :class="ui.fieldset()">
|
||||
<legend v-if="legend || $slots.legend" :class="ui.legend()">
|
||||
<legend v-if="legend || !!slots.legend" :class="ui.legend()">
|
||||
<slot name="legend">
|
||||
{{ legend }}
|
||||
</slot>
|
||||
@@ -127,7 +127,7 @@ function onUpdate() {
|
||||
<Label :class="ui.label()" :for="option.id">
|
||||
<slot name="label" v-bind="{ option }">{{ option.label }}</slot>
|
||||
</Label>
|
||||
<p v-if="option.description || $slots.description" :class="ui.description()">
|
||||
<p v-if="option.description || !!slots.description" :class="ui.description()">
|
||||
<slot name="description" v-bind="{ option }">
|
||||
{{ option.description }}
|
||||
</slot>
|
||||
|
||||
173
src/runtime/components/Select.vue
Normal file
173
src/runtime/components/Select.vue
Normal file
@@ -0,0 +1,173 @@
|
||||
<script lang="ts">
|
||||
import { tv, type VariantProps } from 'tailwind-variants'
|
||||
import type { SelectRootProps, SelectRootEmits, SelectContentProps, SelectArrowProps, SelectItemProps } from 'radix-vue'
|
||||
import type { AppConfig } from '@nuxt/schema'
|
||||
import _appConfig from '#build/app.config'
|
||||
import theme from '#build/ui/select'
|
||||
import type { UseComponentIconsProps } from '#ui/composables/useComponentIcons'
|
||||
import type { AvatarProps, ChipProps, InputProps } from '#ui/types'
|
||||
import type { AcceptableValue } from '#ui/types/utils'
|
||||
|
||||
const appConfig = _appConfig as AppConfig & { ui: { select: Partial<typeof theme> } }
|
||||
|
||||
const select = tv({ extend: tv(theme), ...(appConfig.ui?.select || {}) })
|
||||
|
||||
export interface SelectItem extends Pick<SelectItemProps, 'disabled' | 'value'> {
|
||||
label?: string
|
||||
icon?: string
|
||||
avatar?: AvatarProps
|
||||
chip?: ChipProps
|
||||
/**
|
||||
* The item type.
|
||||
* @defaultValue `'item'`
|
||||
*/
|
||||
type?: 'label' | 'separator' | 'item'
|
||||
}
|
||||
|
||||
type SelectVariants = VariantProps<typeof select>
|
||||
|
||||
export interface SelectProps<T> extends Omit<SelectRootProps, 'asChild' | 'dir'>, UseComponentIconsProps {
|
||||
id?: string
|
||||
/** The placeholder text when the select is empty. */
|
||||
placeholder?: string
|
||||
color?: SelectVariants['color']
|
||||
variant?: SelectVariants['variant']
|
||||
size?: SelectVariants['size']
|
||||
/**
|
||||
* The icon displayed to open the menu.
|
||||
* @defaultValue `appConfig.ui.icons.chevronDown`
|
||||
*/
|
||||
trailingIcon?: string
|
||||
/**
|
||||
* The icon displayed when an item is selected.
|
||||
* @defaultValue `appConfig.ui.icons.check`
|
||||
*/
|
||||
selectedIcon?: string
|
||||
content?: Omit<SelectContentProps, 'asChild' | 'forceMount'>
|
||||
arrow?: boolean | Omit<SelectArrowProps, 'asChild'>
|
||||
portal?: boolean
|
||||
items?: T[] | T[][]
|
||||
class?: any
|
||||
ui?: Partial<typeof select.slots>
|
||||
}
|
||||
|
||||
export interface SelectEmits extends SelectRootEmits {}
|
||||
|
||||
type SlotProps<T> = (props: { item: T, index: number }) => any
|
||||
|
||||
export interface SelectSlots<T> {
|
||||
'leading'(): any
|
||||
'trailing'(): any
|
||||
'item': SlotProps<T>
|
||||
'item-leading': SlotProps<T>
|
||||
'item-label': SlotProps<T>
|
||||
'item-trailing': SlotProps<T>
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts" generic="T extends SelectItem | AcceptableValue">
|
||||
import { computed, toRef } from 'vue'
|
||||
import { SelectRoot, SelectTrigger, SelectValue, SelectPortal, SelectContent, SelectViewport, SelectLabel, SelectGroup, SelectItem, SelectItemIndicator, SelectItemText, SelectSeparator, useForwardPropsEmits } from 'radix-vue'
|
||||
import { defu } from 'defu'
|
||||
import { reactivePick } from '@vueuse/core'
|
||||
import { useAppConfig, useComponentIcons, useFormField, useButtonGroup } from '#imports'
|
||||
import { UIcon, UChip, UAvatar } from '#components'
|
||||
|
||||
const props = withDefaults(defineProps<SelectProps<T>>(), {
|
||||
portal: true
|
||||
})
|
||||
const emits = defineEmits<SelectEmits>()
|
||||
const slots = defineSlots<SelectSlots<T>>()
|
||||
|
||||
const appConfig = useAppConfig()
|
||||
const rootProps = useForwardPropsEmits(reactivePick(props, 'modelValue', 'defaultValue', 'open', 'defaultOpen', 'disabled', 'autocomplete', 'required'), emits)
|
||||
const contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffset: 8, position: 'popper' }) as SelectContentProps)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { emitFormBlur, emitFormInput, size: formGroupSize, color, id, name, disabled } = useFormField<InputProps>(props)
|
||||
const { orientation, size: buttonGroupSize } = useButtonGroup<InputProps>(props)
|
||||
const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons<InputProps>(defu(props, { trailingIcon: appConfig.ui.icons.chevronDown }))
|
||||
|
||||
const selectSize = computed(() => buttonGroupSize.value || formGroupSize.value)
|
||||
|
||||
const ui = computed(() => tv({ extend: select, slots: props.ui })({
|
||||
color: color.value,
|
||||
variant: props.variant,
|
||||
size: selectSize?.value,
|
||||
loading: props.loading,
|
||||
leading: isLeading.value || !!slots.leading,
|
||||
trailing: isTrailing.value || !!slots.trailing,
|
||||
buttonGroup: orientation.value
|
||||
}))
|
||||
|
||||
const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0]) ? props.items : [props.items]) as SelectItem[][] : [])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectRoot v-bind="rootProps" :id="id" :name="name" :disabled="disabled">
|
||||
<SelectTrigger :class="ui.base({ class: props.class })">
|
||||
<span v-if="isLeading || !!slots.leading" :class="ui.leading()">
|
||||
<slot name="leading">
|
||||
<UIcon v-if="leadingIconName" :name="leadingIconName" :class="ui.leadingIcon()" />
|
||||
</slot>
|
||||
</span>
|
||||
|
||||
<SelectValue :placeholder="placeholder" :class="ui.placeholder()" />
|
||||
|
||||
<span v-if="isTrailing || !!slots.trailing" :class="ui.trailing()">
|
||||
<slot name="trailing">
|
||||
<UIcon v-if="trailingIconName" :name="trailingIconName" :class="ui.trailingIcon()" />
|
||||
</slot>
|
||||
</span>
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectPortal :disabled="!portal">
|
||||
<SelectContent :class="ui.content()" v-bind="contentProps">
|
||||
<SelectViewport :class="ui.viewport()">
|
||||
<SelectGroup v-for="(group, groupIndex) in groups" :key="`group-${groupIndex}`" :class="ui.group()">
|
||||
<template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
|
||||
<SelectLabel v-if="item?.type === 'label'" :class="ui.label()">
|
||||
{{ item.label }}
|
||||
</SelectLabel>
|
||||
<SelectSeparator v-else-if="item?.type === 'separator'" :class="ui.separator()" />
|
||||
<SelectItem
|
||||
v-else
|
||||
:class="ui.item()"
|
||||
:disabled="item.disabled"
|
||||
:value="typeof item === 'object' ? item.value : item"
|
||||
>
|
||||
<slot name="item" :item="(item as T)" :index="index">
|
||||
<slot name="item-leading" :item="(item as T)" :index="index">
|
||||
<UAvatar v-if="item.avatar" size="2xs" v-bind="item.avatar" :class="ui.itemLeadingAvatar()" />
|
||||
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon()" />
|
||||
<UChip
|
||||
v-else-if="item.chip"
|
||||
size="md"
|
||||
inset
|
||||
standalone
|
||||
v-bind="item.chip"
|
||||
:class="ui.itemLeadingChip()"
|
||||
/>
|
||||
</slot>
|
||||
|
||||
<SelectItemText :class="ui.itemLabel()">
|
||||
<slot name="item-label" :item="(item as T)" :index="index">
|
||||
{{ typeof item === 'object' ? item.label : item }}
|
||||
</slot>
|
||||
</SelectItemText>
|
||||
|
||||
<span :class="ui.itemTrailing()">
|
||||
<slot name="item-trailing" :item="(item as T)" :index="index" />
|
||||
|
||||
<SelectItemIndicator as-child>
|
||||
<UIcon :name="selectedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingSelectedIcon()" />
|
||||
</SelectItemIndicator>
|
||||
</span>
|
||||
</slot>
|
||||
</SelectItem>
|
||||
</template>
|
||||
</SelectGroup>
|
||||
</SelectViewport>
|
||||
</SelectContent>
|
||||
</SelectPortal>
|
||||
</SelectRoot>
|
||||
</template>
|
||||
@@ -38,7 +38,7 @@ const props = withDefaults(defineProps<SeparatorProps>(), {
|
||||
as: 'div',
|
||||
orientation: 'horizontal'
|
||||
})
|
||||
defineSlots<SeparatorSlots>()
|
||||
const slots = defineSlots<SeparatorSlots>()
|
||||
|
||||
const rootProps = useForwardProps(reactivePick(props, 'as', 'decorative', 'orientation'))
|
||||
|
||||
@@ -54,7 +54,7 @@ const ui = computed(() => tv({ extend: separator, slots: props.ui })({
|
||||
<Separator v-bind="rootProps" :class="ui.root({ class: props.class })">
|
||||
<div :class="ui.border()" />
|
||||
|
||||
<template v-if="label || icon || avatar || $slots.default">
|
||||
<template v-if="label || icon || avatar || !!slots.default">
|
||||
<div :class="ui.container()">
|
||||
<slot>
|
||||
<span v-if="label" :class="ui.label()">{{ label }}</span>
|
||||
|
||||
@@ -54,7 +54,7 @@ const props = withDefaults(defineProps<SlideoverProps>(), {
|
||||
side: 'right'
|
||||
})
|
||||
const emits = defineEmits<SlideoverEmits>()
|
||||
defineSlots<SlideoverSlots>()
|
||||
const slots = defineSlots<SlideoverSlots>()
|
||||
|
||||
const rootProps = useForwardPropsEmits(reactivePick(props, 'open', 'defaultOpen', 'modal'), emits)
|
||||
const contentProps = toRef(() => props.content)
|
||||
@@ -79,7 +79,7 @@ const ui = computed(() => tv({ extend: slideover, slots: props.ui })({
|
||||
|
||||
<template>
|
||||
<DialogRoot v-bind="rootProps">
|
||||
<DialogTrigger v-if="$slots.default" as-child>
|
||||
<DialogTrigger v-if="!!slots.default" as-child>
|
||||
<slot />
|
||||
</DialogTrigger>
|
||||
|
||||
@@ -88,15 +88,15 @@ const ui = computed(() => tv({ extend: slideover, slots: props.ui })({
|
||||
|
||||
<DialogContent :data-side="side" :class="ui.content({ class: props.class })" v-bind="contentProps" v-on="contentEvents">
|
||||
<slot name="content">
|
||||
<div v-if="$slots.header || (title || $slots.title) || (description || $slots.description) || (close !== null || $slots.close)" :class="ui.header()">
|
||||
<div v-if="!!slots.header || (title || !!slots.title) || (description || !!slots.description) || (close !== null || !!slots.close)" :class="ui.header()">
|
||||
<slot name="header">
|
||||
<DialogTitle v-if="title || $slots.title" :class="ui.title()">
|
||||
<DialogTitle v-if="title || !!slots.title" :class="ui.title()">
|
||||
<slot name="title">
|
||||
{{ title }}
|
||||
</slot>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogDescription v-if="description || $slots.description" :class="ui.description()">
|
||||
<DialogDescription v-if="description || !!slots.description" :class="ui.description()">
|
||||
<slot name="description">
|
||||
{{ description }}
|
||||
</slot>
|
||||
@@ -123,7 +123,7 @@ const ui = computed(() => tv({ extend: slideover, slots: props.ui })({
|
||||
<slot name="body" />
|
||||
</div>
|
||||
|
||||
<div v-if="$slots.footer" :class="ui.footer()">
|
||||
<div v-if="!!slots.footer" :class="ui.footer()">
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</slot>
|
||||
|
||||
@@ -11,7 +11,7 @@ const slider = tv({ extend: tv(theme), ...(appConfig.ui?.slider || {}) })
|
||||
|
||||
type SliderVariants = VariantProps<typeof slider>
|
||||
|
||||
export interface SliderProps extends Omit<SliderRootProps, 'asChild' | 'defaultValue' | 'dir'> {
|
||||
export interface SliderProps extends Omit<SliderRootProps, 'asChild' | 'modelValue' | 'defaultValue' | 'dir'> {
|
||||
size?: SliderVariants['size']
|
||||
color?: SliderVariants['color']
|
||||
defaultValue?: number | number[]
|
||||
|
||||
@@ -38,7 +38,7 @@ import { reactivePick } from '@vueuse/core'
|
||||
import { useId, useAppConfig, useFormField } from '#imports'
|
||||
|
||||
const props = defineProps<SwitchProps>()
|
||||
defineSlots<SwitchSlots>()
|
||||
const slots = defineSlots<SwitchSlots>()
|
||||
|
||||
const modelValue = defineModel<boolean | undefined>({ default: undefined })
|
||||
|
||||
@@ -86,13 +86,13 @@ async function onChecked() {
|
||||
</SwitchThumb>
|
||||
</SwitchRoot>
|
||||
</div>
|
||||
<div v-if="(label || $slots.label) || (description || $slots.description)" :class="ui.wrapper()">
|
||||
<Label v-if="label || $slots.label" :for="id" :class="ui.label()">
|
||||
<div v-if="(label || !!slots.label) || (description || !!slots.description)" :class="ui.wrapper()">
|
||||
<Label v-if="label || !!slots.label" :for="id" :class="ui.label()">
|
||||
<slot name="label" :label="label">
|
||||
{{ label }}
|
||||
</slot>
|
||||
</Label>
|
||||
<p v-if="description || $slots.description" :class="ui.description()">
|
||||
<p v-if="description || !!slots.description" :class="ui.description()">
|
||||
<slot name="description" :description="description">
|
||||
{{ description }}
|
||||
</slot>
|
||||
|
||||
@@ -49,7 +49,7 @@ const props = withDefaults(defineProps<TabsProps<T>>(), {
|
||||
orientation: 'horizontal'
|
||||
})
|
||||
const emits = defineEmits<TabsEmits>()
|
||||
defineSlots<TabsSlots<T>>()
|
||||
const slots = defineSlots<TabsSlots<T>>()
|
||||
|
||||
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'defaultValue', 'orientation', 'activationMode', 'modelValue'), emits)
|
||||
const contentProps = toRef(() => defu(props.content, { forceMount: true }) as TabsContentProps)
|
||||
@@ -68,7 +68,7 @@ const ui = computed(() => tv({ extend: tabs, slots: props.ui })({ orientation: p
|
||||
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.leadingIcon()" />
|
||||
</slot>
|
||||
|
||||
<span v-if="item.label || $slots.default" :class="ui.label()">
|
||||
<span v-if="item.label || !!slots.default" :class="ui.label()">
|
||||
<slot :item="item" :index="index">{{ item.label }}</slot>
|
||||
</span>
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ defineOptions({ inheritAttrs: false })
|
||||
const props = withDefaults(defineProps<TextareaProps>(), {
|
||||
rows: 3,
|
||||
maxrows: 0,
|
||||
autofocusDelay: 100
|
||||
autofocusDelay: 0
|
||||
})
|
||||
defineSlots<TextareaSlots>()
|
||||
const emits = defineEmits<TextareaEmits>()
|
||||
|
||||
@@ -45,7 +45,7 @@ import { UIcon, UAvatar } from '#components'
|
||||
|
||||
const props = defineProps<ToastProps>()
|
||||
const emits = defineEmits<ToastEmits>()
|
||||
defineSlots<ToastSlots>()
|
||||
const slots = defineSlots<ToastSlots>()
|
||||
|
||||
const toaster = inject<ToasterContext>('Toaster')
|
||||
|
||||
@@ -89,12 +89,12 @@ defineExpose({
|
||||
</slot>
|
||||
|
||||
<div :class="ui.wrapper()">
|
||||
<ToastTitle v-if="title || $slots.title" :class="ui.title()">
|
||||
<ToastTitle v-if="title || !!slots.title" :class="ui.title()">
|
||||
<slot name="title">
|
||||
{{ title }}
|
||||
</slot>
|
||||
</ToastTitle>
|
||||
<template v-if="description || $slots.description">
|
||||
<template v-if="description || !!slots.description">
|
||||
<ToastDescription v-if="description && isVNode(description)" :as="description" />
|
||||
<ToastDescription v-else :class="ui.description()">
|
||||
<slot name="description">
|
||||
|
||||
@@ -37,7 +37,7 @@ import { UKbd } from '#components'
|
||||
|
||||
const props = withDefaults(defineProps<TooltipProps>(), { portal: true })
|
||||
const emits = defineEmits<TooltipEmits>()
|
||||
defineSlots<TooltipSlots>()
|
||||
const slots = defineSlots<TooltipSlots>()
|
||||
|
||||
const rootProps = useForwardPropsEmits(reactivePick(props, 'defaultOpen', 'open', 'delayDuration'), emits)
|
||||
const contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffset: 8 }) as TooltipContentProps)
|
||||
@@ -48,7 +48,7 @@ const ui = computed(() => tv({ extend: tooltip, slots: props.ui })({ side: conte
|
||||
|
||||
<template>
|
||||
<TooltipRoot v-bind="rootProps">
|
||||
<TooltipTrigger v-if="$slots.default" as-child>
|
||||
<TooltipTrigger v-if="!!slots.default" as-child>
|
||||
<slot />
|
||||
</TooltipTrigger>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user