feat: rewrite to use app config and rework docs (#143)

Co-authored-by: Daniel Roe <daniel@roe.dev>
Co-authored-by: Sébastien Chopin <seb@nuxt.com>
This commit is contained in:
Benjamin Canac
2023-05-04 14:49:19 +02:00
committed by GitHub
parent 56230ea915
commit 6da0db0113
144 changed files with 10470 additions and 8109 deletions

View File

@@ -1,91 +1,79 @@
<template>
<div v-if="isOpen" ref="container" :class="[containerClass, widthClass]">
<transition appear v-bind="transitionClass">
<div :class="[baseClass, ringClass, roundedClass, shadowClass, backgroundClass]">
<div v-if="isOpen" ref="container" :class="[ui.container, ui.width]">
<transition appear v-bind="ui.transition">
<div :class="[ui.base, ui.ring, ui.rounded, ui.shadow, ui.background]">
<slot />
</div>
</transition>
</div>
</template>
<script setup lang="ts">
<script lang="ts">
import { computed, toRef, defineComponent } from 'vue'
import type { PropType, Ref } from 'vue'
import { computed, toRef } from 'vue'
import { defu } from 'defu'
import { onClickOutside } from '@vueuse/core'
import type { VirtualElement } from '@popperjs/core'
import { usePopper } from '../../composables/usePopper'
import type { PopperOptions } from '../../types'
import $ui from '#build/ui'
import { useAppConfig } from '#imports'
// TODO: Remove
// @ts-expect-error
import appConfig from '#build/app.config'
const props = defineProps({
modelValue: {
type: Boolean,
default: false
// const appConfig = useAppConfig()
export default defineComponent({
props: {
modelValue: {
type: Boolean,
default: false
},
virtualElement: {
type: Object,
required: true
},
popper: {
type: Object as PropType<PopperOptions>,
default: () => ({})
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.contextMenu>>,
default: () => appConfig.ui.contextMenu
}
},
virtualElement: {
type: Object,
required: true
},
wrapperClass: {
type: String,
default: () => $ui.contextMenu.wrapper
},
containerClass: {
type: String,
default: () => $ui.contextMenu.container
},
widthClass: {
type: String,
default: () => $ui.contextMenu.width
},
backgroundClass: {
type: String,
default: () => $ui.contextMenu.background
},
shadowClass: {
type: String,
default: () => $ui.contextMenu.shadow
},
roundedClass: {
type: String,
default: () => $ui.contextMenu.rounded
},
ringClass: {
type: String,
default: () => $ui.contextMenu.ring
},
baseClass: {
type: String,
default: () => $ui.contextMenu.base
},
transitionClass: {
type: Object,
default: () => $ui.contextMenu.transition
},
popperOptions: {
type: Object as PropType<PopperOptions>,
default: () => ({})
emits: ['update:modelValue', 'close'],
setup (props, { emit }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.contextMenu>>(() => defu({}, props.ui, appConfig.ui.contextMenu))
const popper = computed<PopperOptions>(() => defu({}, props.popper, ui.value.popper as PopperOptions))
const isOpen = computed({
get () {
return props.modelValue
},
set (value) {
emit('update:modelValue', value)
}
})
const virtualElement = toRef(props, 'virtualElement') as Ref<VirtualElement>
const [, container] = usePopper(popper.value, virtualElement)
onClickOutside(container, () => {
isOpen.value = false
})
return {
// eslint-disable-next-line vue/no-dupe-keys
ui,
isOpen,
container
}
}
})
const emit = defineEmits(['update:modelValue', 'close'])
const isOpen = computed({
get () {
return props.modelValue
},
set (value) {
emit('update:modelValue', value)
}
})
const virtualElement = toRef(props, 'virtualElement') as Ref<VirtualElement>
const popperOptions = computed<PopperOptions>(() => defu({}, props.popperOptions, $ui.contextMenu.popperOptions))
const [, container] = usePopper(popperOptions.value, virtualElement)
</script>
<script lang="ts">
export default { name: 'UContextMenu' }
</script>

View File

@@ -1,39 +1,15 @@
<template>
<TransitionRoot :appear="appear" :show="isOpen" as="template">
<Dialog :class="wrapperClass" @close="close">
<TransitionChild
v-if="overlay"
as="template"
:appear="appear"
v-bind="overlayTransition"
>
<div class="fixed inset-0 transition-opacity" :class="overlayBackgroundClass" />
<Dialog :class="ui.wrapper" @close="close">
<TransitionChild v-if="overlay" as="template" :appear="appear" v-bind="ui.overlay.transition">
<div :class="[ui.overlay.base, ui.overlay.background]" />
</TransitionChild>
<div :class="innerClass" :style="innerStyle">
<div :class="containerClass">
<TransitionChild
as="template"
:appear="appear"
v-bind="modalTransition"
>
<DialogPanel :class="modalClass">
<Card
base-class=""
background-class=""
shadow-class=""
ring-class=""
rounded-class=""
v-bind="$attrs"
>
<template v-if="$slots.header" #header>
<slot name="header" />
</template>
<slot />
<template v-if="$slots.footer" #footer>
<slot name="footer" />
</template>
</Card>
<div :class="ui.inner">
<div :class="[ui.container, ui.spacing]">
<TransitionChild as="template" :appear="appear" v-bind="ui.transition">
<DialogPanel :class="[ui.base, ui.width, ui.height, ui.background, ui.ring, ui.rounded, ui.shadow]">
<slot />
</DialogPanel>
</TransitionChild>
</div>
@@ -42,131 +18,75 @@
</TransitionRoot>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { Dialog, DialogPanel, TransitionRoot, TransitionChild } from '@headlessui/vue'
import { classNames } from '../../utils'
import Card from '../layout/Card.vue'
import $ui from '#build/ui'
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
appear: {
type: Boolean,
default: false
},
wrapperClass: {
type: String,
default: () => $ui.modal.wrapper
},
innerClass: {
type: String,
default: () => $ui.modal.inner
},
innerStyle: {
type: Object,
default: () => ({})
},
containerClass: {
type: String,
default: () => $ui.modal.container
},
baseClass: {
type: String,
default: () => $ui.modal.base
},
backgroundClass: {
type: String,
default: () => $ui.modal.background
},
overlay: {
type: Boolean,
default: true
},
overlayBackgroundClass: {
type: String,
default: () => $ui.modal.overlay.background
},
overlayTransitionClass: {
type: Object,
default: () => $ui.modal.overlay.transition
},
shadowClass: {
type: String,
default: () => $ui.modal.shadow
},
ringClass: {
type: String,
default: () => $ui.modal.ring
},
roundedClass: {
type: String,
default: () => $ui.modal.rounded
},
widthClass: {
type: String,
default: () => $ui.modal.width
},
transition: {
type: Boolean,
default: true
},
transitionClass: {
type: Object,
default: () => $ui.modal.transition
}
})
const emit = defineEmits(['update:modelValue', 'close'])
const isOpen = computed({
get () {
return props.modelValue
},
set (value) {
emit('update:modelValue', value)
}
})
const modalClass = computed(() => {
return classNames(
props.baseClass,
props.widthClass,
props.backgroundClass,
props.shadowClass,
props.ringClass,
props.roundedClass
)
})
const overlayTransition = computed(() => {
if (!props.transition) {
return {}
}
return props.overlayTransitionClass
})
const modalTransition = computed(() => {
if (!props.transition) {
return {}
}
return props.transitionClass
})
function close (value: boolean) {
isOpen.value = value
emit('close')
}
</script>
<script lang="ts">
export default {
name: 'UModal',
inheritAttrs: false
}
import { computed, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { defu } from 'defu'
import { Dialog, DialogPanel, TransitionRoot, TransitionChild } from '@headlessui/vue'
import { useAppConfig } from '#imports'
// TODO: Remove
// @ts-expect-error
import appConfig from '#build/app.config'
// const appConfig = useAppConfig()
export default defineComponent({
components: {
// eslint-disable-next-line vue/no-reserved-component-names
Dialog,
DialogPanel,
TransitionRoot,
TransitionChild
},
props: {
modelValue: {
type: Boolean,
default: false
},
appear: {
type: Boolean,
default: false
},
overlay: {
type: Boolean,
default: true
},
transition: {
type: Boolean,
default: true
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.modal>>,
default: () => appConfig.ui.modal
}
},
emits: ['update:modelValue', 'close'],
setup (props, { emit }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.modal>>(() => defu({}, props.ui, appConfig.ui.modal))
const isOpen = computed({
get () {
return props.modelValue
},
set (value) {
emit('update:modelValue', value)
}
})
function close (value: boolean) {
isOpen.value = value
emit('close')
}
return {
// eslint-disable-next-line vue/no-dupe-keys
ui,
isOpen,
close
}
}
})
</script>

View File

@@ -1,211 +1,191 @@
<template>
<transition appear v-bind="transitionClass">
<transition appear v-bind="ui.transition">
<div
:class="['z-50 w-full pointer-events-auto', backgroundClass, roundedClass, shadowClass]"
:class="[ui.wrapper, ui.background, ui.rounded, ui.shadow]"
@mouseover="onMouseover"
@mouseleave="onMouseleave"
>
<div :class="['relative overflow-hidden', roundedClass, ringClass]">
<div :class="[ui.container, ui.rounded, ui.ring]">
<div class="p-4">
<div class="flex gap-3" :class="{ 'items-start': description, 'items-center': !description }">
<div v-if="iconName" class="flex-shrink-0">
<Icon :name="iconName" :class="iconClass" />
</div>
<Icon v-if="icon" :name="icon" :class="ui.icon" />
<Avatar v-if="avatar" v-bind="avatar" :class="ui.avatar" />
<div class="w-0 flex-1">
<p class="text-sm font-medium u-text-gray-900">
<p :class="ui.title">
{{ title }}
</p>
<p v-if="description" class="mt-1 text-sm leading-5 u-text-gray-500">
<p v-if="description" :class="ui.description">
{{ description }}
</p>
<div v-if="description && actions.length" class="mt-3 flex items-center gap-6">
<button v-for="(action, index) of actions" :key="index" type="button" class="text-sm font-medium focus:outline-none text-primary-500 dark:text-primary-400 hover:text-primary-400 dark:hover:text-primary-500" @click.stop="onAction(action)">
{{ action.label }}
</button>
<div v-if="description && actions.length" class="mt-3 flex items-center gap-2">
<Button v-for="(action, index) of actions" :key="index" v-bind="{ ...ui.default.action, ...action }" @click.stop="onAction(action)" />
</div>
</div>
<div class="flex-shrink-0 flex items-center gap-3">
<div v-if="!description && actions.length" class="flex items-center gap-2">
<button v-for="(action, index) of actions" :key="index" type="button" class="text-sm font-medium focus:outline-none text-primary-500 dark:text-primary-400 hover:text-primary-400 dark:hover:text-primary-500" @click.stop="onAction(action)">
{{ action.label }}
</button>
<Button v-for="(action, index) of actions" :key="index" v-bind="{ ...ui.default.action, ...action }" @click.stop="onAction(action)" />
</div>
<button
class="inline-flex transition duration-150 ease-in-out u-text-gray-400 focus:outline-none hover:u-text-gray-500 focus:u-text-gray-500"
@click.stop="onClose"
>
<span class="sr-only">Close</span>
<Icon :name="$ui.notification.close.icon.name" class="w-5 h-5" />
</button>
<Button v-if="close" v-bind="{ ...ui.default.close, ...close }" @click.stop="onClose" />
</div>
</div>
</div>
<div v-if="timeout" class="absolute bottom-0 left-0 right-0 h-1">
<div class="h-1 bg-primary-500" :style="progressBarStyle" />
</div>
<div v-if="timeout" :class="ui.progress" :style="progressBarStyle" />
</div>
</div>
</transition>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watchEffect } from 'vue'
<script lang="ts">
import { ref, computed, onMounted, onUnmounted, watchEffect, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { defu } from 'defu'
import Icon from '../elements/Icon.vue'
import Avatar from '../elements/Avatar.vue'
import Button from '../elements/Button.vue'
import { useTimer } from '../../composables/useTimer'
import { classNames } from '../../utils'
import type { ToastNotificationAction } from '../../types'
import $ui from '#build/ui'
import type { Avatar as AvatarType } from '../../types/avatar'
import type { Button as ButtonType } from '../../types/button'
import { useAppConfig } from '#imports'
// TODO: Remove
// @ts-expect-error
import appConfig from '#build/app.config'
const props = defineProps({
id: {
type: String,
required: true
// const appConfig = useAppConfig()
export default defineComponent({
components: {
Icon,
Avatar,
// eslint-disable-next-line vue/no-reserved-component-names
Button
},
type: {
type: String,
default: null,
validator (value: string) {
return Object.keys($ui.notification.type).includes(value)
props: {
id: {
type: [String, Number],
required: true
},
title: {
type: String,
required: true
},
description: {
type: String,
default: null
},
icon: {
type: String,
default: null
},
avatar: {
type: Object as PropType<Partial<AvatarType>>,
default: null
},
close: {
type: Object as PropType<Partial<ButtonType>>,
default: () => appConfig.ui.notification.default.close
},
timeout: {
type: Number,
default: 5000
},
actions: {
type: Array as PropType<ToastNotificationAction[]>,
default: () => []
},
callback: {
type: Function,
default: null
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.notification>>,
default: () => appConfig.ui.notification
}
},
title: {
type: String,
required: true
},
description: {
type: String,
default: null
},
backgroundClass: {
type: String,
default: () => $ui.notification.background
},
shadowClass: {
type: String,
default: () => $ui.notification.shadow
},
ringClass: {
type: String,
default: () => $ui.notification.ring
},
roundedClass: {
type: String,
default: () => $ui.notification.rounded
},
transitionClass: {
type: Object,
default: () => $ui.notification.transition
},
customClass: {
type: String,
default: null
},
icon: {
type: String,
default: null
},
iconBaseClass: {
type: String,
default: () => $ui.notification.icon.base
},
timeout: {
type: Number,
default: 5000
},
actions: {
type: Array as PropType<{
label: string,
click: Function
}[]>,
default: () => []
},
callback: {
type: Function,
default: null
}
})
emits: ['close'],
setup (props, { emit }) {
// TODO: Remove
const appConfig = useAppConfig()
const emit = defineEmits(['close'])
const ui = computed<Partial<typeof appConfig.ui.notification>>(() => defu({}, props.ui, appConfig.ui.notification))
let timer: any = null
const remaining = ref(props.timeout)
let timer: any = null
const remaining = ref(props.timeout)
const iconName = computed(() => {
return props.icon || $ui.notification.type[props.type]
})
const progressBarStyle = computed(() => {
const remainingPercent = remaining.value / props.timeout * 100
const iconClass = computed(() => {
return classNames(
props.iconBaseClass,
$ui.notification.icon.color[props.type] || 'u-text-gray-400'
)
})
return { width: `${remainingPercent || 0}%` }
})
const progressBarStyle = computed(() => {
const remainingPercent = remaining.value / props.timeout * 100
return { width: `${remainingPercent || 0}%` }
})
function onMouseover () {
if (timer) {
timer.pause()
}
}
function onMouseover () {
if (timer) {
timer.pause()
}
}
function onMouseleave () {
if (timer) {
timer.resume()
}
}
function onMouseleave () {
if (timer) {
timer.resume()
}
}
function onClose () {
if (timer) {
timer.stop()
}
function onClose () {
if (timer) {
timer.stop()
}
if (props.callback) {
props.callback()
}
if (props.callback) {
props.callback()
}
emit('close')
}
emit('close')
}
function onAction (action: ToastNotificationAction) {
if (timer) {
timer.stop()
}
function onAction (action: ToastNotificationAction) {
if (timer) {
timer.stop()
}
if (action.click) {
action.click()
}
if (action.click) {
action.click()
}
emit('close')
}
emit('close')
}
onMounted(() => {
if (!props.timeout) {
return
}
onMounted(() => {
if (!props.timeout) {
return
}
timer = useTimer(() => {
onClose()
}, props.timeout)
timer = useTimer(() => {
onClose()
}, props.timeout)
watchEffect(() => {
remaining.value = timer.remaining.value
})
})
watchEffect(() => {
remaining.value = timer.remaining.value
})
})
onUnmounted(() => {
if (timer) {
timer.stop()
}
})
onUnmounted(() => {
if (timer) {
timer.stop()
return {
// eslint-disable-next-line vue/no-dupe-keys
ui,
progressBarStyle,
onMouseover,
onMouseleave,
onClose,
onAction
}
}
})
</script>
<script lang="ts">
export default { name: 'UNotification' }
</script>

View File

@@ -1,31 +1,57 @@
<template>
<div class="fixed bottom-0 right-0 flex flex-col justify-end w-full z-[55] sm:w-96">
<div v-if="notifications.length" class="px-4 py-6 space-y-3 overflow-y-auto sm:px-6">
<div
v-for="notification of notifications"
:key="notification.id"
>
<div :class="ui.wrapper">
<div v-if="notifications.length" :class="ui.container">
<div v-for="notification of notifications" :key="notification.id">
<Notification
v-bind="notification"
:class="notification.click && 'cursor-pointer'"
@click="notification.click && notification.click(notification)"
@close="toast.removeNotification(notification.id)"
@close="toast.remove(notification.id)"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
<script lang="ts">
import { defineComponent } from 'vue'
import type { PropType } from 'vue'
import { defu } from 'defu'
import type { ToastNotification } from '../../types'
import { useToast } from '../../composables/useToast'
import Notification from './Notification.vue'
import { useState } from '#imports'
import { useState, useAppConfig } from '#imports'
// TODO: Remove
// @ts-expect-error
import appConfig from '#build/app.config'
const toast = useToast()
const notifications = useState<ToastNotification[]>('notifications', () => [])
</script>
// const appConfig = useAppConfig()
<script lang="ts">
export default { name: 'UNotifications' }
export default defineComponent({
components: {
Notification
},
props: {
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.notifications>>,
default: () => appConfig.ui.notifications
}
},
setup (props) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.notifications>>(() => defu({}, props.ui, appConfig.ui.notifications))
const toast = useToast()
const notifications = useState<ToastNotification[]>('notifications', () => [])
return {
// eslint-disable-next-line vue/no-dupe-keys
ui,
toast,
notifications
}
}
})
</script>

View File

@@ -1,5 +1,5 @@
<template>
<Popover v-slot="{ open, close }" :class="wrapperClass" @mouseleave="onMouseLeave">
<Popover v-slot="{ open, close }" :class="ui.wrapper" @mouseleave="onMouseLeave">
<PopoverButton
ref="trigger"
as="div"
@@ -15,9 +15,9 @@
</slot>
</PopoverButton>
<div v-if="open" ref="container" :class="[containerClass, widthClass]" @mouseover="onMouseOver">
<transition appear v-bind="transitionClass">
<PopoverPanel :class="[baseClass, ringClass, roundedClass, shadowClass, backgroundClass]" static>
<div v-if="open" ref="container" :class="[ui.container, ui.width]" @mouseover="onMouseOver">
<transition appear v-bind="ui.transition">
<PopoverPanel :class="[ui.base, ui.background, ui.ring, ui.rounded, ui.shadow]" static>
<slot name="panel" :open="open" :close="close" />
</PopoverPanel>
</transition>
@@ -25,140 +25,131 @@
</Popover>
</template>
<script setup lang="ts">
import { computed, ref, onMounted } from 'vue'
<script lang="ts">
import { computed, ref, onMounted, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { defu } from 'defu'
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
import { usePopper } from '../../composables/usePopper'
import type { PopperOptions } from '../../types'
import $ui from '#build/ui'
import { useAppConfig } from '#imports'
// TODO: Remove
// @ts-expect-error
import appConfig from '#build/app.config'
const props = defineProps({
mode: {
type: String,
default: 'click',
validator: (value: string) => {
return ['click', 'hover'].includes(value)
// const appConfig = useAppConfig()
export default defineComponent({
components: {
Popover,
PopoverButton,
PopoverPanel
},
props: {
mode: {
type: String,
default: 'click',
validator: (value: string) => {
return ['click', 'hover'].includes(value)
}
},
disabled: {
type: Boolean,
default: false
},
openDelay: {
type: Number,
default: 50
},
closeDelay: {
type: Number,
default: 0
},
popper: {
type: Object as PropType<PopperOptions>,
default: () => ({})
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.popover>>,
default: () => appConfig.ui.popover
}
},
disabled: {
type: Boolean,
default: false
},
wrapperClass: {
type: String,
default: () => $ui.popover.wrapper
},
containerClass: {
type: String,
default: () => $ui.popover.container
},
widthClass: {
type: String,
default: () => $ui.popover.width
},
baseClass: {
type: String,
default: () => $ui.popover.base
},
backgroundClass: {
type: String,
default: () => $ui.popover.background
},
shadowClass: {
type: String,
default: () => $ui.popover.shadow
},
roundedClass: {
type: String,
default: () => $ui.popover.rounded
},
ringClass: {
type: String,
default: () => $ui.popover.ring
},
transitionClass: {
type: Object,
default: () => $ui.popover.transition
},
popperOptions: {
type: Object as PropType<PopperOptions>,
default: () => ({})
},
openDelay: {
type: Number,
default: 50
},
closeDelay: {
type: Number,
default: 0
}
})
setup (props) {
// TODO: Remove
const appConfig = useAppConfig()
const popperOptions = computed<PopperOptions>(() => defu({}, props.popperOptions, $ui.popover.popperOptions))
const ui = computed<Partial<typeof appConfig.ui.popover>>(() => defu({}, props.ui, appConfig.ui.popover))
const [trigger, container] = usePopper(popperOptions.value)
const popper = computed<PopperOptions>(() => defu({}, props.popper, ui.value.popper as PopperOptions))
// https://github.com/tailwindlabs/headlessui/blob/f66f4926c489fc15289d528294c23a3dc2aee7b1/packages/%40headlessui-vue/src/components/popover/popover.ts#L151
const popoverApi = ref<any>(null)
const [trigger, container] = usePopper(popper.value)
let openTimeout: NodeJS.Timeout | null = null
let closeTimeout: NodeJS.Timeout | null = null
// https://github.com/tailwindlabs/headlessui/blob/f66f4926c489fc15289d528294c23a3dc2aee7b1/packages/%40headlessui-vue/src/components/popover/popover.ts#L151
const popoverApi = ref<any>(null)
onMounted(() => {
setTimeout(() => {
// @ts-expect-error internals
const popoverProvides = trigger.value?.$.provides
if (!popoverProvides) {
return
let openTimeout: NodeJS.Timeout | null = null
let closeTimeout: NodeJS.Timeout | null = null
onMounted(() => {
setTimeout(() => {
// @ts-expect-error internals
const popoverProvides = trigger.value?.$.provides
if (!popoverProvides) {
return
}
const popoverProvidesSymbols = Object.getOwnPropertySymbols(popoverProvides)
popoverApi.value = popoverProvidesSymbols.length && popoverProvides[popoverProvidesSymbols[0]]
}, 200)
})
function onMouseOver () {
if (props.mode !== 'hover' || !popoverApi.value) {
return
}
// cancel programmed closing
if (closeTimeout) {
clearTimeout(closeTimeout)
closeTimeout = null
}
// dropdown already open
if (popoverApi.value.popoverState === 0) {
return
}
openTimeout = openTimeout || setTimeout(() => {
popoverApi.value.togglePopover && popoverApi.value.togglePopover()
openTimeout = null
}, props.openDelay)
}
const popoverProvidesSymbols = Object.getOwnPropertySymbols(popoverProvides)
popoverApi.value = popoverProvidesSymbols.length && popoverProvides[popoverProvidesSymbols[0]]
}, 200)
function onMouseLeave () {
if (props.mode !== 'hover' || !popoverApi.value) {
return
}
// cancel programmed opening
if (openTimeout) {
clearTimeout(openTimeout)
openTimeout = null
}
// dropdown already closed
if (popoverApi.value.popoverState === 1) {
return
}
closeTimeout = closeTimeout || setTimeout(() => {
popoverApi.value.closePopover && popoverApi.value.closePopover()
closeTimeout = null
}, props.closeDelay)
}
return {
// eslint-disable-next-line vue/no-dupe-keys
ui,
trigger,
container,
onMouseOver,
onMouseLeave
}
}
})
function onMouseOver () {
if (props.mode !== 'hover' || !popoverApi.value) {
return
}
// cancel programmed closing
if (closeTimeout) {
clearTimeout(closeTimeout)
closeTimeout = null
}
// dropdown already open
if (popoverApi.value.popoverState === 0) {
return
}
openTimeout = openTimeout || setTimeout(() => {
popoverApi.value.togglePopover && popoverApi.value.togglePopover()
openTimeout = null
}, props.openDelay)
}
function onMouseLeave () {
if (props.mode !== 'hover' || !popoverApi.value) {
return
}
// cancel programmed opening
if (openTimeout) {
clearTimeout(openTimeout)
openTimeout = null
}
// dropdown already closed
if (popoverApi.value.popoverState === 1) {
return
}
closeTimeout = closeTimeout || setTimeout(() => {
popoverApi.value.closePopover && popoverApi.value.closePopover()
closeTimeout = null
}, props.closeDelay)
}
</script>
<script lang="ts">
export default { name: 'UPopover' }
</script>

View File

@@ -1,24 +1,12 @@
<template>
<TransitionRoot as="template" :appear="appear" :show="isOpen">
<Dialog :class="[wrapperClass, { 'justify-end': side === 'right' }]" @close="close">
<TransitionChild
v-if="overlay"
as="template"
:appear="appear"
v-bind="overlayTransition"
>
<div class="fixed inset-0 transition-opacity" :class="overlayBackgroundClass" />
<Dialog :class="[ui.wrapper, { 'justify-end': side === 'right' }]" @close="close">
<TransitionChild v-if="overlay" as="template" :appear="appear" v-bind="ui.overlay.transition">
<div :class="[ui.overlay.base, ui.overlay.background]" />
</TransitionChild>
<TransitionChild
as="template"
:appear="appear"
v-bind="slideoverTransition"
>
<DialogPanel :class="slideoverClass">
<div v-if="$slots.header" :class="headerClass">
<slot name="header" />
</div>
<TransitionChild as="template" :appear="appear" v-bind="transitionClass">
<DialogPanel :class="[ui.base, ui.width, ui.background, ui.ring]">
<slot />
</DialogPanel>
</TransitionChild>
@@ -26,116 +14,95 @@
</TransitionRoot>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { WritableComputedRef, PropType } from 'vue'
import { Dialog, DialogPanel, TransitionRoot, TransitionChild } from '@headlessui/vue'
import { classNames } from '../../utils'
import $ui from '#build/ui'
const props = defineProps({
modelValue: {
type: Boolean as PropType<boolean>,
default: false
},
appear: {
type: Boolean,
default: false
},
side: {
type: String,
default: 'left',
validator: (value: string) => ['left', 'right'].includes(value)
},
wrapperClass: {
type: String,
default: () => $ui.slideover.wrapper
},
baseClass: {
type: String,
default: () => $ui.slideover.base
},
backgroundClass: {
type: String,
default: () => $ui.slideover.background
},
overlay: {
type: Boolean,
default: true
},
overlayBackgroundClass: {
type: String,
default: () => $ui.slideover.overlay.background
},
overlayTransitionClass: {
type: Object,
default: () => $ui.slideover.overlay.transition
},
widthClass: {
type: String,
default: () => $ui.slideover.width
},
headerClass: {
type: String,
default: () => $ui.slideover.header
},
transition: {
type: Boolean,
default: true
},
transitionClass: {
type: Object,
default: () => $ui.slideover.transition
}
})
const emit = defineEmits(['update:modelValue', 'close'])
const isOpen: WritableComputedRef<boolean> = computed({
get () {
return props.modelValue
},
set (value) {
emit('update:modelValue', value)
}
})
const slideoverClass = computed(() => {
return classNames(
props.baseClass,
props.widthClass,
props.backgroundClass
)
})
const overlayTransition = computed(() => {
if (!props.transition) {
return {}
}
return props.overlayTransitionClass
})
const slideoverTransition = computed(() => {
if (!props.transition) {
return {}
}
return {
enterFrom: props.side === 'left' ? '-translate-x-full' : 'translate-x-full',
enterTo: 'translate-x-0',
leaveFrom: 'translate-x-0',
leaveTo: props.side === 'left' ? '-translate-x-full' : 'translate-x-full',
...props.transitionClass
}
})
function close (value: boolean) {
isOpen.value = value
emit('close')
}
</script>
<script lang="ts">
export default { name: 'USlideover' }
import { computed, defineComponent } from 'vue'
import type { WritableComputedRef, PropType } from 'vue'
import { defu } from 'defu'
import { Dialog, DialogPanel, TransitionRoot, TransitionChild } from '@headlessui/vue'
import { useAppConfig } from '#imports'
// TODO: Remove
// @ts-expect-error
import appConfig from '#build/app.config'
// const appConfig = useAppConfig()
export default defineComponent({
components: {
// eslint-disable-next-line vue/no-reserved-component-names
Dialog,
DialogPanel,
TransitionRoot,
TransitionChild
},
props: {
modelValue: {
type: Boolean as PropType<boolean>,
default: false
},
appear: {
type: Boolean,
default: false
},
side: {
type: String,
default: 'left',
validator: (value: string) => ['left', 'right'].includes(value)
},
overlay: {
type: Boolean,
default: true
},
transition: {
type: Boolean,
default: true
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.slideover>>,
default: () => appConfig.ui.slideover
}
},
emits: ['update:modelValue', 'close'],
setup (props, { emit }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.slideover>>(() => defu({}, props.ui, appConfig.ui.slideover))
const isOpen: WritableComputedRef<boolean> = computed({
get () {
return props.modelValue
},
set (value) {
emit('update:modelValue', value)
}
})
const transitionClass = computed(() => {
if (!props.transition) {
return {}
}
return {
...ui.value.transition,
enterFrom: props.side === 'left' ? '-translate-x-full' : 'translate-x-full',
enterTo: 'translate-x-0',
leaveFrom: 'translate-x-0',
leaveTo: props.side === 'left' ? '-translate-x-full' : 'translate-x-full'
}
})
function close (value: boolean) {
isOpen.value = value
emit('close')
}
return {
// eslint-disable-next-line vue/no-dupe-keys
ui,
isOpen,
transitionClass,
close
}
}
})
</script>

View File

@@ -1,19 +1,19 @@
<template>
<div ref="trigger" :class="wrapperClass" @mouseover="onMouseOver" @mouseleave="onMouseLeave">
<div ref="trigger" :class="ui.wrapper" @mouseover="onMouseOver" @mouseleave="onMouseLeave">
<slot :open="open">
Hover me
hover
</slot>
<div v-if="open && !prevent" ref="container" :class="[containerClass, widthClass]">
<transition appear v-bind="transitionClass">
<div :class="[baseClass, backgroundClass, roundedClass, shadowClass, ringClass]">
<div v-if="open && !prevent" ref="container" :class="[ui.container, ui.width]">
<transition appear v-bind="ui.transition">
<div :class="[ui.base, ui.background, ui.rounded, ui.shadow, ui.ring]">
<slot name="text">
{{ text }}
</slot>
<span v-if="shortcuts?.length" :class="shortcutsClass">
<span class="mr-1 u-text-gray-700">&middot;</span>
<kbd v-for="shortcut of shortcuts" :key="shortcut" class="flex items-center justify-center font-sans px-1 h-4 min-w-[16px] text-[10px] u-bg-gray-100 rounded u-text-gray-900">
<span v-if="shortcuts?.length" :class="ui.shortcuts">
<span class="mr-1 text-gray-700 dark:text-gray-200">&middot;</span>
<kbd v-for="shortcut of shortcuts" :key="shortcut" class="flex items-center justify-center font-sans px-1 h-4 min-w-[16px] text-[10px] bg-gray-100 dark:bg-gray-800 rounded text-gray-900 dark:text-white">
{{ shortcut }}
</kbd>
</span>
@@ -23,125 +23,108 @@
</div>
</template>
<script setup lang="ts">
<script lang="ts">
import { computed, ref, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { computed, ref } from 'vue'
import { defu } from 'defu'
import { usePopper } from '../../composables/usePopper'
import type { PopperOptions } from '../../types'
import $ui from '#build/ui'
import { useAppConfig } from '#imports'
// TODO: Remove
// @ts-expect-error
import appConfig from '#build/app.config'
const props = defineProps({
text: {
type: String,
default: null
// const appConfig = useAppConfig()
export default defineComponent({
props: {
text: {
type: String,
default: null
},
prevent: {
type: Boolean,
default: false
},
shortcuts: {
type: Array as PropType<string[]>,
default: () => []
},
openDelay: {
type: Number,
default: 0
},
closeDelay: {
type: Number,
default: 0
},
popper: {
type: Object as PropType<PopperOptions>,
default: () => ({})
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.tooltip>>,
default: () => appConfig.ui.tooltip
}
},
prevent: {
type: Boolean,
default: false
},
shortcuts: {
type: Array as PropType<string[]>,
default: () => []
},
wrapperClass: {
type: String,
default: () => $ui.tooltip.wrapper
},
containerClass: {
type: String,
default: () => $ui.tooltip.container
},
widthClass: {
type: String,
default: () => $ui.tooltip.width
},
backgroundClass: {
type: String,
default: () => $ui.tooltip.background
},
shadowClass: {
type: String,
default: () => $ui.tooltip.shadow
},
ringClass: {
type: String,
default: () => $ui.tooltip.ring
},
roundedClass: {
type: String,
default: () => $ui.tooltip.rounded
},
baseClass: {
type: String,
default: () => $ui.tooltip.base
},
transitionClass: {
type: Object,
default: () => $ui.tooltip.transition
},
popperOptions: {
type: Object as PropType<PopperOptions>,
default: () => ({})
},
shortcutsClass: {
type: String,
default: () => $ui.tooltip.shortcuts
},
openDelay: {
type: Number,
default: 0
},
closeDelay: {
type: Number,
default: 0
setup (props) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.tooltip>>(() => defu({}, props.ui, appConfig.ui.tooltip))
const popper = computed<PopperOptions>(() => defu({}, props.popper, ui.value.popper as PopperOptions))
const [trigger, container] = usePopper(popper.value)
const open = ref(false)
let openTimeout: NodeJS.Timeout | null = null
let closeTimeout: NodeJS.Timeout | null = null
// Methods
function onMouseOver () {
// cancel programmed closing
if (closeTimeout) {
clearTimeout(closeTimeout)
closeTimeout = null
}
// dropdown already open
if (open.value) {
return
}
openTimeout = openTimeout || setTimeout(() => {
open.value = true
openTimeout = null
}, props.openDelay)
}
function onMouseLeave () {
// cancel programmed opening
if (openTimeout) {
clearTimeout(openTimeout)
openTimeout = null
}
// dropdown already closed
if (!open.value) {
return
}
closeTimeout = closeTimeout || setTimeout(() => {
open.value = false
closeTimeout = null
}, props.closeDelay)
}
return {
// eslint-disable-next-line vue/no-dupe-keys
ui,
trigger,
container,
open,
onMouseOver,
onMouseLeave
}
}
})
const popperOptions = computed<PopperOptions>(() => defu({}, props.popperOptions, $ui.tooltip.popperOptions))
const [trigger, container] = usePopper(popperOptions.value)
const open = ref(false)
let openTimeout: NodeJS.Timeout | null = null
let closeTimeout: NodeJS.Timeout | null = null
// Methods
function onMouseOver () {
// cancel programmed closing
if (closeTimeout) {
clearTimeout(closeTimeout)
closeTimeout = null
}
// dropdown already open
if (open.value) {
return
}
openTimeout = openTimeout || setTimeout(() => {
open.value = true
openTimeout = null
}, props.openDelay)
}
function onMouseLeave () {
// cancel programmed opening
if (openTimeout) {
clearTimeout(openTimeout)
openTimeout = null
}
// dropdown already closed
if (!open.value) {
return
}
closeTimeout = closeTimeout || setTimeout(() => {
open.value = false
closeTimeout = null
}, props.closeDelay)
}
</script>
<script lang="ts">
export default { name: 'UTooltip' }
</script>