/** * @factory/ui — Toast (thin React layer over the PURE `ToastStore`). * * `toastStore` (default export singleton) uses the real clock; tests build * their own `ToastStore` with an injected clock (src/toast-store.ts). * `ToastProvider` renders the store's current items; `useToasts` exposes * push/dismiss to call sites. */ import * as React from "react"; import { clsx } from "clsx"; import { ToastStore, type Toast, type ToastKind } from "../toast-store.js"; /** App-wide singleton store (real timers). */ export const toastStore = new ToastStore(); const KIND_COLOR: Record = { info: "var(--info)", success: "var(--success)", warning: "var(--warning)", error: "var(--destructive)", }; export function useToasts() { const push = React.useCallback( (kind: ToastKind, message: string, ttl?: number) => toastStore.push(kind, message, ttl), [], ); const dismiss = React.useCallback((id: string) => toastStore.dismiss(id), []); return { push, dismiss } as const; } export interface ToastProviderProps { /** Override the store (tests / per-route stores). */ store?: ToastStore; /** Max toasts rendered at once (default 5, newest win). */ max?: number; children?: React.ReactNode; } export function ToastProvider({ store = toastStore, max = 5, children }: ToastProviderProps) { const [items, setItems] = React.useState(() => store.items()); React.useEffect(() => store.subscribe(setItems), [store]); const visible = items.slice(-max); return ( <> {children}
{visible.map((t) => (
{t.message}
))}
); }