"use client"; import { AlertTriangle, CheckCircle2, Info, X } from "lucide-react"; import { createContext, useCallback, useContext, useMemo, useRef, useState, type ReactNode } from "react"; import { cn } from "@/components/ui/cn"; export type ToastTone = "ok" | "warn" | "danger" | "info"; export interface ToastInput { title: string; description?: string; tone?: ToastTone; /** ms; default 4500, 0 = sticky */ duration?: number; } interface ToastItem extends ToastInput { id: number; } const Ctx = createContext<{ toast: (t: ToastInput) => void } | null>(null); export function useToast(): (t: ToastInput) => void { const ctx = useContext(Ctx); if (!ctx) throw new Error("useToast outside ToastProvider"); return ctx.toast; } const ICON: Record = { ok: CheckCircle2, warn: AlertTriangle, danger: AlertTriangle, info: Info }; const TONE: Record = { ok: "text-ok", warn: "text-warn", danger: "text-danger", info: "text-info" }; export function ToastProvider({ children }: { children: ReactNode }) { const [items, setItems] = useState([]); const seq = useRef(0); const dismiss = useCallback((id: number) => setItems((p) => p.filter((t) => t.id !== id)), []); const toast = useCallback( (t: ToastInput) => { const id = ++seq.current; setItems((p) => [...p.slice(-4), { ...t, id }]); const ms = t.duration ?? (t.tone === "danger" ? 7000 : 4500); if (ms > 0) setTimeout(() => dismiss(id), ms); }, [dismiss], ); const value = useMemo(() => ({ toast }), [toast]); return ( {children}
{items.map((t) => { const Icon = ICON[t.tone ?? "info"]; return (
{t.title}
{t.description &&
{t.description}
}
); })}
); }