// shared/toast.js — non-blocking notification helper available to every // tool via window.zddc.toast(msg, level, opts). // // Toasts collect in a bottom-right STACK. Error/warning toasts are STICKY — // they stay until the user dismisses them (per-toast × or a "Clear all" // button) so the message can be read, selected, and copied while // troubleshooting. info/success toasts auto-dismiss. The message text is // always selectable. // // Usage: // window.zddc.toast('Saved.', 'success'); // auto-dismiss // window.zddc.toast('Could not load: ' + e.message, 'error'); // sticky // window.zddc.toast('Note', 'info', { durationMs: 3000 }); // window.zddc.toast('Heads up', 'info', { durationMs: 0 }); // force sticky // // Levels: 'info' (default) | 'success' | 'warning' | 'error'. (function () { 'use strict'; if (!window.zddc) window.zddc = {}; if (typeof window.zddc.toast === 'function') return; var DEFAULT_DURATION_MS = 5000; var FADE_MS = 300; // Levels that persist until the user dismisses them (troubleshooting). var STICKY = { error: true, warning: true }; function container() { var c = document.getElementById('zddc-toasts'); if (c) return c; c = document.createElement('div'); c.id = 'zddc-toasts'; c.className = 'zddc-toasts'; document.body.appendChild(c); return c; } // Show/hide a "Clear all" control when 2+ toasts are stacked. function refreshClearAll(c) { var bar = c.querySelector('.zddc-toasts__clear'); var count = c.querySelectorAll('.zddc-toast').length; if (count >= 2) { if (!bar) { bar = document.createElement('button'); bar.type = 'button'; bar.className = 'zddc-toasts__clear'; bar.textContent = 'Clear all'; bar.addEventListener('click', function () { var all = c.querySelectorAll('.zddc-toast'); for (var i = 0; i < all.length; i++) dismiss(all[i]); }); c.insertBefore(bar, c.firstChild); } } else if (bar) { bar.remove(); } } function dismiss(el) { if (el._dismissed) return; el._dismissed = true; if (el._timer) clearTimeout(el._timer); el.classList.add('zddc-toast--fade'); setTimeout(function () { if (el.parentNode) el.parentNode.removeChild(el); refreshClearAll(container()); }, FADE_MS); } function toast(message, level, opts) { opts = opts || {}; var lvl = (level === 'success' || level === 'error' || level === 'warning') ? level : 'info'; var c = container(); var el = document.createElement('div'); el.className = 'zddc-toast zddc-toast--' + lvl; // ARIA: errors get assertive (interrupts SR queue), others polite. el.setAttribute('role', lvl === 'error' ? 'alert' : 'status'); el.setAttribute('aria-live', lvl === 'error' ? 'assertive' : 'polite'); // Selectable, copyable message text (its own element so clicking to // select doesn't dismiss the toast — only the × does). var msg = document.createElement('span'); msg.className = 'zddc-toast__msg'; msg.textContent = message == null ? '' : String(message); el.appendChild(msg); var close = document.createElement('button'); close.type = 'button'; close.className = 'zddc-toast__close'; close.setAttribute('aria-label', 'Dismiss'); close.textContent = '×'; close.addEventListener('click', function () { dismiss(el); }); el.appendChild(close); c.appendChild(el); // Sticky (error/warning, or opts.durationMs === 0) persists; otherwise // auto-dismiss after the (overridable) duration. var sticky = opts.durationMs === 0 || (typeof opts.durationMs !== 'number' && STICKY[lvl]); if (!sticky) { var dur = typeof opts.durationMs === 'number' ? opts.durationMs : DEFAULT_DURATION_MS; el._timer = setTimeout(function () { dismiss(el); }, dur); } refreshClearAll(c); return el; } window.zddc.toast = toast; // Route window.alert() into the toast helper (non-blocking, ARIA-announced, // consistent). Native alert preserved on window.alertNative. alert() maps // to an error toast (sticky) since that's its usual purpose. if (typeof window.alert === 'function' && !window.alertNative) { window.alertNative = window.alert.bind(window); window.alert = function (msg) { toast(String(msg == null ? '' : msg), 'error'); }; } })();