Five things from the second playtest. A status line at the top of every screen: the game, the pack, your current title, and the version plus the commit it came from. `tools/stamp.js` writes the commit into src/build-info.js before serving and before building, so a distributed dist/theladder.html states exactly which commit produced it. An uncommitted working tree gets a `+` suffix — "4cab9fc+" is honest in a way a bare SHA would not be. The whole game is now playable on Enter. Every screen marks one control and the app focuses it after each render: the first *available* option while choosing, the next-turn button once the result is in, "keep going" on the retrospective. Tab and Shift+Tab reach the other options. Locked options needed no special handling — the browser's tab order already skips disabled controls, so they stay visible without being in the way. "What is notable? under What changed?" — two fixes. `notable` is the retrospective's own list of moments; a display rule can now say `inLedger: false` and it is left out, because "Notable — changed" tells nobody anything. Flags are the opposite and stay, but as sentences the pack supplies rather than as booleans: "The envelope is in your locker." instead of "the envelope: now true". Exported logs live in logs/, gitignored, and `npm run analyze` reads the newest one there when given no argument. A log records what a real person did turn by turn, including whatever they typed into the feedback box — committing one once would put it in the history for good. Version set to 0.3.0 on the reasoning that the engine was 0.1 and the interface 0.2; say if you want a different scheme and I will renumber. 119 tests. Reasoning in docs/DECISIONS.md §25-28. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VMSFHyVPitUoosW5wyEADj
49 lines
1.8 KiB
JavaScript
49 lines
1.8 KiB
JavaScript
// 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;
|
|
}
|