// A very small DOM helper. Not a framework — just enough to build elements // without a wall of createElement calls. export function h(tag, props = {}, ...children) { const el = document.createElement(tag); for (const [key, value] of Object.entries(props ?? {})) { if (value === null || value === undefined || value === false) continue; if (key === 'class') el.className = value; else if (key === 'dataset') Object.assign(el.dataset, value); else if (key.startsWith('on') && typeof value === 'function') { el.addEventListener(key.slice(2).toLowerCase(), value); } else if (key in el && key !== 'list') el[key] = value; else el.setAttribute(key, value); } for (const child of children.flat(Infinity)) { if (child === null || child === undefined || child === false) continue; el.append(child instanceof Node ? child : document.createTextNode(String(child))); } return el; } /** Replace an element's contents. */ export function fill(container, ...children) { container.replaceChildren(...children.flat(Infinity).filter((c) => c !== null && c !== undefined && c !== false)); return container; } /** * Focus whatever the screen marked with `dataset.autofocus`. * * Every screen names the one control a player most likely wants next — the * first available option while choosing, "next day" once the result is in — so * a whole run can be played on Enter alone, with Tab and Shift+Tab to pick a * different option. Disabled buttons are skipped by the browser's own tab * order, so locked options are visible without being in the way. */ export function focusMarked(root) { const queue = [root]; while (queue.length) { const node = queue.shift(); if (node?.dataset?.autofocus) { node.focus?.(); return node; } queue.push(...(node?.childNodes ?? [])); } return null; }