import { useEffect, useState } from 'react' import { CheckCircle, XCircle, Info, AlertCircle, X } from 'lucide-react' import clsx from 'clsx' export interface ToastAction { label: string onClick: () => void } export interface Toast { id: string type: 'success' | 'error' | 'info' | 'warning' title: string message?: string duration?: number action?: ToastAction } // Global toast state (in production, use Zustand or Context) let toastListeners: ((toasts: Toast[]) => void)[] = [] let toastList: Toast[] = [] export const toast = { success: (title: string, message?: string, action?: ToastAction) => addToast('success', title, message, 5000, action), error: (title: string, message?: string, action?: ToastAction) => addToast('error', title, message, 5000, action), info: (title: string, message?: string, action?: ToastAction) => addToast('info', title, message, 5000, action), warning: (title: string, message?: string, action?: ToastAction) => addToast('warning', title, message, 5000, action), } function addToast( type: Toast['type'], title: string, message?: string, duration = 5000, action?: ToastAction ) { const id = Date.now().toString() + Math.random().toString(36).slice(2, 6) const newToast: Toast = { id, type, title, message, duration, action } toastList = [...toastList, newToast] toastListeners.forEach(listener => listener(toastList)) // Auto-remove after duration. Toasts with an action get a longer window // so the user has time to actually click Undo. const removeAfter = action ? Math.max(duration, 8000) : duration setTimeout(() => { removeToast(id) }, removeAfter) } function removeToast(id: string) { toastList = toastList.filter(t => t.id !== id) toastListeners.forEach(listener => listener(toastList)) } export function ToastContainer() { const [toasts, setToasts] = useState([]) useEffect(() => { const listener = (newToasts: Toast[]) => setToasts(newToasts) toastListeners.push(listener) return () => { toastListeners = toastListeners.filter(l => l !== listener) } }, []) // Subdued icons — smaller and muted so the toast reads as a // background notification rather than a modal. The colored tint // comes from the border-left accent, not a filled background. const icons = { success: , error: , info: , warning: , } // Single thin left accent bar per type instead of a full-border + // tinted fill. Keeps the toast visually quiet — the user can still // glance it but it doesn't compete with the rest of the UI. const accents = { success: 'border-l-pick', error: 'border-l-reject', info: 'border-l-primary', warning: 'border-l-star', } return (
{toasts.map((toast) => (
{icons[toast.type]}
{toast.title}
{toast.message && (
{toast.message}
)}
{toast.action && ( )}
))}
) }