import { catalog, getArtifact, validateCatalog } from "./catalog.js"; import { createSession, selectConstraint, revealOutcomes, selectComparison, incrementVote, decrementVote, resolveVote, continueFromWinner, inspectNode } from "./domain.js"; import { createPersistence } from "./persistence.js"; import { mountSignalGarden } from "./artifact.js"; import { renderRound, renderHistory } from "./render.js"; import { focusControl } from "./focus.js"; const validation = validateCatalog(catalog); if (!validation.valid) throw new Error(`Catalog validation failed: ${validation.errors.join(" ")}`); const artifactView = document.querySelector("#artifact-view"); const roundView = document.querySelector("#round-view"); const historyView = document.querySelector("#history-view"); const polite = document.querySelector("#polite-status"); const assertive = document.querySelector("#assertive-status"); const storageSummary = document.querySelector("#storage-summary"); const resetSession = document.querySelector("#reset-session"); const resetConfirmation = document.querySelector("#reset-confirmation"); const cancelReset = document.querySelector("#cancel-reset"); const confirmReset = document.querySelector("#confirm-reset"); let browserStorage; try { browserStorage = window.localStorage; } catch { browserStorage = { getItem() { throw new Error("Storage unavailable"); }, setItem() { throw new Error("Storage unavailable"); }, removeItem() { throw new Error("Storage unavailable"); } }; } const persistence = createPersistence(browserStorage, catalog); const loaded = persistence.load(); let state = loaded.state ?? createSession(catalog); let runtime = null; if (loaded.notice) storageSummary.textContent = loaded.notice; if (loaded.recovered) announce(loaded.notice, true); function announce(message, urgent = false) { const region = urgent ? assertive : polite; region.textContent = ""; requestAnimationFrame(() => { region.textContent = message; }); } function focusAfterRender(selector) { requestAnimationFrame(() => document.querySelector(selector)?.focus()); } function focusedControlKey() { const key = document.activeElement?.dataset.focusKey; return typeof key === "string" ? key : null; } function restoreControlFocus(key, fallbackKey = null) { if (!key) return; requestAnimationFrame(() => focusControl(document, key, fallbackKey)); } function commit(next, message = "", preserveFocus = true, fallbackFocusKey = null) { const focusKey = preserveFocus ? focusedControlKey() : null; state = next; const result = persistence.save(state); storageSummary.textContent = result.notice; render(); restoreControlFocus(focusKey, fallbackFocusKey); if (message) announce(message); } function render() { runtime?.destroy(); runtime = mountSignalGarden(artifactView, getArtifact(catalog, state.nodesById[state.inspectedNodeId].artifactId)); renderRound(roundView, state, catalog, { selectConstraint: (id) => commit(selectConstraint(state, catalog, id)), reveal: () => { commit(revealOutcomes(state, catalog), "Two pre-authored outcomes are ready to compare.", false); focusAfterRender("#comparison-heading"); }, compare: (slot) => commit(selectComparison(state, slot), "", true, "artifact-heading"), vote: (slot, change) => { const next = change > 0 ? incrementVote(state, slot) : decrementVote(state, slot); const total = next.roundsById[next.currentRoundId].votes[slot]; const fallback = change < 0 && total === 0 ? `vote-${slot}-add` : null; commit(next, `Outcome ${slot.toUpperCase()} now has ${total} votes.`, true, fallback); }, resolve: () => { const next = resolveVote(state, catalog); commit(next, `Outcome ${next.roundsById[next.currentRoundId].winnerSlot.toUpperCase()} won the round.`, false); focusAfterRender("#result-heading"); }, continue: () => { const next = continueFromWinner(state, catalog); commit(next, next.phase === "complete" ? "This pre-authored branch is complete." : "", false); focusAfterRender("#round-heading"); } }); renderHistory(historyView, state, catalog, (nodeId) => commit(inspectNode(state, nodeId))); } function closeResetConfirmation({ restoreFocus = true } = {}) { resetConfirmation.hidden = true; resetSession.setAttribute("aria-expanded", "false"); if (restoreFocus) resetSession.focus(); } resetSession.addEventListener("click", () => { resetConfirmation.hidden = false; resetSession.setAttribute("aria-expanded", "true"); cancelReset.focus(); }); cancelReset.addEventListener("click", () => closeResetConfirmation()); resetConfirmation.addEventListener("keydown", (event) => { if (event.key !== "Escape") return; event.preventDefault(); closeResetConfirmation(); }); confirmReset.addEventListener("click", () => { closeResetConfirmation({ restoreFocus: false }); runtime?.destroy(); const result = persistence.reset(); state = result.state; storageSummary.textContent = result.notice; render(); focusAfterRender("#play-heading"); announce(result.notice); }); render();