/* ============================================================ Shared app core: API client (CSRF), auth guard, layout chrome, toasts, modals, confirm dialogs, helpers. ============================================================ */ "use strict"; /* ---------- tiny DOM helpers ---------- */ const $ = (sel, root) => (root || document).querySelector(sel); const $$ = (sel, root) => Array.from((root || document).querySelectorAll(sel)); function el(tag, attrs = {}, ...children) { const node = document.createElement(tag); for (const [k, v] of Object.entries(attrs)) { if (k === "class") node.className = v; else if (k === "html") node.innerHTML = v; else if (k.startsWith("on") && typeof v === "function") node.addEventListener(k.slice(2), v); else if (v !== null && v !== undefined) node.setAttribute(k, v); } for (const c of children) { if (c === null || c === undefined) continue; node.append(c instanceof Node ? c : document.createTextNode(String(c))); } return node; } function esc(s) { return String(s ?? "").replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); } function debounce(fn, ms = 300) { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; } function fmtBytes(n) { if (n == null) return "—"; if (n < 1024) return n + " B"; if (n < 1048576) return (n / 1024).toFixed(1) + " KB"; return (n / 1048576).toFixed(2) + " MB"; }