Files
undercooked/app/static/js/screens/dom.js
T
ducoterra 95ca3fd1fa phase: 07_canvas_client_and_hud
**Final report — phase 07 final verification pass**

- **Verified:** all 5 tasks present in `complete/`; app boots (`/healthz` 200, `/` 200, static + atlas + `/api/levels` served); `docs/CLIENT.md` module map matches the on-disk `app/static/` tree; no code defects found.
- **Tests/lint/coverage:** `bash .agents/validate.sh` → ruff clean, pyright 0 errors, `undercooked-levels validate` pass, **full pytest 1838 passed, 97.21% coverage (floor 90%)**, slow sim stage 2 passed, repo hygiene clean. `uv run pytest tests/e2e -q` → **53 passed** (all 8 suites added this phase).
- **Criteria:** (1) two tabs create→join→ready→start→serve one dish, keyboard only — green via `test_us01/02/03/04` ✔; (2) e2e green ✔; (3) canvas non-blank, pixels change after a move — `test_us04_render_smoke` ✔; (4) HUD score within 200 ms of the `served` event — asserted, green ✔; (5) zero console errors + zero off-origin requests — `test_atlas_loads` / `test_us01_landing_surface` ✔; (6) `docs/CLIENT.md` matches layout ✔; (7) 100% suite + 97.21% ✔; (8) docs pass below ✔; (9) no regression in completed phases (all prior suites green) ✔.
- **README.md fixed:** status still said "next pending phase is 07" → now 07 complete, 08 pending; the phase-07 client text buried in the phase-05 bullet → its own "canvas client, art, HUD (phase 07)" bullet; stale "What does not exist yet" (claimed movement/controls missing) → rewritten to phases 08–14; "once the room flow ships" → "live in the browser today"; Layout `app/static/` "renderer arrives with the rest of phase 07" → full shipped-client map; Layout `docs/` "CLIENT lands with phase 07" → `CLIENT.md` listed; added `gen_art_index.py` Layout entry.
- **AGENTS.md fixed:** "the eight screens" (7 listed) → "the seven screens" (+ `dom.js` is the shared builder, not a screen); e2e naming line → notes the non-story guard `test_atlas_loads.py`.
- **Decisions:** no code changes were needed; only the two doc fixes above. Docs re-verified by `test_docs_commands`/`test_docs_index` (44 passed) plus the final full gate.
- **Next pending phase:** `08_client_prediction_and_touch`.
2026-10-10 18:03:52 -04:00

73 lines
2.7 KiB
JavaScript

// Undercooked — the screens' tiny DOM helper (phase 07 task 02).
//
// The screens are built with `document.createElement` and `textContent`
// only — never `innerHTML` with server data (names, messages and level
// ids arrive from the server and must not parse as HTML). This module
// is that rule made cheap: `el()` builds a subtree from a declarative
// description, and every string lands in a text node or an attribute.
/**
* Build a DOM node.
*
* `el("button", { class: "btn", type: "button", "aria-label": "Copy" },
* el("span", {}, "Copy"), "Copy")`
*
* Children may be nodes, strings (become text nodes) or null (skipped).
* Attribute values that are boolean are set/unset as flags; numbers are
* converted; `value`/`textContent` shortcuts are not needed — `text` in
* the attributes object sets `textContent` (and forbids children).
*/
export function el(tag, attrs = {}, ...children) {
const node = document.createElement(tag);
for (const [name, value] of Object.entries(attrs)) {
if (value === null || value === undefined || value === false) continue;
if (name === "text") {
node.textContent = String(value);
} else if (name === "value") {
node.value = value;
} else if (name === "dataset") {
for (const [key, item] of Object.entries(value)) {
node.dataset[key] = item;
}
} else if (name.startsWith("on") && typeof value === "function") {
// `onClick` → the DOM event `click` (listeners are case-sensitive).
node.addEventListener(name.slice(2).toLowerCase(), value);
} else if (value === true) {
node.setAttribute(name, "");
} else {
node.setAttribute(name, String(value));
}
}
if ("text" in attrs) return node;
for (const child of children) {
if (child === null || child === undefined) continue;
node.append(child instanceof Node ? child : document.createTextNode(String(child)));
}
return node;
}
/** A `document.createDocumentFragment` of the given children. */
export function frag(...children) {
const node = document.createDocumentFragment();
for (const child of children) {
if (child === null || child === undefined) continue;
node.append(child instanceof Node ? child : document.createTextNode(String(child)));
}
return node;
}
/**
* The 5-character code the user typed, the way the server normalises it
* (docs/PROTOCOL.md: Crockford base32 — the confusables `I L O U` are
* the server's to map; the client only uppercases and keeps what a code
* can contain). Returns "" when the input is not a full code.
*/
export function normalizeCode(value) {
return String(value ?? "")
.toUpperCase()
.replace(/[^0-9A-Z]/g, "")
.slice(0, 5);
}
export const CODE_LENGTH = 5;