Files
kite/frontend/src/components/ui/ToastContainer.vue
T
curo1305 f92d98d067 feat(10-04): implement toast store, ToastContainer, mount in App.vue
- toast.js: reactive toasts array, show/dismiss with auto-dismiss via setTimeout
- ToastContainer.vue: Options API, Teleport to body, TransitionGroup, data-test attr
- type-to-class maps: accentClass/iconName/iconColorClass for success/error/warning/info
- App.vue: import and mount <ToastContainer /> after layout div
- all 10 tests pass; SettingsAccountTab and TotpEnrollment regressions green
2026-06-15 20:11:12 +02:00

74 lines
2.0 KiB
Vue

<template>
<Teleport to="body">
<div class="fixed bottom-4 right-4 z-[9999] flex flex-col-reverse gap-2 pointer-events-none">
<TransitionGroup name="toast">
<div
v-for="toast in toastStore.toasts"
:key="toast.id"
data-test="toast"
class="pointer-events-auto flex items-center gap-3 bg-white rounded-xl shadow-lg border border-gray-100 overflow-hidden max-w-sm min-w-[280px] cursor-pointer"
@click="toastStore.dismiss(toast.id)"
>
<div class="w-1 self-stretch shrink-0" :class="accentClass(toast.type)"></div>
<AppIcon :name="iconName(toast.type)" class="w-5 h-5 shrink-0" :class="iconColorClass(toast.type)" />
<p class="text-sm text-gray-800 flex-1 py-3 pr-4">{{ toast.message }}</p>
</div>
</TransitionGroup>
</div>
</Teleport>
</template>
<script>
import AppIcon from './AppIcon.vue'
import { useToastStore } from '../../stores/toast.js'
export default {
name: 'ToastContainer',
components: { AppIcon },
data() {
return { toastStore: useToastStore() }
},
methods: {
accentClass(type) {
const map = {
success: 'bg-green-500',
error: 'bg-red-500',
warning: 'bg-amber-400',
info: 'bg-sky-400',
}
return map[type] ?? 'bg-gray-400'
},
iconName(type) {
const map = {
success: 'checkCircle',
error: 'exclamationCircle',
warning: 'warning',
info: 'exclamationCircle',
}
return map[type] ?? 'exclamationCircle'
},
iconColorClass(type) {
const map = {
success: 'text-green-500',
error: 'text-red-500',
warning: 'text-amber-500',
info: 'text-sky-500',
}
return map[type] ?? 'text-gray-500'
},
},
}
</script>
<style scoped>
.toast-enter-active,
.toast-leave-active {
transition: all 0.2s ease;
}
.toast-enter-from,
.toast-leave-to {
opacity: 0;
transform: translateX(20px);
}
</style>