mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-14 12:14:41 +01:00
199 lines
6.5 KiB
Vue
199 lines
6.5 KiB
Vue
<script lang="ts">
|
|
import type { ToastRootProps, ToastRootEmits } from 'reka-ui'
|
|
import type { AppConfig } from '@nuxt/schema'
|
|
import theme from '#build/ui/toast'
|
|
import type { AvatarProps, ButtonProps, ProgressProps } from '../types'
|
|
import type { StringOrVNode, ComponentConfig } from '../types/utils'
|
|
|
|
type Toast = ComponentConfig<typeof theme, AppConfig, 'toast'>
|
|
|
|
export interface ToastProps extends Pick<ToastRootProps, 'defaultOpen' | 'open' | 'type' | 'duration'> {
|
|
/**
|
|
* The element or component this component should render as.
|
|
* @defaultValue 'li'
|
|
*/
|
|
as?: any
|
|
title?: StringOrVNode
|
|
description?: StringOrVNode
|
|
/**
|
|
* @IconifyIcon
|
|
*/
|
|
icon?: string
|
|
avatar?: AvatarProps
|
|
/**
|
|
* @defaultValue 'primary'
|
|
*/
|
|
color?: Toast['variants']['color']
|
|
/**
|
|
* The orientation between the content and the actions.
|
|
* @defaultValue 'vertical'
|
|
*/
|
|
orientation?: Toast['variants']['orientation']
|
|
/**
|
|
* Display a close button to dismiss the toast.
|
|
* `{ size: 'md', color: 'neutral', variant: 'link' }`{lang="ts-type"}
|
|
* @defaultValue true
|
|
*/
|
|
close?: boolean | Partial<ButtonProps>
|
|
/**
|
|
* The icon displayed in the close button.
|
|
* @defaultValue appConfig.ui.icons.close
|
|
* @IconifyIcon
|
|
*/
|
|
closeIcon?: string
|
|
/**
|
|
* Display a list of actions:
|
|
* - under the title and description when orientation is `vertical`
|
|
* - next to the close button when orientation is `horizontal`
|
|
* `{ size: 'xs' }`{lang="ts-type"}
|
|
*/
|
|
actions?: ButtonProps[]
|
|
/**
|
|
* Display a progress bar showing the toast's remaining duration.
|
|
* `{ size: 'sm' }`{lang="ts-type"}
|
|
* @defaultValue true
|
|
*/
|
|
progress?: boolean | Pick<ProgressProps, 'color'>
|
|
class?: any
|
|
ui?: Toast['slots']
|
|
}
|
|
|
|
export interface ToastEmits extends ToastRootEmits {}
|
|
|
|
export interface ToastSlots {
|
|
leading(props?: {}): any
|
|
title(props?: {}): any
|
|
description(props?: {}): any
|
|
actions(props?: {}): any
|
|
close(props: { ui: { [K in keyof Required<Toast['slots']>]: (props?: Record<string, any>) => string } }): any
|
|
}
|
|
</script>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, onMounted, nextTick } from 'vue'
|
|
import { ToastRoot, ToastTitle, ToastDescription, ToastAction, ToastClose, useForwardPropsEmits } from 'reka-ui'
|
|
import { reactivePick } from '@vueuse/core'
|
|
import { useAppConfig } from '#imports'
|
|
import { useLocale } from '../composables/useLocale'
|
|
import { tv } from '../utils/tv'
|
|
import UIcon from './Icon.vue'
|
|
import UAvatar from './Avatar.vue'
|
|
import UButton from './Button.vue'
|
|
import UProgress from './Progress.vue'
|
|
|
|
const props = withDefaults(defineProps<ToastProps>(), {
|
|
orientation: 'vertical',
|
|
close: true,
|
|
progress: true
|
|
})
|
|
const emits = defineEmits<ToastEmits>()
|
|
const slots = defineSlots<ToastSlots>()
|
|
|
|
const { t } = useLocale()
|
|
const appConfig = useAppConfig() as Toast['AppConfig']
|
|
|
|
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'defaultOpen', 'open', 'duration', 'type'), emits)
|
|
|
|
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.toast || {}) })({
|
|
color: props.color,
|
|
orientation: props.orientation,
|
|
title: !!props.title || !!slots.title
|
|
}))
|
|
|
|
const el = ref()
|
|
const height = ref(0)
|
|
|
|
onMounted(() => {
|
|
if (!el.value) {
|
|
return
|
|
}
|
|
|
|
nextTick(() => {
|
|
height.value = el.value?.$el?.getBoundingClientRect()?.height
|
|
})
|
|
})
|
|
|
|
defineExpose({
|
|
height
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<ToastRoot
|
|
ref="el"
|
|
v-slot="{ remaining, duration, open }"
|
|
v-bind="rootProps"
|
|
:data-orientation="orientation"
|
|
:class="ui.root({ class: [props.ui?.root, props.class] })"
|
|
:style="{ '--height': height }"
|
|
>
|
|
<slot name="leading">
|
|
<UAvatar v-if="avatar" :size="((props.ui?.avatarSize || ui.avatarSize()) as AvatarProps['size'])" v-bind="avatar" :class="ui.avatar({ class: props.ui?.avatar })" />
|
|
<UIcon v-else-if="icon" :name="icon" :class="ui.icon({ class: props.ui?.icon })" />
|
|
</slot>
|
|
|
|
<div :class="ui.wrapper({ class: props.ui?.wrapper })">
|
|
<ToastTitle v-if="title || !!slots.title" :class="ui.title({ class: props.ui?.title })">
|
|
<slot name="title">
|
|
<component :is="title()" v-if="typeof title === 'function'" />
|
|
<component :is="title" v-else-if="typeof title === 'object'" />
|
|
<template v-else>
|
|
{{ title }}
|
|
</template>
|
|
</slot>
|
|
</ToastTitle>
|
|
<ToastDescription v-if="description || !!slots.description" :class="ui.description({ class: props.ui?.description })">
|
|
<slot name="description">
|
|
<component :is="description()" v-if="typeof description === 'function'" />
|
|
<component :is="description" v-else-if="typeof description === 'object'" />
|
|
<template v-else>
|
|
{{ description }}
|
|
</template>
|
|
</slot>
|
|
</ToastDescription>
|
|
|
|
<div v-if="orientation === 'vertical' && (actions?.length || !!slots.actions)" :class="ui.actions({ class: props.ui?.actions })">
|
|
<slot name="actions">
|
|
<ToastAction v-for="(action, index) in actions" :key="index" :alt-text="action.label || 'Action'" as-child @click.stop>
|
|
<UButton size="xs" :color="color" v-bind="action" />
|
|
</ToastAction>
|
|
</slot>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="(orientation === 'horizontal' && (actions?.length || !!slots.actions)) || close" :class="ui.actions({ class: props.ui?.actions, orientation: 'horizontal' })">
|
|
<template v-if="orientation === 'horizontal' && (actions?.length || !!slots.actions)">
|
|
<slot name="actions">
|
|
<ToastAction v-for="(action, index) in actions" :key="index" :alt-text="action.label || 'Action'" as-child @click.stop>
|
|
<UButton size="xs" :color="color" v-bind="action" />
|
|
</ToastAction>
|
|
</slot>
|
|
</template>
|
|
|
|
<ToastClose v-if="close || !!slots.close" as-child>
|
|
<slot name="close" :ui="ui">
|
|
<UButton
|
|
v-if="close"
|
|
:icon="closeIcon || appConfig.ui.icons.close"
|
|
color="neutral"
|
|
variant="link"
|
|
:aria-label="t('toast.close')"
|
|
v-bind="(typeof close === 'object' ? close as Partial<ButtonProps> : {})"
|
|
:class="ui.close({ class: props.ui?.close })"
|
|
@click.stop
|
|
/>
|
|
</slot>
|
|
</ToastClose>
|
|
</div>
|
|
|
|
<UProgress
|
|
v-if="progress && open && remaining > 0 && duration"
|
|
:model-value="remaining / duration * 100"
|
|
:color="color"
|
|
v-bind="(typeof progress === 'object' ? progress as Partial<ProgressProps> : {})"
|
|
size="sm"
|
|
:class="ui.progress({ class: props.ui?.progress })"
|
|
/>
|
|
</ToastRoot>
|
|
</template>
|