feat(module)!: use tailwind-merge for class merging (#509)

This commit is contained in:
Benjamin Canac
2023-08-12 17:17:00 +02:00
parent 6d7973f6e1
commit 8880bdc456
47 changed files with 685 additions and 376 deletions

View File

@@ -1,9 +1,9 @@
<template>
<div :class="ui.wrapper">
<div :class="wrapperClass">
<HDisclosure v-for="(item, index) in items" v-slot="{ open, close }" :key="index" :default-open="defaultOpen || item.defaultOpen">
<HDisclosureButton :ref="() => buttonRefs[index] = close" as="template" :disabled="item.disabled">
<slot :item="item" :index="index" :open="open" :close="close">
<UButton v-bind="{ ...omit(ui.default, ['openIcon', 'closeIcon']), ...$attrs, ...omit(item, ['slot', 'disabled', 'content', 'defaultOpen']) }">
<UButton v-bind="{ ...omit(ui.default, ['openIcon', 'closeIcon']), ...attrs, ...omit(item, ['slot', 'disabled', 'content', 'defaultOpen']) }">
<template #trailing>
<UIcon
:name="!open ? openIcon : closeIcon ? closeIcon : openIcon"
@@ -43,10 +43,11 @@
import { ref, computed, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { Disclosure as HDisclosure, DisclosureButton as HDisclosureButton, DisclosurePanel as HDisclosurePanel } from '@headlessui/vue'
import { defu } from 'defu'
import { omit } from 'lodash-es'
import { twMerge } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue'
import UButton from '../elements/Button.vue'
import { defuTwMerge } from '../../utils'
import StateEmitter from '../../utils/StateEmitter'
import type { AccordionItem } from '../../types/accordion'
import { useAppConfig } from '#imports'
@@ -87,17 +88,19 @@ export default defineComponent({
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.accordion>>,
default: () => appConfig.ui.accordion
default: () => ({})
}
},
setup (props) {
setup (props, { attrs }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.accordion>>(() => defu({}, props.ui, appConfig.ui.accordion))
const ui = computed<Partial<typeof appConfig.ui.accordion>>(() => defuTwMerge({}, props.ui, appConfig.ui.accordion))
const uiButton = computed<Partial<typeof appConfig.ui.button>>(() => appConfig.ui.button)
const wrapperClass = computed(() => twMerge(ui.value.wrapper, attrs.class as string))
const buttonRefs = ref<Function[]>([])
function closeOthers (itemIndex: number) {
@@ -136,9 +139,11 @@ export default defineComponent({
}
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
uiButton,
wrapperClass,
buttonRefs,
closeOthers,
omit,

View File

@@ -1,5 +1,5 @@
<template>
<div :class="alertClass">
<div :class="alertClass" v-bind="attrs">
<div class="flex gap-3" :class="{ 'items-start': (description || $slots.description), 'items-center': !description && !$slots.description }">
<UIcon v-if="icon" :name="icon" :class="ui.icon.base" />
<UAvatar v-if="avatar" v-bind="{ size: ui.avatar.size, ...avatar }" :class="ui.avatar.base" />
@@ -34,17 +34,18 @@
<script lang="ts">
import { computed, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { defu } from 'defu'
import { twMerge, twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue'
import UAvatar from '../elements/Avatar.vue'
import UButton from '../elements/Button.vue'
import type { Avatar } from '../../types/avatar'
import type { Button } from '../../types/button'
import { classNames } from '../../utils'
import { defuTwMerge } from '../../utils'
import { useAppConfig } from '#imports'
// TODO: Remove
// @ts-expect-error
import appConfig from '#build/app.config'
import { omit } from 'lodash-es'
// const appConfig = useAppConfig()
@@ -54,6 +55,7 @@ export default defineComponent({
UAvatar,
UButton
},
inheritAttrs: false,
props: {
title: {
type: String,
@@ -98,29 +100,30 @@ export default defineComponent({
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.alert>>,
default: () => appConfig.ui.alert
default: () => ({})
}
},
emits: ['close'],
setup (props) {
setup (props, { attrs }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.alert>>(() => defu({}, props.ui, appConfig.ui.alert))
const ui = computed<Partial<typeof appConfig.ui.alert>>(() => defuTwMerge({}, props.ui, appConfig.ui.alert))
const alertClass = computed(() => {
const variant = ui.value.color?.[props.color as string]?.[props.variant as string] || ui.value.variant[props.variant]
return classNames(
return twMerge(twJoin(
ui.value.wrapper,
ui.value.rounded,
ui.value.shadow,
ui.value.padding,
variant?.replaceAll('{color}', props.color)
)
), attrs.class as string)
})
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
alertClass

View File

@@ -2,10 +2,10 @@
<span :class="wrapperClass">
<img
v-if="url && !error"
:class="avatarClass"
:class="imgClass"
:alt="alt"
:src="url"
v-bind="$attrs"
v-bind="attrs"
@error="onError"
>
<span v-else-if="text" :class="ui.text">{{ text }}</span>
@@ -22,13 +22,14 @@
<script lang="ts">
import { defineComponent, ref, computed, watch } from 'vue'
import type { PropType } from 'vue'
import { defu } from 'defu'
import { twMerge, twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue'
import { classNames } from '../../utils'
import { defuTwMerge } from '../../utils'
import { useAppConfig } from '#imports'
// TODO: Remove
// @ts-expect-error
import appConfig from '#build/app.config'
import { omit } from 'lodash-es'
// const appConfig = useAppConfig()
@@ -79,16 +80,20 @@ export default defineComponent({
type: [String, Number],
default: null
},
imgClass: {
type: String,
default: ''
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.avatar>>,
default: () => appConfig.ui.avatar
default: () => ({})
}
},
setup (props) {
setup (props, { attrs }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.avatar>>(() => defu({}, props.ui, appConfig.ui.avatar))
const ui = computed<Partial<typeof appConfig.ui.avatar>>(() => defuTwMerge({}, props.ui, appConfig.ui.avatar))
const url = computed(() => {
if (typeof props.src === 'boolean') {
@@ -102,30 +107,30 @@ export default defineComponent({
})
const wrapperClass = computed(() => {
return classNames(
return twMerge(twJoin(
ui.value.wrapper,
(error.value || !url.value) && ui.value.background,
ui.value.rounded,
ui.value.size[props.size]
)
), attrs.class as string)
})
const avatarClass = computed(() => {
return classNames(
const imgClass = computed(() => {
return twMerge(twJoin(
ui.value.rounded,
ui.value.size[props.size]
)
), props.imgClass)
})
const iconClass = computed(() => {
return classNames(
return twJoin(
ui.value.icon.base,
ui.value.icon.size[props.size]
)
})
const chipClass = computed(() => {
return classNames(
return twJoin(
ui.value.chip.base,
ui.value.chip.size[props.size],
ui.value.chip.position[props.chipPosition],
@@ -146,8 +151,10 @@ export default defineComponent({
}
return {
attrs: omit(attrs, ['class']),
wrapperClass,
avatarClass,
// eslint-disable-next-line vue/no-dupe-keys
imgClass,
iconClass,
chipClass,
url,

View File

@@ -1,7 +1,8 @@
import { h, cloneVNode, computed, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { defu } from 'defu'
import { classNames, getSlotsChildren } from '../../utils'
import { omit } from 'lodash-es'
import { twMerge, twJoin } from 'tailwind-merge'
import { defuTwMerge, getSlotsChildren } from '../../utils'
import Avatar from './Avatar.vue'
import { useAppConfig } from '#imports'
// TODO: Remove
@@ -11,6 +12,7 @@ import appConfig from '#build/app.config'
// const appConfig = useAppConfig()
export default defineComponent({
inheritAttrs: false,
props: {
size: {
type: String,
@@ -25,14 +27,14 @@ export default defineComponent({
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.avatarGroup>>,
default: () => appConfig.ui.avatarGroup
default: () => ({})
}
},
setup (props, { slots }) {
setup (props, { attrs, slots }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.avatarGroup>>(() => defu({}, props.ui, appConfig.ui.avatarGroup))
const ui = computed<Partial<typeof appConfig.ui.avatarGroup>>(() => defuTwMerge({}, props.ui, appConfig.ui.avatarGroup))
const children = computed(() => getSlotsChildren(slots))
@@ -46,13 +48,8 @@ export default defineComponent({
vProps.size = props.size
}
vProps.ui = node.props.ui || {}
vProps.ui.wrapper = classNames(
appConfig.ui.avatar.wrapper,
vProps.ui.wrapper || '',
ui.value.ring,
ui.value.margin
)
vProps.class = node.props.class || ''
vProps.class = twMerge(twJoin(vProps.class, ui.value.ring, ui.value.margin), vProps.class)
return cloneVNode(node, vProps)
}
@@ -61,19 +58,13 @@ export default defineComponent({
return h(Avatar, {
size: props.size,
text: `+${children.value.length - max.value}`,
ui: {
wrapper: classNames(
appConfig.ui.avatar.wrapper,
ui.value.ring,
ui.value.margin
)
}
class: twJoin(ui.value.ring, ui.value.margin)
})
}
return null
}).filter(Boolean).reverse())
return () => h('div', { class: ui.value.wrapper }, clones.value)
return () => h('div', { class: twMerge(ui.value.wrapper, attrs.class as string), ...omit(attrs, ['class']) }, clones.value)
}
})

View File

@@ -1,5 +1,5 @@
<template>
<span :class="badgeClass">
<span :class="badgeClass" v-bind="attrs">
<slot>{{ label }}</slot>
</span>
</template>
@@ -7,8 +7,9 @@
<script lang="ts">
import { computed, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { defu } from 'defu'
import { classNames } from '../../utils'
import { omit } from 'lodash-es'
import { twMerge, twJoin } from 'tailwind-merge'
import { defuTwMerge } from '../../utils'
import { useAppConfig } from '#imports'
// TODO: Remove
// @ts-expect-error
@@ -17,6 +18,7 @@ import appConfig from '#build/app.config'
// const appConfig = useAppConfig()
export default defineComponent({
inheritAttrs: false,
props: {
size: {
type: String,
@@ -48,28 +50,29 @@ export default defineComponent({
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.badge>>,
default: () => appConfig.ui.badge
default: () => ({})
}
},
setup (props) {
setup (props, { attrs }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.badge>>(() => defu({}, props.ui, appConfig.ui.badge))
const ui = computed<Partial<typeof appConfig.ui.badge>>(() => defuTwMerge({}, props.ui, appConfig.ui.badge))
const badgeClass = computed(() => {
const variant = ui.value.color?.[props.color as string]?.[props.variant as string] || ui.value.variant[props.variant]
return classNames(
return twMerge(twJoin(
ui.value.base,
ui.value.font,
ui.value.rounded,
ui.value.size[props.size],
variant?.replaceAll('{color}', props.color)
)
), attrs.class as string)
})
return {
attrs: omit(attrs, ['class']),
badgeClass
}
}

View File

@@ -1,5 +1,5 @@
<template>
<ULink :type="type" :disabled="disabled || loading" :class="buttonClass">
<ULink :type="type" :disabled="disabled || loading" :class="buttonClass" v-bind="attrs">
<slot name="leading" :disabled="disabled" :loading="loading">
<UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="leadingIconClass" aria-hidden="true" />
</slot>
@@ -17,12 +17,13 @@
</template>
<script lang="ts">
import { computed, defineComponent, useSlots } from 'vue'
import { computed, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { defu } from 'defu'
import { omit } from 'lodash-es'
import { twMerge, twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue'
import ULink from '../elements/Link.vue'
import { classNames } from '../../utils'
import { defuTwMerge } from '../../utils'
import { useAppConfig } from '#imports'
// TODO: Remove
// @ts-expect-error
@@ -35,6 +36,7 @@ export default defineComponent({
UIcon,
ULink
},
inheritAttrs: false,
props: {
type: {
type: String,
@@ -118,16 +120,14 @@ export default defineComponent({
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.button>>,
default: () => appConfig.ui.button
default: () => ({})
}
},
setup (props) {
setup (props, { attrs, slots }) {
// TODO: Remove
const appConfig = useAppConfig()
const slots = useSlots()
const ui = computed<Partial<typeof appConfig.ui.button>>(() => defu({}, props.ui, appConfig.ui.button))
const ui = computed<Partial<typeof appConfig.ui.button>>(() => defuTwMerge({}, props.ui, appConfig.ui.button))
const isLeading = computed(() => {
return (props.icon && props.leading) || (props.icon && !props.trailing) || (props.loading && !props.trailing) || props.leadingIcon
@@ -142,7 +142,7 @@ export default defineComponent({
const buttonClass = computed(() => {
const variant = ui.value.color?.[props.color as string]?.[props.variant as string] || ui.value.variant[props.variant]
return classNames(
return twMerge(twJoin(
ui.value.base,
ui.value.font,
ui.value.rounded,
@@ -151,7 +151,7 @@ export default defineComponent({
props.padded && ui.value[isSquare.value ? 'square' : 'padding'][props.size],
variant?.replaceAll('{color}', props.color),
props.block ? 'w-full flex justify-center items-center' : 'inline-flex items-center'
)
), attrs.class as string)
})
const leadingIconName = computed(() => {
@@ -171,7 +171,7 @@ export default defineComponent({
})
const leadingIconClass = computed(() => {
return classNames(
return twJoin(
ui.value.icon.base,
ui.value.icon.size[props.size],
props.loading && 'animate-spin'
@@ -179,7 +179,7 @@ export default defineComponent({
})
const trailingIconClass = computed(() => {
return classNames(
return twJoin(
ui.value.icon.base,
ui.value.icon.size[props.size],
props.loading && !isLeading.value && 'animate-spin'
@@ -187,6 +187,7 @@ export default defineComponent({
})
return {
attrs: omit(attrs, ['class']),
isLeading,
isTrailing,
isSquare,

View File

@@ -1,7 +1,8 @@
import { h, cloneVNode, computed, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { defu } from 'defu'
import { getSlotsChildren } from '../../utils'
import { omit } from 'lodash-es'
import { twMerge, twJoin } from 'tailwind-merge'
import { defuTwMerge, getSlotsChildren } from '../../utils'
import { useAppConfig } from '#imports'
// TODO: Remove
// @ts-expect-error
@@ -10,6 +11,7 @@ import appConfig from '#build/app.config'
// const appConfig = useAppConfig()
export default defineComponent({
inheritAttrs: false,
props: {
size: {
type: String,
@@ -20,14 +22,14 @@ export default defineComponent({
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.buttonGroup>>,
default: () => appConfig.ui.buttonGroup
default: () => ({})
}
},
setup (props, { slots }) {
setup (props, { attrs, slots }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.buttonGroup>>(() => defu({}, props.ui, appConfig.ui.buttonGroup))
const ui = computed<Partial<typeof appConfig.ui.buttonGroup>>(() => defuTwMerge({}, props.ui, appConfig.ui.buttonGroup))
const children = computed(() => getSlotsChildren(slots))
@@ -50,22 +52,20 @@ export default defineComponent({
vProps.size = props.size
}
vProps.class = node.props?.class || ''
vProps.class += ' !shadow-none'
vProps.ui = node.props?.ui || {}
vProps.ui.rounded = ''
const classes = ['shadow-none', 'rounded-none']
if (index === 0) {
vProps.ui.rounded = rounded.value.left
classes.push(rounded.value.left)
}
if (index === children.value.length - 1) {
classes.push(rounded.value.right)
}
if (index === children.value.length - 1) {
vProps.ui.rounded = rounded.value.right
}
vProps.class = node.props?.class || ''
vProps.class = twMerge(twJoin(...classes), vProps.class)
return cloneVNode(node, vProps)
}))
return () => h('div', { class: [ui.value.wrapper, ui.value.rounded, ui.value.shadow] }, clones.value)
return () => h('div', { class: twMerge(twJoin(ui.value.wrapper, ui.value.rounded, ui.value.shadow), attrs.class as string), ...omit(attrs, ['class']) }, clones.value)
}
})

View File

@@ -1,5 +1,5 @@
<template>
<HMenu v-slot="{ open }" as="div" :class="ui.wrapper" @mouseleave="onMouseLeave">
<HMenu v-slot="{ open }" as="div" :class="wrapperClass" v-bind="attrs" @mouseleave="onMouseLeave">
<HMenuButton
ref="trigger"
as="div"
@@ -50,11 +50,13 @@ import type { PropType } from 'vue'
import { Menu as HMenu, MenuButton as HMenuButton, MenuItems as HMenuItems, MenuItem as HMenuItem } from '@headlessui/vue'
import { defu } from 'defu'
import { omit } from 'lodash-es'
import { twMerge } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue'
import UAvatar from '../elements/Avatar.vue'
import UKbd from '../elements/Kbd.vue'
import ULink from '../elements/Link.vue'
import { usePopper } from '../../composables/usePopper'
import { defuTwMerge } from '../../utils'
import type { DropdownItem } from '../../types/dropdown'
import type { PopperOptions } from '../../types'
import { useAppConfig } from '#imports'
@@ -75,6 +77,7 @@ export default defineComponent({
UKbd,
ULink
},
inheritAttrs: false,
props: {
items: {
type: Array as PropType<DropdownItem[][]>,
@@ -105,14 +108,14 @@ export default defineComponent({
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.dropdown>>,
default: () => appConfig.ui.dropdown
default: () => ({})
}
},
setup (props) {
setup (props, { attrs }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.dropdown>>(() => defu({}, props.ui, appConfig.ui.dropdown))
const ui = computed<Partial<typeof appConfig.ui.dropdown>>(() => defuTwMerge({}, props.ui, appConfig.ui.dropdown))
const popper = computed<PopperOptions>(() => defu(props.mode === 'hover' ? { offsetDistance: 0 } : {}, props.popper, ui.value.popper as PopperOptions))
@@ -142,6 +145,8 @@ export default defineComponent({
return props.mode === 'hover' ? { paddingTop: `${offsetDistance}px`, paddingBottom: `${offsetDistance}px` } : {}
})
const wrapperClass = computed(() => twMerge(ui.value.wrapper, attrs.class as string))
function onMouseOver () {
if (props.mode !== 'hover' || !menuApi.value) {
return
@@ -183,11 +188,13 @@ export default defineComponent({
}
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
trigger,
container,
containerStyle,
wrapperClass,
onMouseOver,
onMouseLeave,
omit

View File

@@ -1,5 +1,5 @@
<template>
<kbd :class="[ui.base, ui.size[size], ui.padding, ui.rounded, ui.font, ui.background, ui.ring]">
<kbd :class="kbdClass" v-bind="attrs">
<slot>{{ value }}</slot>
</kbd>
</template>
@@ -7,7 +7,9 @@
<script lang="ts">
import { defineComponent, computed } from 'vue'
import type { PropType } from 'vue'
import { defu } from 'defu'
import { omit } from 'lodash-es'
import { twMerge, twJoin } from 'tailwind-merge'
import { defuTwMerge } from '../../utils'
import { useAppConfig } from '#imports'
// TODO: Remove
// @ts-expect-error
@@ -16,6 +18,7 @@ import appConfig from '#build/app.config'
// const appConfig = useAppConfig()
export default defineComponent({
inheritAttrs: false,
props: {
value: {
type: String,
@@ -30,18 +33,32 @@ export default defineComponent({
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.kbd>>,
default: () => appConfig.ui.kbd
default: () => ({})
}
},
setup (props) {
setup (props, { attrs }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.kbd>>(() => defu({}, props.ui, appConfig.ui.kbd))
const ui = computed<Partial<typeof appConfig.ui.kbd>>(() => defuTwMerge({}, props.ui, appConfig.ui.kbd))
const kbdClass = computed(() => {
return twMerge(twJoin(
ui.value.base,
ui.value.size[props.size],
ui.value.padding,
ui.value.rounded,
ui.value.font,
ui.value.background,
ui.value.ring
), attrs.class as string)
})
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui
ui,
kbdClass
}
}
})