UX: always-show set list (remove hide toggle), add practice timers, '+' add button
- Removed the set-list show/hide toggle and the R shortcut / Esc-close / close ✕. The panel is always visible: sticky side column on desktop, stacked below the metronome on mobile. Theme/help buttons stay right-justified. - Added practice timers in the gap/ramp area: an Elapsed (count-up) timer and an adjustable Countdown (minutes; 0 = off), each with a reset. Both advance only while the metronome runs; countdown reaching 0 stops it (turns amber under 10s). - '+ Add meter' button is now just '+'. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7010ba7cb8
commit
a447df39e9
1 changed files with 49 additions and 17 deletions
66
index.html
66
index.html
|
|
@ -115,17 +115,16 @@
|
|||
.log-head { font-weight:600; font-size:13px; margin-bottom:3px; }
|
||||
.log-seg { font-size:12px; color:var(--muted); margin:2px 0 0 12px; font-family:"Courier New",monospace; }
|
||||
.practice { border-top:1px solid var(--edge); margin-top:16px; padding-top:4px; }
|
||||
#routineToggle { background:#2c3a4d; border-color:#3b5168; color:#cfe3ff; font-weight:600; }
|
||||
/* docked side panel: pushes the metronome (no overlap); sticky so it stays in view */
|
||||
#routineTray { display:none; flex:0 0 340px; align-self:flex-start; position:sticky; top:18px;
|
||||
/* set-list panel: always shown — sticky beside the metronome on desktop,
|
||||
stacks below it on narrow screens */
|
||||
#routineTray { flex:0 0 340px; align-self:flex-start; position:sticky; top:18px;
|
||||
max-height:calc(100vh - 36px); overflow:auto; background:linear-gradient(180deg, var(--panel), var(--bg));
|
||||
border:1px solid var(--edge); border-radius:14px; padding:16px; box-shadow:0 10px 30px rgba(0,0,0,.25); }
|
||||
#routineTray.open { display:block; }
|
||||
/* narrow screens: fall back to an overlay drawer */
|
||||
.tval { font-family:"Courier New",monospace; font-size:13px; color:var(--hot); min-width:42px; }
|
||||
.tval.low { color:#ffb454; }
|
||||
@media (max-width: 820px) {
|
||||
#app { display:block; }
|
||||
#routineTray { position:fixed; top:0; right:0; height:100%; max-height:none; width:min(360px,92vw);
|
||||
border-radius:0; border:none; border-left:1px solid var(--edge); box-shadow:-12px 0 40px rgba(0,0,0,.45); z-index:60; }
|
||||
#routineTray { position:static; max-height:none; width:auto; margin-top:18px; }
|
||||
}
|
||||
.tray-head { display:flex; align-items:center; justify-content:space-between; margin-bottom:14px; }
|
||||
.practice-col { border-left:1px solid var(--edge); padding-left:18px; }
|
||||
|
|
@ -170,8 +169,7 @@
|
|||
<div class="row" style="align-items:baseline; justify-content:space-between; gap:14px; margin-bottom:12px">
|
||||
<h1 style="margin:0">Stackable Metronome <span class="lane-meta" id="appVersion" title="build version">v0.0.1-dev</span></h1>
|
||||
<div style="display:flex; align-items:center; gap:10px">
|
||||
<span class="kbd-legend">Space play · T tap · ↑↓ tempo (⇧×10) · A add · R set lists · N next · ? help</span>
|
||||
<button id="routineToggle" title="show / hide set lists (R)">☰ Set Lists</button>
|
||||
<span class="kbd-legend">Space play · T tap · ↑↓ tempo (⇧×10) · A add · N next · ? help</span>
|
||||
<button id="themeBtn" title="toggle light / dark theme">☀</button>
|
||||
<button id="helpBtn" title="keyboard shortcuts (?)">?</button>
|
||||
</div>
|
||||
|
|
@ -212,6 +210,17 @@
|
|||
<label style="font-size:12px" title="negative ramps down, positive ramps up"><input type="number" class="num" id="rampAmt" min="-30" max="30" value="5"> BPM</label>
|
||||
<label style="font-size:12px">every <input type="number" class="num" id="rampEvery" min="1" max="16" value="4"> bars</label>
|
||||
</div>
|
||||
<div class="checkrow" style="margin:12px 0 6px; gap:8px"><b style="font-size:11px; text-transform:uppercase; letter-spacing:1px; color:var(--muted)">Timers</b><span class="hint" style="margin:0">run while playing</span></div>
|
||||
<div class="row" style="gap:10px; align-items:center">
|
||||
<label style="font-size:12px">Elapsed</label>
|
||||
<span class="tval" id="elapsedVal">0:00</span>
|
||||
<button class="iconbtn" id="elapsedReset" title="reset elapsed timer">⟲</button>
|
||||
</div>
|
||||
<div class="row" style="gap:10px; align-items:center; margin-top:6px">
|
||||
<label style="font-size:12px" title="0 = no countdown">Countdown <input type="number" class="num" id="countMin" min="0" max="120" value="5"> min</label>
|
||||
<span class="tval" id="countVal">5:00</span>
|
||||
<button class="iconbtn" id="countReset" title="reset countdown">⟲</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -222,7 +231,7 @@
|
|||
<span class="hint" style="margin:0; flex:1">Click a beat pad to toggle it (rest) — e.g. snare on 2 & 4</span>
|
||||
</div>
|
||||
<div id="meters"></div>
|
||||
<div style="margin-top:10px"><button class="add" id="addMeterBtn">+ Add meter</button></div>
|
||||
<div style="margin-top:10px"><button class="add" id="addMeterBtn" title="add meter lane (A)">+</button></div>
|
||||
|
||||
<!-- (presets moved into the Transport card; set lists live in the slide-out tray;
|
||||
status now shows under the BPM in the display) -->
|
||||
|
|
@ -234,7 +243,6 @@
|
|||
<h2 style="margin:0">Set Lists</h2>
|
||||
<div style="display:flex; gap:6px; position:relative">
|
||||
<button class="x" id="trayMenuBtn" title="log & backup" style="margin-left:0">⋯</button>
|
||||
<button class="x" id="routineClose" title="close" style="margin-left:0">✕</button>
|
||||
<div id="trayMenu" class="menu" hidden>
|
||||
<button id="shareSettingsBtn">🔗 Share settings link</button>
|
||||
<button id="shareSetlistBtn">🔗 Share set-list link</button>
|
||||
|
|
@ -277,7 +285,6 @@
|
|||
<tr><td><kbd>↑</kbd> <kbd>↓</kbd></td><td>Tempo ±1 BPM</td></tr>
|
||||
<tr><td><kbd>⇧↑</kbd> <kbd>⇧↓</kbd></td><td>Tempo ±10 BPM</td></tr>
|
||||
<tr><td><kbd>A</kbd></td><td>Add meter lane</td></tr>
|
||||
<tr><td><kbd>R</kbd></td><td>Set lists tray</td></tr>
|
||||
<tr><td><kbd>N</kbd></td><td>Load next set-list item</td></tr>
|
||||
<tr><td><kbd>⌥↑</kbd> <kbd>⌥↓</kbd></td><td>Reorder selected item</td></tr>
|
||||
<tr><td><kbd>1</kbd>–<kbd>9</kbd></td><td>Mute lane 1–9</td></tr>
|
||||
|
|
@ -936,9 +943,35 @@ function drawLoop() {
|
|||
updateStatus(now);
|
||||
}
|
||||
for (const m of meters) renderLaneStrip(m);
|
||||
tickTimers();
|
||||
requestAnimationFrame(drawLoop);
|
||||
}
|
||||
|
||||
/* =========================================================================
|
||||
PRACTICE TIMERS — advance only while the metronome is running
|
||||
========================================================================= */
|
||||
const timers = { elapsedMs: 0, totalMs: 5 * 60000, remainingMs: 5 * 60000, last: 0 };
|
||||
function fmtClock(ms) { const s = Math.max(0, Math.round(ms / 1000)); return Math.floor(s / 60) + ":" + String(s % 60).padStart(2, "0"); }
|
||||
function tickTimers() {
|
||||
const now = Date.now();
|
||||
const dt = timers.last ? Math.min(now - timers.last, 1000) : 0; // clamp so backgrounded gaps don't jump
|
||||
timers.last = now;
|
||||
if (state.running) {
|
||||
timers.elapsedMs += dt;
|
||||
if (timers.totalMs > 0) {
|
||||
timers.remainingMs -= dt;
|
||||
if (timers.remainingMs <= 0) { timers.remainingMs = 0; if (state.running) toggleTransport(); } // time's up → stop
|
||||
}
|
||||
}
|
||||
renderTimers();
|
||||
}
|
||||
function renderTimers() {
|
||||
$("elapsedVal").textContent = fmtClock(timers.elapsedMs);
|
||||
const cd = $("countVal");
|
||||
if (timers.totalMs <= 0) { cd.textContent = "off"; cd.classList.remove("low"); }
|
||||
else { cd.textContent = fmtClock(timers.remainingMs); cd.classList.toggle("low", state.running && timers.remainingMs <= 10000); }
|
||||
}
|
||||
|
||||
// Status shows in the display, under the BPM. Stopped → meter count; running →
|
||||
// bar + trainer/ramp flags (kept short for the narrow display column).
|
||||
function updateStatus() {
|
||||
|
|
@ -1004,8 +1037,9 @@ $("rampStart").addEventListener("input", (e) => ramp.startBpm = +e.target.value)
|
|||
$("rampAmt").addEventListener("input", (e) => ramp.amount = +e.target.value);
|
||||
$("rampEvery").addEventListener("input", (e) => ramp.everyBars = +e.target.value);
|
||||
$("addMeterBtn").addEventListener("click", () => addMeter("4", 1, "claves"));
|
||||
$("routineToggle").addEventListener("click", () => $("routineTray").classList.toggle("open"));
|
||||
$("routineClose").addEventListener("click", () => $("routineTray").classList.remove("open"));
|
||||
$("countMin").addEventListener("input", (e) => { timers.totalMs = (+e.target.value || 0) * 60000; timers.remainingMs = timers.totalMs; renderTimers(); });
|
||||
$("elapsedReset").addEventListener("click", () => { timers.elapsedMs = 0; renderTimers(); });
|
||||
$("countReset").addEventListener("click", () => { timers.remainingMs = timers.totalMs; renderTimers(); });
|
||||
$("trayMenuBtn").addEventListener("click", (e) => { e.stopPropagation(); $("trayMenu").hidden = !$("trayMenu").hidden; });
|
||||
document.addEventListener("click", (e) => { const m = $("trayMenu"); if (m && !m.hidden && !m.contains(e.target) && e.target.id !== "trayMenuBtn") m.hidden = true; });
|
||||
$("newSetlistBtn").addEventListener("click", newSetlist);
|
||||
|
|
@ -1039,10 +1073,9 @@ window.addEventListener("keydown", (e) => {
|
|||
else if (k === "ArrowUp") { e.preventDefault(); setBpm(state.bpm + (e.shiftKey ? 10 : 1)); }
|
||||
else if (k === "ArrowDown") { e.preventDefault(); setBpm(state.bpm - (e.shiftKey ? 10 : 1)); }
|
||||
else if (k === "a" || k === "A") { addMeter("4", 1, "claves"); }
|
||||
else if (k === "r" || k === "R") { $("routineTray").classList.toggle("open"); }
|
||||
else if (k === "n" || k === "N") { nextItem(); }
|
||||
else if (k === "?") { toggleShortcuts(true); }
|
||||
else if (k === "Escape") { if (!$("shareOverlay").hidden) $("shareOverlay").hidden = true; else if (!$("shortcutsOverlay").hidden) toggleShortcuts(false); else $("routineTray").classList.remove("open"); }
|
||||
else if (k === "Escape") { if (!$("shareOverlay").hidden) $("shareOverlay").hidden = true; else if (!$("shortcutsOverlay").hidden) toggleShortcuts(false); }
|
||||
else if (k >= "1" && k <= "9") {
|
||||
const m = meters[+k - 1];
|
||||
if (m) { m.mute = !m.mute; const cb = m.el.querySelector(`#m${m.id}_mute`); if (cb) cb.checked = m.mute; }
|
||||
|
|
@ -1067,7 +1100,6 @@ renderSetlists();
|
|||
renderLog();
|
||||
updateCtx();
|
||||
$("appVersion").textContent = "v" + APP_VERSION;
|
||||
if (window.innerWidth > 820) $("routineTray").classList.add("open"); // docked-open on desktop, closed on mobile
|
||||
requestAnimationFrame(drawLoop);
|
||||
</script>
|
||||
</body>
|
||||
|
|
|
|||
Loading…
Reference in a new issue