import { useCallback, useMemo, useRef, useState } from "react"; import type { ReactNode } from "react"; import { ToastContext, type ToastApi, type ToastItem, type ToastTone, } from "./toast-context"; const AUTO_DISMISS_MS = 4500; /** Mounts the toast stack and provides the imperative toast API to descendants. */ export function ToastProvider({ children }: { children: ReactNode }) { const [toasts, setToasts] = useState([]); const nextId = useRef(1); const dismiss = useCallback((id: number) => { setToasts((prev) => prev.filter((t) => t.id !== id)); }, []); const push = useCallback( (tone: ToastTone, title: string, message?: string) => { const id = nextId.current++; setToasts((prev) => [...prev, { id, tone, title, message }]); window.setTimeout(() => dismiss(id), AUTO_DISMISS_MS); }, [dismiss], ); const api = useMemo( () => ({ success: (title, message) => push("success", title, message), error: (title, message) => push("error", title, message), info: (title, message) => push("info", title, message), }), [push], ); return ( {children} {/* Polite region for success/info; errors below are assertive. */}
{toasts.map((t) => (
{t.title}
{t.message &&
{t.message}
}
))}
); } function ToastGlyph({ tone }: { tone: ToastTone }) { const common = { width: 16, height: 16, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round" as const, strokeLinejoin: "round" as const, }; if (tone === "success") { return ( ); } if (tone === "error") { return ( ); } return ( ); }