import { useEffect } from 'react' import { create } from 'zustand' import { cn } from '@/lib/cn' type ToastTone = 'info' | 'error' interface Toast { id: number message: string tone: ToastTone } interface ToastState { toasts: Toast[] push: (message: string, tone?: ToastTone) => void dismiss: (id: number) => void } let nextId = 1 export const useToastStore = create((set) => ({ toasts: [], push: (message, tone = 'info') => set((s) => ({ toasts: [...s.toasts, { id: nextId++, message, tone }] })), dismiss: (id) => set((s) => ({ toasts: s.toasts.filter((t) => t.id !== id) })), })) /** Fire a toast from anywhere (including non-component code). */ export const toast = { info: (m: string) => useToastStore.getState().push(m, 'info'), error: (m: string) => useToastStore.getState().push(m, 'error'), } // Param is `item`, not `toast`, so it doesn't shadow the module's `toast` export. function ToastItem({ item }: { item: Toast }) { const dismiss = useToastStore((s) => s.dismiss) useEffect(() => { const t = setTimeout(() => dismiss(item.id), 4000) return () => clearTimeout(t) }, [item.id, dismiss]) return (
{item.message}
) } /** Fixed stack of active toasts. Mount once near the app root. */ export function Toaster() { const toasts = useToastStore((s) => s.toasts) if (toasts.length === 0) return null return (
{toasts.map((t) => ( ))}
) }