feat(Toast): implement progress duration

Resolves #51
This commit is contained in:
Benjamin Canac
2024-04-11 15:40:37 +02:00
parent 7350e8e46b
commit d726e4ddac
5 changed files with 29 additions and 17 deletions

View File

@@ -2,7 +2,7 @@ export default defineAppConfig({
toaster: {
position: 'bottom-right' as const,
expand: true,
duration: 60000
duration: 5000
},
ui: {
primary: 'sky',

View File

@@ -109,7 +109,7 @@ function removeToast () {
<template>
<div class="flex flex-col items-center gap-8">
<div>
<div class="flex flex-col gap-2">
<URadioGroup v-model="appConfig.toaster.position" :options="positions" />
<UCheckbox v-model="appConfig.toaster.expand" label="Expand" class="mt-1" />
<UInput v-model="appConfig.toaster.duration" label="Duration" type="number" class="mt-1" />

View File

@@ -5,7 +5,7 @@ import type { ToastRootProps, ToastRootEmits } from 'radix-vue'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/toast'
import type { AvatarProps, ButtonProps, IconProps } from '#ui/types'
import type { AvatarProps, ButtonProps, IconProps, ToasterContext } from '#ui/types'
const appConfig = _appConfig as AppConfig & { ui: { toast: Partial<typeof theme> } }
@@ -19,17 +19,17 @@ export interface ToastProps extends Omit<ToastRootProps, 'asChild' | 'forceMount
icon?: IconProps['name']
avatar?: AvatarProps
color?: ToastVariants['color']
actions?: (ButtonProps & { click?: () => void })[]
actions?: ButtonProps[]
close?: ButtonProps | null
class?: any
ui?: Partial<typeof toast.slots>
}
export interface ToastEmits extends ToastRootEmits { }
export interface ToastEmits extends ToastRootEmits {}
</script>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ref, computed, inject, onMounted } from 'vue'
import { ToastRoot, ToastTitle, ToastDescription, ToastAction, ToastClose, useForwardPropsEmits } from 'radix-vue'
import { reactivePick } from '@vueuse/core'
import { useAppConfig } from '#imports'
@@ -38,14 +38,15 @@ import { UIcon, UAvatar } from '#components'
const props = defineProps<ToastProps>()
const emits = defineEmits<ToastEmits>()
const toaster = inject<ToasterContext>('Toaster')
const appConfig = useAppConfig()
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'defaultOpen', 'duration', 'open', 'type'), emits)
const multiline = computed(() => !!props.title && !!props.description)
const duration = computed(() => props.duration || toaster?.value.duration)
const ui = computed(() => tv({ extend: toast, slots: props.ui })({
color: props.color
}))
const ui = computed(() => tv({ extend: toast, slots: props.ui })({ color: props.color }))
const el = ref()
const height = ref(0)
@@ -56,7 +57,7 @@ onMounted(() => {
}
setTimeout(() => {
height.value = el.value.$el.getBoundingClientRect().height
height.value = el.value.$el.getBoundingClientRect()?.height
}, 0)
})
@@ -66,7 +67,13 @@ defineExpose({
</script>
<template>
<ToastRoot ref="el" v-bind="rootProps" :class="ui.root({ class: props.class, multiline })" :style="{ '--height': height }">
<ToastRoot
ref="el"
v-slot="{ remaining }"
v-bind="rootProps"
:class="ui.root({ class: props.class, multiline })"
:style="{ '--height': height }"
>
<UAvatar v-if="avatar" size="2xl" v-bind="avatar" :class="ui.avatar()" />
<UIcon v-else-if="icon" :name="icon" :class="ui.icon()" />
@@ -110,7 +117,6 @@ defineExpose({
</ToastClose>
</div>
<div :class="ui.progress()" />
<div :class="ui.mask()" />
<div v-if="remaining && duration" :class="ui.progress()" :style="{ width: `${remaining / duration * 100}%` }" />
</ToastRoot>
</template>

View File

@@ -1,4 +1,5 @@
<script lang="ts">
import type { ComputedRef } from 'vue'
import { tv, type VariantProps } from 'tailwind-variants'
import type { ToastProviderProps } from 'radix-vue'
import type { AppConfig } from '@nuxt/schema'
@@ -17,17 +18,21 @@ export interface ToasterProps extends Omit<ToastProviderProps, 'swipeDirection'>
class?: any
ui?: Partial<typeof toaster.slots>
}
export type ToasterContext = ComputedRef<{
duration: number
}>
</script>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ref, computed, provide } from 'vue'
import { ToastProvider, ToastViewport, useForwardProps } from 'radix-vue'
import { reactivePick } from '@vueuse/core'
import { useToast } from '#imports'
import { UToast } from '#components'
import { omit } from '#ui/utils'
const props = withDefaults(defineProps<ToasterProps>(), { expand: true })
const props = withDefaults(defineProps<ToasterProps>(), { expand: true, duration: 5000 })
const providerProps = useForwardProps(reactivePick(props, 'duration', 'label', 'swipeThreshold'))
@@ -73,6 +78,8 @@ const frontHeight = computed(() => refs.value[refs.value.length - 1]?.height ||
function getOffset (index: number) {
return refs.value.slice(index + 1).reduce((acc, { height }) => acc + height + 16, 0)
}
provide<ToasterContext>('Toaster', providerProps)
</script>
<template>

View File

@@ -7,8 +7,7 @@ export default (config: { colors: string[] }) => ({
icon: 'shrink-0 size-5',
avatar: 'shrink-0',
actions: 'flex gap-1.5 shrink-0',
progress: 'absolute inset-0 rounded-lg border-b-2 z-[-1]',
mask: 'absolute top-0 inset-0 bottom-[2px] bg-white dark:bg-gray-900 z-[-1]',
progress: 'absolute inset-0 border-b-2 z-[-1]',
close: 'p-1'
},
variants: {