import { useEffect, useState } from "react"; import { useShallow } from "zustand/react/shallow"; import { useAppState, type Toast } from "../../store/appState"; const TONE: Record = { error: { border: "var(--error)", bg: "var(--error-muted)", fg: "var(--error)", glyph: "▲", }, success: { border: "var(--success)", bg: "var(--success-muted)", fg: "var(--success)", glyph: "✓", }, info: { border: "var(--border-color)", bg: "var(--accent-muted)", fg: "var(--accent)", glyph: "●", }, }; function ToastCard({ toast, onDismiss }: { toast: Toast; onDismiss: () => void }) { const [expanded, setExpanded] = useState(false); const tone = TONE[toast.kind]; // Errors stay until dismissed; transient confirmations time out. useEffect(() => { if (toast.kind === "error") return; const timer = setTimeout(onDismiss, 6000); return () => clearTimeout(timer); }, [toast.kind, onDismiss]); return (
{/* Clamped. A toast message is normally a sentence, but some of them quote text a *container* wrote — and this card is `z-[60]`, above every modal, with its dismiss button at the top. An unclamped message of a few kilobytes is a card taller than the viewport whose ✕ has been pushed off-screen, i.e. an unclosable overlay. The `detail` block below has always had `max-h-40 overflow-auto`; this half did not. */}
{toast.message}
{toast.detail && ( <> {expanded && (
                {toast.detail}
              
)} )}
); } /** Bottom-right stack. Errors get a home here instead of a 12px card line. */ export default function ToastHost() { const { toasts, dismissToast } = useAppState( useShallow((s) => ({ toasts: s.toasts, dismissToast: s.dismissToast })), ); if (toasts.length === 0) return null; return (
{toasts.map((toast) => ( dismissToast(toast.id)} /> ))}
); }