// Toast notifications ported from mnw.js:44-68. /** Maximum simultaneously-visible toasts. Beyond this the oldest drops first so * a burst of HTMX errors can't bury the viewport. */ const TOAST_MAX_VISIBLE = 5; interface ToastDetail { message?: string; type?: string; } /** How long the fade-out animation runs before the node is removed. Matches the * `.fade-out` transition in the stylesheet, so shortening it here drops the * toast mid-animation. */ const TOAST_FADE_MS = 300; /** Fade `toast` out after `lifetimeMs`, then remove it. * * Extracted because `initHtmxGlue`'s error toast had its own verbatim copy of * the same two nested timers. The lifetime stays the caller's business: this * renderer's toasts live 3000ms and an error toast lives 6000ms, and which of * those is right is a question for the semantic-timing work rather than for a * helper that only exists to stop the dismissal being written twice. */ export function autoDismiss(toast: HTMLElement, lifetimeMs: number): void { setTimeout(() => { toast.classList.add('fade-out'); setTimeout(() => toast.remove(), TOAST_FADE_MS); }, lifetimeMs); } /** Fire a toast. Rendered by the delegated listener installed by `initToasts`. */ export function showToast(message: string, type = 'error'): void { document.body.dispatchEvent(new CustomEvent('showToast', { detail: { message, type } })); } /** Install the toast renderer (caps the stack, auto-dismisses). Call once at * startup. Ported from mnw.js:47. */ export function initToasts(): void { document.body.addEventListener('showToast', (evt) => { const detail = (evt as CustomEvent).detail; const container = document.getElementById('notifications'); if (!container) return; while (container.childElementCount >= TOAST_MAX_VISIBLE) { container.firstElementChild?.remove(); } const toast = document.createElement('div'); toast.className = 'toast toast-' + (detail.type || 'info'); toast.textContent = detail.message || 'Action completed'; container.appendChild(toast); autoDismiss(toast, 3000); }); }