Put the settings out of reach once a match is under way
Every control in the settings sheet calls resetClocks(), so opening it mid-match could throw away two live clocks. The gear now only appears on an untouched match: before the first tap, or after a two-tap reset. "Untouched" is derived rather than tracked — phase, turn, moves, score and both reserves matching matchTime() — so it cannot fall out of sync the way a stored flag could. The phase alone would be wrong: a score change runs newGame(), which returns to IDLE waiting for the next game's first tap while the match is still very much on. The reserve check also catches a clock corrected through the score sheet. The button uses visibility rather than [hidden], so its slot stays put and the rest of the bar doesn't jump the instant the clock starts; verified pixel-for-pixel that reset, play/pause, score and sound do not move. visibility:hidden also drops it from the tab order and the accessibility tree. The rule is enforced in the click handler as well, because a programmatic click still reaches a visibility:hidden button — CSS is the affordance, the guard is the rule. The trade, deliberately: a mistake in the time or delay now costs a reset and a restarted match. Correcting a clock mid-match is the score sheet's job. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+25
-1
@@ -165,6 +165,10 @@
|
||||
.btn svg{width:clamp(23px,6.4vw,32px);height:auto;display:block}
|
||||
.btn svg[hidden]{display:none}
|
||||
.btn[hidden]{display:none} /* .btn's own display would otherwise win */
|
||||
/* not [hidden]: that would collapse the slot and shift the whole bar. This
|
||||
keeps the space, and still drops the button out of the tab order, the
|
||||
accessibility tree and the path of a stray tap. */
|
||||
.btn--vacant{visibility:hidden}
|
||||
.btn:focus-visible{outline:2px solid var(--icon-hot);outline-offset:-6px;border-radius:8px}
|
||||
|
||||
/* reset armed for a second tap — the ring unwinds over the same 2s as the timeout */
|
||||
@@ -591,6 +595,7 @@
|
||||
var btnSound = document.getElementById("sound");
|
||||
var btnReset = document.getElementById("reset");
|
||||
var btnScore = document.getElementById("score");
|
||||
var btnSettings = document.getElementById("settings");
|
||||
var sheet = document.getElementById("sheet");
|
||||
var scoreSheet = document.getElementById("scoresheet");
|
||||
var scrim = document.getElementById("scrim");
|
||||
@@ -681,6 +686,18 @@
|
||||
return Math.max(0, st.reserve[i] - consumed(i));
|
||||
}
|
||||
|
||||
/* Every timer control in the settings resets both clocks, so the sheet is only
|
||||
safe to reach in exactly the state resetAll() leaves behind. Checking the
|
||||
phase alone would not do: a score change runs newGame(), which returns to
|
||||
IDLE waiting for the next game's first tap, with the match still very much
|
||||
on. The score and reserve checks are what cover that. */
|
||||
function matchPristine(){
|
||||
return st.phase === IDLE && st.active === -1 &&
|
||||
st.moves[0] === 0 && st.moves[1] === 0 &&
|
||||
st.score[0] === 0 && st.score[1] === 0 &&
|
||||
st.reserve[0] === matchTime() && st.reserve[1] === matchTime();
|
||||
}
|
||||
|
||||
/* ---- write only on change ----
|
||||
render() runs 60 times a second but the digits turn over once a second, and
|
||||
.time is a ~190px glyph run: rewriting its text node every frame forces a
|
||||
@@ -735,6 +752,10 @@
|
||||
(st.phase === FLAG && i === st.active) ? "flagged"
|
||||
: (i === st.active && st.phase !== IDLE) ? "active" : "idle");
|
||||
}
|
||||
|
||||
// here rather than in the mutators, so it can never go stale; the
|
||||
// two-argument toggle leaves the DOM alone when the state already matches
|
||||
btnSettings.classList.toggle("btn--vacant", !matchPristine());
|
||||
}
|
||||
|
||||
function loop(){ render(); requestAnimationFrame(loop); }
|
||||
@@ -1026,7 +1047,10 @@
|
||||
}
|
||||
function closeSheet(){ openSheet(null); }
|
||||
|
||||
document.getElementById("settings").addEventListener("click", function(){
|
||||
btnSettings.addEventListener("click", function(){
|
||||
// the CSS only hides the button; the rule itself lives here, so no route in
|
||||
// can reach the timer controls once the match is under way
|
||||
if(!matchPristine()) return;
|
||||
pauseNow(); // nobody's clock runs while a sheet is open
|
||||
disarm();
|
||||
paintSettings();
|
||||
|
||||
Reference in New Issue
Block a user