diff --git a/dashboard/src/components/ToastContainer.tsx b/dashboard/src/components/ToastContainer.tsx index dc91ca45..013b6e8b 100644 --- a/dashboard/src/components/ToastContainer.tsx +++ b/dashboard/src/components/ToastContainer.tsx @@ -2,7 +2,8 @@ * components/ToastContainer.tsx — Global toast notification renderer. */ -import { X } from 'lucide-react'; +import { useEffect, useState } from 'react'; +import { X, CheckCircle, AlertTriangle, Info, AlertCircle, Trash2 } from 'lucide-react'; import { useToastStore } from '../store/useToastStore'; import type { ToastType } from '../store/useToastStore'; @@ -13,14 +14,42 @@ const TYPE_STYLES: Record = { warning: 'border-yellow-500/50 bg-yellow-950/80 text-yellow-200', }; +const TYPE_ICONS: Record = { + error: AlertCircle, + success: CheckCircle, + info: Info, + warning: AlertTriangle, +}; + +const AUTO_DISMISS_MS = 4000; + function ToastItem({ id, type, title, description }: { id: string; type: ToastType; title: string; description?: string }) { const removeToast = useToastStore((s) => s.removeToast); + const [progress, setProgress] = useState(100); + const Icon = TYPE_ICONS[type]; + + useEffect(() => { + const start = Date.now(); + const interval = setInterval(() => { + const elapsed = Date.now() - start; + const remaining = Math.max(0, 100 - (elapsed / AUTO_DISMISS_MS) * 100); + setProgress(remaining); + if (remaining <= 0) clearInterval(interval); + }, 50); + return () => clearInterval(interval); + }, [id]); return (
+ {/* Progress bar */} +
+

{title}

{description &&

{description}

} @@ -38,11 +67,24 @@ function ToastItem({ id, type, title, description }: { id: string; type: ToastTy export default function ToastContainer() { const toasts = useToastStore((s) => s.toasts); + const removeToast = useToastStore((s) => s.removeToast); if (toasts.length === 0) return null; return (
+ {toasts.length > 1 && ( +
+ +
+ )} {toasts.map((t) => (