diff --git a/dashboard/components/Toast.js b/dashboard/components/Toast.js new file mode 100644 index 0000000..566afeb --- /dev/null +++ b/dashboard/components/Toast.js @@ -0,0 +1,125 @@ +import { html, useState, useEffect, useCallback, useRef } from '../lib/preact.js'; + +/** + * Lightweight toast notification system. + * Tracks error counts and surfaces persistent issues. + */ + +// Singleton state for toast management (shared across components) +let toastListeners = []; +let toastIdCounter = 0; + +export function showToast(message, type = 'error', duration = 5000) { + const id = ++toastIdCounter; + const toast = { id, message, type, duration }; + toastListeners.forEach(listener => listener(toast)); + return id; +} + +export function ToastContainer() { + const [toasts, setToasts] = useState([]); + const timeoutIds = useRef(new Map()); + + useEffect(() => { + const listener = (toast) => { + setToasts(prev => [...prev, toast]); + if (toast.duration > 0) { + const timeoutId = setTimeout(() => { + timeoutIds.current.delete(toast.id); + setToasts(prev => prev.filter(t => t.id !== toast.id)); + }, toast.duration); + timeoutIds.current.set(toast.id, timeoutId); + } + }; + toastListeners.push(listener); + return () => { + toastListeners = toastListeners.filter(l => l !== listener); + // Clear all pending timeouts on unmount + timeoutIds.current.forEach(id => clearTimeout(id)); + timeoutIds.current.clear(); + }; + }, []); + + const dismiss = useCallback((id) => { + // Clear auto-dismiss timeout if exists + const timeoutId = timeoutIds.current.get(id); + if (timeoutId) { + clearTimeout(timeoutId); + timeoutIds.current.delete(id); + } + setToasts(prev => prev.filter(t => t.id !== id)); + }, []); + + if (toasts.length === 0) return null; + + return html` +