import { createContext, useCallback, useContext, useEffect, useRef, useState, type ReactNode, } from 'react' // ─── Types ─────────────────────────────────────────────────────────────────── export interface Toast { id: string message: string level: 'info' | 'warn' | 'error' | 'success' context?: string } interface ToastContextValue { addToast: (message: string, level: Toast['level'], context?: string) => void } // ─── Context ───────────────────────────────────────────────────────────────── const ToastContext = createContext(null) // ─── Level → border color ──────────────────────────────────────────────────── const LEVEL_COLOR: Record = { info: '#6366f1', warn: '#f59e0b', error: '#ef4444', success: '#22c55e', } const DISMISS_DELAY: Record = { info: 4000, success: 4000, warn: 7000, error: 7000, } const MAX_VISIBLE = 4 // ─── ToastItem ──────────────────────────────────────────────────────────────── interface ToastItemProps { toast: Toast onDismiss: (id: string) => void } function ToastItem({ toast, onDismiss }: ToastItemProps) { const [visible, setVisible] = useState(false) // Slide-in on mount useEffect(() => { const raf = requestAnimationFrame(() => setVisible(true)) return () => cancelAnimationFrame(raf) }, []) const handleDismiss = () => { setVisible(false) setTimeout(() => onDismiss(toast.id), 220) } const borderColor = LEVEL_COLOR[toast.level] return (
{/* Level dot */} {/* Content */}
{toast.context && ( [{toast.context}] )} {toast.message}
{/* Dismiss button */}
) } // ─── ToastProvider ──────────────────────────────────────────────────────────── export function ToastProvider({ children }: { children: ReactNode }) { const [toasts, setToasts] = useState([]) const timersRef = useRef>>(new Map()) const removeToast = useCallback((id: string) => { setToasts((prev) => prev.filter((t) => t.id !== id)) const timer = timersRef.current.get(id) if (timer !== undefined) { clearTimeout(timer) timersRef.current.delete(id) } }, []) const addToast = useCallback( (message: string, level: Toast['level'], context?: string) => { const id = Date.now().toString() const toast: Toast = { id, message, level, context } setToasts((prev) => { const next = [...prev, toast] // Keep only the last MAX_VISIBLE toasts return next.slice(-MAX_VISIBLE) }) const delay = DISMISS_DELAY[level] const timer = setTimeout(() => removeToast(id), delay) timersRef.current.set(id, timer) }, [removeToast], ) // Cleanup all timers on unmount useEffect(() => { const timers = timersRef.current return () => { timers.forEach((timer) => clearTimeout(timer)) timers.clear() } }, []) return ( {children} {/* Toast container */}
{toasts.map((toast) => (
))}
) } // ─── useToast ───────────────────────────────────────────────────────────────── export function useToast(): ToastContextValue { const ctx = useContext(ToastContext) if (!ctx) { throw new Error('useToast must be used inside ') } return ctx }