feat(module)!: migrate to reka-ui (#2448)

This commit is contained in:
Benjamin Canac
2024-12-03 16:11:32 +01:00
committed by GitHub
parent c440c91a29
commit 81ac076219
229 changed files with 13487 additions and 13466 deletions

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { tv } from 'tailwind-variants'
import type { AccordionRootProps, AccordionRootEmits } from 'radix-vue'
import type { AccordionRootProps, AccordionRootEmits } from 'reka-ui'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/accordion'
@@ -22,7 +22,7 @@ export interface AccordionItem {
disabled?: boolean
}
export interface AccordionProps<T> extends Pick<AccordionRootProps, 'collapsible' | 'defaultValue' | 'modelValue' | 'type' | 'disabled'> {
export interface AccordionProps<T> extends Pick<AccordionRootProps, 'collapsible' | 'defaultValue' | 'modelValue' | 'type' | 'disabled' | 'unmountOnHide'> {
/**
* The element or component this component should render as.
* @defaultValue 'div'
@@ -89,7 +89,7 @@ extendDevtoolsMeta({
<script setup lang="ts" generic="T extends AccordionItem">
import { computed } from 'vue'
import { AccordionRoot, AccordionItem, AccordionHeader, AccordionTrigger, AccordionContent, useForwardPropsEmits } from 'radix-vue'
import { AccordionRoot, AccordionItem, AccordionHeader, AccordionTrigger, AccordionContent, useForwardPropsEmits } from 'reka-ui'
import { reactivePick } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { get } from '../utils'
@@ -104,7 +104,7 @@ const emits = defineEmits<AccordionEmits>()
const slots = defineSlots<AccordionSlots<T>>()
const appConfig = useAppConfig()
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'collapsible', 'defaultValue', 'disabled', 'modelValue', 'type'), emits)
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'collapsible', 'defaultValue', 'disabled', 'modelValue', 'type', 'unmountOnHide'), emits)
const ui = computed(() => accordion({
disabled: props.disabled

View File

@@ -64,7 +64,7 @@ extendDevtoolsMeta<AlertProps>({ defaultProps: { title: 'Heads up!' } })
<script setup lang="ts">
import { computed } from 'vue'
import { Primitive } from 'radix-vue'
import { Primitive } from 'reka-ui'
import { useAppConfig } from '#imports'
import { useLocale } from '../composables/useLocale'
import UIcon from './Icon.vue'
@@ -75,8 +75,8 @@ const props = defineProps<AlertProps>()
const emits = defineEmits<AlertEmits>()
const slots = defineSlots<AlertSlots>()
const appConfig = useAppConfig()
const { t } = useLocale()
const appConfig = useAppConfig()
const multiline = computed(() => !!props.title && !!props.description)

View File

@@ -1,10 +1,10 @@
<script lang="ts">
import type { ConfigProviderProps, TooltipProviderProps } from 'radix-vue'
import type { ConfigProviderProps, TooltipProviderProps } from 'reka-ui'
import { localeContextInjectionKey } from '../composables/useLocale'
import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
import type { ToasterProps, Locale } from '../types'
export interface AppProps extends Omit<ConfigProviderProps, 'useId' | 'dir'> {
export interface AppProps extends Omit<ConfigProviderProps, 'useId' | 'dir' | 'locale'> {
tooltip?: TooltipProviderProps
toaster?: ToasterProps | null
locale?: Locale
@@ -23,7 +23,7 @@ extendDevtoolsMeta({ ignore: true })
<script setup lang="ts">
import { toRef, useId, provide } from 'vue'
import { ConfigProvider, TooltipProvider, useForwardProps } from 'radix-vue'
import { ConfigProvider, TooltipProvider, useForwardProps } from 'reka-ui'
import { reactivePick } from '@vueuse/core'
import UToaster from './Toaster.vue'
import UModalProvider from './ModalProvider.vue'
@@ -41,7 +41,7 @@ provide(localeContextInjectionKey, locale)
</script>
<template>
<ConfigProvider :use-id="() => (useId() as string)" :dir="locale?.dir" v-bind="configProviderProps">
<ConfigProvider :use-id="() => (useId() as string)" :dir="locale?.dir" :locale="locale?.code" v-bind="configProviderProps">
<TooltipProvider v-bind="tooltipProps">
<UToaster v-if="toaster !== null" v-bind="toasterProps">
<slot />

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { tv, type VariantProps } from 'tailwind-variants'
import type { AvatarFallbackProps } from 'radix-vue'
import type { AvatarFallbackProps } from 'reka-ui'
import type { AppConfig } from '@nuxt/schema'
import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
import _appConfig from '#build/app.config'
@@ -32,7 +32,7 @@ extendDevtoolsMeta<AvatarProps>({ defaultProps: { src: 'https://avatars.githubus
<script setup lang="ts">
import { computed } from 'vue'
import { AvatarRoot, AvatarImage, AvatarFallback, useForwardProps } from 'radix-vue'
import { AvatarRoot, AvatarImage, AvatarFallback, useForwardProps } from 'reka-ui'
import { reactivePick } from '@vueuse/core'
import { useAvatarGroup } from '../composables/useAvatarGroup'
import UIcon from './Icon.vue'

View File

@@ -35,7 +35,7 @@ extendDevtoolsMeta({ example: 'AvatarGroupExample' })
<script setup lang="ts">
import { computed, provide } from 'vue'
import { Primitive } from 'radix-vue'
import { Primitive } from 'reka-ui'
import { avatarGroupInjectionKey } from '../composables/useAvatarGroup'
import UAvatar from './Avatar.vue'

View File

@@ -36,7 +36,7 @@ export interface BadgeSlots {
<script setup lang="ts">
import { computed } from 'vue'
import { Primitive } from 'radix-vue'
import { Primitive } from 'reka-ui'
import { useComponentIcons } from '../composables/useComponentIcons'
import UIcon from './Icon.vue'

View File

@@ -77,7 +77,7 @@ extendDevtoolsMeta({
<script setup lang="ts" generic="T extends BreadcrumbItem">
import { computed } from 'vue'
import { Primitive } from 'radix-vue'
import { Primitive } from 'reka-ui'
import { useAppConfig } from '#imports'
import { useLocale } from '../composables/useLocale'
import { get } from '../utils'

View File

@@ -43,7 +43,7 @@ export interface ButtonSlots {
<script setup lang="ts">
import { type Ref, computed, ref, inject } from 'vue'
import { useForwardProps } from 'radix-vue'
import { useForwardProps } from 'reka-ui'
import { useComponentIcons } from '../composables/useComponentIcons'
import { useButtonGroup } from '../composables/useButtonGroup'
import { formLoadingInjectionKey } from '../composables/useFormField'

View File

@@ -35,7 +35,7 @@ extendDevtoolsMeta({ example: 'ButtonGroupExample' })
<script setup lang="ts">
import { provide, computed } from 'vue'
import { Primitive } from 'radix-vue'
import { Primitive } from 'reka-ui'
import { buttonGroupInjectionKey } from '../composables/useButtonGroup'
const props = withDefaults(defineProps<ButtonGroupProps>(), {

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { tv, type VariantProps } from 'tailwind-variants'
import type { CalendarRootProps, CalendarRootEmits, RangeCalendarRootEmits, DateRange, CalendarCellTriggerProps } from 'radix-vue'
import type { CalendarRootProps, CalendarRootEmits, RangeCalendarRootEmits, DateRange, CalendarCellTriggerProps } from 'reka-ui'
import type { DateValue } from '@internationalized/date'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
@@ -73,8 +73,8 @@ export interface CalendarSlots {
<script setup lang="ts" generic="R extends boolean = false, M extends boolean = false">
import { computed } from 'vue'
import { useForwardPropsEmits } from 'radix-vue'
import { Calendar as SingleCalendar, RangeCalendar } from 'radix-vue/namespaced'
import { useForwardPropsEmits } from 'reka-ui'
import { Calendar as SingleCalendar, RangeCalendar } from 'reka-ui/namespaced'
import { reactiveOmit } from '@vueuse/core'
import { useLocale } from '../composables/useLocale'
import UButton from './Button.vue'

View File

@@ -29,7 +29,7 @@ extendDevtoolsMeta({ example: 'CardExample' })
</script>
<script setup lang="ts">
import { Primitive } from 'radix-vue'
import { Primitive } from 'reka-ui'
const props = defineProps<CardProps>()
const slots = defineSlots<CardSlots>()

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { tv, type VariantProps } from 'tailwind-variants'
import type { AppConfig } from '@nuxt/schema'
import type { AcceptableValue } from 'reka-ui'
import type { EmblaCarouselType, EmblaOptionsType, EmblaPluginType } from 'embla-carousel'
import type { AutoplayOptionsType } from 'embla-carousel-autoplay'
import type { AutoScrollOptionsType } from 'embla-carousel-auto-scroll'
@@ -12,7 +13,7 @@ import _appConfig from '#build/app.config'
import theme from '#build/ui/carousel'
import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
import type { ButtonProps } from '../types'
import type { AcceptableValue, PartialString } from '../types/utils'
import type { PartialString } from '../types/utils'
const appConfig = _appConfig as AppConfig & { ui: { carousel: Partial<typeof theme> } }
@@ -21,6 +22,11 @@ const carousel = tv({ extend: tv(theme), ...(appConfig.ui?.carousel || {}) })
type CarouselVariants = VariantProps<typeof carousel>
export interface CarouselProps<T> extends Omit<EmblaOptionsType, 'axis' | 'container' | 'slides' | 'direction'> {
/**
* The element or component this component should render as.
* @defaultValue 'div'
*/
as?: any
/**
* Configure the prev button when arrows are enabled.
* @defaultValue { size: 'md', color: 'neutral', variant: 'link' }
@@ -97,7 +103,7 @@ extendDevtoolsMeta({ example: 'CarouselExample' })
<script setup lang="ts" generic="T extends AcceptableValue">
import { computed, ref, watch, onMounted } from 'vue'
import useEmblaCarousel from 'embla-carousel-vue'
import { useForwardProps } from 'radix-vue'
import { Primitive, useForwardProps } from 'reka-ui'
import { reactivePick, computedAsync } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { useLocale } from '../composables/useLocale'
@@ -254,7 +260,8 @@ defineExpose({
</script>
<template>
<div
<Primitive
:as="as"
role="region"
aria-roledescription="carousel"
tabindex="0"
@@ -311,5 +318,5 @@ defineExpose({
</template>
</div>
</div>
</div>
</Primitive>
</template>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { tv, type VariantProps } from 'tailwind-variants'
import type { CheckboxRootProps } from 'radix-vue'
import type { CheckboxRootProps } from 'reka-ui'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/checkbox'
@@ -12,7 +12,12 @@ const checkbox = tv({ extend: tv(theme), ...(appConfig.ui?.checkbox || {}) })
type CheckboxVariants = VariantProps<typeof checkbox>
export interface CheckboxProps extends Pick<CheckboxRootProps, 'disabled' | 'required' | 'name' | 'value' | 'id'> {
export interface CheckboxProps extends Pick<CheckboxRootProps, 'disabled' | 'required' | 'name' | 'value' | 'id' | 'defaultValue'> {
/**
* The element or component this component should render as.
* @defaultValue 'div'
*/
as?: any
label?: string
description?: string
color?: CheckboxVariants['color']
@@ -22,21 +27,17 @@ export interface CheckboxProps extends Pick<CheckboxRootProps, 'disabled' | 'req
* @defaultValue appConfig.ui.icons.check
*/
icon?: string
indeterminate?: boolean
/**
* The icon displayed when the checkbox is indeterminate.
* @defaultValue appConfig.ui.icons.minus
*/
indeterminateIcon?: string
/** The checked state of the checkbox when it is initially rendered. Use when you do not need to control its checked state. */
defaultValue?: boolean
class?: any
ui?: Partial<typeof checkbox.slots>
}
export interface CheckboxEmits {
(e: 'update:modelValue', payload: boolean): void
(e: 'change', payload: Event): void
export type CheckboxEmits = {
change: [payload: Event]
}
export interface CheckboxSlots {
@@ -49,7 +50,7 @@ extendDevtoolsMeta({ defaultProps: { label: 'Check me!' } })
<script setup lang="ts">
import { computed, useId } from 'vue'
import { CheckboxRoot, CheckboxIndicator, Label, useForwardProps } from 'radix-vue'
import { Primitive, CheckboxRoot, CheckboxIndicator, Label, useForwardProps } from 'reka-ui'
import { reactivePick } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { useFormField } from '../composables/useFormField'
@@ -59,29 +60,20 @@ const props = defineProps<CheckboxProps>()
const slots = defineSlots<CheckboxSlots>()
const emits = defineEmits<CheckboxEmits>()
const modelValue = defineModel<boolean | undefined>({ default: undefined })
const modelValue = defineModel<boolean | 'indeterminate'>({ default: undefined })
const rootProps = useForwardProps(reactivePick(props, 'required', 'value'))
const rootProps = useForwardProps(reactivePick(props, 'required', 'value', 'defaultValue'))
const appConfig = useAppConfig()
const { id: _id, emitFormChange, emitFormInput, size, color, name, disabled } = useFormField<CheckboxProps>(props)
const id = _id.value ?? useId()
const checked = computed({
get() {
return props.indeterminate ? 'indeterminate' : modelValue.value
},
set(value) {
modelValue.value = value === 'indeterminate' ? undefined : value
}
})
const ui = computed(() => checkbox({
size: size.value,
color: color.value,
required: props.required,
disabled: disabled.value,
checked: (modelValue.value ?? props.defaultValue) || props.indeterminate
checked: Boolean(modelValue.value ?? props.defaultValue)
}))
function onUpdate(value: any) {
@@ -93,23 +85,25 @@ function onUpdate(value: any) {
}
</script>
<!-- eslint-disable vue/no-template-shadow -->
<template>
<div :class="ui.root({ class: [props.class, props.ui?.root] })">
<Primitive :as="as" :class="ui.root({ class: [props.class, props.ui?.root] })">
<div :class="ui.container({ class: props.ui?.container })">
<CheckboxRoot
:id="id"
v-model:checked="checked"
:default-checked="defaultValue"
v-bind="rootProps"
v-model="modelValue"
:name="name"
:disabled="disabled"
:class="ui.base({ class: props.ui?.base })"
@update:checked="onUpdate"
@update:model-value="onUpdate"
>
<CheckboxIndicator as-child>
<UIcon v-if="indeterminate" :name="indeterminateIcon || appConfig.ui.icons.minus" :class="ui.icon({ class: props.ui?.icon })" />
<UIcon v-else :name="icon || appConfig.ui.icons.check" :class="ui.icon({ class: props.ui?.icon })" />
</CheckboxIndicator>
<template #default="{ modelValue }">
<CheckboxIndicator as-child>
<UIcon v-if="modelValue === 'indeterminate'" :name="indeterminateIcon || appConfig.ui.icons.minus" :class="ui.icon({ class: props.ui?.icon })" />
<UIcon v-else :name="icon || appConfig.ui.icons.check" :class="ui.icon({ class: props.ui?.icon })" />
</CheckboxIndicator>
</template>
</CheckboxRoot>
</div>
@@ -125,5 +119,5 @@ function onUpdate(value: any) {
</slot>
</p>
</div>
</div>
</Primitive>
</template>

View File

@@ -44,7 +44,7 @@ extendDevtoolsMeta({ example: 'ChipExample' })
<script setup lang="ts">
import { computed } from 'vue'
import { Primitive, Slot } from 'radix-vue'
import { Primitive, Slot } from 'reka-ui'
import { useAvatarGroup } from '../composables/useAvatarGroup'
defineOptions({ inheritAttrs: false })

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { tv } from 'tailwind-variants'
import type { CollapsibleRootProps, CollapsibleRootEmits } from 'radix-vue'
import type { CollapsibleRootProps, CollapsibleRootEmits } from 'reka-ui'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/collapsible'
@@ -10,7 +10,7 @@ const appConfig = _appConfig as AppConfig & { ui: { collapsible: Partial<typeof
const collapsible = tv({ extend: tv(theme), ...(appConfig.ui?.collapsible || {}) })
export interface CollapsibleProps extends Pick<CollapsibleRootProps, 'defaultOpen' | 'open' | 'disabled'> {
export interface CollapsibleProps extends Pick<CollapsibleRootProps, 'defaultOpen' | 'open' | 'disabled' | 'unmountOnHide'> {
/**
* The element or component this component should render as.
* @defaultValue 'div'
@@ -31,14 +31,14 @@ extendDevtoolsMeta({ example: 'CollapsibleExample' })
</script>
<script setup lang="ts">
import { CollapsibleRoot, CollapsibleTrigger, CollapsibleContent, useForwardPropsEmits } from 'radix-vue'
import { CollapsibleRoot, CollapsibleTrigger, CollapsibleContent, useForwardPropsEmits } from 'reka-ui'
import { reactivePick } from '@vueuse/core'
const props = defineProps<CollapsibleProps>()
const emits = defineEmits<CollapsibleEmits>()
const slots = defineSlots<CollapsibleSlots>()
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'defaultOpen', 'open', 'disabled'), emits)
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'defaultOpen', 'open', 'disabled', 'unmountOnHide'), emits)
// eslint-disable-next-line vue/no-dupe-keys
const ui = collapsible()

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { tv } from 'tailwind-variants'
import type { ComboboxRootProps, ComboboxRootEmits } from 'radix-vue'
import type { ListboxRootProps, ListboxRootEmits } from 'reka-ui'
import type { FuseResult } from 'fuse.js'
import type { AppConfig } from '@nuxt/schema'
import type { UseFuseOptions } from '@vueuse/integrations/useFuse'
@@ -37,17 +37,17 @@ export interface CommandPaletteGroup<T> {
items?: T[]
/**
* Whether to filter group items with [useFuse](https://vueuse.org/integrations/useFuse).
* When `false`, items will not be filtered which is useful for custom filtering (useAsyncData, useFetch, etc.).
* @defaultValue true
* When `true`, items will not be filtered which is useful for custom filtering (useAsyncData, useFetch, etc.).
* @defaultValue false
*/
filter?: boolean
ignoreFilter?: boolean
/** Filter group items after the search happened. */
postFilter?: (searchTerm: string, items: T[]) => T[]
/** The icon displayed when an item is highlighted. */
highlightedIcon?: string
}
export interface CommandPaletteProps<G, T> extends Pick<ComboboxRootProps, 'multiple' | 'disabled' | 'modelValue' | 'defaultValue' | 'selectedValue' | 'resetSearchTermOnBlur'>, Pick<UseComponentIconsProps, 'loading' | 'loadingIcon'> {
export interface CommandPaletteProps<G, T> extends Pick<ListboxRootProps, 'multiple' | 'disabled' | 'modelValue' | 'defaultValue' | 'highlightOnHover'>, Pick<UseComponentIconsProps, 'loading' | 'loadingIcon'> {
/**
* The element or component this component should render as.
* @defaultValue 'div'
@@ -102,7 +102,9 @@ export interface CommandPaletteProps<G, T> extends Pick<ComboboxRootProps, 'mult
ui?: PartialString<typeof commandPalette.slots>
}
export type CommandPaletteEmits<T> = ComboboxRootEmits<T>
export type CommandPaletteEmits<T> = ListboxRootEmits<T> & {
'update:open': [value: boolean]
}
type SlotProps<T> = (props: { item: T, index: number }) => any
@@ -120,7 +122,7 @@ extendDevtoolsMeta({ example: 'CommandPaletteExample', ignoreProps: ['groups'] }
<script setup lang="ts" generic="G extends CommandPaletteGroup<T>, T extends CommandPaletteItem">
import { computed } from 'vue'
import { ComboboxRoot, ComboboxInput, ComboboxPortal, ComboboxContent, ComboboxEmpty, ComboboxViewport, ComboboxGroup, ComboboxLabel, ComboboxItem, ComboboxItemIndicator, useForwardProps, useForwardPropsEmits } from 'radix-vue'
import { ListboxRoot, ListboxFilter, ListboxContent, ListboxGroup, ListboxGroupLabel, ListboxItem, ListboxItemIndicator, useForwardProps, useForwardPropsEmits } from 'reka-ui'
import { defu } from 'defu'
import { reactivePick } from '@vueuse/core'
import { useFuse } from '@vueuse/integrations/useFuse'
@@ -145,9 +147,10 @@ const slots = defineSlots<CommandPaletteSlots<G, T>>()
const searchTerm = defineModel<string>('searchTerm', { default: '' })
const appConfig = useAppConfig()
const { t } = useLocale()
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'disabled', 'multiple', 'modelValue', 'defaultValue', 'selectedValue', 'resetSearchTermOnBlur'), emits)
const appConfig = useAppConfig()
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'disabled', 'multiple', 'modelValue', 'defaultValue', 'highlightOnHover'), emits)
const inputProps = useForwardProps(reactivePick(props, 'loading', 'loadingIcon', 'placeholder'))
// eslint-disable-next-line vue/no-dupe-keys
@@ -169,7 +172,7 @@ const items = computed(() => props.groups?.filter((group) => {
return false
}
if (group.filter === false) {
if (group.ignoreFilter) {
return false
}
@@ -217,7 +220,7 @@ const groups = computed(() => {
return getGroupWithItems(group, items)
}).filter(group => !!group)
const nonFuseGroups = props.groups?.filter(group => group.filter === false && group.items?.length).map((group) => {
const nonFuseGroups = props.groups?.filter(group => group.ignoreFilter && group.items?.length).map((group) => {
return getGroupWithItems(group, group.items || [])
}) || []
@@ -230,8 +233,8 @@ const groups = computed(() => {
<!-- eslint-disable vue/no-v-html -->
<template>
<ComboboxRoot v-bind="rootProps" v-model:search-term="searchTerm" open :class="ui.root({ class: [props.class, props.ui?.root] })">
<ComboboxInput as-child>
<ListboxRoot v-bind="rootProps" :class="ui.root({ class: [props.class, props.ui?.root] })">
<ListboxFilter v-model="searchTerm" as-child>
<UInput
variant="none"
autofocus
@@ -256,72 +259,70 @@ const groups = computed(() => {
</slot>
</template>
</UInput>
</ComboboxInput>
</ListboxFilter>
<ComboboxPortal disabled>
<ComboboxContent :class="ui.content({ class: props.ui?.content })" :dismissable="false">
<ComboboxEmpty :class="ui.empty({ class: props.ui?.empty })">
<slot name="empty" :search-term="searchTerm">
{{ searchTerm ? t('commandPalette.noMatch', { searchTerm }) : t('commandPalette.noData') }}
</slot>
</ComboboxEmpty>
<ListboxContent :class="ui.content({ class: props.ui?.content })">
<div v-if="groups?.length" :class="ui.viewport({ class: props.ui?.viewport })">
<ListboxGroup v-for="(group, groupIndex) in groups" :key="`group-${groupIndex}`" :class="ui.group({ class: props.ui?.group })">
<ListboxGroupLabel v-if="get(group, props.labelKey as string)" :class="ui.label({ class: props.ui?.label })">
{{ get(group, props.labelKey as string) }}
</ListboxGroupLabel>
<ComboboxViewport :class="ui.viewport({ class: props.ui?.viewport })">
<ComboboxGroup v-for="(group, groupIndex) in groups" :key="`group-${groupIndex}`" :class="ui.group({ class: props.ui?.group })">
<ComboboxLabel v-if="get(group, props.labelKey as string)" :class="ui.label({ class: props.ui?.label })">
{{ get(group, props.labelKey as string) }}
</ComboboxLabel>
<ListboxItem
v-for="(item, index) in group.items"
:key="`group-${groupIndex}-${index}`"
:value="omit(item, ['matches' as any, 'group' as any, 'onSelect', 'labelHtml', 'suffixHtml'])"
:disabled="item.disabled"
:class="ui.item({ class: props.ui?.item, active: item.active })"
@select="item.onSelect"
>
<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">
<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: 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: item.active })" />
<UChip
v-else-if="item.chip"
:size="((props.ui?.itemLeadingChipSize || ui.itemLeadingChipSize()) as ChipProps['size'])"
inset
standalone
v-bind="item.chip"
:class="ui.itemLeadingChip({ class: props.ui?.itemLeadingChip, active: item.active })"
/>
</slot>
<ComboboxItem
v-for="(item, index) in group.items"
:key="`group-${groupIndex}-${index}`"
:value="omit(item, ['matches' as any, 'group' as any, 'onSelect', 'labelHtml', 'suffixHtml'])"
:disabled="item.disabled"
:class="ui.item({ class: props.ui?.item, active: item.active })"
@select="item.onSelect"
>
<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">
<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: 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: item.active })" />
<UChip
v-else-if="item.chip"
:size="((props.ui?.itemLeadingChipSize || ui.itemLeadingChipSize()) as ChipProps['size'])"
inset
standalone
v-bind="item.chip"
:class="ui.itemLeadingChip({ class: props.ui?.itemLeadingChip, active: item.active })"
/>
<span v-if="item.labelHtml || get(item, props.labelKey as string) || !!slots[item.slot ? `${item.slot}-label` : group.slot ? `${group.slot}-label` : `item-label`]" :class="ui.itemLabel({ class: props.ui?.itemLabel, 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.prefix" :class="ui.itemLabelPrefix({ class: props.ui?.itemLabelPrefix })">{{ item.prefix }}</span>
<span :class="ui.itemLabelBase({ class: props.ui?.itemLabelBase, active: item.active })" v-html="item.labelHtml || get(item, props.labelKey as string)" />
<span :class="ui.itemLabelSuffix({ class: props.ui?.itemLabelSuffix, active: item.active })" v-html="item.suffixHtml || item.suffix" />
</slot>
</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">
<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>
<UIcon v-else-if="group.highlightedIcon" :name="group.highlightedIcon" :class="ui.itemTrailingHighlightedIcon({ class: props.ui?.itemTrailingHighlightedIcon })" />
</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: item.active })">
<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({ class: props.ui?.itemLabelPrefix })">{{ item.prefix }}</span>
<ListboxItemIndicator as-child>
<UIcon :name="selectedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: props.ui?.itemTrailingIcon })" />
</ListboxItemIndicator>
</span>
</slot>
</ListboxItem>
</ListboxGroup>
</div>
<span :class="ui.itemLabelBase({ class: props.ui?.itemLabelBase, active: item.active })" v-html="item.labelHtml || get(item, props.labelKey as string)" />
<span :class="ui.itemLabelSuffix({ class: props.ui?.itemLabelSuffix, active: item.active })" v-html="item.suffixHtml || item.suffix" />
</slot>
</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">
<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>
<UIcon v-else-if="group.highlightedIcon" :name="group.highlightedIcon" :class="ui.itemTrailingHighlightedIcon({ class: props.ui?.itemTrailingHighlightedIcon })" />
</slot>
<ComboboxItemIndicator as-child>
<UIcon :name="selectedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: props.ui?.itemTrailingIcon })" />
</ComboboxItemIndicator>
</span>
</slot>
</ComboboxItem>
</ComboboxGroup>
</ComboboxViewport>
</ComboboxContent>
</ComboboxPortal>
</ComboboxRoot>
<div v-else :class="ui.empty({ class: props.ui?.empty })">
<slot name="empty" :search-term="searchTerm">
{{ searchTerm ? t('commandPalette.noMatch', { searchTerm }) : t('commandPalette.noData') }}
</slot>
</div>
</ListboxContent>
</ListboxRoot>
</template>

View File

@@ -26,7 +26,7 @@ extendDevtoolsMeta({ example: 'ContainerExample' })
</script>
<script setup lang="ts">
import { Primitive } from 'radix-vue'
import { Primitive } from 'reka-ui'
const props = defineProps<ContainerProps>()
defineSlots<ContainerSlots>()

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { tv, type VariantProps } from 'tailwind-variants'
import type { ContextMenuRootProps, ContextMenuRootEmits, ContextMenuContentProps } from 'radix-vue'
import type { ContextMenuRootProps, ContextMenuRootEmits, ContextMenuContentProps } from 'reka-ui'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/context-menu'
@@ -143,7 +143,7 @@ extendDevtoolsMeta({
<script setup lang="ts" generic="T extends ContextMenuItem">
import { computed, toRef } from 'vue'
import { ContextMenuRoot, ContextMenuTrigger, useForwardPropsEmits } from 'radix-vue'
import { ContextMenuRoot, ContextMenuTrigger, useForwardPropsEmits } from 'reka-ui'
import { reactivePick } from '@vueuse/core'
import { omit } from '../utils'
import UContextMenuContent from './ContextMenuContent.vue'

View File

@@ -1,12 +1,12 @@
<script lang="ts">
import { tv } from 'tailwind-variants'
import type { ContextMenuContentProps as RadixContextMenuContentProps, ContextMenuContentEmits as RadixContextMenuContentEmits } from 'radix-vue'
import type { ContextMenuContentProps as RekaContextMenuContentProps, ContextMenuContentEmits as RekaContextMenuContentEmits } from 'reka-ui'
import theme from '#build/ui/context-menu'
import type { KbdProps, AvatarProps, ContextMenuItem, ContextMenuSlots } from '../types'
const _contextMenu = tv(theme)()
interface ContextMenuContentProps<T> extends Omit<RadixContextMenuContentProps, 'as' | 'asChild' | 'forceMount'> {
interface ContextMenuContentProps<T> extends Omit<RekaContextMenuContentProps, 'as' | 'asChild' | 'forceMount'> {
items?: T[] | T[][]
portal?: boolean
sub?: boolean
@@ -18,13 +18,13 @@ interface ContextMenuContentProps<T> extends Omit<RadixContextMenuContentProps,
uiOverride?: any
}
interface ContextMenuContentEmits extends RadixContextMenuContentEmits {}
interface ContextMenuContentEmits extends RekaContextMenuContentEmits {}
</script>
<script setup lang="ts" generic="T extends ContextMenuItem">
import { computed } from 'vue'
import { ContextMenu } from 'radix-vue/namespaced'
import { useForwardPropsEmits } from 'radix-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'

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { tv } from 'tailwind-variants'
import type { DrawerRootProps, DrawerRootEmits } from 'vaul-vue'
import type { DialogContentProps } from 'radix-vue'
import type { DialogContentProps } from 'reka-ui'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/drawer'
@@ -58,7 +58,7 @@ extendDevtoolsMeta({ example: 'DrawerExample' })
<script setup lang="ts">
import { computed, toRef } from 'vue'
import { useForwardPropsEmits } from 'radix-vue'
import { useForwardPropsEmits } from 'reka-ui'
import { DrawerRoot, DrawerTrigger, DrawerPortal, DrawerOverlay, DrawerContent, DrawerTitle, DrawerDescription } from 'vaul-vue'
import { reactivePick } from '@vueuse/core'

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { tv, type VariantProps } from 'tailwind-variants'
import type { DropdownMenuRootProps, DropdownMenuRootEmits, DropdownMenuContentProps, DropdownMenuArrowProps } from 'radix-vue'
import type { DropdownMenuRootProps, DropdownMenuRootEmits, DropdownMenuContentProps, DropdownMenuArrowProps } from 'reka-ui'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/dropdown-menu'
@@ -140,7 +140,7 @@ extendDevtoolsMeta({
<script setup lang="ts" generic="T extends DropdownMenuItem">
import { computed, toRef } from 'vue'
import { defu } from 'defu'
import { DropdownMenuRoot, DropdownMenuTrigger, DropdownMenuArrow, useForwardPropsEmits } from 'radix-vue'
import { DropdownMenuRoot, DropdownMenuTrigger, DropdownMenuArrow, useForwardPropsEmits } from 'reka-ui'
import { reactivePick } from '@vueuse/core'
import { omit } from '../utils'
import UDropdownMenuContent from './DropdownMenuContent.vue'

View File

@@ -1,13 +1,13 @@
<!-- eslint-disable vue/block-tag-newline -->
<script lang="ts">
import { tv } from 'tailwind-variants'
import type { DropdownMenuContentProps as RadixDropdownMenuContentProps, DropdownMenuContentEmits as RadixDropdownMenuContentEmits } from 'radix-vue'
import type { DropdownMenuContentProps as RekaDropdownMenuContentProps, DropdownMenuContentEmits as RekaDropdownMenuContentEmits } from 'reka-ui'
import theme from '#build/ui/dropdown-menu'
import type { KbdProps, AvatarProps, DropdownMenuItem, DropdownMenuSlots } from '../types'
const _dropdownMenu = tv(theme)()
interface DropdownMenuContentProps<T> extends Omit<RadixDropdownMenuContentProps, 'as' | 'asChild' | 'forceMount'> {
interface DropdownMenuContentProps<T> extends Omit<RekaDropdownMenuContentProps, 'as' | 'asChild' | 'forceMount'> {
items?: T[] | T[][]
portal?: boolean
sub?: boolean
@@ -19,7 +19,7 @@ interface DropdownMenuContentProps<T> extends Omit<RadixDropdownMenuContentProps
uiOverride?: any
}
interface DropdownMenuContentEmits extends RadixDropdownMenuContentEmits {}
interface DropdownMenuContentEmits extends RekaDropdownMenuContentEmits {}
type DropdownMenuContentSlots<T extends { slot?: string }> = Omit<DropdownMenuSlots<T>, 'default'> & {
default(props?: {}): any
@@ -29,8 +29,8 @@ type DropdownMenuContentSlots<T extends { slot?: string }> = Omit<DropdownMenuSl
<script setup lang="ts" generic="T extends DropdownMenuItem">
import { computed } from 'vue'
import { DropdownMenu } from 'radix-vue/namespaced'
import { useForwardPropsEmits } from 'radix-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'

View File

@@ -12,6 +12,11 @@ const formField = tv({ extend: tv(theme), ...(appConfig.ui?.formField || {}) })
type FormFieldVariants = VariantProps<typeof formField>
export interface FormFieldProps {
/**
* The element or component this component should render as.
* @defaultValue 'div'
*/
as?: any
/** The name of the FormField. Also used to match form errors. */
name?: string
/** A regular expression to match form error names. */
@@ -43,7 +48,7 @@ extendDevtoolsMeta({ example: 'FormFieldExample', defaultProps: { label: 'Label'
<script setup lang="ts">
import { computed, ref, inject, provide, type Ref, useId } from 'vue'
import { Label } from 'radix-vue'
import { Primitive, Label } from 'reka-ui'
import { formFieldInjectionKey, inputIdInjectionKey } from '../composables/useFormField'
import type { FormError, FormFieldInjectedOptions } from '../types/form'
@@ -74,7 +79,7 @@ provide(formFieldInjectionKey, computed(() => ({
</script>
<template>
<div :class="ui.root({ class: [props.class, props.ui?.root] })">
<Primitive :as="as" :class="ui.root({ class: [props.class, props.ui?.root] })">
<div :class="ui.wrapper({ class: props.ui?.wrapper })">
<div v-if="label || !!slots.label" :class="ui.labelWrapper({ class: props.ui?.labelWrapper })">
<Label :for="id" :class="ui.label({ class: props.ui?.label })">
@@ -110,5 +115,5 @@ provide(formFieldInjectionKey, computed(() => ({
</slot>
</p>
</div>
</div>
</Primitive>
</template>

View File

@@ -13,7 +13,7 @@ export interface IconProps {
</script>
<script setup lang="ts">
import { useForwardProps } from 'radix-vue'
import { useForwardProps } from 'reka-ui'
import { reactivePick } from '@vueuse/core'
const props = defineProps<IconProps>()

View File

@@ -15,6 +15,11 @@ const input = tv({ extend: tv(theme), ...(appConfig.ui?.input || {}) })
type InputVariants = VariantProps<typeof input>
export interface InputProps extends UseComponentIconsProps {
/**
* The element or component this component should render as.
* @defaultValue 'div'
*/
as?: any
id?: string
name?: string
type?: InputHTMLAttributes['type']
@@ -49,6 +54,7 @@ export interface InputSlots {
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { Primitive } from 'reka-ui'
import { useButtonGroup } from '../composables/useButtonGroup'
import { useComponentIcons } from '../composables/useComponentIcons'
import { useFormField } from '../composables/useFormField'
@@ -147,7 +153,7 @@ onMounted(() => {
</script>
<template>
<div :class="ui.root({ class: [props.class, props.ui?.root] })">
<Primitive :as="as" :class="ui.root({ class: [props.class, props.ui?.root] })">
<input
:id="id"
ref="inputRef"
@@ -179,5 +185,5 @@ onMounted(() => {
<UIcon v-if="trailingIconName" :name="trailingIconName" :class="ui.trailingIcon({ class: props.ui?.trailingIcon })" />
</slot>
</span>
</div>
</Primitive>
</template>

View File

@@ -1,14 +1,14 @@
<script lang="ts">
import type { InputHTMLAttributes } from 'vue'
import { tv, type VariantProps } from 'tailwind-variants'
import type { ComboboxRootProps, ComboboxRootEmits, ComboboxContentProps, ComboboxArrowProps } from 'radix-vue'
import type { ComboboxRootProps, ComboboxRootEmits, ComboboxContentProps, ComboboxArrowProps, AcceptableValue } 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 { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
import type { AvatarProps, ChipProps, InputProps } from '../types'
import type { AcceptableValue, ArrayOrWrapped, PartialString, MaybeArrayOfArray, MaybeArrayOfArrayItem, SelectModelValue, SelectModelValueEmits, SelectItemKey } from '../types/utils'
import type { PartialString, MaybeArrayOfArray, MaybeArrayOfArrayItem, SelectModelValue, SelectModelValueEmits, SelectItemKey } from '../types/utils'
const appConfig = _appConfig as AppConfig & { ui: { inputMenu: Partial<typeof theme> } }
@@ -30,7 +30,7 @@ export interface InputMenuItem {
type InputMenuVariants = VariantProps<typeof inputMenu>
export interface InputMenuProps<T extends MaybeArrayOfArrayItem<I>, I extends MaybeArrayOfArray<InputMenuItem | AcceptableValue> = MaybeArrayOfArray<InputMenuItem | AcceptableValue>, V extends SelectItemKey<T> | undefined = undefined, M extends boolean = false> extends Pick<ComboboxRootProps<T>, 'defaultValue' | 'selectedValue' | 'open' | 'defaultOpen' | 'searchTerm' | 'disabled' | 'name' | 'resetSearchTermOnBlur'>, UseComponentIconsProps {
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 {
/**
* The element or component this component should render as.
* @defaultValue 'div'
@@ -77,13 +77,6 @@ export interface InputMenuProps<T extends MaybeArrayOfArrayItem<I>, I extends Ma
* @defaultValue true
*/
portal?: boolean
/**
* Whether to filter items or not, can be an array of fields to filter. Defaults to `[labelKey]`.
* When `false`, items will not be filtered which is useful for custom filtering (useAsyncData, useFetch, etc.).
* `['label']`{lang="ts-type"}
* @defaultValue true
*/
filter?: boolean | string[]
/**
* When `items` is an array of objects, select the field to use as the value instead of the object itself.
* @defaultValue undefined
@@ -95,33 +88,45 @@ export interface InputMenuProps<T extends MaybeArrayOfArrayItem<I>, I extends Ma
*/
labelKey?: V
items?: I
/** 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>
/** The controlled value of the InputMenu. Can be binded-with with `v-model`. */
modelValue?: SelectModelValue<T, V, M>
/** Whether multiple options can be selected or not. */
multiple?: M & boolean
/** Highlight the ring color like a focus state. */
highlight?: boolean
/**
* Determines if custom user input that does not exist in options can be added.
* @defaultValue false
*/
createItem?: boolean | 'always' | { placement?: 'top' | 'bottom', when?: 'empty' | 'always' }
createItem?: boolean | 'always' | { position?: 'top' | 'bottom', when?: 'empty' | 'always' }
/**
* Fields to filter items by.
* @defaultValue [labelKey]
*/
filterFields?: string[]
/**
* When `true`, disable the default filters, useful for custom filtering (useAsyncData, useFetch, etc.).
* @defaultValue false
*/
ignoreFilter?: boolean
class?: any
ui?: PartialString<typeof inputMenu.slots>
/** The controlled value of the Combobox. Can be binded-with with `v-model`. */
modelValue?: SelectModelValue<T, V, M>
/** Whether multiple options can be selected or not. */
multiple?: M & boolean
}
export type InputMenuEmits<T, V, M extends boolean> = Omit<ComboboxRootEmits<T>, 'update:modelValue'> & {
change: [payload: Event]
blur: [payload: FocusEvent]
focus: [payload: FocusEvent]
create: [payload: Event, item: T]
create: [item: string]
} & SelectModelValueEmits<T, V, M>
type SlotProps<T> = (props: { item: T, index: number }) => any
export interface InputMenuSlots<T> {
'leading'(props: { modelValue: T, open: boolean, ui: any }): any
'trailing'(props: { modelValue: T, open: boolean, ui: any }): 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
'empty'(props: { searchTerm?: string }): any
'item': SlotProps<T>
'item-leading': SlotProps<T>
@@ -129,24 +134,23 @@ export interface InputMenuSlots<T> {
'item-trailing': SlotProps<T>
'tags-item-text': SlotProps<T>
'tags-item-delete': SlotProps<T>
'create-item-label'(props: { item: T }): any
'create-item-label'(props: { item: string }): any
}
extendDevtoolsMeta({ defaultProps: { items: ['Option 1', 'Option 2', 'Option 3'] } })
</script>
<script setup lang="ts" generic="T extends MaybeArrayOfArrayItem<I>, I extends MaybeArrayOfArray<InputMenuItem | AcceptableValue> = MaybeArrayOfArray<InputMenuItem | AcceptableValue>, V extends SelectItemKey<T> | undefined = undefined, M extends boolean = false">
<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">
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 } from 'radix-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'
import { isEqual } from 'ohash'
import { reactivePick, createReusableTemplate } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { useButtonGroup } from '../composables/useButtonGroup'
import { useComponentIcons } from '../composables/useComponentIcons'
import { useFormField } from '../composables/useFormField'
import { useLocale } from '../composables/useLocale'
import { get, escapeRegExp } from '../utils'
import { get, compare } from '../utils'
import UIcon from './Icon.vue'
import UAvatar from './Avatar.vue'
import UChip from './Chip.vue'
@@ -157,17 +161,18 @@ const props = withDefaults(defineProps<InputMenuProps<T, I, V, M>>(), {
type: 'text',
autofocusDelay: 0,
portal: true,
filter: true,
labelKey: 'label' as never
})
const emits = defineEmits<InputMenuEmits<T, V, M>>()
const slots = defineSlots<InputMenuSlots<T>>()
const slots = defineSlots<InputMenuSlots<T, M>>()
const searchTerm = defineModel<string>('searchTerm', { default: '' })
const appConfig = useAppConfig()
const { t } = useLocale()
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'selectedValue', 'open', 'defaultOpen', 'multiple', 'resetSearchTermOnBlur'), emits)
const appConfig = useAppConfig()
const { contains } = useFilter({ sensitivity: 'base' })
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'open', 'defaultOpen', 'multiple', 'resetSearchTermOnBlur', 'highlightOnHover', 'ignoreFilter'), emits)
const contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffset: 8, collisionPadding: 8, position: 'popper' }) as ComboboxContentProps)
const arrowProps = toRef(() => props.arrow as ComboboxArrowProps)
@@ -175,10 +180,10 @@ const { emitFormBlur, emitFormChange, emitFormInput, size: formGroupSize, color,
const { orientation, size: buttonGroupSize } = useButtonGroup<InputProps>(props)
const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(toRef(() => defu(props, { trailingIcon: appConfig.ui.icons.chevronDown })))
const [DefineCreateItemTemplate, ReuseCreateItemTemplate] = createReusableTemplate()
const inputSize = computed(() => buttonGroupSize.value || formGroupSize.value)
const [DefineCreateItemTemplate, ReuseCreateItemTemplate] = createReusableTemplate()
const ui = computed(() => inputMenu({
color: color.value,
variant: props.variant,
@@ -196,69 +201,49 @@ function displayValue(value: T): string {
return value && (typeof value === 'object' ? get(value, props.labelKey as string) : value)
}
const item = items.value.find(item => isEqual(get(item as Record<string, any>, props.valueKey as string), value))
const item = items.value.find(item => compare(typeof item === 'object' ? get(item, props.valueKey as string) : item, value))
return item && (typeof item === 'object' ? get(item, props.labelKey as string) : item)
}
function filterFunction(
inputItems: ArrayOrWrapped<T> = items.value as ArrayOrWrapped<T>,
filterSearchTerm: string = searchTerm.value,
comparator = (item: any, term: string) => String(item).search(new RegExp(term, 'i')) !== -1
): ArrayOrWrapped<T> {
if (props.filter === false) {
return inputItems
}
const fields = Array.isArray(props.filter) ? props.filter : [props.labelKey]
const escapedSearchTerm = escapeRegExp(filterSearchTerm ?? '')
return inputItems.filter((item) => {
if (typeof item !== 'object') {
return comparator(item, escapedSearchTerm)
}
return fields.some((field) => {
const child = get(item, field as string)
return child !== null && child !== undefined && comparator(child, escapedSearchTerm)
})
}) as ArrayOrWrapped<T>
}
const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0]) ? props.items : [props.items]) as InputMenuItem[][] : [])
// eslint-disable-next-line vue/no-dupe-keys
const items = computed(() => groups.value.flatMap(group => group) as T[])
const creatable = computed(() => {
if (!props.createItem) {
const filteredGroups = computed(() => {
if (props.ignoreFilter || !searchTerm.value) {
return groups.value
}
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 (item.type && ['label', 'separator'].includes(item.type)) {
return true
}
return fields.some(field => contains(get(item, field), searchTerm.value))
})).filter(group => group.filter(item => !item.type || !['label', 'separator'].includes(item.type)).length > 0)
})
const filteredItems = computed(() => filteredGroups.value.flatMap(group => group) as T[])
const createItem = computed(() => {
if (!props.createItem || !searchTerm.value) {
return false
}
const isModelValueCustom = props.modelValue && filterFunction((props.multiple && Array.isArray(props.modelValue) ? props.modelValue : [props.modelValue]) as ArrayOrWrapped<T>, searchTerm.value, (item, term) => String(item) === term).length === 1
if (isModelValueCustom) {
return false
}
const filteredItems = filterFunction()
const newItem = searchTerm.value && {
item: props.valueKey ? { [props.valueKey]: searchTerm.value, [props.labelKey ?? 'label']: searchTerm.value } : searchTerm.value,
position: ((typeof props.createItem === 'object' && props.createItem.placement) || 'bottom') as 'top' | 'bottom'
}
const newItem = props.valueKey ? { [props.valueKey]: searchTerm.value } as T : searchTerm.value
if ((typeof props.createItem === 'object' && props.createItem.when === 'always') || props.createItem === 'always') {
return (filteredItems.length === 1 && filterFunction(filteredItems, searchTerm.value, (item, term) => String(item) === term).length === 1) ? false : newItem
return !filteredItems.value.find(item => compare(item, newItem, props.valueKey))
}
return filteredItems.length > 0 ? false : newItem
return !filteredItems.value.length
})
const rootItems = computed(() => [
...(creatable.value && creatable.value.position === 'top' ? [creatable.value.item] : []),
...filterFunction(),
...(creatable.value && creatable.value.position === 'bottom' ? [creatable.value.item] : [])
] as ArrayOrWrapped<T>)
const createItemPosition = computed(() => typeof props.createItem === 'object' ? props.createItem.position : 'bottom')
const inputRef = ref<InstanceType<typeof ComboboxInput> | null>(null)
@@ -310,17 +295,18 @@ defineExpose({
})
</script>
<!-- eslint-disable vue/no-template-shadow -->
<template>
<DefineCreateItemTemplate>
<ComboboxGroup v-if="creatable" :class="ui.group({ class: props.ui?.group })">
<ComboboxGroup :class="ui.group({ class: props.ui?.group })">
<ComboboxItem
:class="ui.item({ class: props.ui?.item })"
:value="valueKey && typeof creatable.item === 'object' ? get(creatable.item, props.valueKey as string) : creatable.item"
@select="e => emits('create', e, (creatable as any).item as T)"
:value="searchTerm"
@select.prevent="emits('create', searchTerm)"
>
<span :class="ui.itemLabel({ class: props.ui?.itemLabel })">
<slot name="create-item-label" :item="(creatable.item as T)">
{{ t('inputMenu.create', { label: typeof creatable.item === 'object' ? get(creatable.item, props.labelKey as string) : creatable.item }) }}
<slot name="create-item-label" :item="searchTerm">
{{ t('inputMenu.create', { label: searchTerm }) }}
</slot>
</span>
</ComboboxItem>
@@ -331,13 +317,11 @@ defineExpose({
:id="id"
v-slot="{ modelValue, open }"
v-bind="rootProps"
v-model:search-term="searchTerm"
:name="name"
:disabled="disabled"
:display-value="displayValue"
:filter-function="() => rootItems"
:class="ui.root({ class: [props.class, props.ui?.root] })"
:as-child="!!multiple"
ignore-filter
@update:model-value="onUpdate"
@update:open="onUpdateOpen"
@keydown.enter="$event.preventDefault()"
@@ -345,7 +329,7 @@ defineExpose({
<ComboboxAnchor :as-child="!multiple" :class="ui.base({ class: props.ui?.base })">
<TagsInputRoot
v-if="multiple"
v-slot="{ modelValue: tags }: { modelValue: AcceptableValue[] }"
v-slot="{ modelValue: tags }"
:model-value="(modelValue as string[])"
:disabled="disabled"
delimiter=""
@@ -367,7 +351,7 @@ defineExpose({
</TagsInputItemDelete>
</TagsInputItem>
<ComboboxInput as-child>
<ComboboxInput v-model="searchTerm" :display-value="displayValue" as-child>
<TagsInputInput
ref="inputRef"
v-bind="$attrs"
@@ -382,24 +366,25 @@ defineExpose({
<ComboboxInput
v-else
ref="inputRef"
v-model="searchTerm"
:display-value="displayValue"
v-bind="$attrs"
:type="type"
:placeholder="placeholder"
:required="required"
:class="ui.base({ class: props.ui?.base })"
@blur="onBlur"
@focus="onFocus"
/>
<span v-if="isLeading || !!avatar || !!slots.leading" :class="ui.leading({ class: props.ui?.leading })">
<slot name="leading" :model-value="(modelValue as T)" :open="open" :ui="ui">
<slot name="leading" :model-value="(modelValue as M extends true ? T[] : T)" :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 T)" :open="open" :ui="ui">
<slot name="trailing" :model-value="(modelValue as M extends true ? T[] : T)" :open="open" :ui="ui">
<UIcon v-if="trailingIconName" :name="trailingIconName" :class="ui.trailingIcon({ class: props.ui?.trailingIcon })" />
</slot>
</ComboboxTrigger>
@@ -414,9 +399,9 @@ defineExpose({
</ComboboxEmpty>
<ComboboxViewport :class="ui.viewport({ class: props.ui?.viewport })">
<ReuseCreateItemTemplate v-if="creatable && creatable.position === 'top'" />
<ReuseCreateItemTemplate v-if="createItem && createItemPosition === 'top'" />
<ComboboxGroup v-for="(group, groupIndex) in groups" :key="`group-${groupIndex}`" :class="ui.group({ class: props.ui?.group })">
<ComboboxGroup v-for="(group, groupIndex) in filteredGroups" :key="`group-${groupIndex}`" :class="ui.group({ class: props.ui?.group })">
<template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
<ComboboxLabel v-if="item?.type === 'label'" :class="ui.label({ class: props.ui?.label })">
{{ get(item, props.labelKey as string) }}
@@ -463,7 +448,7 @@ defineExpose({
</template>
</ComboboxGroup>
<ReuseCreateItemTemplate v-if="creatable && creatable.position === 'bottom'" />
<ReuseCreateItemTemplate v-if="createItem && createItemPosition === 'bottom'" />
</ComboboxViewport>
<ComboboxArrow v-if="!!arrow" v-bind="arrowProps" :class="ui.arrow({ class: props.ui?.arrow })" />

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { tv, type VariantProps } from 'tailwind-variants'
import type { NumberFieldRootProps } from 'radix-vue'
import type { NumberFieldRootProps } from 'reka-ui'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/input-number'
@@ -75,7 +75,7 @@ export interface InputNumberSlots {
<script setup lang="ts">
import { onMounted, ref, computed } from 'vue'
import { NumberFieldRoot, NumberFieldInput, NumberFieldDecrement, NumberFieldIncrement, useForwardPropsEmits } from 'radix-vue'
import { NumberFieldRoot, NumberFieldInput, NumberFieldDecrement, NumberFieldIncrement, useForwardPropsEmits } from 'reka-ui'
import { reactivePick } from '@vueuse/core'
import { useFormField } from '../composables/useFormField'
import { useLocale } from '../composables/useLocale'

View File

@@ -31,7 +31,7 @@ extendDevtoolsMeta({ defaultProps: { value: 'K' } })
</script>
<script setup lang="ts">
import { Primitive } from 'radix-vue'
import { Primitive } from 'reka-ui'
import { useKbd } from '../composables/useKbd'
const props = withDefaults(defineProps<KbdProps>(), {

View File

@@ -95,7 +95,7 @@ extendDevtoolsMeta({ example: 'LinkExample' })
<script setup lang="ts">
import { computed } from 'vue'
import { isEqual, diff } from 'ohash'
import { useForwardProps } from 'radix-vue'
import { useForwardProps } from 'reka-ui'
import { reactiveOmit } from '@vueuse/core'
import { useRoute } from '#imports'
import ULinkBase from './LinkBase.vue'

View File

@@ -13,7 +13,7 @@ export interface LinkBaseProps {
</script>
<script setup lang="ts">
import { Primitive } from 'radix-vue'
import { Primitive } from 'reka-ui'
const props = withDefaults(defineProps<LinkBaseProps>(), {
as: 'button',

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { tv } from 'tailwind-variants'
import type { DialogRootProps, DialogRootEmits, DialogContentProps } from 'radix-vue'
import type { DialogRootProps, DialogRootEmits, DialogContentProps } from 'reka-ui'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/modal'
@@ -74,7 +74,7 @@ extendDevtoolsMeta({ example: 'ModalExample' })
<script setup lang="ts">
import { computed, toRef } from 'vue'
import { DialogRoot, DialogTrigger, DialogPortal, DialogOverlay, DialogContent, DialogTitle, DialogDescription, DialogClose, useForwardPropsEmits } from 'radix-vue'
import { DialogRoot, DialogTrigger, DialogPortal, DialogOverlay, DialogContent, DialogTitle, DialogDescription, DialogClose, useForwardPropsEmits } from 'reka-ui'
import { reactivePick } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { useLocale } from '../composables/useLocale'
@@ -90,6 +90,9 @@ const props = withDefaults(defineProps<ModalProps>(), {
const emits = defineEmits<ModalEmits>()
const slots = defineSlots<ModalSlots>()
const { t } = useLocale()
const appConfig = useAppConfig()
const rootProps = useForwardPropsEmits(reactivePick(props, 'open', 'defaultOpen', 'modal'), emits)
const contentProps = toRef(() => props.content)
const contentEvents = computed(() => {
@@ -110,9 +113,6 @@ const contentEvents = computed(() => {
}
})
const appConfig = useAppConfig()
const { t } = useLocale()
const ui = computed(() => modal({
transition: props.transition,
fullscreen: props.fullscreen

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { tv, type VariantProps } from 'tailwind-variants'
import type { NavigationMenuRootProps, NavigationMenuRootEmits, NavigationMenuContentProps, CollapsibleRootProps } from 'radix-vue'
import type { NavigationMenuRootProps, NavigationMenuRootEmits, NavigationMenuContentProps, CollapsibleRootProps } from 'reka-ui'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/navigation-menu'
@@ -31,7 +31,7 @@ export interface NavigationMenuItem extends Omit<LinkProps, 'raw' | 'custom'>, P
type NavigationMenuVariants = VariantProps<typeof navigationMenu>
export interface NavigationMenuProps<T> extends Pick<NavigationMenuRootProps, 'defaultValue' | 'delayDuration' | 'disableClickTrigger' | 'disableHoverTrigger' | 'modelValue' | 'skipDelayDuration'> {
export interface NavigationMenuProps<T> extends Pick<NavigationMenuRootProps, 'modelValue' | 'defaultValue' | 'delayDuration' | 'disableClickTrigger' | 'disableHoverTrigger' | 'skipDelayDuration' | 'disablePointerLeaveClose' | 'unmountOnHide'> {
/**
* The element or component this component should render as.
* @defaultValue 'div'
@@ -122,8 +122,8 @@ extendDevtoolsMeta({
</script>
<script setup lang="ts" generic="T extends MaybeArrayOfArrayItem<I>, I extends MaybeArrayOfArray<NavigationMenuItem>">
import { computed, reactive, toRef } from 'vue'
import { NavigationMenuRoot, NavigationMenuList, NavigationMenuItem, NavigationMenuTrigger, NavigationMenuContent, NavigationMenuLink, NavigationMenuIndicator, NavigationMenuViewport, useForwardPropsEmits } from 'radix-vue'
import { computed, toRef } from 'vue'
import { NavigationMenuRoot, NavigationMenuList, NavigationMenuItem, NavigationMenuTrigger, NavigationMenuContent, NavigationMenuLink, NavigationMenuIndicator, NavigationMenuViewport, useForwardPropsEmits } from 'reka-ui'
import { createReusableTemplate } from '@vueuse/core'
import { get } from '../utils'
import { pickLinkProps } from '../utils/link'
@@ -137,19 +137,24 @@ import UCollapsible from './Collapsible.vue'
const props = withDefaults(defineProps<NavigationMenuProps<I>>(), {
orientation: 'horizontal',
delayDuration: 0,
labelKey: 'label'
labelKey: 'label',
unmountOnHide: true
})
const emits = defineEmits<NavigationMenuEmits>()
const slots = defineSlots<NavigationMenuSlots<T>>()
const rootProps = useForwardPropsEmits(reactive({
const rootProps = useForwardPropsEmits(computed(() => ({
as: props.as,
modelValue: props.modelValue,
defaultValue: props.defaultValue,
delayDuration: props.delayDuration,
skipDelayDuration: props.skipDelayDuration,
orientation: props.orientation
}), emits)
orientation: props.orientation,
disableClickTrigger: props.disableClickTrigger,
disableHoverTrigger: props.disableHoverTrigger,
disablePointerLeaveClose: props.disablePointerLeaveClose,
unmountOnHide: props.unmountOnHide
})), emits)
const contentProps = toRef(() => props.content)
@@ -199,7 +204,7 @@ const lists = computed(() => props.items?.length ? (Array.isArray(props.items[0]
</slot>
</DefineItemTemplate>
<NavigationMenuRoot v-bind="rootProps" :data-orientation="orientation" :class="ui.root({ class: [props.class, props.ui?.root] })">
<NavigationMenuRoot v-bind="rootProps" :class="ui.root({ class: [props.class, props.ui?.root] })">
<template v-for="(list, listIndex) in lists" :key="`list-${listIndex}`">
<NavigationMenuList :class="ui.list({ class: props.ui?.list })">
<component
@@ -209,6 +214,7 @@ const lists = computed(() => props.items?.length ? (Array.isArray(props.items[0]
as="li"
:value="item.value || String(index)"
:default-open="item.defaultOpen"
:unmount-on-hide="(item.children?.length && orientation === 'vertical') ? unmountOnHide : undefined"
:open="item.open"
:class="ui.item({ class: props.ui?.item })"
>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { tv } from 'tailwind-variants'
import type { PaginationRootProps, PaginationRootEmits } from 'radix-vue'
import type { PaginationRootProps, PaginationRootEmits } from 'reka-ui'
import type { AppConfig } from '@nuxt/schema'
import type { RouteLocationRaw } from '#vue-router'
import _appConfig from '#build/app.config'
@@ -12,7 +12,7 @@ const appConfig = _appConfig as AppConfig & { ui: { pagination: Partial<typeof t
const pagination = tv({ extend: tv(theme), ...(appConfig.ui?.pagination || {}) })
export interface PaginationProps extends Pick<PaginationRootProps, 'defaultPage' | 'disabled' | 'itemsPerPage' | 'page' | 'showEdges' | 'siblingCount' | 'total'> {
export interface PaginationProps extends Partial<Pick<PaginationRootProps, 'defaultPage' | 'disabled' | 'itemsPerPage' | 'page' | 'showEdges' | 'siblingCount' | 'total'>> {
/**
* The element or component this component should render as.
* @defaultValue 'div'
@@ -104,7 +104,7 @@ extendDevtoolsMeta({ defaultProps: { total: 50 } })
<script setup lang="ts">
import { computed } from 'vue'
import { PaginationRoot, PaginationList, PaginationListItem, PaginationFirst, PaginationPrev, PaginationEllipsis, PaginationNext, PaginationLast, useForwardPropsEmits } from 'radix-vue'
import { PaginationRoot, PaginationList, PaginationListItem, PaginationFirst, PaginationPrev, PaginationEllipsis, PaginationNext, PaginationLast, useForwardPropsEmits } from 'reka-ui'
import { reactivePick } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { useLocale } from '../composables/useLocale'

View File

@@ -2,7 +2,7 @@
import _appConfig from '#build/app.config'
import theme from '#build/ui/pin-input'
import type { AppConfig } from '@nuxt/schema'
import type { PinInputRootEmits, PinInputRootProps } from 'radix-vue'
import type { PinInputRootEmits, PinInputRootProps } from 'reka-ui'
import { tv, type VariantProps } from 'tailwind-variants'
import type { PartialString } from '../types/utils'
@@ -35,7 +35,7 @@ export type PinInputEmits = PinInputRootEmits & {
<script setup lang="ts">
import { ref, computed } from 'vue'
import { PinInputInput, PinInputRoot, useForwardPropsEmits } from 'radix-vue'
import { PinInputInput, PinInputRoot, useForwardPropsEmits } from 'reka-ui'
import { reactivePick } from '@vueuse/core'
import { useFormField } from '../composables/useFormField'
import { looseToNumber } from '../utils'

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { tv } from 'tailwind-variants'
import type { PopoverRootProps, HoverCardRootProps, PopoverRootEmits, PopoverContentProps, PopoverArrowProps } from 'radix-vue'
import type { PopoverRootProps, HoverCardRootProps, PopoverRootEmits, PopoverContentProps, PopoverArrowProps } from 'reka-ui'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/popover'
@@ -53,8 +53,8 @@ extendDevtoolsMeta({ example: 'PopoverExample' })
<script setup lang="ts">
import { computed, toRef } from 'vue'
import { defu } from 'defu'
import { useForwardPropsEmits } from 'radix-vue'
import { Popover, HoverCard } from 'radix-vue/namespaced'
import { useForwardPropsEmits } from 'reka-ui'
import { Popover, HoverCard } from 'reka-ui/namespaced'
import { reactivePick } from '@vueuse/core'
const props = withDefaults(defineProps<PopoverProps>(), {

View File

@@ -1,7 +1,7 @@
<!-- eslint-disable vue/block-tag-newline -->
<script lang="ts">
import { tv, type VariantProps } from 'tailwind-variants'
import type { ProgressRootProps, ProgressRootEmits } from 'radix-vue'
import type { ProgressRootProps, ProgressRootEmits } from 'reka-ui'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/progress'
@@ -48,7 +48,7 @@ export type ProgressSlots = {
<script setup lang="ts">
import { computed } from 'vue'
import { ProgressIndicator, ProgressRoot, useForwardPropsEmits } from 'radix-vue'
import { Primitive, ProgressRoot, ProgressIndicator, useForwardPropsEmits } from 'reka-ui'
import { reactivePick } from '@vueuse/core'
import { useLocale } from '../composables/useLocale'
@@ -62,7 +62,7 @@ defineSlots<ProgressSlots>()
const { dir } = useLocale()
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'getValueLabel', 'modelValue'), emits)
const rootProps = useForwardPropsEmits(reactivePick(props, 'getValueLabel', 'modelValue'), emits)
const isIndeterminate = computed(() => rootProps.value.modelValue === null)
const hasSteps = computed(() => Array.isArray(props.max))
@@ -159,7 +159,7 @@ const ui = computed(() => progress({
</script>
<template>
<div :class="ui.root({ class: [props.class, props.ui?.root] })">
<Primitive :as="as" :class="ui.root({ class: [props.class, props.ui?.root] })">
<div v-if="!isIndeterminate && (status || $slots.status)" :class="ui.status({ class: props.ui?.status })" :style="statusStyle">
<slot name="status" :percent="percent">
{{ percent }}%
@@ -177,5 +177,5 @@ const ui = computed(() => progress({
</slot>
</div>
</div>
</div>
</Primitive>
</template>

View File

@@ -1,11 +1,10 @@
<script lang="ts">
import { tv, type VariantProps } from 'tailwind-variants'
import type { RadioGroupRootProps, RadioGroupRootEmits } from 'radix-vue'
import type { RadioGroupRootProps, RadioGroupRootEmits, AcceptableValue } from 'reka-ui'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/radio-group'
import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
import type { AcceptableValue } from '../types/utils'
const appConfig = _appConfig as AppConfig & { ui: { radioGroup: Partial<typeof theme> } }
@@ -58,7 +57,7 @@ export type RadioGroupEmits = RadioGroupRootEmits & {
change: [payload: Event]
}
type SlotProps<T> = (props: { item: T, modelValue?: string }) => any
type SlotProps<T> = (props: { item: T, modelValue?: AcceptableValue }) => any
export interface RadioGroupSlots<T> {
legend(props?: {}): any
@@ -71,7 +70,7 @@ extendDevtoolsMeta({ defaultProps: { items: ['Option 1', 'Option 2', 'Option 3']
<script setup lang="ts" generic="T extends RadioGroupItem | AcceptableValue">
import { computed, useId } from 'vue'
import { RadioGroupRoot, RadioGroupItem, RadioGroupIndicator, Label, useForwardPropsEmits } from 'radix-vue'
import { RadioGroupRoot, RadioGroupItem, RadioGroupIndicator, Label, useForwardPropsEmits } from 'reka-ui'
import { reactivePick } from '@vueuse/core'
import { useFormField } from '../composables/useFormField'
import { get } from '../utils'

View File

@@ -1,13 +1,13 @@
<script lang="ts">
import { tv, type VariantProps } from 'tailwind-variants'
import type { SelectRootProps, SelectRootEmits, SelectContentProps, SelectArrowProps } from 'radix-vue'
import type { SelectRootProps, SelectRootEmits, SelectContentProps, SelectArrowProps, AcceptableValue } 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 { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
import type { AvatarProps, ChipProps, InputProps } from '../types'
import type { AcceptableValue, PartialString, MaybeArrayOfArray, MaybeArrayOfArrayItem, SelectModelValue, SelectModelValueEmits, SelectItemKey } from '../types/utils'
import type { PartialString, MaybeArrayOfArray, MaybeArrayOfArrayItem, SelectModelValue, SelectModelValueEmits, SelectItemKey } from '../types/utils'
const appConfig = _appConfig as AppConfig & { ui: { select: Partial<typeof theme> } }
@@ -29,7 +29,7 @@ export interface SelectItem {
type SelectVariants = VariantProps<typeof select>
export interface SelectProps<T extends MaybeArrayOfArrayItem<I>, I extends MaybeArrayOfArray<SelectItem | AcceptableValue> = MaybeArrayOfArray<SelectItem | AcceptableValue>, V extends SelectItemKey<T> | undefined = undefined> extends Omit<SelectRootProps, 'dir' | 'modelValue'>, UseComponentIconsProps {
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 {
id?: string
/** The placeholder text when the select is empty. */
placeholder?: string
@@ -72,25 +72,30 @@ export interface SelectProps<T extends MaybeArrayOfArrayItem<I>, I extends Maybe
*/
labelKey?: V
items?: I
/** 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>
/** The controlled value of the Select. Can be bind as `v-model`. */
modelValue?: SelectModelValue<T, V, M, T extends { value: infer U } ? U : never>
/** Whether multiple options can be selected or not. */
multiple?: M & boolean
/** Highlight the ring color like a focus state. */
highlight?: boolean
class?: any
ui?: PartialString<typeof select.slots>
/** The controlled value of the Select. Can be bind as `v-model`. */
modelValue?: SelectModelValue<T, V, false, T extends { value: infer U } ? U : never>
}
export type SelectEmits<T, V> = Omit<SelectRootEmits, 'update:modelValue'> & {
export type SelectEmits<T, V, M extends boolean> = Omit<SelectRootEmits<T>, 'update:modelValue'> & {
change: [payload: Event]
blur: [payload: FocusEvent]
focus: [payload: FocusEvent]
} & SelectModelValueEmits<T, V, false, T extends { value: infer U } ? U : never>
} & SelectModelValueEmits<T, V, M, T extends { value: infer U } ? U : never>
type SlotProps<T> = (props: { item: T, index: number }) => any
export interface SelectSlots<T> {
'leading'(props: { modelValue: string, open: boolean, ui: any }): any
'trailing'(props: { modelValue: string, open: boolean, ui: any }): 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
'item': SlotProps<T>
'item-leading': SlotProps<T>
'item-label': SlotProps<T>
@@ -100,30 +105,30 @@ export interface SelectSlots<T> {
extendDevtoolsMeta({ defaultProps: { items: ['Option 1', 'Option 2', 'Option 3'] } })
</script>
<script setup lang="ts" generic="T extends MaybeArrayOfArrayItem<I>, I extends MaybeArrayOfArray<SelectItem | AcceptableValue> = MaybeArrayOfArray<SelectItem | AcceptableValue>, V extends SelectItemKey<T> | undefined = undefined">
<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">
import { computed, toRef } from 'vue'
import { SelectRoot, SelectArrow, SelectTrigger, SelectValue, SelectPortal, SelectContent, SelectViewport, SelectLabel, SelectGroup, SelectItem, SelectItemIndicator, SelectItemText, SelectSeparator, useForwardPropsEmits } from 'radix-vue'
import { SelectRoot, SelectArrow, SelectTrigger, SelectPortal, SelectContent, SelectViewport, SelectLabel, SelectGroup, SelectItem, SelectItemIndicator, SelectItemText, SelectSeparator, useForwardPropsEmits } from 'reka-ui'
import { defu } from 'defu'
import { reactivePick } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { useButtonGroup } from '../composables/useButtonGroup'
import { useComponentIcons } from '../composables/useComponentIcons'
import { useFormField } from '../composables/useFormField'
import { get } from '../utils'
import { get, compare } from '../utils'
import UIcon from './Icon.vue'
import UAvatar from './Avatar.vue'
import UChip from './Chip.vue'
const props = withDefaults(defineProps<SelectProps<T, I, V>>(), {
const props = withDefaults(defineProps<SelectProps<T, I, V, M>>(), {
valueKey: 'value' as never,
labelKey: 'label' as never,
portal: true
})
const emits = defineEmits<SelectEmits<T, V>>()
const slots = defineSlots<SelectSlots<T>>()
const emits = defineEmits<SelectEmits<T, V, M>>()
const slots = defineSlots<SelectSlots<T, M>>()
const appConfig = useAppConfig()
const rootProps = useForwardPropsEmits(reactivePick(props, 'modelValue', 'defaultValue', 'open', 'defaultOpen', 'disabled', 'autocomplete', 'required'), emits)
const rootProps = useForwardPropsEmits(reactivePick(props, 'modelValue', 'defaultValue', 'open', 'defaultOpen', 'disabled', 'autocomplete', 'required', 'multiple'), emits)
const contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffset: 8, collisionPadding: 8, position: 'popper' }) as SelectContentProps)
const arrowProps = toRef(() => props.arrow as SelectArrowProps)
@@ -145,6 +150,17 @@ const ui = computed(() => select({
}))
const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0]) ? props.items : [props.items]) as SelectItem[][] : [])
// 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 {
if (props.multiple && Array.isArray(value)) {
return value.map(v => displayValue(v)).filter(Boolean).join(', ')
}
const item = items.value.find(item => compare(typeof item === 'object' ? get(item, props.valueKey as string) : item, value))
return item && (typeof item === 'object' ? get(item, props.labelKey as string) : item)
}
function onUpdate(value: any) {
// @ts-expect-error - 'target' does not exist in type 'EventInit'
@@ -166,14 +182,13 @@ function onUpdateOpen(value: boolean) {
}
</script>
<!-- eslint-disable vue/no-template-shadow -->
<template>
<SelectRoot
:id="id"
v-slot="{ modelValue, open }"
v-bind="rootProps"
:name="name"
:default-value="(defaultValue as string)"
:model-value="(modelValue as string)"
:autocomplete="autocomplete"
:disabled="disabled"
@update:model-value="onUpdate"
@@ -181,16 +196,25 @@ function onUpdateOpen(value: boolean) {
>
<SelectTrigger :class="ui.base({ class: [props.class, props.ui?.base] })">
<span v-if="isLeading || !!avatar || !!slots.leading" :class="ui.leading({ class: props.ui?.leading })">
<slot name="leading" :model-value="modelValue" :open="open" :ui="ui">
<slot name="leading" :model-value="(modelValue as M extends true ? AcceptableValue[] : AcceptableValue)" :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>
<SelectValue :placeholder="placeholder ?? '&nbsp;'" :class="ui.value({ class: props.ui?.value })" />
<slot :model-value="(modelValue as M extends true ? AcceptableValue[] : AcceptableValue)" :open="open">
<template v-for="displayedModelValue in [displayValue(modelValue)]" :key="displayedModelValue">
<span v-if="displayedModelValue" :class="ui.value({ class: props.ui?.value })">
{{ displayedModelValue }}
</span>
<span v-else :class="ui.placeholder({ class: props.ui?.placeholder })">
{{ placeholder ?? '&nbsp;' }}
</span>
</template>
</slot>
<span v-if="isTrailing || !!slots.trailing" :class="ui.trailing({ class: props.ui?.trailing })">
<slot name="trailing" :model-value="modelValue" :open="open" :ui="ui">
<slot name="trailing" :model-value="(modelValue as M extends true ? AcceptableValue[] : AcceptableValue)" :open="open" :ui="ui">
<UIcon v-if="trailingIconName" :name="trailingIconName" :class="ui.trailingIcon({ class: props.ui?.trailingIcon })" />
</slot>
</span>

View File

@@ -1,13 +1,13 @@
<script lang="ts">
import { tv, type VariantProps } from 'tailwind-variants'
import type { ComboboxRootProps, ComboboxRootEmits, ComboboxContentProps, ComboboxArrowProps } from 'radix-vue'
import type { ComboboxRootProps, ComboboxRootEmits, ComboboxContentProps, ComboboxArrowProps, AcceptableValue } 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 { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
import type { AvatarProps, ChipProps, InputProps } from '../types'
import type { AcceptableValue, ArrayOrWrapped, PartialString, MaybeArrayOfArray, MaybeArrayOfArrayItem, SelectModelValue, SelectModelValueEmits, SelectItemKey } from '../types/utils'
import type { PartialString, MaybeArrayOfArray, MaybeArrayOfArrayItem, SelectModelValue, SelectModelValueEmits, SelectItemKey } from '../types/utils'
const appConfig = _appConfig as AppConfig & { ui: { selectMenu: Partial<typeof theme> } }
@@ -29,7 +29,7 @@ export interface SelectMenuItem {
type SelectMenuVariants = VariantProps<typeof selectMenu>
export interface SelectMenuProps<T extends MaybeArrayOfArrayItem<I>, I extends MaybeArrayOfArray<SelectMenuItem | AcceptableValue> = MaybeArrayOfArray<SelectMenuItem | AcceptableValue>, V extends SelectItemKey<T> | undefined = undefined, M extends boolean = false> extends Pick<ComboboxRootProps<T>, 'defaultValue' | 'selectedValue' | 'open' | 'defaultOpen' | 'searchTerm' | 'disabled' | 'name' | 'resetSearchTermOnBlur'>, UseComponentIconsProps {
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 {
id?: string
/** The placeholder text when the select is empty. */
placeholder?: string
@@ -69,13 +69,6 @@ export interface SelectMenuProps<T extends MaybeArrayOfArrayItem<I>, I extends M
* @defaultValue true
*/
portal?: boolean
/**
* Whether to filter items or not, can be an array of fields to filter. Defaults to `[labelKey]`.
* When `false`, items will not be filtered which is useful for custom filtering (useAsyncData, useFetch, etc.).
* `['label']`{lang="ts-type"}
* @defaultValue true
*/
filter?: boolean | string[]
/**
* When `items` is an array of objects, select the field to use as the value instead of the object itself.
* @defaultValue undefined
@@ -87,92 +80,100 @@ export interface SelectMenuProps<T extends MaybeArrayOfArrayItem<I>, I extends M
*/
labelKey?: V
items?: I
/** 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>
/** The controlled value of the SelectMenu. Can be binded-with with `v-model`. */
modelValue?: SelectModelValue<T, V, M>
/** Whether multiple options can be selected or not. */
multiple?: M & boolean
/** Highlight the ring color like a focus state. */
highlight?: boolean
/**
* Determines if custom user input that does not exist in options can be added.
* @defaultValue false
*/
createItem?: boolean | 'always' | { placement?: 'top' | 'bottom', when?: 'empty' | 'always' }
createItem?: boolean | 'always' | { position?: 'top' | 'bottom', when?: 'empty' | 'always' }
/**
* Fields to filter items by.
* @defaultValue [labelKey]
*/
filterFields?: string[]
/**
* When `true`, disable the default filters, useful for custom filtering (useAsyncData, useFetch, etc.).
* @defaultValue false
*/
ignoreFilter?: boolean
class?: any
ui?: PartialString<typeof selectMenu.slots>
/** The controlled value of the Combobox. Can be binded-with with `v-model`. */
modelValue?: SelectModelValue<T, V, M>
/** Whether multiple options can be selected or not. */
multiple?: M & boolean
}
export type SelectMenuEmits<T, V, M extends boolean> = Omit<ComboboxRootEmits<T>, 'update:modelValue'> & {
change: [payload: Event]
blur: [payload: FocusEvent]
focus: [payload: FocusEvent]
create: [payload: Event, item: T]
create: [item: string]
} & SelectModelValueEmits<T, V, M>
type SlotProps<T> = (props: { item: T, index: number }) => any
export interface SelectMenuSlots<T> {
'leading'(props: { modelValue: T, open: boolean, ui: any }): any
'default'(props: { modelValue: T, open: boolean }): any
'trailing'(props: { modelValue: T, open: boolean, ui: any }): 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
'empty'(props: { searchTerm?: string }): any
'item': SlotProps<T>
'item-leading': SlotProps<T>
'item-label': SlotProps<T>
'item-trailing': SlotProps<T>
'create-item-label'(props: { item: T }): any
'create-item-label'(props: { item: string }): any
}
extendDevtoolsMeta({ defaultProps: { items: ['Option 1', 'Option 2', 'Option 3'] } })
</script>
<script setup lang="ts" generic="T extends MaybeArrayOfArrayItem<I>, I extends MaybeArrayOfArray<SelectMenuItem | AcceptableValue> = MaybeArrayOfArray<SelectMenuItem | AcceptableValue>, V extends SelectItemKey<T> | undefined = undefined, M extends boolean = false">
<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">
import { computed, toRef, toRaw } from 'vue'
import { ComboboxRoot, ComboboxArrow, ComboboxAnchor, ComboboxInput, ComboboxTrigger, ComboboxPortal, ComboboxContent, ComboboxViewport, ComboboxEmpty, ComboboxGroup, ComboboxLabel, ComboboxSeparator, ComboboxItem, ComboboxItemIndicator, useForwardPropsEmits } from 'radix-vue'
import { ComboboxRoot, ComboboxArrow, ComboboxAnchor, ComboboxInput, ComboboxTrigger, ComboboxPortal, ComboboxContent, ComboboxViewport, ComboboxEmpty, ComboboxGroup, ComboboxLabel, ComboboxSeparator, ComboboxItem, ComboboxItemIndicator, useForwardPropsEmits, useFilter } from 'reka-ui'
import { defu } from 'defu'
import { isEqual } from 'ohash'
import { reactivePick, createReusableTemplate } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { useButtonGroup } from '../composables/useButtonGroup'
import { useComponentIcons } from '../composables/useComponentIcons'
import { useFormField } from '../composables/useFormField'
import { useLocale } from '../composables/useLocale'
import { get, escapeRegExp } from '../utils'
import { get, compare } from '../utils'
import UIcon from './Icon.vue'
import UAvatar from './Avatar.vue'
import UChip from './Chip.vue'
import UInput from './Input.vue'
const props = withDefaults(defineProps<SelectMenuProps<T, I, V, M>>(), {
search: true,
portal: true,
autofocusDelay: 0,
searchInput: true,
filter: true,
labelKey: 'label' as never
})
const emits = defineEmits<SelectMenuEmits<T, V, M>>()
const slots = defineSlots<SelectMenuSlots<T>>()
const slots = defineSlots<SelectMenuSlots<T, M>>()
const searchTerm = defineModel<string>('searchTerm', { default: '' })
const appConfig = useAppConfig()
const { t } = useLocale()
const rootProps = useForwardPropsEmits(reactivePick(props, 'modelValue', 'defaultValue', 'selectedValue', 'open', 'defaultOpen', 'multiple', 'resetSearchTermOnBlur'), emits)
const appConfig = useAppConfig()
const { contains } = useFilter({ sensitivity: 'base' })
const rootProps = useForwardPropsEmits(reactivePick(props, 'modelValue', 'defaultValue', 'open', 'defaultOpen', 'multiple', 'resetSearchTermOnBlur', 'highlightOnHover'), emits)
const contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffset: 8, collisionPadding: 8, position: 'popper' }) as ComboboxContentProps)
const arrowProps = toRef(() => props.arrow as ComboboxArrowProps)
const searchInputProps = toRef(() => defu(props.searchInput, { placeholder: 'Search...', variant: 'none' }) as InputProps)
const [DefineCreateItemTemplate, ReuseCreateItemTemplate] = createReusableTemplate()
const { emitFormBlur, emitFormInput, emitFormChange, size: formGroupSize, color, id, name, highlight, disabled } = useFormField<InputProps>(props)
const { orientation, size: buttonGroupSize } = useButtonGroup<InputProps>(props)
const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(toRef(() => defu(props, { trailingIcon: appConfig.ui.icons.chevronDown })))
const selectSize = computed(() => buttonGroupSize.value || formGroupSize.value)
const [DefineCreateItemTemplate, ReuseCreateItemTemplate] = createReusableTemplate()
const ui = computed(() => selectMenu({
color: color.value,
variant: props.variant,
@@ -193,69 +194,49 @@ function displayValue(value: T | T[]): string {
return value && (typeof value === 'object' ? get(value, props.labelKey as string) : value)
}
const item = items.value.find(item => isEqual(get(item as Record<string, any>, props.valueKey as string), value)) ?? (props.createItem && value)
const item = items.value.find(item => compare(typeof item === 'object' ? get(item, props.valueKey as string) : item, value))
return item && (typeof item === 'object' ? get(item, props.labelKey as string) : item)
}
function filterFunction(
inputItems: ArrayOrWrapped<T> = items.value as ArrayOrWrapped<T>,
filterSearchTerm: string = searchTerm.value,
comparator = (item: any, term: string) => String(item).search(new RegExp(term, 'i')) !== -1
): ArrayOrWrapped<T> {
if (props.filter === false) {
return inputItems
}
const fields = Array.isArray(props.filter) ? props.filter : [props.labelKey]
const escapedSearchTerm = escapeRegExp(filterSearchTerm)
return inputItems.filter((item: T) => {
if (typeof item !== 'object') {
return comparator(item, escapedSearchTerm)
}
return fields.some((field) => {
const child = get(item, field as string)
return child !== null && child !== undefined && comparator(child, escapedSearchTerm)
})
}) as ArrayOrWrapped<T>
}
const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0]) ? props.items : [props.items]) as SelectMenuItem[][] : [])
// eslint-disable-next-line vue/no-dupe-keys
const items = computed(() => groups.value.flatMap(group => group) as T[])
const creatable = computed(() => {
if (!props.createItem) {
const filteredGroups = computed(() => {
if (props.ignoreFilter || !searchTerm.value) {
return groups.value
}
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 (item.type && ['label', 'separator'].includes(item.type)) {
return true
}
return fields.some(field => contains(get(item, field), searchTerm.value))
})).filter(group => group.filter(item => !item.type || !['label', 'separator'].includes(item.type)).length > 0)
})
const filteredItems = computed(() => filteredGroups.value.flatMap(group => group) as T[])
const createItem = computed(() => {
if (!props.createItem || !searchTerm.value) {
return false
}
const isModelValueCustom = props.modelValue && filterFunction((props.multiple && Array.isArray(props.modelValue) ? props.modelValue : [props.modelValue]) as ArrayOrWrapped<T>, searchTerm.value, (item, term) => String(item) === term).length === 1
if (isModelValueCustom) {
return false
}
const filteredItems = filterFunction()
const newItem = searchTerm.value && {
item: props.valueKey ? { [props.valueKey]: searchTerm.value, [props.labelKey ?? 'label']: searchTerm.value } : searchTerm.value,
position: ((typeof props.createItem === 'object' && props.createItem.placement) || 'bottom') as 'top' | 'bottom'
}
const newItem = props.valueKey ? { [props.valueKey]: searchTerm.value } as T : searchTerm.value
if ((typeof props.createItem === 'object' && props.createItem.when === 'always') || props.createItem === 'always') {
return (filteredItems.length === 1 && filterFunction(filteredItems, searchTerm.value, (item, term) => String(item) === term).length === 1) ? false : newItem
return !filteredItems.value.find(item => compare(item, newItem, props.valueKey))
}
return filteredItems.length > 0 ? false : newItem
return !filteredItems.value.length
})
const rootItems = computed(() => [
...(creatable.value && creatable.value.position === 'top' ? [creatable.value.item] : []),
...filterFunction(),
...(creatable.value && creatable.value.position === 'bottom' ? [creatable.value.item] : [])
] as ArrayOrWrapped<T>)
const createItemPosition = computed(() => typeof props.createItem === 'object' ? props.createItem.position : 'bottom')
function onUpdate(value: any) {
if (toRaw(props.modelValue) === value) {
@@ -280,17 +261,18 @@ function onUpdateOpen(value: boolean) {
}
</script>
<!-- eslint-disable vue/no-template-shadow -->
<template>
<DefineCreateItemTemplate>
<ComboboxGroup v-if="creatable" :class="ui.group({ class: props.ui?.group })">
<ComboboxGroup :class="ui.group({ class: props.ui?.group })">
<ComboboxItem
:class="ui.item({ class: props.ui?.item })"
:value="valueKey && typeof creatable.item === 'object' ? get(creatable.item, props.valueKey as string) : creatable.item"
@select="e => emits('create', e, (creatable as any).item as T)"
:value="searchTerm"
@select.prevent="emits('create', searchTerm)"
>
<span :class="ui.itemLabel({ class: props.ui?.itemLabel })">
<slot name="create-item-label" :item="(creatable.item as T)">
{{ t('selectMenu.create', { label: typeof creatable.item === 'object' ? get(creatable.item, props.labelKey as string) : creatable.item }) }}
<slot name="create-item-label" :item="searchTerm">
{{ t('selectMenu.create', { label: searchTerm }) }}
</slot>
</span>
</ComboboxItem>
@@ -301,26 +283,24 @@ function onUpdateOpen(value: boolean) {
:id="id"
v-slot="{ modelValue, open }"
v-bind="rootProps"
v-model:search-term="searchTerm"
ignore-filter
as-child
:name="name"
:disabled="disabled"
:display-value="() => searchTerm"
:filter-function="() => rootItems"
@update:model-value="onUpdate"
@update:open="onUpdateOpen"
>
<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 T)" :open="open" :ui="ui">
<slot name="leading" :model-value="(modelValue as M extends true ? T[] : T)" :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 T)" :open="open">
<template v-for="displayedModelValue in [displayValue(modelValue)]" :key="displayedModelValue">
<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">
<span v-if="displayedModelValue" :class="ui.value({ class: props.ui?.value })">
{{ displayedModelValue }}
</span>
@@ -331,7 +311,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 T)" :open="open" :ui="ui">
<slot name="trailing" :model-value="(modelValue as M extends true ? T[] : T)" :open="open" :ui="ui">
<UIcon v-if="trailingIconName" :name="trailingIconName" :class="ui.trailingIcon({ class: props.ui?.trailingIcon })" />
</slot>
</span>
@@ -340,7 +320,7 @@ function onUpdateOpen(value: boolean) {
<ComboboxPortal :disabled="!portal">
<ComboboxContent :class="ui.content({ class: props.ui?.content })" v-bind="contentProps">
<ComboboxInput v-if="!!searchInput" as-child>
<ComboboxInput v-if="!!searchInput" v-model="searchTerm" :display-value="() => searchTerm" as-child>
<UInput autofocus autocomplete="off" v-bind="searchInputProps" :class="ui.input({ class: props.ui?.input })" />
</ComboboxInput>
@@ -351,9 +331,9 @@ function onUpdateOpen(value: boolean) {
</ComboboxEmpty>
<ComboboxViewport :class="ui.viewport({ class: props.ui?.viewport })">
<ReuseCreateItemTemplate v-if="creatable && creatable.position === 'top'" />
<ReuseCreateItemTemplate v-if="createItem && createItemPosition === 'top'" />
<ComboboxGroup v-for="(group, groupIndex) in groups" :key="`group-${groupIndex}`" :class="ui.group({ class: props.ui?.group })">
<ComboboxGroup v-for="(group, groupIndex) in filteredGroups" :key="`group-${groupIndex}`" :class="ui.group({ class: props.ui?.group })">
<template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
<ComboboxLabel v-if="item?.type === 'label'" :class="ui.label({ class: props.ui?.label })">
{{ get(item, props.labelKey as string) }}
@@ -400,7 +380,7 @@ function onUpdateOpen(value: boolean) {
</template>
</ComboboxGroup>
<ReuseCreateItemTemplate v-if="creatable && creatable.position === 'bottom'" />
<ReuseCreateItemTemplate v-if="createItem && createItemPosition === 'bottom'" />
</ComboboxViewport>
<ComboboxArrow v-if="!!arrow" v-bind="arrowProps" :class="ui.arrow({ class: props.ui?.arrow })" />

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { tv, type VariantProps } from 'tailwind-variants'
import type { SeparatorProps as _SeparatorProps } from 'radix-vue'
import type { SeparatorProps as _SeparatorProps } from 'reka-ui'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/separator'
@@ -43,7 +43,7 @@ export interface SeparatorSlots {
<script setup lang="ts">
import { computed } from 'vue'
import { Separator, useForwardProps } from 'radix-vue'
import { Separator, useForwardProps } from 'reka-ui'
import { reactivePick } from '@vueuse/core'
import UIcon from './Icon.vue'
import UAvatar from './Avatar.vue'

View File

@@ -22,7 +22,7 @@ extendDevtoolsMeta({ example: 'SkeletonExample' })
</script>
<script setup lang="ts">
import { Primitive } from 'radix-vue'
import { Primitive } from 'reka-ui'
const props = defineProps<SkeletonProps>()
</script>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { tv, type VariantProps } from 'tailwind-variants'
import type { DialogRootProps, DialogRootEmits, DialogContentProps } from 'radix-vue'
import type { DialogRootProps, DialogRootEmits, DialogContentProps } from 'reka-ui'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/slideover'
@@ -72,7 +72,7 @@ extendDevtoolsMeta({ example: 'SlideoverExample' })
<script setup lang="ts">
import { computed, toRef } from 'vue'
import { DialogRoot, DialogTrigger, DialogPortal, DialogOverlay, DialogContent, DialogTitle, DialogDescription, DialogClose, useForwardPropsEmits } from 'radix-vue'
import { DialogRoot, DialogTrigger, DialogPortal, DialogOverlay, DialogContent, DialogTitle, DialogDescription, DialogClose, useForwardPropsEmits } from 'reka-ui'
import { reactivePick } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { useLocale } from '../composables/useLocale'
@@ -89,6 +89,9 @@ const props = withDefaults(defineProps<SlideoverProps>(), {
const emits = defineEmits<SlideoverEmits>()
const slots = defineSlots<SlideoverSlots>()
const { t } = useLocale()
const appConfig = useAppConfig()
const rootProps = useForwardPropsEmits(reactivePick(props, 'open', 'defaultOpen', 'modal'), emits)
const contentProps = toRef(() => props.content)
const contentEvents = computed(() => {
@@ -109,9 +112,6 @@ const contentEvents = computed(() => {
}
})
const appConfig = useAppConfig()
const { t } = useLocale()
const ui = computed(() => slideover({
transition: props.transition,
side: props.side

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { tv, type VariantProps } from 'tailwind-variants'
import type { SliderRootProps } from 'radix-vue'
import type { SliderRootProps } from 'reka-ui'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/slider'
@@ -38,7 +38,7 @@ export interface SliderEmits {
<script setup lang="ts">
import { computed } from 'vue'
import { SliderRoot, SliderRange, SliderTrack, SliderThumb, useForwardPropsEmits } from 'radix-vue'
import { SliderRoot, SliderRange, SliderTrack, SliderThumb, useForwardPropsEmits } from 'reka-ui'
import { reactivePick } from '@vueuse/core'
import { useFormField } from '../composables/useFormField'

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { tv, type VariantProps } from 'tailwind-variants'
import type { SwitchRootProps } from 'radix-vue'
import type { SwitchRootProps } from 'reka-ui'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/switch'
@@ -13,7 +13,7 @@ const switchTv = tv({ extend: tv(theme), ...(appConfig.ui?.switch || {}) })
type SwitchVariants = VariantProps<typeof switchTv>
export interface SwitchProps extends Pick<SwitchRootProps, 'disabled' | 'id' | 'name' | 'required' | 'value'> {
export interface SwitchProps extends Pick<SwitchRootProps, 'disabled' | 'id' | 'name' | 'required' | 'value' | 'defaultValue'> {
/**
* The element or component this component should render as.
* @defaultValue 'div'
@@ -34,15 +34,12 @@ export interface SwitchProps extends Pick<SwitchRootProps, 'disabled' | 'id' | '
uncheckedIcon?: string
label?: string
description?: string
/** The state of the switch when it is initially rendered. Use when you do not need to control its state. */
defaultValue?: boolean
class?: any
ui?: PartialString<typeof switchTv.slots>
}
export interface SwitchEmits {
(e: 'update:modelValue', payload: boolean): void
(e: 'change', payload: Event): void
export type SwitchEmits = {
change: [payload: Event]
}
export interface SwitchSlots {
@@ -55,7 +52,7 @@ extendDevtoolsMeta({ defaultProps: { label: 'Switch me!' } })
<script setup lang="ts">
import { computed, useId } from 'vue'
import { SwitchRoot, SwitchThumb, useForwardProps, Label } from 'radix-vue'
import { Primitive, SwitchRoot, SwitchThumb, useForwardProps, Label } from 'reka-ui'
import { reactivePick } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { useFormField } from '../composables/useFormField'
@@ -65,10 +62,10 @@ const props = defineProps<SwitchProps>()
const slots = defineSlots<SwitchSlots>()
const emits = defineEmits<SwitchEmits>()
const modelValue = defineModel<boolean | undefined>({ default: undefined })
const modelValue = defineModel<boolean>({ default: undefined })
const appConfig = useAppConfig()
const rootProps = useForwardProps(reactivePick(props, 'as', 'required', 'value'))
const rootProps = useForwardProps(reactivePick(props, 'required', 'value', 'defaultValue'))
const { id: _id, emitFormChange, emitFormInput, size, color, name, disabled } = useFormField<SwitchProps>(props)
const id = _id.value ?? useId()
@@ -91,17 +88,16 @@ function onUpdate(value: any) {
</script>
<template>
<div :class="ui.root({ class: [props.class, props.ui?.root] })">
<Primitive :as="as" :class="ui.root({ class: [props.class, props.ui?.root] })">
<div :class="ui.container({ class: props.ui?.container })">
<SwitchRoot
:id="id"
v-model:checked="modelValue"
:default-checked="defaultValue"
v-bind="rootProps"
v-model="modelValue"
:name="name"
:disabled="disabled || loading"
:class="ui.base({ class: props.ui?.base })"
@update:checked="onUpdate"
@update:model-value="onUpdate"
>
<SwitchThumb :class="ui.thumb({ class: props.ui?.thumb })">
<UIcon v-if="loading" :name="loadingIcon || appConfig.ui.icons.loading" :class="ui.icon({ class: props.ui?.icon, checked: true, unchecked: true })" />
@@ -124,5 +120,5 @@ function onUpdate(value: any) {
</slot>
</p>
</div>
</div>
</Primitive>
</template>

View File

@@ -50,6 +50,11 @@ export interface TableData {
}
export interface TableProps<T> {
/**
* The element or component this component should render as.
* @defaultValue 'div'
*/
as?: any
data?: T[]
columns?: TableColumn<T>[]
caption?: string
@@ -114,14 +119,8 @@ export type TableSlots<T> = {
<script setup lang="ts" generic="T extends TableData">
import { computed } from 'vue'
import {
FlexRender,
getCoreRowModel,
getFilteredRowModel,
getSortedRowModel,
getExpandedRowModel,
useVueTable
} from '@tanstack/vue-table'
import { Primitive } from 'reka-ui'
import { FlexRender, getCoreRowModel, getFilteredRowModel, getSortedRowModel, getExpandedRowModel, useVueTable } from '@tanstack/vue-table'
import { upperFirst } from 'scule'
import { useLocale } from '../composables/useLocale'
@@ -129,6 +128,7 @@ const props = defineProps<TableProps<T>>()
defineSlots<TableSlots<T>>()
const { t } = useLocale()
const data = computed(() => props.data ?? [])
const columns = computed<TableColumn<T>[]>(() => props.columns ?? Object.keys(data.value[0] ?? {}).map((accessorKey: string) => ({ accessorKey, header: upperFirst(accessorKey) })))
@@ -203,7 +203,7 @@ defineExpose({
</script>
<template>
<div :class="ui.root({ class: [props.class, props.ui?.root] })">
<Primitive :as="as" :class="ui.root({ class: [props.class, props.ui?.root] })">
<table :class="ui.base({ class: [props.ui?.base] })">
<caption v-if="caption" :class="ui.caption({ class: [props.ui?.caption] })">
<slot name="caption">
@@ -258,5 +258,5 @@ defineExpose({
</tr>
</tbody>
</table>
</div>
</Primitive>
</template>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { tv, type VariantProps } from 'tailwind-variants'
import type { TabsRootProps, TabsRootEmits, TabsContentProps } from 'radix-vue'
import type { TabsRootProps, TabsRootEmits } from 'reka-ui'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/tabs'
@@ -25,7 +25,7 @@ export interface TabsItem {
type TabsVariants = VariantProps<typeof tabs>
export interface TabsProps<T> extends Pick<TabsRootProps<string | number>, 'defaultValue' | 'modelValue' | 'activationMode'> {
export interface TabsProps<T> extends Pick<TabsRootProps<string | number>, 'defaultValue' | 'modelValue' | 'activationMode' | 'unmountOnHide'> {
/**
* The element or component this component should render as.
* @defaultValue 'div'
@@ -44,7 +44,7 @@ export interface TabsProps<T> extends Pick<TabsRootProps<string | number>, 'defa
* The content of the tabs, can be disabled to prevent rendering the content.
* @defaultValue true
*/
content?: boolean | Omit<TabsContentProps, 'as' | 'asChild' | 'value'>
content?: boolean
/**
* The key used to get the label from the item.
* @defaultValue 'label'
@@ -85,9 +85,8 @@ extendDevtoolsMeta({
</script>
<script setup lang="ts" generic="T extends TabsItem">
import { computed, toRef } from 'vue'
import { defu } from 'defu'
import { TabsRoot, TabsList, TabsIndicator, TabsTrigger, TabsContent, useForwardPropsEmits } from 'radix-vue'
import { computed } from 'vue'
import { TabsRoot, TabsList, TabsIndicator, TabsTrigger, TabsContent, useForwardPropsEmits } from 'reka-ui'
import { reactivePick } from '@vueuse/core'
import { get } from '../utils'
import UIcon from './Icon.vue'
@@ -102,8 +101,7 @@ const props = withDefaults(defineProps<TabsProps<T>>(), {
const emits = defineEmits<TabsEmits>()
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)
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'orientation', 'activationMode', 'unmountOnHide'), emits)
const ui = computed(() => tabs({
color: props.color,
@@ -133,7 +131,7 @@ const ui = computed(() => tabs({
</TabsList>
<template v-if="!!content">
<TabsContent v-for="(item, index) of items" :key="index" v-bind="contentProps" :value="item.value || String(index)" :class="ui.content({ class: props.ui?.content })">
<TabsContent v-for="(item, index) of items" :key="index" :value="item.value || String(index)" :class="ui.content({ class: props.ui?.content })">
<slot :name="item.slot || 'content'" :item="item" :index="index">
{{ item.content }}
</slot>

View File

@@ -11,6 +11,11 @@ const textarea = tv({ extend: tv(theme), ...(appConfig.ui?.textarea || {}) })
type TextareaVariants = VariantProps<typeof textarea>
export interface TextareaProps {
/**
* The element or component this component should render as.
* @defaultValue 'div'
*/
as?: any
id?: string
name?: string
/** The placeholder text when the textarea is empty. */
@@ -44,6 +49,7 @@ export interface TextareaSlots {
<script setup lang="ts">
import { ref, computed, onMounted, nextTick, watch } from 'vue'
import { Primitive } from 'reka-ui'
import { useFormField } from '../composables/useFormField'
import { looseToNumber } from '../utils'
@@ -167,7 +173,7 @@ onMounted(() => {
</script>
<template>
<div :class="ui.root({ class: [props.class, props.ui?.root] })">
<Primitive :as="as" :class="ui.root({ class: [props.class, props.ui?.root] })">
<textarea
:id="id"
ref="textareaRef"
@@ -185,5 +191,5 @@ onMounted(() => {
/>
<slot />
</div>
</Primitive>
</template>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { tv, type VariantProps } from 'tailwind-variants'
import type { ToastRootProps, ToastRootEmits } from 'radix-vue'
import type { ToastRootProps, ToastRootEmits } from 'reka-ui'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/toast'
@@ -61,7 +61,7 @@ extendDevtoolsMeta<ToastProps>({ ignore: true })
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ToastRoot, ToastTitle, ToastDescription, ToastAction, ToastClose, useForwardPropsEmits } from 'radix-vue'
import { ToastRoot, ToastTitle, ToastDescription, ToastAction, ToastClose, useForwardPropsEmits } from 'reka-ui'
import { reactivePick } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { useLocale } from '../composables/useLocale'
@@ -75,8 +75,9 @@ const props = withDefaults(defineProps<ToastProps>(), {
const emits = defineEmits<ToastEmits>()
const slots = defineSlots<ToastSlots>()
const appConfig = useAppConfig()
const { t } = useLocale()
const appConfig = useAppConfig()
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'defaultOpen', 'open', 'duration', 'type'), emits)
const multiline = computed(() => !!props.title && !!props.description)

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { tv, type VariantProps } from 'tailwind-variants'
import type { ToastProviderProps } from 'radix-vue'
import type { ToastProviderProps } from 'reka-ui'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/toaster'
@@ -41,7 +41,7 @@ extendDevtoolsMeta({ example: 'ToasterExample' })
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ToastProvider, ToastViewport, ToastPortal, useForwardProps } from 'radix-vue'
import { ToastProvider, ToastViewport, ToastPortal, useForwardProps } from 'reka-ui'
import { reactivePick } from '@vueuse/core'
import { useToast } from '../composables/useToast'
import { omit } from '../utils'

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { tv } from 'tailwind-variants'
import type { TooltipRootProps, TooltipRootEmits, TooltipContentProps, TooltipArrowProps } from 'radix-vue'
import type { TooltipRootProps, TooltipRootEmits, TooltipContentProps, TooltipArrowProps } from 'reka-ui'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/tooltip'
@@ -48,7 +48,7 @@ extendDevtoolsMeta({ example: 'TooltipExample', defaultProps: { text: 'Hello wor
<script setup lang="ts">
import { computed, toRef } from 'vue'
import { defu } from 'defu'
import { TooltipRoot, TooltipTrigger, TooltipPortal, TooltipContent, TooltipArrow, useForwardPropsEmits } from 'radix-vue'
import { TooltipRoot, TooltipTrigger, TooltipPortal, TooltipContent, TooltipArrow, useForwardPropsEmits } from 'reka-ui'
import { reactivePick } from '@vueuse/core'
import UKbd from './Kbd.vue'

View File

@@ -1,6 +1,6 @@
@keyframes accordion-up {
from {
height: var(--radix-accordion-content-height);
height: var(--reka-accordion-content-height);
}
to {
@@ -14,13 +14,13 @@
}
to {
height: var(--radix-accordion-content-height);
height: var(--reka-accordion-content-height);
}
}
@keyframes collapsible-up {
from {
height: var(--radix-collapsible-content-height);
height: var(--reka-collapsible-content-height);
}
to {
@@ -34,7 +34,7 @@
}
to {
height: var(--radix-collapsible-content-height);
height: var(--reka-collapsible-content-height);
}
}

View File

@@ -17,10 +17,6 @@ export type GetObjectField<MaybeObject, Key extends string> = MaybeObject extend
? MaybeObject[Key]
: never
export type AcceptableValue = string | number | boolean | Record<string, any>
export type ArrayOrWrapped<T> = T extends any[] ? T : Array<T>
export type PartialString<T> = {
[K in keyof T]?: string
}

View File

@@ -1,3 +1,5 @@
import { isEqual } from 'ohash'
export function pick<Data extends object, Keys extends keyof Data>(data: Data, keys: Keys[]): Pick<Data, Keys> {
const result = {} as Pick<Data, Keys>
@@ -60,6 +62,22 @@ export function looseToNumber(val: any): any {
return Number.isNaN(n) ? val : n
}
export function escapeRegExp(string: string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, match => `\\${match}`)
export function compare<T>(value?: T, currentValue?: T, comparator?: string | ((a: T, b: T) => boolean)) {
if (value === undefined || currentValue === undefined) {
return false
}
if (typeof value === 'string') {
return value === currentValue
}
if (typeof comparator === 'function') {
return comparator(value, currentValue)
}
if (typeof comparator === 'string') {
return get(value!, comparator) === get(currentValue!, comparator)
}
return isEqual(value, currentValue)
}

View File

@@ -92,7 +92,7 @@ export interface LinkSlots {
<script setup lang="ts">
import { computed } from 'vue'
import { isEqual, diff } from 'ohash'
import { useForwardProps } from 'radix-vue'
import { useForwardProps } from 'reka-ui'
import { reactiveOmit } from '@vueuse/core'
import { hasProtocol } from 'ufo'
import { useRoute } from '#imports'

View File

@@ -3,7 +3,7 @@ import { buttonGroupVariant } from './button-group'
export default (options: Required<ModuleOptions>) => ({
slots: {
base: ['rounded-[calc(var(--ui-radius)*1.5)] font-medium inline-flex items-center focus:outline-none disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75', options.theme.transitions && 'transition-colors'],
base: ['rounded-[calc(var(--ui-radius)*1.5)] font-medium inline-flex items-center focus:outline-hidden disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75', options.theme.transitions && 'transition-colors'],
label: 'truncate',
leadingIcon: 'shrink-0',
leadingAvatar: 'shrink-0',

View File

@@ -3,7 +3,7 @@ import type { ModuleOptions } from '../module'
export default (options: Required<ModuleOptions>) => ({
slots: {
root: 'relative flex items-start',
base: 'shrink-0 flex items-center justify-center rounded-[var(--ui-radius)] text-[var(--ui-bg)] ring ring-inset ring-[var(--ui-border-accented)] focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2',
base: 'shrink-0 flex items-center justify-center rounded-[var(--ui-radius)] text-[var(--ui-bg)] ring ring-inset ring-[var(--ui-border-accented)] focus-visible:outline-2 focus-visible:outline-offset-2',
container: 'flex items-center',
wrapper: 'ms-2',
icon: 'shrink-0 size-full',

View File

@@ -5,8 +5,8 @@ export default (options: Required<ModuleOptions>) => ({
root: 'flex flex-col min-h-0 min-w-0 divide-y divide-[var(--ui-border)]',
input: '[&>input]:h-12',
close: '',
content: 'relative overflow-hidden',
viewport: 'divide-y divide-[var(--ui-border)] scroll-py-1',
content: 'relative overflow-hidden flex flex-col',
viewport: 'relative divide-y divide-[var(--ui-border)] scroll-py-1 overflow-y-auto flex-1 focus:outline-none',
group: 'p-1 isolate',
empty: 'py-6 text-center text-sm text-[var(--ui-text-muted)]',
label: 'px-2 py-1.5 text-xs font-semibold text-[var(--ui-text-highlighted)]',

View File

@@ -2,7 +2,7 @@ import type { ModuleOptions } from '../module'
export default (options: Required<ModuleOptions>) => ({
slots: {
content: 'min-w-32 bg-[var(--ui-bg)] shadow-lg rounded-[calc(var(--ui-radius)*1.5)] ring ring-[var(--ui-border)] divide-y divide-[var(--ui-border)] overflow-y-auto scroll-py-1 data-[state=open]:animate-[scale-in_100ms_ease-out]',
content: 'min-w-32 bg-[var(--ui-bg)] shadow-lg rounded-[calc(var(--ui-radius)*1.5)] ring ring-[var(--ui-border)] divide-y divide-[var(--ui-border)] overflow-y-auto scroll-py-1 data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in]',
group: 'p-1 isolate',
label: 'w-full flex items-center font-semibold text-[var(--ui-text-highlighted)]',
separator: '-mx-1 my-1 h-px bg-[var(--ui-border)]',

View File

@@ -8,7 +8,7 @@ export default (options: Required<ModuleOptions>) => {
base: () => ['rounded-[calc(var(--ui-radius)*1.5)]', options.theme.transitions && 'transition-colors'],
trailing: 'group absolute inset-y-0 end-0 flex items-center disabled:cursor-not-allowed disabled:opacity-75',
arrow: 'fill-[var(--ui-border)]',
content: 'max-h-60 w-[var(--radix-popper-anchor-width)] bg-[var(--ui-bg)] shadow-lg rounded-[calc(var(--ui-radius)*1.5)] ring ring-[var(--ui-border)] overflow-hidden data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in]',
content: 'max-h-60 w-[var(--reka-popper-anchor-width)] bg-[var(--ui-bg)] shadow-lg rounded-[calc(var(--ui-radius)*1.5)] ring ring-[var(--ui-border)] overflow-hidden data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in]',
viewport: 'divide-y divide-[var(--ui-border)] scroll-py-1',
group: 'p-1 isolate',
empty: 'py-2 text-center text-sm text-[var(--ui-text-muted)]',
@@ -33,7 +33,6 @@ export default (options: Required<ModuleOptions>) => {
multiple: {
true: {
root: 'flex-wrap',
base: '',
tagsInput: 'border-0 bg-transparent placeholder:text-[var(--ui-text-dimmed)] focus:outline-none disabled:cursor-not-allowed disabled:opacity-75'
},
false: {
@@ -102,12 +101,12 @@ export default (options: Required<ModuleOptions>) => {
color,
multiple: true,
variant: ['outline', 'subtle'],
class: `has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-[var(--${color})]`
class: `has-focus-visible:ring-2 has-focus-visible:ring-[var(--ui-${color})]`
})), {
color: 'neutral',
multiple: true,
variant: ['outline', 'subtle'],
class: 'has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-[var(--ui-border-inverted)]'
class: 'has-focus-visible:ring-2 has-focus-visible:ring-[var(--ui-border-inverted)]'
}]
}, input(options))
}

View File

@@ -25,7 +25,7 @@ export default (options: Required<ModuleOptions>) => ({
childLinkDescription: 'text-sm text-[var(--ui-text-muted)]',
separator: 'px-2 h-px bg-[var(--ui-border)]',
viewportWrapper: 'absolute top-full left-0 flex w-full justify-center',
viewport: 'relative overflow-hidden bg-[var(--ui-bg)] shadow-lg rounded-[calc(var(--ui-radius)*1.5)] ring ring-[var(--ui-border)] h-[var(--radix-navigation-menu-viewport-height)] w-full transition-[width,height] origin-[top_center] data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in]',
viewport: 'relative overflow-hidden bg-[var(--ui-bg)] shadow-lg rounded-[calc(var(--ui-radius)*1.5)] ring ring-[var(--ui-border)] h-[var(--reka-navigation-menu-viewport-height)] w-full transition-[width,height] origin-[top_center] data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in]',
content: 'absolute top-0 left-0 w-full data-[motion=from-start]:animate-[enter-from-left_200ms_ease] data-[motion=from-end]:animate-[enter-from-right_200ms_ease] data-[motion=to-start]:animate-[exit-to-left_200ms_ease] data-[motion=to-end]:animate-[exit-to-right_200ms_ease]',
indicator: 'data-[state=visible]:animate-[fade-in_100ms_ease-out] data-[state=hidden]:animate-[fade-out_100ms_ease-in] bottom-0 z-[1] flex h-2.5 items-end justify-center overflow-hidden transition-transform duration-200 ease-out',
arrow: 'relative top-[50%] size-2.5 rotate-45 border border-[var(--ui-border)] bg-[var(--ui-bg)] z-[1] rounded-[calc(var(--ui-radius)/2)]'

View File

@@ -6,7 +6,7 @@ export default (options: Required<ModuleOptions>) => ({
fieldset: 'flex',
legend: 'mb-1 block font-medium text-[var(--ui-text)]',
item: 'flex items-start',
base: 'rounded-full ring ring-inset ring-[var(--ui-border-accented)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-offset-[var(--ui-bg)]',
base: 'rounded-full ring ring-inset ring-[var(--ui-border-accented)] focus-visible:outline-2 focus-visible:outline-offset-2',
indicator: 'flex items-center justify-center size-full rounded-full after:bg-[var(--ui-bg)] after:rounded-full',
container: 'flex items-center',
wrapper: 'ms-2',

View File

@@ -5,8 +5,6 @@ import select from './select'
export default (options: Required<ModuleOptions>) => {
return defu({
slots: {
value: 'truncate',
placeholder: 'truncate text-[var(--ui-text-dimmed)]',
input: 'border-b border-[var(--ui-border)]'
}
}, select(options))

View File

@@ -8,9 +8,10 @@ export default (options: Required<ModuleOptions>) => {
slots: {
root: () => undefined,
base: () => ['relative group rounded-[calc(var(--ui-radius)*1.5)] inline-flex items-center focus:outline-none disabled:cursor-not-allowed disabled:opacity-75', options.theme.transitions && 'transition-colors'],
value: 'truncate group-data-placeholder:text-[var(--ui-text-dimmed)]',
value: 'truncate pointer-events-none',
placeholder: 'truncate text-[var(--ui-text-dimmed)]',
arrow: 'fill-[var(--ui-border)]',
content: 'max-h-60 w-[var(--radix-popper-anchor-width)] bg-[var(--ui-bg)] shadow-lg rounded-[calc(var(--ui-radius)*1.5)] ring ring-[var(--ui-border)] overflow-hidden data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in]',
content: 'max-h-60 w-[var(--reka-popper-anchor-width)] bg-[var(--ui-bg)] shadow-lg rounded-[calc(var(--ui-radius)*1.5)] ring ring-[var(--ui-border)] overflow-hidden data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in]',
viewport: 'divide-y divide-[var(--ui-border)] scroll-py-1',
group: 'p-1 isolate',
empty: 'py-2 text-center text-sm text-[var(--ui-text-muted)]',

View File

@@ -5,13 +5,13 @@ export default (options: Required<ModuleOptions>) => ({
root: 'relative flex items-center select-none touch-none',
track: 'relative bg-[var(--ui-bg-accented)] overflow-hidden rounded-full grow',
range: 'absolute rounded-full',
thumb: 'rounded-full bg-[var(--ui-bg)] ring-2 focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2'
thumb: 'rounded-full bg-[var(--ui-bg)] ring-2 focus-visible:outline-2 focus-visible:outline-offset-2'
},
variants: {
color: {
...Object.fromEntries((options.theme.colors || []).map((color: string) => [color, {
range: `bg-[var(--ui-${color})]`,
thumb: `ring-[var(--ui-${color})] focus-visible:outline-[var(--ui-${color})]`
thumb: `ring-[var(--ui-${color})] focus-visible:outline-[var(--ui-${color})]/50`
}])),
neutral: {
range: 'bg-[var(--ui-bg-inverted)]',

View File

@@ -3,9 +3,9 @@ import type { ModuleOptions } from '../module'
export default (options: Required<ModuleOptions>) => ({
slots: {
root: 'relative flex items-start',
base: ['inline-flex items-center shrink-0 rounded-full border-2 border-transparent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--ui-bg)] data-[state=unchecked]:bg-[var(--ui-bg-accented)]', options.theme.transitions && 'transition-colors duration-200'],
base: ['inline-flex items-center shrink-0 rounded-full border-2 border-transparent focus-visible:outline-2 focus-visible:outline-offset-2 data-[state=unchecked]:bg-[var(--ui-bg-accented)]', options.theme.transitions && 'transition-colors duration-200'],
container: 'flex items-center',
thumb: 'group pointer-events-none block rounded-full bg-[var(--ui-bg)] shadow-lg ring-0 transition-transform duration-200 data-[state=unchecked]:translate-x-0 data-[state=unchecked]:rtl:-translate-x-0 flex items-center justify-center',
thumb: 'group pointer-events-none rounded-full bg-[var(--ui-bg)] shadow-lg ring-0 transition-transform duration-200 data-[state=unchecked]:translate-x-0 data-[state=unchecked]:rtl:-translate-x-0 flex items-center justify-center',
icon: ['absolute shrink-0 group-data-[state=unchecked]:text-[var(--ui-text-dimmed)] opacity-0 size-10/12', options.theme.transitions && 'transition-[color,opacity] duration-200'],
wrapper: 'ms-2',
label: 'block font-medium text-[var(--ui-text)]',
@@ -14,11 +14,11 @@ export default (options: Required<ModuleOptions>) => ({
variants: {
color: {
...Object.fromEntries((options.theme.colors || []).map((color: string) => [color, {
base: `data-[state=checked]:bg-[var(--ui-${color})] focus-visible:ring-[var(--ui-${color})]`,
base: `data-[state=checked]:bg-[var(--ui-${color})] focus-visible:outline-[var(--ui-${color})]`,
icon: `group-data-[state=checked]:text-[var(--ui-${color})]`
}])),
neutral: {
base: 'data-[state=checked]:bg-[var(--ui-bg-inverted)] focus-visible:ring-[var(--ui-border-inverted)]',
base: 'data-[state=checked]:bg-[var(--ui-bg-inverted)] focus-visible:outline-[var(--ui-border-inverted)]',
icon: 'group-data-[state=checked]:text-[var(--ui-text-highlighted)]'
}
},

View File

@@ -5,7 +5,7 @@ export default (options: Required<ModuleOptions>) => ({
root: 'flex items-center gap-2',
list: 'relative flex p-1 group',
indicator: 'absolute transition-[translate,width] duration-200',
trigger: ['relative inline-flex items-center shrink-0 data-[state=inactive]:text-[var(--ui-text-muted)] hover:data-[state=inactive]:text-[var(--ui-text)] font-medium rounded-[calc(var(--ui-radius)*1.5)] disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none', options.theme.transitions && 'transition-colors'],
trigger: ['relative inline-flex items-center shrink-0 data-[state=inactive]:text-[var(--ui-text-muted)] hover:data-[state=inactive]:text-[var(--ui-text)] font-medium rounded-[calc(var(--ui-radius)*1.5)] disabled:cursor-not-allowed disabled:opacity-75 focus:outline-hidden', options.theme.transitions && 'transition-colors'],
content: 'focus:outline-none w-full',
leadingIcon: 'shrink-0',
leadingAvatar: 'shrink-0',
@@ -32,12 +32,12 @@ export default (options: Required<ModuleOptions>) => ({
horizontal: {
root: 'flex-col',
list: 'w-full',
indicator: 'left-0 w-[var(--radix-tabs-indicator-size)] translate-x-[var(--radix-tabs-indicator-position)]',
indicator: 'left-0 w-[var(--reka-tabs-indicator-size)] translate-x-[var(--reka-tabs-indicator-position)]',
trigger: 'justify-center'
},
vertical: {
list: 'flex-col',
indicator: 'top-0 h-[var(--radix-tabs-indicator-size)] translate-y-[var(--radix-tabs-indicator-position)]'
indicator: 'top-0 h-[var(--reka-tabs-indicator-size)] translate-y-[var(--reka-tabs-indicator-position)]'
}
},
size: {

View File

@@ -45,10 +45,10 @@ export default {
}
}, {
swipeDirection: ['left', 'right'],
class: 'data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=cancel]:translate-x-0'
class: 'data-[swipe=move]:translate-x-[var(--reka-toast-swipe-move-x)] data-[swipe=end]:translate-x-[var(--reka-toast-swipe-end-x)] data-[swipe=cancel]:translate-x-0'
}, {
swipeDirection: ['up', 'down'],
class: 'data-[swipe=move]:translate-y-[var(--radix-toast-swipe-move-y)] data-[swipe=end]:translate-y-[var(--radix-toast-swipe-end-y)] data-[swipe=cancel]:translate-y-0'
class: 'data-[swipe=move]:translate-y-[var(--reka-toast-swipe-move-y)] data-[swipe=end]:translate-y-[var(--reka-toast-swipe-end-y)] data-[swipe=cancel]:translate-y-0'
}],
defaultVariants: {
position: 'bottom-right'