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,11 +1,11 @@
<template>
<UTooltip :text="color.value" class="capitalize" :open-delay="500">
<UButton
color="gray"
color="transparent"
square
:ui="{
color: {
gray: {
transparent: {
solid: 'bg-gray-100 dark:bg-gray-800',
ghost: 'hover:bg-gray-50 dark:hover:bg-gray-800/50'
}

View File

@@ -8,7 +8,7 @@
v-model="componentProps[prop.name]"
:name="`prop-${prop.name}`"
tabindex="-1"
:ui="{ wrapper: 'relative flex items-start justify-center' }"
class="justify-center"
/>
<USelectMenu
v-else-if="prop.type === 'string' && prop.options.length"
@@ -16,8 +16,9 @@
:options="prop.options"
:name="`prop-${prop.name}`"
variant="none"
:ui-menu="{ width: 'w-32 !-mt-px', rounded: 'rounded-b-md', wrapper: 'relative inline-flex' }"
class="!py-0"
class="inline-flex"
:ui-menu="{ width: 'w-32 !-mt-px', rounded: 'rounded-t-none' }"
select-class="py-0"
tabindex="-1"
:popper="{ strategy: 'fixed', placement: 'bottom-start' }"
/>
@@ -28,7 +29,7 @@
:name="`prop-${prop.name}`"
variant="none"
autocomplete="off"
class="!py-0"
input-class="py-0"
tabindex="-1"
@update:model-value="val => componentProps[prop.name] = prop.type === 'number' ? Number(val) : val"
/>

View File

@@ -1,6 +1,6 @@
<template>
<UFormGroup v-slot="{ error }" label="Email" :error="!email && 'You must enter an email'" help="This is a nice email!">
<UInput v-model="email" type="email" placeholder="Enter email" :trailing-icon="error && 'i-heroicons-exclamation-triangle-20-solid'" />
<UInput v-model="email" type="email" placeholder="Enter email" :trailing-icon="error ? 'i-heroicons-exclamation-triangle-20-solid' : undefined" />
</UFormGroup>
</template>

View File

@@ -231,7 +231,7 @@ const { data: todos, pending } = await useLazyAsyncData('todos', () => $fetch<{
:total="pageTotal"
:ui="{
wrapper: 'flex items-center gap-1',
rounded: 'rounded-full min-w-[32px] justify-center',
rounded: '!rounded-full min-w-[32px] justify-center',
default: {
activeButton: {
variant: 'outline'

View File

@@ -9,7 +9,7 @@ const items = ref(Array(55))
:total="items.length"
:ui="{
wrapper: 'flex items-center gap-1',
rounded: 'rounded-full min-w-[32px] justify-center'
rounded: '!rounded-full min-w-[32px] justify-center'
}"
:prev-button="null"
:next-button="{

View File

@@ -25,8 +25,8 @@ const links = [{
:links="links"
:ui="{
wrapper: 'border-s border-gray-200 dark:border-gray-800 space-y-2',
base: 'group block border-s -ms-px lg:leading-6',
padding: 'ps-4',
base: 'group block border-s -ms-px lg:leading-6 before:hidden',
padding: 'p-0 ps-4',
rounded: '',
font: '',
ring: '',

View File

@@ -77,6 +77,8 @@ This can also happen when you bind a dynamic color to a component: `<UBadge :col
## Components
### `app.config.ts`
Components are styled with Tailwind CSS but classes are all defined in the default [app.config.ts](https://github.com/nuxtlabs/ui/blob/dev/src/runtime/app.config.ts) file. You can override those in your own `app.config.ts`.
```ts [app.config.ts]
@@ -89,6 +91,8 @@ export default defineAppConfig({
})
```
### `ui` prop
Each component has a `ui` prop that allows you to customize everything specifically.
```vue
@@ -103,6 +107,45 @@ Each component has a `ui` prop that allows you to customize everything specifica
You can find the default classes for each component under the `Preset` section.
::
Thanks to [tailwind-merge](https://github.com/dcastil/tailwind-merge), the `ui` prop is smartly merged with the config. This means you don't have to rewrite everything. :u-badge{label="Edge" class="!rounded-full" variant="subtle"}
For example, the default preset of the `FormGroup` component looks like this:
```json
{
...
"label": {
"base": "block font-medium text-gray-700 dark:text-gray-200",
...
}
...
}
```
To change the font of the `label`, you only need to write:
```vue
<UFormGroup name="email" label="Email" :ui="{ label: { base: 'font-semibold' } }">
...
</UFormGroup>
```
This will smartly replace the `font-medium` by `font-semibold` and prevent any class duplication and any class priority issue.
### `class` attribute
You can also use the `class` attribute to add classes to the component.
```vue
<template>
<UButton label="Button" class="rounded-full" />
</template>
```
Again, with [tailwind-merge](https://github.com/dcastil/tailwind-merge), this will smartly merge the classes with the `ui` prop and the config. :u-badge{label="Edge" class="!rounded-full" variant="subtle"}
### Default values
Some component props like `size`, `color`, `variant`, etc. have a default value that you can override in your `app.config.ts`.
```ts [app.config.ts]

View File

@@ -223,8 +223,8 @@ const links = [{
:links="links"
:ui="{
wrapper: 'border-s border-gray-200 dark:border-gray-800 space-y-2',
base: 'group block border-s -ms-px lg:leading-6',
padding: 'ps-4',
base: 'group block border-s -ms-px lg:leading-6 before:hidden',
padding: 'p-0 ps-4',
rounded: '',
font: '',
ring: '',
@@ -254,7 +254,7 @@ const items = ref(Array(55))
:total="items.length"
:ui="{
wrapper: 'flex items-center gap-1',
rounded: 'rounded-full min-w-[32px] justify-center'
rounded: '!rounded-full min-w-[32px] justify-center'
}"
:prev-button="null"
:next-button="{

View File

@@ -142,8 +142,6 @@ baseProps:
props:
label: 'Email'
error: true
ui:
error: 'hidden'
excludedProps:
- ui
- error

View File

@@ -47,6 +47,7 @@
"defu": "^6.1.2",
"fuse.js": "^6.6.2",
"lodash-es": "^4.17.21",
"tailwind-merge": "^1.14.0",
"tailwindcss": "^3.3.3"
},
"devDependencies": {
@@ -54,13 +55,13 @@
"@nuxt/module-builder": "^0.4.0",
"@release-it/conventional-changelog": "^7.0.0",
"eslint": "^8.47.0",
"joi": "^17.9.2",
"nuxt": "^3.6.5",
"release-it": "^16.1.4",
"typescript": "^5.1.6",
"unbuild": "^1.2.1",
"vue-tsc": "^1.8.8",
"yup": "^1.2.0",
"joi": "^17.9.2",
"zod": "^3.21.4"
}
}

35
pnpm-lock.yaml generated
View File

@@ -1,9 +1,5 @@
lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
@@ -59,6 +55,9 @@ importers:
lodash-es:
specifier: ^4.17.21
version: 4.17.21
tailwind-merge:
specifier: ^1.14.0
version: 1.14.0
tailwindcss:
specifier: ^3.3.3
version: 3.3.3
@@ -111,7 +110,7 @@ importers:
version: 1.1.12
'@iconify-json/simple-icons':
specifier: latest
version: 1.1.65
version: 1.1.63
'@nuxt/content':
specifier: ^2.7.2
version: 2.7.2(rollup@3.26.2)
@@ -1179,7 +1178,7 @@ packages:
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
dependencies:
eslint: 8.47.0
eslint-visitor-keys: 3.4.3
eslint-visitor-keys: 3.4.1
dev: true
/@eslint-community/regexpp@4.5.1:
@@ -1267,8 +1266,8 @@ packages:
dependencies:
'@iconify/types': 2.0.0
/@iconify-json/simple-icons@1.1.65:
resolution: {integrity: sha512-pCL5nF80e5YrvnR9VFt7EROEcWE6vvtBaThx+hC1Xaw3e1DkCTc4JioiNnYrON/p2iNVvncAQAgQP1Us/XPInw==}
/@iconify-json/simple-icons@1.1.63:
resolution: {integrity: sha512-aIbo99YLjwZ53XxU9HC+CO9Br9H0vRuGmq8b0TOaGA1g7HX2ZlpmzPvWAte5xbmbohfRzAMX/OVsFhOT+CL+Aw==}
dependencies:
'@iconify/types': 2.0.0
dev: true
@@ -1566,7 +1565,7 @@ packages:
birpc: 0.2.12
boxen: 7.1.1
consola: 3.2.3
error-stack-parser-es: 0.1.0
error-stack-parser-es: 0.1.1
execa: 7.2.0
fast-folder-size: 2.1.0
fast-glob: 3.3.1
@@ -1591,7 +1590,7 @@ packages:
sirv: 2.0.3
unimport: 3.1.3(rollup@3.26.2)
vite: 4.4.7(@types/node@20.4.5)
vite-plugin-inspect: 0.7.35(@nuxt/kit@3.6.5)(rollup@3.26.2)(vite@4.4.7)
vite-plugin-inspect: 0.7.37(@nuxt/kit@3.6.5)(rollup@3.26.2)(vite@4.4.7)
vite-plugin-vue-inspector: 3.6.0(vite@4.4.7)
wait-on: 7.0.1
which: 3.0.1
@@ -1787,7 +1786,7 @@ packages:
consola: 3.2.3
cssnano: 6.0.1(postcss@8.4.26)
defu: 6.1.2
esbuild: 0.18.17
esbuild: 0.18.13
escape-string-regexp: 5.0.0
estree-walker: 3.0.3
externality: 1.0.2
@@ -5030,8 +5029,8 @@ packages:
dependencies:
is-arrayish: 0.2.1
/error-stack-parser-es@0.1.0:
resolution: {integrity: sha512-K5/Oncl6ZizGM7tqGUc3Sd82zVKGsZ+l8FqhhnF8+10QujC/xT2VKwdaM/8rAR5F1BouVqgemMrhHG23vhOpMw==}
/error-stack-parser-es@0.1.1:
resolution: {integrity: sha512-g/9rfnvnagiNf+DRMHEVGuGuIBlCIMDFoTA616HaP2l9PlCjGjVhD98PNbVSJvmK4TttqT5mV5tInMhoFgi+aA==}
dev: true
/es-abstract@1.21.2:
@@ -6464,7 +6463,6 @@ packages:
/iconv-lite@0.6.3:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
requiresBuild: true
dependencies:
safer-buffer: 2.1.2
dev: true
@@ -8270,7 +8268,7 @@ packages:
defu: 6.1.2
destr: 2.0.0
dot-prop: 7.2.0
esbuild: 0.18.17
esbuild: 0.18.13
escape-string-regexp: 5.0.0
etag: 1.8.1
fs-extra: 11.1.1
@@ -10934,7 +10932,6 @@ packages:
/tailwind-merge@1.14.0:
resolution: {integrity: sha512-3mFKyCo/MBcgyOTlrY8T7odzZFx+w+qKSMAmdFzRvqBfLlSigU6TZnlFHK0lkMwj9Bj8OYU+9yW9lmGuS0QEnQ==}
dev: true
/tailwindcss@3.3.3:
resolution: {integrity: sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==}
@@ -11903,8 +11900,8 @@ packages:
vue-tsc: 1.8.8(typescript@5.1.6)
dev: true
/vite-plugin-inspect@0.7.35(@nuxt/kit@3.6.5)(rollup@3.26.2)(vite@4.4.7):
resolution: {integrity: sha512-e5w5dJAj3vDcHTxn8hHbiH+mVqYs17gaW00f3aGuMTXiqUog+T1Lsxr9Jb4WRiip84cpuhR0KFFBT1egtXboiA==}
/vite-plugin-inspect@0.7.37(@nuxt/kit@3.6.5)(rollup@3.26.2)(vite@4.4.7):
resolution: {integrity: sha512-cRHzaE8g8/UUK0hA5DunAXiN3eJnq7Dpcu2bVf5dCRj/MYBKGeAv0Z27vYMhm2F/oeE5aG3+oYF4tkdhOlpXxg==}
engines: {node: '>=14'}
peerDependencies:
'@nuxt/kit': '*'
@@ -12008,7 +12005,7 @@ packages:
optional: true
dependencies:
'@types/node': 20.4.5
esbuild: 0.18.17
esbuild: 0.18.13
postcss: 8.4.26
rollup: 3.26.3
optionalDependencies:

View File

@@ -516,7 +516,6 @@ const select = {
}
const selectMenu = {
wrapper: 'relative',
container: 'z-20',
width: 'w-full',
height: 'max-h-60',

View File

@@ -1,5 +1,5 @@
<template>
<div :class="ui.wrapper">
<div :class="wrapperClass" v-bind="attrs">
<table :class="[ui.base, ui.divide]">
<thead :class="ui.thead">
<tr :class="ui.tr.base">
@@ -69,8 +69,10 @@
<script lang="ts">
import { ref, computed, defineComponent, toRaw } from 'vue'
import type { PropType } from 'vue'
import { capitalize, orderBy, omit, get } from 'lodash-es'
import { omit, capitalize, orderBy, get } from 'lodash-es'
import { defu } from 'defu'
import { twMerge } from 'tailwind-merge'
import { defuTwMerge } from '../../utils'
import type { Button } from '../../types/button'
import { useAppConfig } from '#imports'
// TODO: Remove
@@ -84,6 +86,7 @@ function defaultComparator<T> (a: T, z: T): boolean {
}
export default defineComponent({
inheritAttrs: false,
props: {
modelValue: {
type: Array,
@@ -135,7 +138,7 @@ export default defineComponent({
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.table>>,
default: () => appConfig.ui.table
default: () => ({})
}
},
emits: ['update:modelValue'],
@@ -143,7 +146,9 @@ export default defineComponent({
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.table>>(() => defu({}, props.ui, appConfig.ui.table))
const ui = computed<Partial<typeof appConfig.ui.table>>(() => defuTwMerge({}, props.ui, appConfig.ui.table))
const wrapperClass = computed(() => twMerge(ui.value.wrapper, attrs.class as string))
const columns = computed(() => props.columns ?? Object.keys(omit(props.rows[0] ?? {}, ['click'])).map((key) => ({ key, label: capitalize(key), sortable: false })))
@@ -235,8 +240,10 @@ export default defineComponent({
}
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
wrapperClass,
// eslint-disable-next-line vue/no-dupe-keys
sort,
// eslint-disable-next-line vue/no-dupe-keys

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
}
}
})

View File

@@ -1,5 +1,5 @@
<template>
<div :class="ui.wrapper">
<div :class="wrapperClass">
<div class="flex items-center h-5">
<input
:id="name"
@@ -13,7 +13,7 @@
type="checkbox"
class="form-checkbox"
:class="inputClass"
v-bind="$attrs"
v-bind="attrs"
@change="onChange"
>
</div>
@@ -32,8 +32,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 { useFormGroup } from '../../composables/useFormGroup'
import { useAppConfig } from '#imports'
// TODO: Remove
@@ -88,17 +89,21 @@ export default defineComponent({
return appConfig.ui.colors.includes(value)
}
},
inputClass: {
type: String,
default: ''
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.checkbox>>,
default: () => appConfig.ui.checkbox
default: () => ({})
}
},
emits: ['update:modelValue', 'change'],
setup (props, { emit }) {
setup (props, { emit, attrs }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.checkbox>>(() => defu({}, props.ui, appConfig.ui.checkbox))
const ui = computed<Partial<typeof appConfig.ui.checkbox>>(() => defuTwMerge({}, props.ui, appConfig.ui.checkbox))
const { emitFormChange, formGroup } = useFormGroup()
const color = computed(() => formGroup?.error?.value ? 'red' : props.color)
@@ -117,21 +122,26 @@ export default defineComponent({
emitFormChange()
}
const wrapperClass = computed(() => twMerge(ui.value.wrapper, attrs.class as string))
const inputClass = computed(() => {
return classNames(
return twMerge(twJoin(
ui.value.base,
ui.value.rounded,
ui.value.background,
ui.value.border,
ui.value.ring.replaceAll('{color}', color.value),
ui.value.color.replaceAll('{color}', color.value)
)
), props.inputClass)
})
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
toggle,
wrapperClass,
// eslint-disable-next-line vue/no-dupe-keys
inputClass,
onChange
}

View File

@@ -1,5 +1,5 @@
<template>
<div :class="ui.wrapper">
<div :class="wrapperClass" v-bind="attrs">
<label>
<div v-if="label" :class="[ui.label.wrapper, size]">
<p :class="[ui.label.base, required ? ui.label.required : '']">{{ label }}</p>
@@ -11,7 +11,7 @@
<div :class="[label ? ui.container : '']">
<slot v-bind="{ error }" />
<p v-if="error" :class="[ui.error, size]">{{ error }}</p>
<p v-if="error && typeof error !== 'boolean'" :class="[ui.error, size]">{{ error }}</p>
<p v-else-if="help" :class="[ui.help, size]">{{ help }}</p>
</div>
</label>
@@ -21,9 +21,11 @@
<script lang="ts">
import { computed, defineComponent, provide, inject } from 'vue'
import type { PropType } from 'vue'
import { defu } from 'defu'
import { omit } from 'lodash-es'
import { twMerge } from 'tailwind-merge'
import type { FormError } from '../../types'
import { defuTwMerge } from '../../utils'
import { useAppConfig } from '#imports'
// TODO: Remove
// @ts-expect-error
import appConfig from '#build/app.config'
@@ -31,6 +33,7 @@ import appConfig from '#build/app.config'
// const appConfig = useAppConfig()
export default defineComponent({
inheritAttrs: false,
props: {
name: {
type: String,
@@ -69,14 +72,16 @@ export default defineComponent({
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.formGroup>>,
default: () => appConfig.ui.formGroup
default: () => ({})
}
},
setup (props) {
setup (props, { attrs }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.formGroup>>(() => defu({}, props.ui, appConfig.ui.formGroup))
const ui = computed<Partial<typeof appConfig.ui.formGroup>>(() => defuTwMerge({}, props.ui, appConfig.ui.formGroup))
const wrapperClass = computed(() => twMerge(ui.value.wrapper, attrs.class as string))
const formErrors = inject<Ref<FormError[]> | null>('form-errors', null)
@@ -95,8 +100,10 @@ export default defineComponent({
})
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
wrapperClass,
// eslint-disable-next-line vue/no-dupe-keys
size,
// eslint-disable-next-line vue/no-dupe-keys

View File

@@ -1,5 +1,5 @@
<template>
<div :class="ui.wrapper">
<div :class="wrapperClass">
<input
ref="input"
:name="name"
@@ -10,7 +10,7 @@
:disabled="disabled || loading"
class="form-input"
:class="inputClass"
v-bind="$attrs"
v-bind="attrs"
@input="onInput"
@blur="onBlur"
>
@@ -33,10 +33,11 @@
<script lang="ts">
import { ref, computed, onMounted, 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 { defuTwMerge } from '../../utils'
import { useFormGroup } from '../../composables/useFormGroup'
import { classNames } from '../../utils'
import { useAppConfig } from '#imports'
// TODO: Remove
// @ts-expect-error
@@ -134,17 +135,21 @@ export default defineComponent({
].includes(value)
}
},
inputClass: {
type: String,
default: null
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.input>>,
default: () => appConfig.ui.input
default: () => ({})
}
},
emits: ['update:modelValue', 'blur'],
setup (props, { emit, slots }) {
setup (props, { emit, attrs, slots }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.input>>(() => defu({}, props.ui, appConfig.ui.input))
const ui = computed<Partial<typeof appConfig.ui.input>>(() => defuTwMerge({}, props.ui, appConfig.ui.input))
const { emitFormBlur, emitFormInput, formGroup } = useFormGroup()
const color = computed(() => formGroup?.error?.value ? 'red' : props.color)
@@ -174,10 +179,12 @@ export default defineComponent({
}, 100)
})
const wrapperClass = computed(() => twMerge(ui.value.wrapper, attrs.class as string))
const inputClass = computed(() => {
const variant = ui.value.color?.[color.value as string]?.[props.variant as string] || ui.value.variant[props.variant]
return classNames(
return twMerge(twJoin(
ui.value.base,
ui.value.rounded,
ui.value.placeholder,
@@ -186,7 +193,7 @@ export default defineComponent({
variant?.replaceAll('{color}', color.value),
(isLeading.value || slots.leading) && ui.value.leading.padding[size.value],
(isTrailing.value || slots.trailing) && ui.value.trailing.padding[size.value]
)
), props.inputClass)
})
const isLeading = computed(() => {
@@ -214,7 +221,7 @@ export default defineComponent({
})
const leadingWrapperIconClass = computed(() => {
return classNames(
return twJoin(
ui.value.icon.leading.wrapper,
ui.value.icon.leading.pointer,
ui.value.icon.leading.padding[size.value]
@@ -222,7 +229,7 @@ export default defineComponent({
})
const leadingIconClass = computed(() => {
return classNames(
return twJoin(
ui.value.icon.base,
appConfig.ui.colors.includes(color.value) && ui.value.icon.color.replaceAll('{color}', color.value),
ui.value.icon.size[size.value],
@@ -231,7 +238,7 @@ export default defineComponent({
})
const trailingWrapperIconClass = computed(() => {
return classNames(
return twJoin(
ui.value.icon.trailing.wrapper,
ui.value.icon.trailing.pointer,
ui.value.icon.trailing.padding[size.value]
@@ -239,7 +246,7 @@ export default defineComponent({
})
const trailingIconClass = computed(() => {
return classNames(
return twJoin(
ui.value.icon.base,
appConfig.ui.colors.includes(color.value) && ui.value.icon.color.replaceAll('{color}', color.value),
ui.value.icon.size[size.value],
@@ -248,11 +255,14 @@ export default defineComponent({
})
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
input,
isLeading,
isTrailing,
wrapperClass,
// eslint-disable-next-line vue/no-dupe-keys
inputClass,
leadingIconName,
leadingIconClass,

View File

@@ -1,5 +1,5 @@
<template>
<div :class="ui.wrapper">
<div :class="wrapperClass">
<div class="flex items-center h-5">
<input
v-model="pick"
@@ -10,7 +10,7 @@
type="radio"
class="form-radio"
:class="inputClass"
v-bind="$attrs"
v-bind="attrs"
>
</div>
<div v-if="label || $slots.label" class="ms-3 text-sm">
@@ -28,8 +28,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 { useFormGroup } from '../../composables/useFormGroup'
import { useAppConfig } from '#imports'
// TODO: Remove
@@ -76,17 +77,21 @@ export default defineComponent({
return appConfig.ui.colors.includes(value)
}
},
inputClass: {
type: String,
default: null
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.radio>>,
default: () => appConfig.ui.radio
default: () => ({})
}
},
emits: ['update:modelValue'],
setup (props, { emit }) {
setup (props, { emit, attrs }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.radio>>(() => defu({}, props.ui, appConfig.ui.radio))
const ui = computed<Partial<typeof appConfig.ui.radio>>(() => defuTwMerge({}, props.ui, appConfig.ui.radio))
const { emitFormChange, formGroup } = useFormGroup()
const color = computed(() => formGroup?.error?.value ? 'red' : props.color)
@@ -103,20 +108,25 @@ export default defineComponent({
}
})
const wrapperClass = computed(() => twMerge(ui.value.wrapper, attrs.class as string))
const inputClass = computed(() => {
return classNames(
return twMerge(twJoin(
ui.value.base,
ui.value.background,
ui.value.border,
ui.value.ring.replaceAll('{color}', color.value),
ui.value.color.replaceAll('{color}', color.value)
)
), props.inputClass)
})
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
pick,
wrapperClass,
// eslint-disable-next-line vue/no-dupe-keys
inputClass
}
}

View File

@@ -10,7 +10,7 @@
:step="step"
type="range"
:class="[inputClass, thumbClass, trackClass]"
v-bind="$attrs"
v-bind="attrs"
@change="onChange"
>
@@ -21,8 +21,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 { useFormGroup } from '../../composables/useFormGroup'
import { useAppConfig } from '#imports'
// TODO: Remove
@@ -70,17 +71,21 @@ export default defineComponent({
return appConfig.ui.colors.includes(value)
}
},
inputClass: {
type: String,
default: null
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.range>>,
default: () => appConfig.ui.range
default: () => ({})
}
},
emits: ['update:modelValue', 'change'],
setup (props, { emit }) {
setup (props, { emit, attrs }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.range>>(() => defu({}, props.ui, appConfig.ui.range))
const ui = computed<Partial<typeof appConfig.ui.range>>(() => defuTwMerge({}, props.ui, appConfig.ui.range))
const { emitFormChange, formGroup } = useFormGroup()
const color = computed(() => formGroup?.error?.value ? 'red' : props.color)
@@ -101,24 +106,24 @@ export default defineComponent({
}
const wrapperClass = computed(() => {
return classNames(
return twMerge(twJoin(
ui.value.wrapper,
ui.value.size[size.value]
)
), attrs.class as string)
})
const inputClass = computed(() => {
return classNames(
return twMerge(twJoin(
ui.value.base,
ui.value.background,
ui.value.rounded,
ui.value.ring.replaceAll('{color}', color.value),
ui.value.size[size.value]
)
), props.inputClass)
})
const thumbClass = computed(() => {
return classNames(
return twJoin(
ui.value.thumb.base,
// Intermediate class to allow thumb ring or background color (set to `current`) as it's impossible to safelist with arbitrary values
ui.value.thumb.color.replaceAll('{color}', color.value),
@@ -129,7 +134,7 @@ export default defineComponent({
})
const trackClass = computed(() => {
return classNames(
return twJoin(
ui.value.track.base,
ui.value.track.background,
ui.value.track.rounded,
@@ -138,7 +143,7 @@ export default defineComponent({
})
const progressClass = computed(() => {
return classNames(
return twJoin(
ui.value.progress.base,
ui.value.progress.rounded,
ui.value.progress.background.replaceAll('{color}', color.value),
@@ -156,10 +161,12 @@ export default defineComponent({
})
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
value,
wrapperClass,
// eslint-disable-next-line vue/no-dupe-keys
inputClass,
thumbClass,
trackClass,

View File

@@ -1,5 +1,5 @@
<template>
<div :class="ui.wrapper">
<div :class="wrapperClass">
<select
:name="name"
:value="modelValue"
@@ -7,7 +7,7 @@
:disabled="disabled || loading"
class="form-select"
:class="selectClass"
v-bind="$attrs"
v-bind="attrs"
@input="onInput"
@change="onChange"
>
@@ -55,10 +55,10 @@
<script lang="ts">
import { computed, defineComponent } from 'vue'
import type { PropType, ComputedRef } from 'vue'
import { get } from 'lodash-es'
import { defu } from 'defu'
import { get, omit } from 'lodash-es'
import { twMerge, twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue'
import { classNames } from '../../utils'
import { defuTwMerge } from '../../utils'
import { useFormGroup } from '../../composables/useFormGroup'
import { useAppConfig } from '#imports'
// TODO: Remove
@@ -161,17 +161,21 @@ export default defineComponent({
type: String,
default: 'value'
},
selectClass: {
type: String,
default: null
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.select>>,
default: () => appConfig.ui.select
default: () => ({})
}
},
emits: ['update:modelValue', 'change'],
setup (props, { emit, slots }) {
setup (props, { emit, attrs, slots }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.select>>(() => defu({}, props.ui, appConfig.ui.select))
const ui = computed<Partial<typeof appConfig.ui.select>>(() => defuTwMerge({}, props.ui, appConfig.ui.select))
const { emitFormChange, formGroup } = useFormGroup()
const color = computed(() => formGroup?.error?.value ? 'red' : props.color)
@@ -239,10 +243,12 @@ export default defineComponent({
return foundOption[props.valueAttribute]
})
const wrapperClass = computed(() => twMerge(ui.value.wrapper, attrs.class as string))
const selectClass = computed(() => {
const variant = ui.value.color?.[color.value as string]?.[props.variant as string] || ui.value.variant[props.variant]
return classNames(
return twMerge(twJoin(
ui.value.base,
ui.value.rounded,
ui.value.size[size.value],
@@ -250,7 +256,7 @@ export default defineComponent({
variant?.replaceAll('{color}', color.value),
(isLeading.value || slots.leading) && ui.value.leading.padding[size.value],
(isTrailing.value || slots.trailing) && ui.value.trailing.padding[size.value]
)
), props.selectClass)
})
const isLeading = computed(() => {
@@ -278,7 +284,7 @@ export default defineComponent({
})
const leadingWrapperIconClass = computed(() => {
return classNames(
return twJoin(
ui.value.icon.leading.wrapper,
ui.value.icon.leading.pointer,
ui.value.icon.leading.padding[size.value]
@@ -286,7 +292,7 @@ export default defineComponent({
})
const leadingIconClass = computed(() => {
return classNames(
return twJoin(
ui.value.icon.base,
appConfig.ui.colors.includes(color.value) && ui.value.icon.color.replaceAll('{color}', color.value),
ui.value.icon.size[size.value],
@@ -295,7 +301,7 @@ export default defineComponent({
})
const trailingWrapperIconClass = computed(() => {
return classNames(
return twJoin(
ui.value.icon.trailing.wrapper,
ui.value.icon.trailing.pointer,
ui.value.icon.trailing.padding[size.value]
@@ -303,7 +309,7 @@ export default defineComponent({
})
const trailingIconClass = computed(() => {
return classNames(
return twJoin(
ui.value.icon.base,
appConfig.ui.colors.includes(color.value) && ui.value.icon.color.replaceAll('{color}', color.value),
ui.value.icon.size[size.value],
@@ -312,12 +318,15 @@ export default defineComponent({
})
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
normalizedOptionsWithPlaceholder,
normalizedValue,
isLeading,
isTrailing,
wrapperClass,
// eslint-disable-next-line vue/no-dupe-keys
selectClass,
leadingIconName,
leadingIconClass,

View File

@@ -8,7 +8,7 @@
:multiple="multiple"
:disabled="disabled || loading"
as="div"
:class="uiMenu.wrapper"
:class="wrapperClass"
@update:model-value="onUpdate"
>
<input
@@ -28,7 +28,7 @@
class="inline-flex w-full"
>
<slot :open="open" :disabled="disabled" :loading="loading">
<button :class="selectClass" :disabled="disabled || loading" type="button" v-bind="$attrs">
<button :class="selectClass" :disabled="disabled || loading" type="button" v-bind="attrs">
<span v-if="(isLeading && leadingIconName) || $slots.leading" :class="leadingWrapperIconClass">
<slot name="leading" :disabled="disabled" :loading="loading">
<UIcon :name="leadingIconName" :class="leadingIconClass" />
@@ -131,9 +131,11 @@ import {
} from '@headlessui/vue'
import { computedAsync, useDebounceFn } from '@vueuse/core'
import { defu } from 'defu'
import { omit } from 'lodash-es'
import { twMerge, twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue'
import UAvatar from '../elements/Avatar.vue'
import { classNames } from '../../utils'
import { defuTwMerge } from '../../utils'
import { usePopper } from '../../composables/usePopper'
import { useFormGroup } from '../../composables/useFormGroup'
import type { PopperOptions } from '../../types'
@@ -284,22 +286,26 @@ export default defineComponent({
type: Object as PropType<PopperOptions>,
default: () => ({})
},
selectClass: {
type: String,
default: null
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.select>>,
default: () => appConfig.ui.select
default: () => ({})
},
uiMenu: {
type: Object as PropType<Partial<typeof appConfig.ui.selectMenu>>,
default: () => appConfig.ui.selectMenu
default: () => ({})
}
},
emits: ['update:modelValue', 'open', 'close', 'change'],
setup (props, { emit, slots }) {
setup (props, { emit, attrs, slots }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.select>>(() => defu({}, props.ui, appConfig.ui.select))
const uiMenu = computed<Partial<typeof appConfig.ui.selectMenu>>(() => defu({}, props.uiMenu, appConfig.ui.selectMenu))
const ui = computed<Partial<typeof appConfig.ui.select>>(() => defuTwMerge({}, props.ui, appConfig.ui.select))
const uiMenu = computed<Partial<typeof appConfig.ui.selectMenu>>(() => defuTwMerge({}, props.uiMenu, appConfig.ui.selectMenu))
const popper = computed<PopperOptions>(() => defu({}, props.popper, uiMenu.value.popper as PopperOptions))
@@ -311,10 +317,12 @@ export default defineComponent({
const query = ref('')
const searchInput = ref<ComponentPublicInstance<HTMLElement>>()
const wrapperClass = computed(() => twMerge(ui.value.wrapper, attrs.class as string))
const selectClass = computed(() => {
const variant = ui.value.color?.[color.value as string]?.[props.variant as string] || ui.value.variant[props.variant]
return classNames(
return twMerge(twJoin(
ui.value.base,
ui.value.rounded,
'text-left cursor-default',
@@ -325,7 +333,7 @@ export default defineComponent({
(isLeading.value || slots.leading) && ui.value.leading.padding[size.value],
(isTrailing.value || slots.trailing) && ui.value.trailing.padding[size.value],
'inline-flex items-center'
)
), props.selectClass)
})
const isLeading = computed(() => {
@@ -353,7 +361,7 @@ export default defineComponent({
})
const leadingWrapperIconClass = computed(() => {
return classNames(
return twJoin(
ui.value.icon.leading.wrapper,
ui.value.icon.leading.pointer,
ui.value.icon.leading.padding[size.value]
@@ -361,7 +369,7 @@ export default defineComponent({
})
const leadingIconClass = computed(() => {
return classNames(
return twJoin(
ui.value.icon.base,
appConfig.ui.colors.includes(color.value) && ui.value.icon.color.replaceAll('{color}', color.value),
ui.value.icon.size[size.value],
@@ -370,7 +378,7 @@ export default defineComponent({
})
const trailingWrapperIconClass = computed(() => {
return classNames(
return twJoin(
ui.value.icon.trailing.wrapper,
ui.value.icon.trailing.pointer,
ui.value.icon.trailing.padding[size.value]
@@ -378,7 +386,7 @@ export default defineComponent({
})
const trailingIconClass = computed(() => {
return classNames(
return twJoin(
ui.value.icon.base,
appConfig.ui.colors.includes(color.value) && ui.value.icon.color.replaceAll('{color}', color.value),
ui.value.icon.size[size.value],
@@ -429,12 +437,15 @@ export default defineComponent({
}
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
uiMenu,
trigger,
container,
isLeading,
isTrailing,
wrapperClass,
// eslint-disable-next-line vue/no-dupe-keys
selectClass,
leadingIconName,
leadingIconClass,

View File

@@ -1,5 +1,5 @@
<template>
<div :class="ui.wrapper">
<div :class="wrapperClass">
<textarea
ref="textarea"
:value="modelValue"
@@ -10,7 +10,7 @@
:placeholder="placeholder"
class="form-textarea"
:class="textareaClass"
v-bind="$attrs"
v-bind="attrs"
@input="onInput"
@blur="onBlur"
/>
@@ -20,8 +20,9 @@
<script lang="ts">
import { ref, computed, watch, onMounted, nextTick, 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 { useFormGroup } from '../../composables/useFormGroup'
import { useAppConfig } from '#imports'
// TODO: Remove
@@ -97,19 +98,23 @@ export default defineComponent({
].includes(value)
}
},
textareaClass: {
type: String,
default: null
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.textarea>>,
default: () => appConfig.ui.textarea
default: () => ({})
}
},
emits: ['update:modelValue', 'blur'],
setup (props, { emit }) {
setup (props, { emit, attrs }) {
const textarea = ref<HTMLTextAreaElement | null>(null)
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.textarea>>(() => defu({}, props.ui, appConfig.ui.textarea))
const ui = computed<Partial<typeof appConfig.ui.textarea>>(() => defuTwMerge({}, props.ui, appConfig.ui.textarea))
const { emitFormBlur, emitFormInput, formGroup } = useFormGroup()
const color = computed(() => formGroup?.error?.value ? 'red' : props.color)
@@ -172,10 +177,12 @@ export default defineComponent({
}, 100)
})
const wrapperClass = computed(() => twMerge(ui.value.wrapper, attrs.class as string))
const textareaClass = computed(() => {
const variant = ui.value.color?.[color.value as string]?.[props.variant as string] || ui.value.variant[props.variant]
return classNames(
return twMerge(twJoin(
ui.value.base,
ui.value.rounded,
ui.value.placeholder,
@@ -183,13 +190,16 @@ export default defineComponent({
props.padded ? ui.value.padding[size.value] : 'p-0',
variant?.replaceAll('{color}', color.value),
!props.resize && 'resize-none'
)
), props.textareaClass)
})
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
textarea,
wrapperClass,
// eslint-disable-next-line vue/no-dupe-keys
textareaClass,
onInput,
onBlur

View File

@@ -4,6 +4,7 @@
:name="name"
:disabled="disabled"
:class="switchClass"
v-bind="attrs"
>
<span :class="[active ? ui.container.active : ui.container.inactive, ui.container.base]">
<span v-if="onIcon" :class="[active ? ui.icon.active : ui.icon.inactive, ui.icon.base]" aria-hidden="true">
@@ -19,10 +20,11 @@
<script lang="ts">
import { computed, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { defu } from 'defu'
import { Switch as HSwitch } from '@headlessui/vue'
import { omit } from 'lodash-es'
import { twMerge, twJoin } from 'tailwind-merge'
import UIcon from '../elements/Icon.vue'
import { classNames } from '../../utils'
import { defuTwMerge } from '../../utils'
import { useFormGroup } from '../../composables/useFormGroup'
import { useAppConfig } from '#imports'
// TODO: Remove
@@ -36,6 +38,7 @@ export default defineComponent({
HSwitch,
UIcon
},
inheritAttrs: false,
props: {
name: {
type: String,
@@ -66,15 +69,15 @@ export default defineComponent({
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.toggle>>,
default: () => appConfig.ui.toggle
default: () => ({})
}
},
emits: ['update:modelValue'],
setup (props, { emit }) {
setup (props, { emit, attrs }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.toggle>>(() => defu({}, props.ui, appConfig.ui.toggle))
const ui = computed<Partial<typeof appConfig.ui.toggle>>(() => defuTwMerge({}, props.ui, appConfig.ui.toggle))
const { emitFormChange, formGroup } = useFormGroup()
const color = computed(() => formGroup?.error?.value ? 'red' : props.color)
@@ -90,27 +93,28 @@ export default defineComponent({
})
const switchClass = computed(() => {
return classNames(
return twMerge(twJoin(
ui.value.base,
ui.value.rounded,
ui.value.ring.replaceAll('{color}', color.value),
(active.value ? ui.value.active : ui.value.inactive).replaceAll('{color}', color.value)
)
), attrs.class as string)
})
const onIconClass = computed(() => {
return classNames(
return twJoin(
ui.value.icon.on.replaceAll('{color}', color.value)
)
})
const offIconClass = computed(() => {
return classNames(
return twJoin(
ui.value.icon.off.replaceAll('{color}', color.value)
)
})
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
active,

View File

@@ -1,8 +1,8 @@
<template>
<component
:is="$attrs.onSubmit ? 'form' : as"
:class="[ui.base, ui.rounded, ui.divide, ui.ring, ui.shadow, ui.background]"
v-bind="$attrs"
:class="cardClass"
v-bind="attrs"
>
<div v-if="$slots.header" :class="[ui.header.base, ui.header.padding, ui.header.background]">
<slot name="header" />
@@ -19,7 +19,9 @@
<script lang="ts">
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 { defuTwMerge } from '../../utils'
import { useAppConfig } from '#imports'
// TODO: Remove
// @ts-expect-error
@@ -36,18 +38,31 @@ export default defineComponent({
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.card>>,
default: () => appConfig.ui.card
default: () => ({})
}
},
setup (props) {
setup (props, { attrs }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.card>>(() => defu({}, props.ui, appConfig.ui.card))
const ui = computed<Partial<typeof appConfig.ui.card>>(() => defuTwMerge({}, props.ui, appConfig.ui.card))
const cardClass = computed(() => {
return twMerge(twJoin(
ui.value.base,
ui.value.rounded,
ui.value.divide,
ui.value.ring,
ui.value.shadow,
ui.value.background
), attrs.class as string)
})
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui
ui,
cardClass
}
}
})

View File

@@ -1,5 +1,5 @@
<template>
<component :is="as" :class="[ui.base, ui.padding, ui.constrained]">
<component :is="as" :class="containerClass" v-bind="attrs">
<slot />
</component>
</template>
@@ -7,7 +7,9 @@
<script lang="ts">
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 { 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: {
as: {
type: String,
@@ -23,18 +26,28 @@ export default defineComponent({
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.container>>,
default: () => appConfig.ui.container
default: () => ({})
}
},
setup (props) {
setup (props, { attrs }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.container>>(() => defu({}, props.ui, appConfig.ui.container))
const ui = computed<Partial<typeof appConfig.ui.container>>(() => defuTwMerge({}, props.ui, appConfig.ui.container))
const containerClass = computed(() => {
return twMerge(twJoin(
ui.value.base,
ui.value.padding,
ui.value.constrained
), attrs.class as string)
})
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui
ui,
containerClass
}
}
})

View File

@@ -1,11 +1,13 @@
<template>
<div :class="[ui.base, ui.background, ui.rounded]" />
<div :class="skeletonClass" v-bind="attrs" />
</template>
<script lang="ts">
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 { defuTwMerge } from '../../utils'
import { useAppConfig } from '#imports'
// TODO: Remove
// @ts-expect-error
@@ -14,21 +16,32 @@ import appConfig from '#build/app.config'
// const appConfig = useAppConfig()
export default defineComponent({
inheritAttrs: false,
props: {
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.skeleton>>,
default: () => appConfig.ui.skeleton
default: () => ({})
}
},
setup (props) {
setup (props, { attrs }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.skeleton>>(() => defu({}, props.ui, appConfig.ui.skeleton))
const ui = computed<Partial<typeof appConfig.ui.skeleton>>(() => defuTwMerge({}, props.ui, appConfig.ui.skeleton))
const skeletonClass = computed(() => {
return twMerge(twJoin(
ui.value.base,
ui.value.background,
ui.value.rounded
), attrs.class as string)
})
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui
ui,
skeletonClass
}
}
})

View File

@@ -4,64 +4,65 @@
:model-value="modelValue"
:multiple="multiple"
:nullable="nullable"
:class="wrapperClass"
v-bind="attrs"
as="div"
@update:model-value="onSelect"
>
<div :class="ui.wrapper">
<div v-show="searchable" :class="ui.input.wrapper">
<UIcon v-if="iconName" :name="iconName" :class="iconClass" aria-hidden="true" />
<HComboboxInput
ref="comboboxInput"
:value="query"
:class="[ui.input.base, ui.input.size, ui.input.height, ui.input.padding, icon && ui.input.icon.padding]"
:placeholder="placeholder"
autocomplete="off"
@change="query = $event.target.value"
/>
<div v-show="searchable" :class="ui.input.wrapper">
<UIcon v-if="iconName" :name="iconName" :class="iconClass" aria-hidden="true" />
<HComboboxInput
ref="comboboxInput"
:value="query"
:class="[ui.input.base, ui.input.size, ui.input.height, ui.input.padding, icon && ui.input.icon.padding]"
:placeholder="placeholder"
autocomplete="off"
@change="query = $event.target.value"
/>
<UButton
v-if="closeButton"
v-bind="{ ...ui.default.closeButton, ...closeButton }"
:class="ui.input.closeButton"
aria-label="Close"
@click="onClear"
/>
</div>
<HComboboxOptions
v-if="groups.length"
static
hold
as="div"
aria-label="Commands"
:class="ui.container"
>
<CommandPaletteGroup
v-for="group of groups"
:key="group.key"
:query="query"
:group="group"
:group-attribute="groupAttribute"
:command-attribute="commandAttribute"
:selected-icon="selectedIcon"
:ui="ui"
>
<template v-for="(_, name) in $slots" #[name]="slotData">
<slot :name="name" v-bind="slotData" />
</template>
</CommandPaletteGroup>
</HComboboxOptions>
<template v-else-if="emptyState">
<slot name="empty-state">
<div :class="ui.emptyState.wrapper">
<UIcon v-if="emptyState.icon" :name="emptyState.icon" :class="ui.emptyState.icon" aria-hidden="true" />
<p :class="query ? ui.emptyState.queryLabel : ui.emptyState.label">
{{ query ? emptyState.queryLabel : emptyState.label }}
</p>
</div>
</slot>
</template>
<UButton
v-if="closeButton"
v-bind="{ ...ui.default.closeButton, ...closeButton }"
:class="ui.input.closeButton"
aria-label="Close"
@click="onClear"
/>
</div>
<HComboboxOptions
v-if="groups.length"
static
hold
as="div"
aria-label="Commands"
:class="ui.container"
>
<CommandPaletteGroup
v-for="group of groups"
:key="group.key"
:query="query"
:group="group"
:group-attribute="groupAttribute"
:command-attribute="commandAttribute"
:selected-icon="selectedIcon"
:ui="ui"
>
<template v-for="(_, name) in $slots" #[name]="slotData">
<slot :name="name" v-bind="slotData" />
</template>
</CommandPaletteGroup>
</HComboboxOptions>
<template v-else-if="emptyState">
<slot name="empty-state">
<div :class="ui.emptyState.wrapper">
<UIcon v-if="emptyState.icon" :name="emptyState.icon" :class="ui.emptyState.icon" aria-hidden="true" />
<p :class="query ? ui.emptyState.queryLabel : ui.emptyState.label">
{{ query ? emptyState.queryLabel : emptyState.label }}
</p>
</div>
</slot>
</template>
</HCombobox>
</template>
@@ -71,7 +72,8 @@ import { Combobox as HCombobox, ComboboxInput as HComboboxInput, ComboboxOptions
import type { ComputedRef, PropType, ComponentPublicInstance } from 'vue'
import { useDebounceFn } from '@vueuse/core'
import { useFuse } from '@vueuse/integrations/useFuse'
import { groupBy, map } from 'lodash-es'
import { twMerge, twJoin } from 'tailwind-merge'
import { groupBy, map, omit } from 'lodash-es'
import { defu } from 'defu'
import type { UseFuseOptions } from '@vueuse/integrations/useFuse'
import type { Group, Command } from '../../types/command-palette'
@@ -79,7 +81,7 @@ import UIcon from '../elements/Icon.vue'
import UButton from '../elements/Button.vue'
import type { Button } from '../../types/button'
import CommandPaletteGroup from './CommandPaletteGroup.vue'
import { classNames } from '../../utils'
import { defuTwMerge } from '../../utils'
import { useAppConfig } from '#imports'
// TODO: Remove
// @ts-expect-error
@@ -96,6 +98,7 @@ export default defineComponent({
UButton,
CommandPaletteGroup
},
inheritAttrs: false,
props: {
modelValue: {
type: [String, Number, Object, Array],
@@ -175,15 +178,15 @@ export default defineComponent({
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.commandPalette>>,
default: () => appConfig.ui.commandPalette
default: () => ({})
}
},
emits: ['update:modelValue', 'close'],
setup (props, { emit, expose }) {
setup (props, { emit, attrs, expose }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.commandPalette>>(() => defu({}, props.ui, appConfig.ui.commandPalette))
const ui = computed<Partial<typeof appConfig.ui.commandPalette>>(() => defuTwMerge({}, props.ui, appConfig.ui.commandPalette))
const query = ref('')
const comboboxInput = ref<ComponentPublicInstance<HTMLInputElement>>()
@@ -268,6 +271,8 @@ export default defineComponent({
}, 0)
})
const wrapperClass = computed(() => twMerge(ui.value.wrapper, attrs.class as string))
const iconName = computed(() => {
if ((props.loading || isLoading.value) && props.loadingIcon) {
return props.loadingIcon
@@ -277,7 +282,7 @@ export default defineComponent({
})
const iconClass = computed(() => {
return classNames(
return twJoin(
ui.value.input.icon.base,
ui.value.input.icon.size,
((props.loading || isLoading.value) && props.loadingIcon) && 'animate-spin'
@@ -325,12 +330,14 @@ export default defineComponent({
})
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
// eslint-disable-next-line vue/no-dupe-keys
groups,
comboboxInput,
query,
wrapperClass,
iconName,
iconClass,
// eslint-disable-next-line vue/no-dupe-keys

View File

@@ -113,7 +113,7 @@ export default defineComponent({
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.commandPalette>>,
default: () => appConfig.ui.commandPalette
default: () => ({})
}
},
setup (props) {

View File

@@ -1,5 +1,5 @@
<template>
<div :class="ui.wrapper">
<div :class="wrapperClass" v-bind="attrs">
<slot name="prev" :on-click="onClickPrev">
<UButton
v-if="prevButton"
@@ -40,8 +40,10 @@
<script lang="ts">
import { computed, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { defu } from 'defu'
import { omit } from 'lodash-es'
import { twMerge } from 'tailwind-merge'
import UButton from '../elements/Button.vue'
import { defuTwMerge } from '../../utils'
import type { Button } from '../../types/button'
import { useAppConfig } from '#imports'
// TODO: Remove
@@ -54,6 +56,7 @@ export default defineComponent({
components: {
UButton
},
inheritAttrs: false,
props: {
modelValue: {
type: Number,
@@ -103,15 +106,15 @@ export default defineComponent({
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.pagination>>,
default: () => appConfig.ui.pagination
default: () => ({})
}
},
emits: ['update:modelValue'],
setup (props, { emit }) {
setup (props, { attrs, emit }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.pagination>>(() => defu({}, props.ui, appConfig.ui.pagination))
const ui = computed<Partial<typeof appConfig.ui.pagination>>(() => defuTwMerge({}, props.ui, appConfig.ui.pagination))
const currentPage = computed({
get () {
@@ -177,6 +180,8 @@ export default defineComponent({
const canGoPrev = computed(() => currentPage.value > 1)
const canGoNext = computed(() => currentPage.value < pages.value.length)
const wrapperClass = computed(() => twMerge(ui.value.wrapper, attrs.class as string))
function onClickPage (page: number | string) {
if (typeof page === 'string') {
return
@@ -202,6 +207,7 @@ export default defineComponent({
}
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
currentPage,
@@ -209,6 +215,7 @@ export default defineComponent({
displayedPages,
canGoPrev,
canGoNext,
wrapperClass,
onClickPrev,
onClickNext,
onClickPage

View File

@@ -1,5 +1,12 @@
<template>
<HTabGroup :vertical="orientation === 'vertical'" :selected-index="selectedIndex" as="div" :class="ui.wrapper" @change="onChange">
<HTabGroup
:vertical="orientation === 'vertical'"
:selected-index="selectedIndex"
as="div"
:class="wrapperClass"
v-bind="attrs"
@change="onChange"
>
<HTabList
ref="listRef"
:class="[ui.list.base, ui.list.background, ui.list.rounded, ui.list.shadow, ui.list.padding, ui.list.width, orientation === 'horizontal' && ui.list.height, orientation === 'horizontal' && 'inline-grid items-center']"
@@ -45,7 +52,9 @@ import { ref, computed, watch, onMounted, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { TabGroup as HTabGroup, TabList as HTabList, Tab as HTab, TabPanels as HTabPanels, TabPanel as HTabPanel } from '@headlessui/vue'
import { useResizeObserver } from '@vueuse/core'
import { defu } from 'defu'
import { omit } from 'lodash-es'
import { twMerge } from 'tailwind-merge'
import { defuTwMerge } from '../../utils'
import type { TabItem } from '../../types/tabs'
import { useAppConfig } from '#imports'
// TODO: Remove
@@ -62,6 +71,7 @@ export default defineComponent({
HTabPanels,
HTabPanel
},
inheritAttrs: false,
props: {
modelValue: {
type: Number,
@@ -82,15 +92,15 @@ export default defineComponent({
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.tabs>>,
default: () => appConfig.ui.tabs
default: () => ({})
}
},
emits: ['update:modelValue', 'change'],
setup (props, { emit }) {
setup (props, { attrs, emit }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.tabs>>(() => defu({}, props.ui, appConfig.ui.tabs))
const ui = computed<Partial<typeof appConfig.ui.tabs>>(() => defuTwMerge({}, props.ui, appConfig.ui.tabs))
const listRef = ref<HTMLElement>()
const itemRefs = ref<HTMLElement[]>([])
@@ -98,6 +108,8 @@ export default defineComponent({
const selectedIndex = ref(props.modelValue || props.defaultIndex)
const wrapperClass = computed(() => twMerge(ui.value.wrapper, attrs.class as string))
// Methods
function calcMarkerSize (index: number) {
@@ -137,12 +149,14 @@ export default defineComponent({
onMounted(() => calcMarkerSize(selectedIndex.value))
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
listRef,
itemRefs,
markerRef,
selectedIndex,
wrapperClass,
onChange
}
}

View File

@@ -1,5 +1,5 @@
<template>
<nav :class="ui.wrapper">
<nav :class="wrapperClass" v-bind="attrs">
<ULink
v-for="(link, index) of links"
v-slot="{ isActive }"
@@ -40,11 +40,12 @@
<script lang="ts">
import { computed, defineComponent } from 'vue'
import type { PropType } from '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 ULink from '../elements/Link.vue'
import { defuTwMerge } from '../../utils'
import type { VerticalNavigationLink } from '../../types/vertical-navigation'
import { useAppConfig } from '#imports'
// TODO: Remove
@@ -59,6 +60,7 @@ export default defineComponent({
UAvatar,
ULink
},
inheritAttrs: false,
props: {
links: {
type: Array as PropType<VerticalNavigationLink[]>,
@@ -66,18 +68,22 @@ export default defineComponent({
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.verticalNavigation>>,
default: () => appConfig.ui.verticalNavigation
default: () => ({})
}
},
setup (props) {
setup (props, { attrs }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.verticalNavigation>>(() => defu({}, props.ui, appConfig.ui.verticalNavigation))
const ui = computed<Partial<typeof appConfig.ui.verticalNavigation>>(() => defuTwMerge({}, props.ui, appConfig.ui.verticalNavigation))
const wrapperClass = computed(() => twMerge(ui.value.wrapper, attrs.class as string))
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
wrapperClass,
omit
}
}

View File

@@ -1,5 +1,5 @@
<template>
<div v-if="isOpen" ref="container" :class="[ui.container, ui.width]">
<div v-if="isOpen" ref="container" :class="wrapperClass" v-bind="attrs">
<Transition appear v-bind="ui.transition">
<div :class="[ui.base, ui.ring, ui.rounded, ui.shadow, ui.background]">
<slot />
@@ -14,7 +14,10 @@ import type { PropType, Ref } from 'vue'
import { defu } from 'defu'
import { onClickOutside } from '@vueuse/core'
import type { VirtualElement } from '@popperjs/core'
import { omit } from 'lodash-es'
import { twMerge, twJoin } from 'tailwind-merge'
import { usePopper } from '../../composables/usePopper'
import { defuTwMerge } from '../../utils'
import type { PopperOptions } from '../../types'
import { useAppConfig } from '#imports'
// TODO: Remove
@@ -24,6 +27,7 @@ import appConfig from '#build/app.config'
// const appConfig = useAppConfig()
export default defineComponent({
inheritAttrs: false,
props: {
modelValue: {
type: Boolean,
@@ -39,15 +43,15 @@ export default defineComponent({
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.contextMenu>>,
default: () => appConfig.ui.contextMenu
default: () => ({})
}
},
emits: ['update:modelValue', 'close'],
setup (props, { emit }) {
setup (props, { attrs, emit }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.contextMenu>>(() => defu({}, props.ui, appConfig.ui.contextMenu))
const ui = computed<Partial<typeof appConfig.ui.contextMenu>>(() => defuTwMerge({}, props.ui, appConfig.ui.contextMenu))
const popper = computed<PopperOptions>(() => defu({}, props.popper, ui.value.popper as PopperOptions))
@@ -64,14 +68,23 @@ export default defineComponent({
const [, container] = usePopper(popper.value, virtualElement)
const wrapperClass = computed(() => {
return twMerge(twJoin(
ui.value.container,
ui.value.width
), attrs.class as string)
})
onClickOutside(container, () => {
isOpen.value = false
})
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
isOpen,
wrapperClass,
container
}
}

View File

@@ -1,6 +1,6 @@
<template>
<TransitionRoot :appear="appear" :show="isOpen" as="template">
<HDialog :class="ui.wrapper" @close="(e) => !preventClose && close(e)">
<HDialog :class="wrapperClass" v-bind="attrs" @close="(e) => !preventClose && close(e)">
<TransitionChild v-if="overlay" as="template" :appear="appear" v-bind="ui.overlay.transition">
<div :class="[ui.overlay.base, ui.overlay.background]" />
</TransitionChild>
@@ -32,8 +32,10 @@
<script lang="ts">
import { computed, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { defu } from 'defu'
import { omit } from 'lodash-es'
import { twMerge } from 'tailwind-merge'
import { Dialog as HDialog, DialogPanel as HDialogPanel, TransitionRoot, TransitionChild } from '@headlessui/vue'
import { defuTwMerge } from '../../utils'
import { useAppConfig } from '#imports'
// TODO: Remove
// @ts-expect-error
@@ -48,6 +50,7 @@ export default defineComponent({
TransitionRoot,
TransitionChild
},
inheritAttrs: false,
props: {
modelValue: {
type: Boolean,
@@ -75,15 +78,15 @@ export default defineComponent({
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.modal>>,
default: () => appConfig.ui.modal
default: () => ({})
}
},
emits: ['update:modelValue', 'close'],
setup (props, { emit }) {
setup (props, { attrs, emit }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.modal>>(() => defu({}, props.ui, appConfig.ui.modal))
const ui = computed<Partial<typeof appConfig.ui.modal>>(() => defuTwMerge({}, props.ui, appConfig.ui.modal))
const isOpen = computed({
get () {
@@ -94,6 +97,8 @@ export default defineComponent({
}
})
const wrapperClass = computed(() => twMerge(ui.value.wrapper, attrs.class as string))
const transitionClass = computed(() => {
if (!props.transition) {
return {}
@@ -111,9 +116,11 @@ export default defineComponent({
}
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
isOpen,
wrapperClass,
transitionClass,
close
}

View File

@@ -1,6 +1,6 @@
<template>
<Transition appear v-bind="ui.transition">
<div :class="[ui.wrapper, ui.background, ui.rounded, ui.shadow]" @mouseover="onMouseover" @mouseleave="onMouseleave">
<div :class="wrapperClass" v-bind="attrs" @mouseover="onMouseover" @mouseleave="onMouseleave">
<div :class="[ui.container, ui.rounded, ui.ring]">
<div :class="ui.padding">
<div class="flex gap-3" :class="{ 'items-start': description || $slots.description, 'items-center': !description && !$slots.description }">
@@ -41,7 +41,8 @@
<script lang="ts">
import { ref, computed, onMounted, onUnmounted, watchEffect, 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 UAvatar from '../elements/Avatar.vue'
import UButton from '../elements/Button.vue'
@@ -49,7 +50,7 @@ import { useTimer } from '../../composables/useTimer'
import type { NotificationAction } from '../../types'
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
@@ -63,6 +64,7 @@ export default defineComponent({
UAvatar,
UButton
},
inheritAttrs: false,
props: {
id: {
type: [String, Number],
@@ -109,34 +111,43 @@ export default defineComponent({
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.notification>>,
default: () => appConfig.ui.notification
default: () => ({})
}
},
emits: ['close'],
setup (props, { emit }) {
setup (props, { attrs, emit }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.notification>>(() => defu({}, props.ui, appConfig.ui.notification))
const ui = computed<Partial<typeof appConfig.ui.notification>>(() => defuTwMerge({}, props.ui, appConfig.ui.notification))
let timer: any = null
const remaining = ref(props.timeout)
const wrapperClass = computed(() => {
return twMerge(twJoin(
ui.value.wrapper,
ui.value.background,
ui.value.rounded,
ui.value.shadow
), attrs.class as string)
})
const progressClass = computed(() => {
return twJoin(
ui.value.progress.base,
ui.value.progress.background?.replaceAll('{color}', props.color)
)
})
const progressStyle = computed(() => {
const remainingPercent = remaining.value / props.timeout * 100
return { width: `${remainingPercent || 0}%` }
})
const progressClass = computed(() => {
return classNames(
ui.value.progress.base,
ui.value.progress.background?.replaceAll('{color}', props.color)
)
})
const iconClass = computed(() => {
return classNames(
return twJoin(
ui.value.icon.base,
ui.value.icon.color?.replaceAll('{color}', props.color)
)
@@ -199,10 +210,12 @@ export default defineComponent({
})
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
progressStyle,
wrapperClass,
progressClass,
progressStyle,
iconClass,
onMouseover,
onMouseleave,

View File

@@ -1,5 +1,5 @@
<template>
<div :class="[ui.wrapper, ui.position, ui.width]">
<div :class="wrapperClass" v-bind="attrs">
<div v-if="notifications.length" :class="ui.container">
<div v-for="notification of notifications" :key="notification.id">
<UNotification
@@ -20,10 +20,12 @@
<script lang="ts">
import { computed, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { defu } from 'defu'
import type { Notification } from '../../types'
import { useToast } from '../../composables/useToast'
import { omit } from 'lodash-es'
import { twMerge, twJoin } from 'tailwind-merge'
import UNotification from './Notification.vue'
import { useToast } from '../../composables/useToast'
import { defuTwMerge } from '../../utils'
import type { Notification } from '../../types'
import { useState, useAppConfig } from '#imports'
// TODO: Remove
// @ts-expect-error
@@ -35,26 +37,37 @@ export default defineComponent({
components: {
UNotification
},
inheritAttrs: false,
props: {
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.notifications>>,
default: () => appConfig.ui.notifications
default: () => ({})
}
},
setup (props) {
setup (props, { attrs }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.notifications>>(() => defu({}, props.ui, appConfig.ui.notifications))
const ui = computed<Partial<typeof appConfig.ui.notifications>>(() => defuTwMerge({}, props.ui, appConfig.ui.notifications))
const toast = useToast()
const notifications = useState<Notification[]>('notifications', () => [])
const wrapperClass = computed(() => {
return twMerge(twJoin(
ui.value.wrapper,
ui.value.position,
ui.value.width
), attrs.class as string)
})
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
toast,
notifications
notifications,
wrapperClass
}
}
})

View File

@@ -1,5 +1,5 @@
<template>
<HPopover ref="popover" v-slot="{ open, close }" :class="ui.wrapper" @mouseleave="onMouseLeave">
<HPopover ref="popover" v-slot="{ open, close }" :class="wrapperClass" v-bind="attrs" @mouseleave="onMouseLeave">
<HPopoverButton
ref="trigger"
as="div"
@@ -29,8 +29,11 @@
import { computed, ref, onMounted, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { defu } from 'defu'
import { omit } from 'lodash-es'
import { twMerge } from 'tailwind-merge'
import { Popover as HPopover, PopoverButton as HPopoverButton, PopoverPanel as HPopoverPanel } from '@headlessui/vue'
import { usePopper } from '../../composables/usePopper'
import { defuTwMerge } from '../../utils'
import type { PopperOptions } from '../../types'
import { useAppConfig } from '#imports'
// TODO: Remove
@@ -45,6 +48,7 @@ export default defineComponent({
HPopoverButton,
HPopoverPanel
},
inheritAttrs: false,
props: {
mode: {
type: String,
@@ -71,14 +75,14 @@ export default defineComponent({
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.popover>>,
default: () => appConfig.ui.popover
default: () => ({})
}
},
setup (props) {
setup (props, { attrs }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.popover>>(() => defu({}, props.ui, appConfig.ui.popover))
const ui = computed<Partial<typeof appConfig.ui.popover>>(() => defuTwMerge({}, props.ui, appConfig.ui.popover))
const popper = computed<PopperOptions>(() => defu(props.mode === 'hover' ? { offsetDistance: 0 } : {}, props.popper, ui.value.popper as PopperOptions))
@@ -106,6 +110,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' || !popoverApi.value) {
return
@@ -147,12 +153,14 @@ export default defineComponent({
}
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
popover,
trigger,
container,
containerStyle,
wrapperClass,
onMouseOver,
onMouseLeave
}

View File

@@ -1,6 +1,6 @@
<template>
<TransitionRoot as="template" :appear="appear" :show="isOpen">
<HDialog :class="[ui.wrapper, { 'justify-end': side === 'right' }]" @close="(e) => !preventClose && close(e)">
<HDialog :class="[wrapperClass, { 'justify-end': side === 'right' }]" v-bind="attrs" @close="(e) => !preventClose && close(e)">
<TransitionChild v-if="overlay" as="template" :appear="appear" v-bind="ui.overlay.transition">
<div :class="[ui.overlay.base, ui.overlay.background]" />
</TransitionChild>
@@ -17,8 +17,10 @@
<script lang="ts">
import { computed, defineComponent } from 'vue'
import type { WritableComputedRef, PropType } from 'vue'
import { defu } from 'defu'
import { Dialog as HDialog, DialogPanel as HDialogPanel, TransitionRoot, TransitionChild } from '@headlessui/vue'
import { omit } from 'lodash-es'
import { twMerge } from 'tailwind-merge'
import { defuTwMerge } from '../../utils'
import { useAppConfig } from '#imports'
// TODO: Remove
// @ts-expect-error
@@ -33,6 +35,7 @@ export default defineComponent({
TransitionRoot,
TransitionChild
},
inheritAttrs: false,
props: {
modelValue: {
type: Boolean as PropType<boolean>,
@@ -61,15 +64,15 @@ export default defineComponent({
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.slideover>>,
default: () => appConfig.ui.slideover
default: () => ({})
}
},
emits: ['update:modelValue', 'close'],
setup (props, { emit }) {
setup (props, { attrs, emit }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.slideover>>(() => defu({}, props.ui, appConfig.ui.slideover))
const ui = computed<Partial<typeof appConfig.ui.slideover>>(() => defuTwMerge({}, props.ui, appConfig.ui.slideover))
const isOpen: WritableComputedRef<boolean> = computed({
get () {
@@ -80,6 +83,8 @@ export default defineComponent({
}
})
const wrapperClass = computed(() => twMerge(ui.value.wrapper, attrs.class as string))
const transitionClass = computed(() => {
if (!props.transition) {
return {}
@@ -100,9 +105,11 @@ export default defineComponent({
}
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
isOpen,
wrapperClass,
transitionClass,
close
}

View File

@@ -1,5 +1,5 @@
<template>
<div ref="trigger" :class="ui.wrapper" @mouseover="onMouseOver" @mouseleave="onMouseLeave">
<div ref="trigger" :class="wrapperClass" v-bind="attrs" @mouseover="onMouseOver" @mouseleave="onMouseLeave">
<slot :open="open">
Hover
</slot>
@@ -27,8 +27,11 @@
import { computed, ref, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { defu } from 'defu'
import { omit } from 'lodash-es'
import { twMerge } from 'tailwind-merge'
import UKbd from '../elements/Kbd.vue'
import { usePopper } from '../../composables/usePopper'
import { defuTwMerge } from '../../utils'
import type { PopperOptions } from '../../types'
import { useAppConfig } from '#imports'
// TODO: Remove
@@ -41,6 +44,7 @@ export default defineComponent({
components: {
UKbd
},
inheritAttrs: false,
props: {
text: {
type: String,
@@ -68,14 +72,14 @@ export default defineComponent({
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.tooltip>>,
default: () => appConfig.ui.tooltip
default: () => ({})
}
},
setup (props) {
setup (props, { attrs }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.tooltip>>(() => defu({}, props.ui, appConfig.ui.tooltip))
const ui = computed<Partial<typeof appConfig.ui.tooltip>>(() => defuTwMerge({}, props.ui, appConfig.ui.tooltip))
const popper = computed<PopperOptions>(() => defu({}, props.popper, ui.value.popper as PopperOptions))
@@ -86,6 +90,8 @@ export default defineComponent({
let openTimeout: NodeJS.Timeout | null = null
let closeTimeout: NodeJS.Timeout | null = null
const wrapperClass = computed(() => twMerge(ui.value.wrapper, attrs.class as string))
// Methods
function onMouseOver () {
@@ -121,11 +127,13 @@ export default defineComponent({
}
return {
attrs: omit(attrs, ['class']),
// eslint-disable-next-line vue/no-dupe-keys
ui,
trigger,
container,
open,
wrapperClass,
onMouseOver,
onMouseLeave
}

View File

@@ -1,6 +1,13 @@
export function classNames (...classes: any[string]) {
return classes.filter(Boolean).join(' ')
}
import { createDefu } from 'defu'
import { twMerge } from 'tailwind-merge'
export const defuTwMerge = createDefu((obj, key, value) => {
if (typeof obj[key] === 'string' && typeof value === 'string' && obj[key] && value) {
// @ts-ignore
obj[key] = twMerge(obj[key], value)
return true
}
})
export const hexToRgb = (hex: string) => {
// Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")