/** * Toast primitive + ToastContainer * Variants: success, error, info, warn * Auto-dismiss, hover-pause, max 5 visible, stacks bottom-right */ const { html } = window; const { useState, useEffect, useRef, useCallback } = hooks; const { h, render } = preact; // ── Singleton state ────────────────────────── let _toasts = []; let _id = 0; let _setToasts = null; export function toast(message, variant = 'info', duration) { if (!duration) duration = variant === 'error' ? 5000 : 3000; const id = ++_id; const t = { id, message, variant, duration }; _toasts = [..._toasts, t]; if (_toasts.length > 5) _toasts = _toasts.slice(-5); if (_setToasts) _setToasts([..._toasts]); return id; } toast.success = (msg, dur) => toast(msg, 'success', dur); toast.error = (msg, dur) => toast(msg, 'error', dur); toast.info = (msg, dur) => toast(msg, 'info', dur); toast.warn = (msg, dur) => toast(msg, 'warn', dur); function dismiss(id) { _toasts = _toasts.filter(t => t.id !== id); if (_setToasts) _setToasts([..._toasts]); } // ── Single Toast ───────────────────────────── function ToastItem({ id, message, variant, duration }) { const [exiting, setExiting] = useState(false); const timer = useRef(null); const hovered = useRef(false); const startTimer = useCallback(() => { clearTimeout(timer.current); timer.current = setTimeout(() => { setExiting(true); setTimeout(() => dismiss(id), 200); }, duration); }, [id, duration]); useEffect(() => { startTimer(); return () => clearTimeout(timer.current); }, [startTimer]); const onEnter = useCallback(() => { hovered.current = true; clearTimeout(timer.current); }, []); const onLeave = useCallback(() => { hovered.current = false; startTimer(); }, [startTimer]); const cls = `sw-toast sw-toast--${variant} ${exiting ? 'sw-toast--exit' : 'sw-toast--enter'}`; return html`