import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type ReactNode, } from "react"; import { XMarkIcon } from "@heroicons/react/20/solid"; export type ToastTone = "info" | "error"; export interface ToastAction { label: string; onClick: () => void; } export interface ToastInput { message: string; tone?: ToastTone; action?: ToastAction; autoDismissMs?: number; } interface ToastRecord extends ToastInput { id: string; tone: ToastTone; } interface ToastContextValue { push: (toast: ToastInput) => string; dismiss: (id: string) => void; clear: () => void; } const ToastContext = createContext(null); function toastClassName(tone: ToastTone): string { return tone === "error" ? "border-coral/40 bg-rose-950/90 text-rose-50" : "border-line bg-panel/95 text-fg-2"; } export function ToastRoot({ toasts, onDismiss, }: { toasts: ToastRecord[]; onDismiss: (id: string) => void; }) { if (toasts.length === 0) return null; return (
{toasts.map((toast) => (

{toast.message}

{toast.tone === "error" && ( )}
{toast.action && (
)}
))}
); } export function ToastProvider({ children, autoDismissMs = 3500, }: { children: ReactNode; autoDismissMs?: number; }) { const [toasts, setToasts] = useState([]); const nextIdRef = useRef(0); const timeoutIdsRef = useRef(new Map>()); const dismiss = useCallback((id: string) => { const timeoutId = timeoutIdsRef.current.get(id); if (timeoutId) { clearTimeout(timeoutId); timeoutIdsRef.current.delete(id); } setToasts((current) => current.filter((toast) => toast.id !== id)); }, []); const clear = useCallback(() => { for (const timeoutId of timeoutIdsRef.current.values()) { clearTimeout(timeoutId); } timeoutIdsRef.current.clear(); setToasts([]); }, []); const push = useCallback((toast: ToastInput) => { const id = `toast-${nextIdRef.current++}`; const record: ToastRecord = { ...toast, id, tone: toast.tone ?? "info", }; setToasts((current) => [...current, record]); if (record.tone !== "error") { const timeoutId = setTimeout(() => dismiss(id), toast.autoDismissMs ?? autoDismissMs); timeoutIdsRef.current.set(id, timeoutId); } return id; }, [autoDismissMs, dismiss]); useEffect(() => clear, [clear]); const value = useMemo(() => ({ push, dismiss, clear }), [push, dismiss, clear]); return ( {children} ); } export function useToast(): ToastContextValue { const value = useContext(ToastContext); if (!value) { throw new Error("useToast must be used within a ToastProvider"); } return value; }