Fix icon toggles, add timing presets and a two-tap reset
The play/pause and sound icons never toggled: SVGElement doesn't inherit
from HTMLElement, so it has no .hidden property and `svg.hidden = true`
only set a JS expando. Both icons then rendered at once because
`.btn svg{display:block}` out-specifies the UA's `[hidden]{display:none}`.
Fixed on both sides — a showIcon() helper sets the attribute, and the CSS
now honours [hidden]. Starting the clock by tapping a panel didn't repaint
the button either, which the working toggle made visible.
Also:
- conventional gear for the settings icon, replacing the clock glyph
- opening settings pauses the clock, and leaves it paused on close
- reset needs two taps within 2s, with a ring that drains over the window
- 2m/12s and 3m/15s presets atop the settings sheet; the highlight is
derived from cfg, so it clears and returns as the steppers move
- ask for persistent storage so settings aren't evicted, and fall back to
the defaults rather than NaN if a stored value is ever corrupt
sw.js cache bumped to v2 so installed devices pick up the new index.html.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+113
-12
@@ -146,8 +146,23 @@
|
||||
.btn:active{color:var(--icon-hot)}
|
||||
.btn[aria-pressed="false"]{color:#5E5954}
|
||||
.btn svg{width:clamp(23px,6.4vw,32px);height:auto;display:block}
|
||||
.btn svg[hidden]{display:none}
|
||||
.btn:focus-visible{outline:2px solid var(--icon-hot);outline-offset:-6px;border-radius:8px}
|
||||
|
||||
/* reset armed for a second tap — the ring drains over the same 2s as the timeout */
|
||||
#reset{position:relative}
|
||||
#reset.armed{color:var(--accent)}
|
||||
#reset.armed::before{
|
||||
content:"";position:absolute;
|
||||
width:clamp(36px,9vw,48px);aspect-ratio:1;
|
||||
border-radius:50%;border:2px solid var(--accent);
|
||||
animation:drain 2s linear forwards;
|
||||
}
|
||||
@keyframes drain{
|
||||
from{opacity:1;transform:scale(1)}
|
||||
to{opacity:0;transform:scale(.72)}
|
||||
}
|
||||
|
||||
/* ---------- settings sheet ---------- */
|
||||
.scrim{
|
||||
position:fixed;inset:0;
|
||||
@@ -184,6 +199,23 @@
|
||||
.row h2{font-size:15px;font-weight:600;letter-spacing:.01em}
|
||||
.row p{font-size:12.5px;color:var(--sheet-dim);margin-top:3px;line-height:1.35}
|
||||
|
||||
.row--presets{gap:11px}
|
||||
.preset{
|
||||
flex:1;
|
||||
padding:13px 10px;
|
||||
border-radius:11px;border:1px solid var(--sheet-line);
|
||||
background:#2C2A28;color:var(--sheet-ink);
|
||||
font-family:var(--ui);font-size:15px;font-weight:700;
|
||||
letter-spacing:.01em;
|
||||
cursor:pointer;
|
||||
transition:background-color .16s ease,color .16s ease;
|
||||
}
|
||||
.preset:active{background:#3A3735}
|
||||
.preset[aria-pressed="true"]{
|
||||
background:var(--accent);color:var(--accent-ink);
|
||||
border-color:transparent;
|
||||
}
|
||||
|
||||
.stepper{display:flex;align-items:center;gap:2px}
|
||||
.step{
|
||||
width:38px;height:38px;flex:none;
|
||||
@@ -275,7 +307,8 @@
|
||||
<span class="gap"></span>
|
||||
<button class="btn" id="settings" aria-label="Settings">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="13.6" r="7.4"/><path d="M12 13.6V9.4"/><path d="M9.4 2.4h5.2"/><path d="M12 2.4v3.6"/><path d="m18.6 6.4 1.9-1.9"/>
|
||||
<circle cx="12" cy="12" r="3.3"/>
|
||||
<path d="M19.6 15a1.7 1.7 0 0 0 .34 1.87l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.7 1.7 0 0 0-1.87-.34 1.7 1.7 0 0 0-1.03 1.56V21a2 2 0 1 1-4 0v-.1A1.7 1.7 0 0 0 9.1 19.3a1.7 1.7 0 0 0-1.87.34l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.7 1.7 0 0 0 4.7 15a1.7 1.7 0 0 0-1.56-1.03H3a2 2 0 1 1 0-4h.1A1.7 1.7 0 0 0 4.7 9.1a1.7 1.7 0 0 0-.34-1.87l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.7 1.7 0 0 0 9 4.7h.08A1.7 1.7 0 0 0 10.1 3.14V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1.03 1.56 1.7 1.7 0 0 0 1.87-.34l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.7 1.7 0 0 0 19.3 9v.08a1.7 1.7 0 0 0 1.56 1.03H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.56 1.03z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn" id="sound" aria-label="Sound" aria-pressed="true">
|
||||
@@ -310,6 +343,11 @@
|
||||
<div class="sheet" id="sheet" role="dialog" aria-modal="true" aria-label="Settings">
|
||||
<div class="grab"></div>
|
||||
|
||||
<div class="row row--presets">
|
||||
<button class="preset" data-base="120000" data-preset-delay="12000" aria-pressed="false">2m / 12s</button>
|
||||
<button class="preset" data-base="180000" data-preset-delay="15000" aria-pressed="false">3m / 15s</button>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div>
|
||||
<h2>Time</h2>
|
||||
@@ -364,9 +402,13 @@
|
||||
/* ============ persistence (degrades to memory if blocked) ============ */
|
||||
var store = {
|
||||
get: function(k, d){ try{ var v = localStorage.getItem(k); return v === null ? d : v; }catch(e){ return d; } },
|
||||
set: function(k, v){ try{ localStorage.setItem(k, v); }catch(e){} }
|
||||
set: function(k, v){ try{ localStorage.setItem(k, v); }catch(e){} },
|
||||
num: function(k, d){ var n = parseInt(store.get(k, ""), 10); return isFinite(n) ? n : d; }
|
||||
};
|
||||
|
||||
// ask the browser not to evict us when the device is low on space
|
||||
try{ if(navigator.storage && navigator.storage.persist) navigator.storage.persist(); }catch(e){}
|
||||
|
||||
var THEMES = [
|
||||
{ id:"sage", accent:"#7E9A79", ink:"#FFFFFF" },
|
||||
{ id:"brass", accent:"#C3A153", ink:"#FFFFFF" },
|
||||
@@ -376,8 +418,8 @@
|
||||
];
|
||||
|
||||
var cfg = {
|
||||
base: parseInt(store.get("bg.base", "180000"), 10),
|
||||
delay: parseInt(store.get("bg.delay", "15000"), 10),
|
||||
base: store.num("bg.base", 180000),
|
||||
delay: store.num("bg.delay", 15000),
|
||||
theme: store.get("bg.theme", "sage"),
|
||||
sound: store.get("bg.sound", "1") === "1"
|
||||
};
|
||||
@@ -408,6 +450,7 @@
|
||||
});
|
||||
var btnPlay = document.getElementById("playpause");
|
||||
var btnSound = document.getElementById("sound");
|
||||
var btnReset = document.getElementById("reset");
|
||||
var sheet = document.getElementById("sheet");
|
||||
var scrim = document.getElementById("scrim");
|
||||
|
||||
@@ -511,6 +554,7 @@
|
||||
function flag(side){
|
||||
st.reserve[side] = 0;
|
||||
st.phase = FLAG;
|
||||
paintPlay();
|
||||
render();
|
||||
sndFlag();
|
||||
buzz([90, 70, 90]);
|
||||
@@ -525,6 +569,7 @@
|
||||
st.turnAt = performance.now();
|
||||
st.ticked = false;
|
||||
st.phase = RUN;
|
||||
paintPlay(); // the bar has to show what the clock is doing
|
||||
sndSwap(); buzz(14);
|
||||
return;
|
||||
}
|
||||
@@ -539,10 +584,17 @@
|
||||
sndSwap(); buzz(14);
|
||||
}
|
||||
|
||||
function pauseNow(){
|
||||
if(st.phase !== RUN) return;
|
||||
st.held = elapsed();
|
||||
st.phase = PAUSE;
|
||||
paintPlay();
|
||||
}
|
||||
|
||||
function togglePause(){
|
||||
if(st.phase === RUN){
|
||||
st.held = elapsed();
|
||||
st.phase = PAUSE;
|
||||
pauseNow();
|
||||
return;
|
||||
}else if(st.phase === PAUSE){
|
||||
st.turnAt = performance.now() - st.held;
|
||||
st.phase = RUN;
|
||||
@@ -550,14 +602,22 @@
|
||||
paintPlay();
|
||||
}
|
||||
|
||||
/* 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){
|
||||
var svg = btn.querySelector('[data-icon="' + name + '"]');
|
||||
if(on) svg.removeAttribute("hidden"); else svg.setAttribute("hidden", "");
|
||||
}
|
||||
|
||||
function paintPlay(){
|
||||
var showPlay = st.phase !== RUN;
|
||||
btnPlay.querySelector('[data-icon="pause"]').hidden = showPlay;
|
||||
btnPlay.querySelector('[data-icon="play"]').hidden = !showPlay;
|
||||
showIcon(btnPlay, "pause", !showPlay);
|
||||
showIcon(btnPlay, "play", showPlay);
|
||||
btnPlay.setAttribute("aria-label", showPlay ? "Start clock" : "Pause clock");
|
||||
}
|
||||
|
||||
function reset(){
|
||||
disarm();
|
||||
st.phase = IDLE; st.active = -1;
|
||||
st.reserve = [cfg.base, cfg.base];
|
||||
st.moves = [0, 0];
|
||||
@@ -566,6 +626,22 @@
|
||||
render();
|
||||
}
|
||||
|
||||
/* reset needs two taps within 2s — one stray tap can't wipe a live game */
|
||||
var armT = null;
|
||||
function disarm(){
|
||||
if(armT){ clearTimeout(armT); armT = null; }
|
||||
btnReset.classList.remove("armed");
|
||||
btnReset.setAttribute("aria-label", "Reset clock");
|
||||
}
|
||||
function armReset(){
|
||||
if(armT){ disarm(); reset(); buzz(20); return; } // second tap inside the window
|
||||
btnReset.classList.remove("armed");
|
||||
void btnReset.offsetWidth; // restart the drain animation
|
||||
btnReset.classList.add("armed");
|
||||
btnReset.setAttribute("aria-label", "Tap again to reset");
|
||||
armT = setTimeout(disarm, 2000);
|
||||
}
|
||||
|
||||
/* ============ theme ============ */
|
||||
function applyTheme(){
|
||||
var t = THEMES.filter(function(x){ return x.id === cfg.theme; })[0] || THEMES[0];
|
||||
@@ -599,10 +675,30 @@
|
||||
document.getElementById("v-delay").textContent = Math.round(cfg.delay / 1000);
|
||||
document.getElementById("t-sound").setAttribute("aria-checked", cfg.sound ? "true" : "false");
|
||||
btnSound.setAttribute("aria-pressed", cfg.sound ? "true" : "false");
|
||||
btnSound.querySelector('[data-icon="on"]').hidden = !cfg.sound;
|
||||
btnSound.querySelector('[data-icon="off"]').hidden = cfg.sound;
|
||||
showIcon(btnSound, "on", cfg.sound);
|
||||
showIcon(btnSound, "off", !cfg.sound);
|
||||
paintPresets();
|
||||
}
|
||||
|
||||
/* highlight is derived from cfg, so it clears when you step away from a
|
||||
preset and comes back by itself when you step onto one */
|
||||
function paintPresets(){
|
||||
[].forEach.call(document.querySelectorAll(".preset"), function(b){
|
||||
var on = (+b.dataset.base === cfg.base && +b.dataset.presetDelay === cfg.delay);
|
||||
b.setAttribute("aria-pressed", on ? "true" : "false");
|
||||
});
|
||||
}
|
||||
|
||||
[].forEach.call(document.querySelectorAll(".preset"), function(b){
|
||||
b.addEventListener("click", function(){
|
||||
cfg.base = parseInt(b.dataset.base, 10);
|
||||
cfg.delay = parseInt(b.dataset.presetDelay, 10);
|
||||
store.set("bg.base", String(cfg.base));
|
||||
store.set("bg.delay", String(cfg.delay));
|
||||
paintSettings(); reset();
|
||||
});
|
||||
});
|
||||
|
||||
[].forEach.call(document.querySelectorAll("[data-time-adj]"), function(b){
|
||||
b.addEventListener("click", function(){
|
||||
var next = cfg.base + parseInt(b.dataset.timeAdj, 10) * 1000;
|
||||
@@ -633,11 +729,16 @@
|
||||
sheet.classList.toggle("on", on);
|
||||
scrim.classList.toggle("on", on);
|
||||
}
|
||||
document.getElementById("settings").addEventListener("click", function(){ paintSettings(); openSheet(true); });
|
||||
document.getElementById("settings").addEventListener("click", function(){
|
||||
pauseNow(); // nobody's clock runs while the sheet is open
|
||||
disarm();
|
||||
paintSettings();
|
||||
openSheet(true);
|
||||
});
|
||||
document.getElementById("done").addEventListener("click", function(){ openSheet(false); });
|
||||
scrim.addEventListener("click", function(){ openSheet(false); });
|
||||
|
||||
document.getElementById("reset").addEventListener("click", reset);
|
||||
btnReset.addEventListener("click", armReset);
|
||||
btnPlay.addEventListener("click", togglePause);
|
||||
|
||||
/* ============ panel taps ============ */
|
||||
|
||||
Reference in New Issue
Block a user