// Shared DOM helpers used by every view. `el` builds nodes without innerHTML // injection; the rest are the small conveniences the views repeat. export function el(tag, attrs = {}, ...children) { const node = document.createElement(tag); for (const [key, value] of Object.entries(attrs)) { if (value == null || value === false) continue; if (key === "class") node.className = value; else if (key === "dataset") Object.assign(node.dataset, value); else if (key.startsWith("on") && typeof value === "function") node.addEventListener(key.slice(2).toLowerCase(), value); else node.setAttribute(key, value); } for (const child of children.flat()) { if (child == null || child === false) continue; node.appendChild(typeof child === "string" ? document.createTextNode(child) : child); } return node; } export function errorBanner(message) { return el("div", { class: "alert", role: "alert" }, message); } /** * Replace a container's contents, dropping the absent ones. * * `el` already ignores null children, but `replaceChildren` does not: it stringifies * them, so an optional node that is not there renders the word "null" on the page. * Every view builds optional nodes — a banner only while a job runs, a conflict only * after a 409 — so the filter belongs here rather than in each caller. It was missing * from one of them, and a documentation screenshot is where that turned up (US09-04). */ export function show(root, ...nodes) { root.replaceChildren(...nodes.flat().filter((node) => node != null && node !== false)); } export function setActiveNav(view) { document.querySelectorAll("nav a[data-nav]").forEach((a) => { if (a.dataset.nav === view) a.setAttribute("aria-current", "page"); else a.removeAttribute("aria-current"); }); }