feat: toast auto-dismiss with undo and pause support

This commit is contained in:
Your Name
2026-02-20 14:38:08 +02:00
parent 6757b7d800
commit d83a832cb1

View File

@@ -6,28 +6,69 @@ export interface Toast {
message: string message: string
type: 'success' | 'error' | 'info' type: 'success' | 'error' | 'info'
exiting?: boolean exiting?: boolean
duration: number
onUndo?: () => void
paused: boolean
timerId: ReturnType<typeof setTimeout> | null
remaining: number
}
const DEFAULT_DURATIONS: Record<Toast['type'], number> = {
success: 4000,
error: 8000,
info: 6000,
}
export interface ToastOptions {
onUndo?: () => void
} }
export const useToastStore = defineStore('toast', () => { export const useToastStore = defineStore('toast', () => {
const toasts = ref<Toast[]>([]) const toasts = ref<Toast[]>([])
let nextId = 0 let nextId = 0
const persistentNotifications = ref(false)
function addToast(message: string, type: Toast['type'] = 'info') { function startTimer(toast: Toast) {
const id = nextId++ if (persistentNotifications.value || toast.paused || toast.remaining <= 0) return
toasts.value.push({ id, message, type }) toast.timerId = setTimeout(() => {
removeToast(toast.id)
// Max 3 visible }, toast.remaining)
if (toasts.value.length > 3) {
toasts.value.shift()
} }
// Auto-dismiss after 3s function addToast(
setTimeout(() => removeToast(id), 3000) message: string,
type: Toast['type'] = 'info',
options?: ToastOptions
) {
const id = nextId++
const duration = DEFAULT_DURATIONS[type]
const toast: Toast = {
id,
message,
type,
duration,
onUndo: options?.onUndo,
paused: false,
timerId: null,
remaining: duration,
}
toasts.value.push(toast)
startTimer(toast)
// Cap at 5, oldest gets exiting state
if (toasts.value.length > 5) {
const oldest = toasts.value.find(t => !t.exiting)
if (oldest) removeToast(oldest.id)
}
} }
function removeToast(id: number) { function removeToast(id: number) {
const toast = toasts.value.find(t => t.id === id) const toast = toasts.value.find(t => t.id === id)
if (toast) { if (toast) {
if (toast.timerId) {
clearTimeout(toast.timerId)
toast.timerId = null
}
toast.exiting = true toast.exiting = true
setTimeout(() => { setTimeout(() => {
toasts.value = toasts.value.filter(t => t.id !== id) toasts.value = toasts.value.filter(t => t.id !== id)
@@ -35,9 +76,71 @@ export const useToastStore = defineStore('toast', () => {
} }
} }
function success(message: string) { addToast(message, 'success') } function pauseToast(id: number) {
function error(message: string) { addToast(message, 'error') } const toast = toasts.value.find(t => t.id === id)
function info(message: string) { addToast(message, 'info') } if (toast && !toast.paused && toast.timerId) {
clearTimeout(toast.timerId)
toast.timerId = null
toast.paused = true
}
}
return { toasts, addToast, removeToast, success, error, info } function resumeToast(id: number) {
const toast = toasts.value.find(t => t.id === id)
if (toast && toast.paused) {
toast.paused = false
startTimer(toast)
}
}
function undoToast(id: number) {
const toast = toasts.value.find(t => t.id === id)
if (toast && toast.onUndo) {
toast.onUndo()
}
removeToast(id)
}
function setPersistentNotifications(val: boolean) {
persistentNotifications.value = val
if (val) {
// Clear all running timers
for (const toast of toasts.value) {
if (toast.timerId) {
clearTimeout(toast.timerId)
toast.timerId = null
}
}
} else {
// Restart timers for non-paused toasts
for (const toast of toasts.value) {
if (!toast.paused && !toast.exiting) {
startTimer(toast)
}
}
}
}
function success(message: string, options?: ToastOptions) {
addToast(message, 'success', options)
}
function error(message: string, options?: ToastOptions) {
addToast(message, 'error', options)
}
function info(message: string, options?: ToastOptions) {
addToast(message, 'info', options)
}
return {
toasts,
addToast,
removeToast,
pauseToast,
resumeToast,
undoToast,
setPersistentNotifications,
success,
error,
info,
}
}) })