Resume the clock on leaving the score sheet, and split the readout in three

Closing the score sheet left the clock paused, so glancing at the score or
nudging a clock cost a tap on play. It now picks the turn back up on the way
out. openScore() records whether it was the thing that paused the clock, so a
pause the players set themselves survives. After a score change there is
nothing to resume — newGame() has already moved to IDLE — and resumeNow()
only acts on PAUSE, so that case needs no special handling. The scrim behaves
the same, since dismissing discards the staged edits and leaves the turn live.

togglePause() had the resume arithmetic inline; it splits out as resumeNow()
to match pauseNow(), leaving the play button and the sheet on one path.

The score line becomes three labelled lines:

  You:   2 out of 5
  Them:  1
  Moves: 7

.head is a two-column grid with a max-content label column, so the values
line up whatever the system font renders — no hand-tuned em width to drift
between iOS and Android. Each line is a display:contents wrapper, which keeps
one element per line to hide when a one-point match drops the score. The gap
above Moves rides on that row's two cells, since a display:contents row can't
carry a margin, and a :not([hidden]) guard drops it when there is no score
line above to separate from.

Each half reads from its own side of the board, so both players see their own
score on the You line. st.score is already indexed by side, so the mirroring
costs nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-11 15:11:31 +02:00
co-authored by Claude Opus 5
parent 9834296722
commit 0f69c78b74
3 changed files with 81 additions and 44 deletions
+54 -30
View File
@@ -75,25 +75,30 @@
.panel--top .inner{padding:14px 20px max(14px,env(safe-area-inset-top))}
.panel--bottom .inner{padding:14px 20px max(14px,env(safe-area-inset-bottom))}
/* the label column sizes itself to the widest label, so the three values
line up whatever the system font turns out to be */
.head{
display:flex;flex-direction:column;align-items:flex-start;
gap:3px;
display:grid;
grid-template-columns:max-content max-content;
justify-content:start;
gap:3px 8px;
order:-1;
}
.score[hidden]{display:none}
.score,.moves{
font-size:clamp(13px,3.7vw,20px);
font-weight:700;
letter-spacing:.01em;
color:var(--idle-mute);
white-space:nowrap;
}
.panel[data-state="active"] .score,
.panel[data-state="active"] .moves,
.panel[data-state="flagged"] .score,
.panel[data-state="flagged"] .moves{color:var(--accent-mute)}
.panel[data-state="flagged"] .score,
.panel[data-state="flagged"] .moves{color:rgba(255,255,255,.55)}
.line{display:contents}
.line[hidden]{display:none}
/* set the moves line apart from the score — a display:contents row can't take
a margin itself, so both its cells carry it and the row shifts down evenly.
The guard drops the space when there's no score line above to separate from. */
.line:not([hidden]) + .line--moves > *{margin-top:7px}
.panel[data-state="active"] .head,
.panel[data-state="flagged"] .head{color:var(--accent-mute)}
.panel[data-state="flagged"] .head{color:rgba(255,255,255,.55)}
.stack{
flex:1;
@@ -313,8 +318,9 @@
<section class="panel panel--top" data-side="0" data-state="idle">
<div class="inner">
<div class="head">
<div class="score" hidden>Score: <span data-score>0 - 0</span>, out of <span data-target>1</span></div>
<div class="moves">Moves: <span data-moves>0</span></div>
<span class="line" hidden><span>You:</span><span><span data-you>0</span> out of <span data-target>1</span></span></span>
<span class="line" hidden><span>Them:</span><span data-them>0</span></span>
<span class="line line--moves"><span>Moves:</span><span data-moves>0</span></span>
</div>
<div class="stack">
<div class="time" data-time>3:00</div>
@@ -373,8 +379,9 @@
<section class="panel panel--bottom" data-side="1" data-state="idle">
<div class="inner">
<div class="head">
<div class="score" hidden>Score: <span data-score>0 - 0</span>, out of <span data-target>1</span></div>
<div class="moves">Moves: <span data-moves>0</span></div>
<span class="line" hidden><span>You:</span><span><span data-you>0</span> out of <span data-target>1</span></span></span>
<span class="line" hidden><span>Them:</span><span data-them>0</span></span>
<span class="line line--moves"><span>Moves:</span><span data-moves>0</span></span>
</div>
<div class="stack">
<div class="time" data-time>3:00</div>
@@ -571,9 +578,10 @@
root: p,
time: p.querySelector("[data-time]"),
moves: p.querySelector("[data-moves]"),
score: p.querySelector("[data-score]"),
you: p.querySelector("[data-you]"),
them: p.querySelector("[data-them]"),
target: p.querySelector("[data-target]"),
scoreLine: p.querySelector(".score"),
scoreLines: [].slice.call(p.querySelectorAll(".line:not(.line--moves)")),
delay: p.querySelector("[data-delay]"),
fill: p.querySelector("[data-fill]"),
dnum: p.querySelector("[data-dnum]")
@@ -691,7 +699,8 @@
el[i].time.textContent = fmt(main);
el[i].moves.textContent = st.moves[i];
el[i].score.textContent = st.score[i] + " - " + st.score[1 - i];
el[i].you.textContent = st.score[i]; // each panel is "You" to its own player
el[i].them.textContent = st.score[1 - i];
el[i].target.textContent = cfg.points;
var showDelay = live && cfg.delay > 0 && dLeft > 0;
@@ -753,18 +762,19 @@
saveGame();
}
function togglePause(){
if(st.phase === RUN){
pauseNow();
return;
}else if(st.phase === PAUSE){
st.turnAt = performance.now() - st.held;
st.phase = RUN;
}else return;
function resumeNow(){
if(st.phase !== PAUSE) return;
st.turnAt = performance.now() - st.held; // pick the turn up where it stopped
st.phase = RUN;
paintPlay();
saveGame();
}
function togglePause(){
if(st.phase === RUN) pauseNow();
else resumeNow();
}
/* SVG elements aren't HTMLElements, so they have no .hidden property —
setting it only makes a JS expando. The attribute has to be set by hand. */
function showIcon(btn, name, on){
@@ -902,7 +912,7 @@
if(many) node.removeAttribute("hidden"); else node.setAttribute("hidden", "");
}
show(btnScore);
el.forEach(function(e){ show(e.scoreLine); });
el.forEach(function(e){ e.scoreLines.forEach(show); });
}
function clock(ms){ // m:ss, for copy rather than the big readout
@@ -1003,12 +1013,23 @@
document.getElementById("done").addEventListener("click", closeSheet);
// tapping outside the score sheet discards the staged edits rather than
// committing them — a stray tap must never restart a game
scrim.addEventListener("click", closeSheet);
scrim.addEventListener("click", function(){
if(openNode === scoreSheet) closeScore(); else closeSheet();
});
/* ============ score sheet ============ */
// Edits are staged here and only reach `st` on Done, so opening the sheet,
// fiddling and tapping away changes nothing.
var edit = null, before = null;
var edit = null, before = null, resumeOnClose = false;
// leaving the sheet with the turn still live carries straight on. After a score
// change there is no turn to resume — newGame() has already moved us to IDLE,
// which the PAUSE check below quietly skips.
function closeScore(){
if(resumeOnClose) resumeNow();
resumeOnClose = false;
closeSheet();
}
function paintScore(){
document.getElementById("v-you").textContent = edit.score[1];
@@ -1021,6 +1042,9 @@
}
function openScore(){
// only the sheet's own pause gets undone on the way out; a pause the players
// set themselves survives
resumeOnClose = (st.phase === RUN);
pauseNow();
disarm();
// seed from what the panels are showing, not from the banked reserve — for
@@ -1074,7 +1098,7 @@
document.getElementById("score").addEventListener("click", openScore);
document.getElementById("score-done").addEventListener("click", function(){
applyScore();
closeSheet();
closeScore();
});
btnReset.addEventListener("click", armReset);