Files
ui/src/runtime/components/Tabs.vue
J-Michalek b9adc83e78 feat(components): add ui field in items (#4060)
Co-authored-by: Jakub <jakub.michalek@freelo.io>
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2025-05-13 17:40:29 +02:00

145 lines
4.9 KiB
Vue

<!-- eslint-disable vue/block-tag-newline -->
<script lang="ts">
import type { TabsRootProps, TabsRootEmits } from 'reka-ui'
import type { AppConfig } from '@nuxt/schema'
import theme from '#build/ui/tabs'
import type { AvatarProps } from '../types'
import type { DynamicSlots, ComponentConfig } from '../types/utils'
type Tabs = ComponentConfig<typeof theme, AppConfig, 'tabs'>
export interface TabsItem {
label?: string
/**
* @IconifyIcon
*/
icon?: string
avatar?: AvatarProps
slot?: string
content?: string
/** A unique value for the tab item. Defaults to the index. */
value?: string | number
disabled?: boolean
class?: any
ui?: Pick<Tabs['slots'], 'trigger' | 'leadingIcon' | 'leadingAvatar' | 'label' | 'content'>
[key: string]: any
}
export interface TabsProps<T extends TabsItem = TabsItem> extends Pick<TabsRootProps<string | number>, 'defaultValue' | 'modelValue' | 'activationMode' | 'unmountOnHide'> {
/**
* The element or component this component should render as.
* @defaultValue 'div'
*/
as?: any
items?: T[]
/**
* @defaultValue 'primary'
*/
color?: Tabs['variants']['color']
/**
* @defaultValue 'pill'
*/
variant?: Tabs['variants']['variant']
/**
* @defaultValue 'md'
*/
size?: Tabs['variants']['size']
/**
* The orientation of the tabs.
* @defaultValue 'horizontal'
*/
orientation?: TabsRootProps['orientation']
/**
* The content of the tabs, can be disabled to prevent rendering the content.
* @defaultValue true
*/
content?: boolean
/**
* The key used to get the label from the item.
* @defaultValue 'label'
*/
labelKey?: string
class?: any
ui?: Tabs['slots']
}
export interface TabsEmits extends TabsRootEmits<string | number> {}
type SlotProps<T extends TabsItem> = (props: { item: T, index: number }) => any
export type TabsSlots<T extends TabsItem = TabsItem> = {
'leading': SlotProps<T>
'default': SlotProps<T>
'trailing': SlotProps<T>
'content': SlotProps<T>
'list-leading': (props?: {}) => any
'list-trailing': (props?: {}) => any
} & DynamicSlots<T, undefined, { index: number }>
</script>
<script setup lang="ts" generic="T extends TabsItem">
import { computed } from 'vue'
import { TabsRoot, TabsList, TabsIndicator, TabsTrigger, TabsContent, useForwardPropsEmits } from 'reka-ui'
import { reactivePick } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { get } from '../utils'
import { tv } from '../utils/tv'
import UIcon from './Icon.vue'
import UAvatar from './Avatar.vue'
const props = withDefaults(defineProps<TabsProps<T>>(), {
content: true,
defaultValue: '0',
orientation: 'horizontal',
unmountOnHide: true,
labelKey: 'label'
})
const emits = defineEmits<TabsEmits>()
const slots = defineSlots<TabsSlots<T>>()
const appConfig = useAppConfig() as Tabs['AppConfig']
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'orientation', 'activationMode', 'unmountOnHide'), emits)
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.tabs || {}) })({
color: props.color,
variant: props.variant,
size: props.size,
orientation: props.orientation
}))
</script>
<template>
<TabsRoot v-bind="rootProps" :class="ui.root({ class: [props.ui?.root, props.class] })">
<TabsList :class="ui.list({ class: props.ui?.list })">
<TabsIndicator :class="ui.indicator({ class: props.ui?.indicator })" />
<slot name="list-leading" />
<TabsTrigger v-for="(item, index) of items" :key="index" :value="item.value || String(index)" :disabled="item.disabled" :class="ui.trigger({ class: [props.ui?.trigger, item.ui?.trigger] })">
<slot name="leading" :item="item" :index="index">
<UIcon v-if="item.icon" :name="item.icon" :class="ui.leadingIcon({ class: [props.ui?.leadingIcon, item.ui?.leadingIcon] })" />
<UAvatar v-else-if="item.avatar" :size="((props.ui?.leadingAvatarSize || ui.leadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.leadingAvatar({ class: [props.ui?.leadingAvatar, item.ui?.leadingAvatar] })" />
</slot>
<span v-if="get(item, props.labelKey as string) || !!slots.default" :class="ui.label({ class: [props.ui?.label, item.ui?.label] })">
<slot :item="item" :index="index">{{ get(item, props.labelKey as string) }}</slot>
</span>
<slot name="trailing" :item="item" :index="index" />
</TabsTrigger>
<slot name="list-trailing" />
</TabsList>
<template v-if="!!content">
<TabsContent v-for="(item, index) of items" :key="index" :value="item.value || String(index)" :class="ui.content({ class: [props.ui?.content, item.ui?.content, item.class] })">
<slot :name="((item.slot || 'content') as keyof TabsSlots<T>)" :item="(item as Extract<T, { slot: string; }>)" :index="index">
{{ item.content }}
</slot>
</TabsContent>
</template>
</TabsRoot>
</template>