| (function () { | |
| const stack = document.createElement('div'); | |
| stack.className = 'toast-stack'; | |
| const mountStack = () => document.body.appendChild(stack); | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', mountStack, { once: true }); | |
| } else { | |
| mountStack(); | |
| } | |
| const createToast = (type, title, message) => { | |
| const toast = document.createElement('div'); | |
| toast.className = `toast ${type}`; | |
| toast.innerHTML = `<div><strong>${title}</strong>${message ? `<small>${message}</small>` : ''}</div>`; | |
| stack.appendChild(toast); | |
| setTimeout(() => toast.remove(), 4500); | |
| }; | |
| const setBadge = (element, text, tone = 'info') => { | |
| if (!element) return; | |
| element.textContent = text; | |
| element.className = `badge ${tone}`; | |
| }; | |
| const showLoading = (container, message = 'Loading data...') => { | |
| if (!container) return; | |
| container.innerHTML = `<div class="loading-indicator">${message}</div>`; | |
| }; | |
| const fadeReplace = (container, html) => { | |
| if (!container) return; | |
| container.innerHTML = html; | |
| container.classList.add('fade-in'); | |
| setTimeout(() => container.classList.remove('fade-in'), 200); | |
| }; | |
| const fetchJSON = async (url, options = {}, context = '') => { | |
| try { | |
| const response = await fetch(url, options); | |
| if (!response.ok) { | |
| const text = await response.text(); | |
| createToast('error', context || 'Request failed', text || response.statusText); | |
| throw new Error(text || response.statusText); | |
| } | |
| return await response.json(); | |
| } catch (err) { | |
| createToast('error', context || 'Network error', err.message || String(err)); | |
| throw err; | |
| } | |
| }; | |
| window.UIFeedback = { | |
| toast: createToast, | |
| setBadge, | |
| showLoading, | |
| fadeReplace, | |
| fetchJSON, | |
| }; | |
| })(); | |