// Shared alert/notification utility for consistent Tailwind styling and Font Awesome icons // Provides: window.alerts.show(message, type?, options?) and compatibility shims (function () { const TYPE_ALIASES = { error: 'danger', success: 'success', warning: 'warning', info: 'info', danger: 'danger' }; const TYPE_CLASSES = { success: { container: 'border-success-200 dark:border-success-800', icon: 'fa-solid fa-circle-check text-success-600 dark:text-success-400' }, danger: { container: 'border-danger-200 dark:border-danger-800', icon: 'fa-solid fa-triangle-exclamation text-danger-600 dark:text-danger-400' }, warning: { container: 'border-warning-200 dark:border-warning-800', icon: 'fa-solid fa-triangle-exclamation text-warning-600 dark:text-warning-400' }, info: { container: 'border-info-200 dark:border-info-800', icon: 'fa-solid fa-circle-info text-info-600 dark:text-info-400' } }; function normalizeType(type) { const key = String(type || 'info').toLowerCase(); return TYPE_ALIASES[key] || 'info'; } function getOrCreateContainer(containerId = 'notification-container') { let container = document.getElementById(containerId); if (!container) { container = document.createElement('div'); container.id = containerId; container.className = 'fixed top-4 right-4 z-50 flex flex-col gap-2 p-0'; document.body.appendChild(container); } return container; } function show(message, type = 'info', options = {}) { const tone = normalizeType(type); const { duration = 5000, dismissible = true, containerId = 'notification-container', role = 'alert', ariaLive = 'polite', html = false, title = null, actions = [], onClose = null, id = null } = options; const container = getOrCreateContainer(containerId); const wrapper = document.createElement('div'); wrapper.className = `alert-notification max-w-sm w-[22rem] bg-white dark:bg-neutral-800 border rounded-lg shadow-lg p-4 transition-all duration-300 translate-x-4 opacity-0 ${ (TYPE_CLASSES[tone] || TYPE_CLASSES.info).container }`; wrapper.setAttribute('role', role); wrapper.setAttribute('aria-live', ariaLive); if (id) wrapper.id = id; const inner = document.createElement('div'); inner.className = 'flex items-start'; const iconWrap = document.createElement('div'); iconWrap.className = 'flex-shrink-0'; const icon = document.createElement('i'); icon.className = (TYPE_CLASSES[tone] || TYPE_CLASSES.info).icon; iconWrap.appendChild(icon); const content = document.createElement('div'); content.className = 'ml-3 flex-1'; if (title) { const titleEl = document.createElement('p'); titleEl.className = 'text-sm font-semibold text-neutral-900 dark:text-neutral-100'; titleEl.textContent = String(title); content.appendChild(titleEl); } const text = document.createElement('div'); text.className = 'text-xs mt-1 text-neutral-800 dark:text-neutral-200'; if (message instanceof Node) { text.appendChild(message); } else if (html) { text.innerHTML = String(message || ''); } else { text.textContent = String(message || ''); } content.appendChild(text); inner.appendChild(iconWrap); inner.appendChild(content); if (dismissible) { const closeWrap = document.createElement('div'); closeWrap.className = 'ml-4 flex-shrink-0'; const closeBtn = document.createElement('button'); closeBtn.setAttribute('aria-label', 'Close'); closeBtn.className = 'text-neutral-400 hover:text-neutral-600 dark:text-neutral-500 dark:hover:text-neutral-300 transition-colors'; closeBtn.addEventListener('click', () => { wrapper.remove(); if (typeof onClose === 'function') onClose(); }); const x = document.createElement('i'); x.className = 'fa-solid fa-xmark'; closeBtn.appendChild(x); closeWrap.appendChild(closeBtn); inner.appendChild(closeWrap); } // Actions (buttons) if (Array.isArray(actions) && actions.length > 0) { const actionsWrap = document.createElement('div'); actionsWrap.className = 'mt-2 flex gap-2 flex-wrap'; actions.forEach((action) => { if (!action || !action.label) return; const btn = document.createElement('button'); btn.type = 'button'; btn.textContent = String(action.label); if (action.ariaLabel) btn.setAttribute('aria-label', action.ariaLabel); btn.className = action.classes || 'px-3 py-1 rounded text-xs transition-colors bg-neutral-200 hover:bg-neutral-300 text-neutral-800 dark:bg-neutral-700 dark:hover:bg-neutral-600 dark:text-neutral-200'; btn.addEventListener('click', (ev) => { try { if (typeof action.onClick === 'function') { action.onClick({ event: ev, wrapper }); } } finally { if (action.autoClose !== false) { wrapper.remove(); if (typeof onClose === 'function') onClose(); } } }); actionsWrap.appendChild(btn); }); content.appendChild(actionsWrap); } wrapper.appendChild(inner); container.appendChild(wrapper); // Animate in requestAnimationFrame(() => { wrapper.classList.remove('translate-x-4', 'opacity-0'); }); if (duration > 0) { setTimeout(() => { wrapper.classList.add('translate-x-4', 'opacity-0'); setTimeout(() => { wrapper.remove(); if (typeof onClose === 'function') onClose(); }, 250); }, duration); } return wrapper; } const alerts = { show, success: (message, options = {}) => show(message, 'success', options), error: (message, options = {}) => show(message, 'danger', options), warning: (message, options = {}) => show(message, 'warning', options), info: (message, options = {}) => show(message, 'info', options), getOrCreateContainer }; // Expose globally window.alerts = alerts; // Backward-compatible shims window.showAlert = (message, type = 'info', duration = 5000) => alerts.show(message, type, { duration }); window.showNotification = (message, type = 'info', duration = 5000) => alerts.show(message, type, { duration }); window.showToast = (message, type = 'info', duration = 3000) => alerts.show(message, type, { duration }); })();