(function () { "use strict"; const banners = new Map(); const GAP = 12; const DEFAULT_ID = "default"; function normalizeId(id) { return String(id || DEFAULT_ID); } function getBanner(id) { id = normalizeId(id); if (banners.has(id)) return banners.get(id).element; const banner = document.createElement("div"); banner.className = "operation-banner"; banner.dataset.operationBannerId = id; banner.setAttribute("role", "status"); banner.setAttribute("aria-live", "polite"); banner.innerHTML = `
`; banner.querySelector(".ob-close").addEventListener("click", () => hide(id)); document.body.appendChild(banner); banners.set(id, { element: banner, timer: null }); return banner; } function clearTimer(entry) { if (entry && entry.timer !== null) { clearTimeout(entry.timer); entry.timer = null; } } function reposition() { let bottom = 20; for (const entry of [...banners.values()]) { const banner = entry.element; if (!banner.classList.contains("is-visible")) continue; banner.style.bottom = `${bottom}px`; bottom += banner.getBoundingClientRect().height + GAP; } } function hide(id = DEFAULT_ID) { id = normalizeId(id); const entry = banners.get(id); if (!entry) return; clearTimer(entry); entry.element.classList.remove("is-visible", "is-busy"); reposition(); } function show(message, options = {}) { const id = normalizeId(options.id); const banner = getBanner(id); const entry = banners.get(id); clearTimer(entry); const type = options.type || "info"; const busy = options.busy === true; const sticky = options.sticky === true; const icons = { info: "ℹ", upload: "↑", download: "↓", success: "✓", error: "!" }; banner.classList.remove( "is-info", "is-upload", "is-download", "is-success", "is-error" ); banner.classList.add("is-" + type); banner.classList.toggle("is-busy", busy); banner.querySelector(".ob-icon").textContent = icons[type] || icons.info; banner.querySelector(".ob-content").textContent = String(message || ""); banner.classList.add("is-visible"); reposition(); if (!busy && !sticky) { const duration = Number.isFinite(options.duration) ? options.duration : (type === "error" ? 6000 : 3500); entry.timer = setTimeout(() => hide(id), duration); } } function setProgress(id, value) { id = normalizeId(id); const banner = getBanner(id); const percent = Math.max(0, Math.min(100, Number(value) || 0)); banner.classList.add("is-determinate"); banner.classList.remove("is-busy"); banner.querySelector(".ob-progress-fill").style.width = `${percent}%`; banner.querySelector(".ob-progress-label").textContent = `${Math.round(percent)}%`; reposition(); } window.operationBanner = { show, hide, setProgress }; })();