Skip to main content

max / makenotwork

2.1 KB · 53 lines History Blame Raw
1 // Toast notifications ported from mnw.js:44-68.
2
3 /** Maximum simultaneously-visible toasts. Beyond this the oldest drops first so
4 * a burst of HTMX errors can't bury the viewport. */
5 const TOAST_MAX_VISIBLE = 5;
6
7 interface ToastDetail {
8 message?: string;
9 type?: string;
10 }
11
12 /** How long the fade-out animation runs before the node is removed. Matches the
13 * `.fade-out` transition in the stylesheet, so shortening it here drops the
14 * toast mid-animation. */
15 const TOAST_FADE_MS = 300;
16
17 /** Fade `toast` out after `lifetimeMs`, then remove it.
18 *
19 * Extracted because `initHtmxGlue`'s error toast had its own verbatim copy of
20 * the same two nested timers. The lifetime stays the caller's business: this
21 * renderer's toasts live 3000ms and an error toast lives 6000ms, and which of
22 * those is right is a question for the semantic-timing work rather than for a
23 * helper that only exists to stop the dismissal being written twice. */
24 export function autoDismiss(toast: HTMLElement, lifetimeMs: number): void {
25 setTimeout(() => {
26 toast.classList.add('fade-out');
27 setTimeout(() => toast.remove(), TOAST_FADE_MS);
28 }, lifetimeMs);
29 }
30
31 /** Fire a toast. Rendered by the delegated listener installed by `initToasts`. */
32 export function showToast(message: string, type = 'error'): void {
33 document.body.dispatchEvent(new CustomEvent<ToastDetail>('showToast', { detail: { message, type } }));
34 }
35
36 /** Install the toast renderer (caps the stack, auto-dismisses). Call once at
37 * startup. Ported from mnw.js:47. */
38 export function initToasts(): void {
39 document.body.addEventListener('showToast', (evt) => {
40 const detail = (evt as CustomEvent<ToastDetail>).detail;
41 const container = document.getElementById('notifications');
42 if (!container) return;
43 while (container.childElementCount >= TOAST_MAX_VISIBLE) {
44 container.firstElementChild?.remove();
45 }
46 const toast = document.createElement('div');
47 toast.className = 'toast toast-' + (detail.type || 'info');
48 toast.textContent = detail.message || 'Action completed';
49 container.appendChild(toast);
50 autoDismiss(toast, 3000);
51 });
52 }
53