feat: toast auto-dismiss with undo and pause support
This commit is contained in:
@@ -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,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user