import { t } from '../i18n/t'; import '../styles/Toast.css'; /** * Toast — host-level notification surface. * * Why this exists * ─────────────── * The host occasionally needs to tell the user something: "link * copied", "upload failed", "add something to your design first." * Before the module split there was an in-App toast system that did * this; it got stripped along with the editor surface during the * split, and host operations have been silently throwing or * console.logging ever since. This restores the surface but scopes * it strictly to host concerns \u2014 the editor module has its own * internal toast catalog for editor concerns (crop blocked, etc.) * and that is deliberately not shared. * * Architecture * ──────────── * State + scheduling live in App.jsx (showToast / dismissToast); this * file is just the renderer. App owns the state because: * \u2022 Multiple host components could need to toast eventually * (currently only App's own handlers do). * \u2022 Lifting matches the existing host pattern (cart, isExporting, * etc. all live in App) and avoids introducing a Context for a * surface this small. * \u2022 Timer lifecycle is co-located with the state it modifies, * which keeps the cancel-on-unmount + re-schedule-cancels-prior * semantics straightforward. * * If the toast surface grows (more callers, error boundaries that * need to surface from anywhere in the tree), refactor to a * ToastContext + useToast() hook. For now, prop-passing the * showToast callback to anything outside App that needs it is the * simpler path. * * Toast shape * ─────────── * { message: string, kind: 'info' | 'success' | 'error', id: number } * * The `id` field is a freshly-stamped Date.now() per call \u2014 used as * a React key so that a new toast retriggers the fade-in animation * even when the previous toast is still on screen. * * Rendering null * ────────────── * Renders null when toast is null so the consumer can always include * in their JSX * without an outer conditional. Simpler caller, same DOM. */ export function Toast({ toast, onDismiss }) { if (!toast) return null; return ( // Wrap exists so the inner pill can use a fade-in animation that // includes a horizontal translateX(-50%) for centering \u2014 if the // animation were on the pill itself, the centering math would // collide with the keyframe transforms. The wrap holds the // positioning, the pill holds the visual chrome.
{toast.message}
); }