public/js/lib/dom.js
// Tiny DOM helpers shared by every station.
// h("div", { class: "x", onclick: fn }, "text", childNode, [more]) -> Element.
// Text always goes in as text nodes, never as HTML.
export function h(tag, attrs = {}, ...children) {
const el = document.createElement(tag);
for (const [k, v] of Object.entries(attrs ?? {})) {
if (k.startsWith("on") && typeof v === "function") el.addEventListener(k.slice(2), v);
else if (k === "style" && typeof v === "object") Object.assign(el.style, v);
else if (v === true) el.setAttribute(k, "");
else if (v !== false && v != null) el.setAttribute(k, String(v));
}
for (const c of children.flat(Infinity)) {
if (c != null && c !== false) el.append(c instanceof Node ? c : document.createTextNode(String(c)));
}
return el;
}
export const $ = (sel, root = document) => root.querySelector(sel);
export const $$ = (sel, root = document) => [...root.querySelectorAll(sel)];
export const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
export const nextFrame = () => new Promise((r) => requestAnimationFrame(r));
export const ms = (n) => (n == null || Number.isNaN(n) ? "—" : n < 10 ? `${n.toFixed(1)} ms` : `${Math.round(n)} ms`);
export const bytes = (n) =>
n == null ? "—" : n < 1024 ? `${n} B` : n < 1048576 ? `${(n / 1024).toFixed(1)} KB` : n < 1073741824 ? `${(n / 1048576).toFixed(1)} MB` : `${(n / 1073741824).toFixed(1)} GB`;
// A station section: title, a one-line lede, and a body.
export function station(title, lede, ...body) {
return h("article", { class: "station" }, h("header", {}, h("h2", {}, title), h("p", { class: "lede" }, lede)), ...body);
}
export function panel(title, ...body) {
return h("section", { class: "panel", "aria-label": title }, h("h3", {}, title), ...body);
}
export function button(label, onclick, attrs = {}) {
return h("button", { type: "button", onclick, ...attrs }, label);
}
// Busy-wait on purpose: the Rendering, Vitals and Threads stations need to
// block the main thread to show what blocking does.
export function blockFor(duration) {
const end = performance.now() + duration;
while (performance.now() < end);
}
export function storage(key, fallback) {
// localStorage can throw (private mode, blocked site data): never let that break the lab.
return {
get() {
try {
const v = localStorage.getItem(key);
return v == null ? fallback : JSON.parse(v);
} catch {
return fallback;
}
},
set(v) {
try {
localStorage.setItem(key, JSON.stringify(v));
} catch {}
},
};
}