From 24b3caf0dacb2c95252e5a76691557bf9f617e0e Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 20 Feb 2026 14:38:08 +0200 Subject: [PATCH] feat: toast auto-dismiss with undo and pause support --- src/stores/toast.ts | 127 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 115 insertions(+), 12 deletions(-) diff --git a/src/stores/toast.ts b/src/stores/toast.ts index 3ae11ab..a32c7ed 100644 --- a/src/stores/toast.ts +++ b/src/stores/toast.ts @@ -6,28 +6,69 @@ export interface Toast { message: string type: 'success' | 'error' | 'info' exiting?: boolean + duration: number + onUndo?: () => void + paused: boolean + timerId: ReturnType | null + remaining: number +} + +const DEFAULT_DURATIONS: Record = { + success: 4000, + error: 8000, + info: 6000, +} + +export interface ToastOptions { + onUndo?: () => void } export const useToastStore = defineStore('toast', () => { const toasts = ref([]) let nextId = 0 + const persistentNotifications = ref(false) - function addToast(message: string, type: Toast['type'] = 'info') { + function startTimer(toast: Toast) { + if (persistentNotifications.value || toast.paused || toast.remaining <= 0) return + toast.timerId = setTimeout(() => { + removeToast(toast.id) + }, toast.remaining) + } + + function addToast( + message: string, + type: Toast['type'] = 'info', + options?: ToastOptions + ) { const id = nextId++ - toasts.value.push({ id, message, type }) - - // Max 3 visible - if (toasts.value.length > 3) { - toasts.value.shift() + 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) - // Auto-dismiss after 3s - setTimeout(() => removeToast(id), 3000) + // 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) { const toast = toasts.value.find(t => t.id === id) if (toast) { + if (toast.timerId) { + clearTimeout(toast.timerId) + toast.timerId = null + } toast.exiting = true setTimeout(() => { 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 error(message: string) { addToast(message, 'error') } - function info(message: string) { addToast(message, 'info') } + function pauseToast(id: number) { + const toast = toasts.value.find(t => t.id === id) + 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, + } })