127 lines
3.3 KiB
Vue
127 lines
3.3 KiB
Vue
<template>
|
|
<div
|
|
class="toast-container pointer-events-none fixed bottom-4 right-4 z-50 flex flex-col gap-2 items-end"
|
|
>
|
|
<TransitionGroup name="toast">
|
|
<div
|
|
v-for="toast in toasts"
|
|
:key="toast.id"
|
|
class="toast-item"
|
|
:class="[
|
|
'transform transition-all duration-300 ease-in-out',
|
|
toast.visible ? 'translate-y-0 opacity-100 pointer-events-auto' : 'translate-y-4 opacity-0 pointer-events-none'
|
|
]"
|
|
>
|
|
<div
|
|
class="alert toast-card relative shadow-md px-3 py-2 text-sm overflow-hidden"
|
|
:class="getToastClasses(toast.type)"
|
|
>
|
|
<div class="flex items-center gap-2">
|
|
<!-- Icon -->
|
|
<div class="flex-shrink-0">
|
|
<IconLucideCheck
|
|
v-if="toast.type === 'success'"
|
|
class="w-4 h-4"
|
|
aria-hidden="true"
|
|
/>
|
|
<IconLucideCircleX
|
|
v-else-if="toast.type === 'error'"
|
|
class="w-4 h-4"
|
|
aria-hidden="true"
|
|
/>
|
|
<IconLucideAlertTriangle
|
|
v-else-if="toast.type === 'warning'"
|
|
class="w-4 h-4"
|
|
aria-hidden="true"
|
|
/>
|
|
<IconLucideInfo
|
|
v-else
|
|
class="w-4 h-4"
|
|
aria-hidden="true"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Message -->
|
|
<div class="flex-1">
|
|
<span class="font-medium">{{ toast.message }}</span>
|
|
</div>
|
|
|
|
<!-- Close button -->
|
|
<button
|
|
class="btn btn-ghost btn-2xs"
|
|
@click="removeToast(toast.id)"
|
|
>
|
|
<IconLucideX class="w-3 h-3" aria-hidden="true" />
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Progress bar for auto-dismiss toasts -->
|
|
<div
|
|
v-if="toast.duration > 0"
|
|
class="absolute bottom-0 left-0 h-0.5 bg-current opacity-30 rounded-full"
|
|
:style="{ animation: `toast-progress ${toast.duration}ms linear forwards` }"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</TransitionGroup>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { useToast } from '~/composables/useToast'
|
|
import IconLucideCheck from '~icons/lucide/check'
|
|
import IconLucideX from '~icons/lucide/x'
|
|
import IconLucideCircleX from '~icons/lucide/circle-x'
|
|
import IconLucideAlertTriangle from '~icons/lucide/alert-triangle'
|
|
import IconLucideInfo from '~icons/lucide/info'
|
|
|
|
const { toasts, removeToast } = useToast()
|
|
|
|
const getToastClasses = (type) => {
|
|
switch (type) {
|
|
case 'success':
|
|
return 'alert-success text-success-content'
|
|
case 'error':
|
|
return 'alert-error text-error-content'
|
|
case 'warning':
|
|
return 'alert-warning text-warning-content'
|
|
case 'info':
|
|
return 'alert-info text-info-content'
|
|
default:
|
|
return 'alert-info'
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.toast-enter-active,
|
|
.toast-leave-active {
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.toast-enter-from {
|
|
opacity: 0;
|
|
transform: translateY(16px);
|
|
}
|
|
|
|
.toast-leave-to {
|
|
opacity: 0;
|
|
transform: translateY(16px);
|
|
}
|
|
|
|
.toast-move {
|
|
transition: transform 0.3s ease;
|
|
}
|
|
|
|
.toast-card {
|
|
max-width: 20rem;
|
|
pointer-events: auto;
|
|
border-radius: 0.75rem;
|
|
}
|
|
|
|
@keyframes toast-progress {
|
|
from { width: 100%; }
|
|
to { width: 0%; }
|
|
}
|
|
</style>
|