import { reactive } from 'vue' /** * App-wide toasts. One shared queue, rendered by (mounted once in * App.vue), so any view or store can call `toast.success(...)` without placing * a component in its own template. */ export type ToastType = 'success' | 'error' | 'info' | 'warning' export interface ToastOptions { title?: string /** ms before it closes by itself; 0 keeps it until the user dismisses it. */ duration?: number /** Custom icon name; defaults to one per type. */ icon?: string } export interface ToastItem { id: number type: ToastType message: string title?: string icon: string duration: number } const DEFAULT_DURATION = 3200 // Older toasts beyond this are dropped so the stack never covers the screen. const MAX_VISIBLE = 3 const ICONS: Record = { success: 'vuesax-linear:tick-circle', error: 'vuesax-linear:close-circle', info: 'vuesax-linear:info-circle', warning: 'vuesax-linear:warning-2', } const state = reactive<{ items: ToastItem[] }>({ items: [] }) let nextId = 1 function show(type: ToastType, message: string, options: ToastOptions = {}): number { const id = nextId++ state.items.push({ id, type, message, title: options.title, icon: options.icon ?? ICONS[type], duration: options.duration ?? DEFAULT_DURATION, }) if (state.items.length > MAX_VISIBLE) state.items.splice(0, state.items.length - MAX_VISIBLE) return id } function dismiss(id: number) { const i = state.items.findIndex((t) => t.id === id) if (i !== -1) state.items.splice(i, 1) } export const toast = { show, dismiss, success: (message: string, options?: ToastOptions) => show('success', message, options), error: (message: string, options?: ToastOptions) => show('error', message, options), info: (message: string, options?: ToastOptions) => show('info', message, options), warning: (message: string, options?: ToastOptions) => show('warning', message, options), } export function useToast() { return { toasts: state.items, ...toast } }