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:
@@ -60,6 +60,13 @@ except installation and the service worker.
|
|||||||
it paused — everything in there resets the clocks anyway — while the score
|
it paused — everything in there resets the clocks anyway — while the score
|
||||||
sheet picks the turn back up when you close it, so glancing at the score
|
sheet picks the turn back up when you close it, so glancing at the score
|
||||||
doesn't cost you a tap on play.
|
doesn't cost you a tap on play.
|
||||||
|
- **Settings are only reachable before a match starts.** Every control in that
|
||||||
|
sheet resets both clocks, so the gear disappears the moment the first tap
|
||||||
|
starts the clock and comes back after a reset. Its space in the bar is kept
|
||||||
|
empty, so nothing else shifts. That means a mistake in the time or delay can
|
||||||
|
only be fixed by resetting and starting the match again — deliberately, since
|
||||||
|
the alternative is wiping two live clocks by accident. Correcting a clock
|
||||||
|
mid-match is what the score sheet is for.
|
||||||
|
|
||||||
Backgammon clocks pause between games, and after a cocked die the delay is
|
Backgammon clocks pause between games, and after a cocked die the delay is
|
||||||
normally restarted — use pause and reset for those.
|
normally restarted — use pause and reset for those.
|
||||||
|
|||||||
+25
-1
@@ -165,6 +165,10 @@
|
|||||||
.btn svg{width:clamp(23px,6.4vw,32px);height:auto;display:block}
|
.btn svg{width:clamp(23px,6.4vw,32px);height:auto;display:block}
|
||||||
.btn svg[hidden]{display:none}
|
.btn svg[hidden]{display:none}
|
||||||
.btn[hidden]{display:none} /* .btn's own display would otherwise win */
|
.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}
|
.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 */
|
/* 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 btnSound = document.getElementById("sound");
|
||||||
var btnReset = document.getElementById("reset");
|
var btnReset = document.getElementById("reset");
|
||||||
var btnScore = document.getElementById("score");
|
var btnScore = document.getElementById("score");
|
||||||
|
var btnSettings = document.getElementById("settings");
|
||||||
var sheet = document.getElementById("sheet");
|
var sheet = document.getElementById("sheet");
|
||||||
var scoreSheet = document.getElementById("scoresheet");
|
var scoreSheet = document.getElementById("scoresheet");
|
||||||
var scrim = document.getElementById("scrim");
|
var scrim = document.getElementById("scrim");
|
||||||
@@ -681,6 +686,18 @@
|
|||||||
return Math.max(0, st.reserve[i] - consumed(i));
|
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 ----
|
/* ---- write only on change ----
|
||||||
render() runs 60 times a second but the digits turn over once a second, and
|
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
|
.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"
|
(st.phase === FLAG && i === st.active) ? "flagged"
|
||||||
: (i === st.active && st.phase !== IDLE) ? "active" : "idle");
|
: (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); }
|
function loop(){ render(); requestAnimationFrame(loop); }
|
||||||
@@ -1026,7 +1047,10 @@
|
|||||||
}
|
}
|
||||||
function closeSheet(){ openSheet(null); }
|
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
|
pauseNow(); // nobody's clock runs while a sheet is open
|
||||||
disarm();
|
disarm();
|
||||||
paintSettings();
|
paintSettings();
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/* Cache-first: once installed the clock never touches the network again. */
|
/* Cache-first: once installed the clock never touches the network again. */
|
||||||
const CACHE = "bgclock-v7";
|
const CACHE = "bgclock-v8";
|
||||||
const FILES = [
|
const FILES = [
|
||||||
"./",
|
"./",
|
||||||
"./index.html",
|
"./index.html",
|
||||||
|
|||||||
Reference in New Issue
Block a user