Single Save button by Tap (disabled when nothing loaded); per-entry + Clear-all history delete; bigger display text

- Move per-item 💾 out of set-list rows into one 💾 Save next to Tap; it
  overwrites the loaded item and is disabled when no item is loaded.
- History list: red ✕ on hover deletes one session; Clear all wipes
  history for the current item only.
- Bump dark-display text again (BPM 80px, timers 26px, status 19px);
  widen the display column to fit.
- README: play key Space -> P.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Me Here 2026-05-25 07:45:26 -05:00
parent 1be6920827
commit 17492fdfb0
2 changed files with 54 additions and 15 deletions

View file

@ -101,7 +101,7 @@ In the setlist panel's **⋯** menu:
| Key | Action | | Key | Action |
|-----|--------| |-----|--------|
| `Space` | start / stop | | `P` | play / stop |
| `T` | tap tempo | | `T` | tap tempo |
| `↑` / `↓` | tempo ±1 (`Shift` = ±10) | | `↑` / `↓` | tempo ±1 (`Shift` = ±10) |
| `A` | add meter lane | | `A` | add meter lane |

View file

@ -54,10 +54,10 @@
.card { background:var(--panel-2); border:1px solid var(--edge); border-radius:12px; padding:13px; flex:1; min-width:240px; } .card { background:var(--panel-2); border:1px solid var(--edge); border-radius:12px; padding:13px; flex:1; min-width:240px; }
.card h2 { font-size:11px; text-transform:uppercase; letter-spacing:1.4px; color:var(--muted); margin:0 0 14px; } .card h2 { font-size:11px; text-transform:uppercase; letter-spacing:1.4px; color:var(--muted); margin:0 0 14px; }
.display { background:#0a0d11; border:1px solid #000; border-radius:8px; padding:8px 14px; text-align:center; box-shadow:inset 0 2px 10px rgba(0,0,0,.7); } .display { background:#0a0d11; border:1px solid #000; border-radius:8px; padding:8px 14px; text-align:center; box-shadow:inset 0 2px 10px rgba(0,0,0,.7); }
.display .big { font-family:"Courier New",monospace; font-weight:700; font-size:58px; color:#ffd166; letter-spacing:2px; line-height:1.05; text-shadow:0 0 12px rgba(255,209,102,.5); } .display .big { font-family:"Courier New",monospace; font-weight:700; font-size:80px; color:#ffd166; letter-spacing:2px; line-height:1.0; text-shadow:0 0 12px rgba(255,209,102,.5); }
.display .dtimers { font-family:"Courier New",monospace; font-size:18px; color:#4dd0e1; margin:4px 0; display:flex; gap:16px; justify-content:center; flex-wrap:wrap; } .display .dtimers { font-family:"Courier New",monospace; font-size:26px; color:#4dd0e1; margin:6px 0; display:flex; gap:18px; justify-content:center; flex-wrap:wrap; }
.display .dtimers[hidden] { display:none; } .display .dtimers[hidden] { display:none; }
.display .ctx { font-family:"Courier New",monospace; font-size:15px; color:#4dd0e1; min-height:18px; line-height:1.3; } .display .ctx { font-family:"Courier New",monospace; font-size:19px; color:#4dd0e1; min-height:22px; line-height:1.25; }
.display .ctx.muted-cue { color:#ffb454; } .display .ctx.muted-cue { color:#ffb454; }
.knob { margin-bottom:10px; } .knob { margin-bottom:10px; }
.knob label { display:flex; justify-content:space-between; font-size:12px; margin-bottom:5px; } .knob label { display:flex; justify-content:space-between; font-size:12px; margin-bottom:5px; }
@ -114,8 +114,13 @@
.np-desc { font-size:12px; color:var(--muted); margin-top:4px; } .np-desc { font-size:12px; color:var(--muted); margin-top:4px; }
.iconbtn { padding:3px 8px; font-size:12px; } .iconbtn { padding:3px 8px; font-size:12px; }
.log-item { padding:8px 10px; border:1px solid var(--edge); border-radius:8px; margin-bottom:6px; background:var(--panel); } .log-item { padding:8px 10px; border:1px solid var(--edge); border-radius:8px; margin-bottom:6px; background:var(--panel); }
.log-head { font-weight:600; font-size:13px; margin-bottom:3px; } .log-head { font-weight:600; font-size:13px; margin-bottom:5px; display:flex; align-items:center; gap:8px; }
.log-seg { font-size:12px; color:var(--muted); margin:2px 0 0 12px; font-family:"Courier New",monospace; } .log-head-nm { flex:1; }
.hist-row { display:flex; align-items:center; gap:6px; margin:2px 0 0 12px; }
.hist-txt { flex:1; font-size:12px; color:var(--muted); font-family:"Courier New",monospace; }
.hist-del { display:none; background:transparent; border:none; color:#ff6b5e; cursor:pointer; font-size:12px; line-height:1; padding:2px 4px; border-radius:4px; }
.hist-del:hover { background:rgba(255,107,94,.15); }
.hist-row:hover .hist-del, .hist-row:focus-within .hist-del { display:inline; }
.practice { border-top:1px solid var(--edge); margin-top:16px; padding-top:4px; } .practice { border-top:1px solid var(--edge); margin-top:16px; padding-top:4px; }
/* set-list panel: always shown — sticky beside the metronome on desktop, /* set-list panel: always shown — sticky beside the metronome on desktop,
stacks below it on narrow screens */ stacks below it on narrow screens */
@ -190,7 +195,7 @@
<div class="row"> <div class="row">
<div class="card" style="flex:1"> <div class="card" style="flex:1">
<div class="row" style="gap:22px; align-items:flex-start"> <div class="row" style="gap:22px; align-items:flex-start">
<div style="flex:0 0 212px; min-width:190px"> <div style="flex:0 0 260px; min-width:230px">
<div class="display"> <div class="display">
<div class="big" id="bpmDisplay">120</div> <div class="big" id="bpmDisplay">120</div>
<div class="dtimers" id="dtimers"> <div class="dtimers" id="dtimers">
@ -199,7 +204,7 @@
</div> </div>
<div class="ctx" id="ctxDisplay">&nbsp;</div> <div class="ctx" id="ctxDisplay">&nbsp;</div>
</div> </div>
<div class="btnrow" style="margin-top:10px"><button class="primary" id="startBtn">▶ Start</button><button id="tapBtn">Tap</button></div> <div class="btnrow" style="margin-top:10px"><button class="primary" id="startBtn">▶ Start</button><button id="tapBtn">Tap</button><button id="saveItemBtn" disabled title="overwrite the loaded set-list item with the current settings">💾 Save</button></div>
</div> </div>
<div style="flex:1; min-width:200px"> <div style="flex:1; min-width:200px">
@ -756,6 +761,7 @@ function toggleTransport() {
// --- now-playing info on the main screen (replaces the old preset dropdown) --- // --- now-playing info on the main screen (replaces the old preset dropdown) ---
function renderNowPlaying() { function renderNowPlaying() {
const sl = getSL(); const it = (sl && activeItem >= 0) ? sl.items[activeItem] : null; const sl = getSL(); const it = (sl && activeItem >= 0) ? sl.items[activeItem] : null;
$("saveItemBtn").disabled = !it; // single save button targets the loaded item
if (!it) { if (!it) {
$("npName").textContent = "Free play"; $("npName").textContent = "Free play";
$("npSub").textContent = "No set-list item loaded — edit the lanes freely."; $("npSub").textContent = "No set-list item loaded — edit the lanes freely.";
@ -790,11 +796,9 @@ function renderItems() {
row.innerHTML = `<span class="nm">${i + 1}. ${it.name}</span> row.innerHTML = `<span class="nm">${i + 1}. ${it.name}</span>
<span class="meta">${it.bpm} · ${it.lanes.map((l) => l.groupsStr).join("/")}</span> <span class="meta">${it.bpm} · ${it.lanes.map((l) => l.groupsStr).join("/")}</span>
<span class="row-actions"> <span class="row-actions">
<button class="iconbtn" data-act="save" title="overwrite this item with the current settings">💾</button>
<button class="x iconbtn" data-act="del" title="remove this item"></button> <button class="x iconbtn" data-act="del" title="remove this item"></button>
</span>`; </span>`;
row.onclick = () => loadItem(i); row.onclick = () => loadItem(i);
row.querySelector('[data-act=save]').onclick = (e) => { e.stopPropagation(); updateItem(i); };
row.querySelector('[data-act=del]').onclick = (e) => { e.stopPropagation(); removeItem(i); }; row.querySelector('[data-act=del]').onclick = (e) => { e.stopPropagation(); removeItem(i); };
box.appendChild(row); box.appendChild(row);
}); });
@ -813,11 +817,41 @@ function logFinalize() {
function renderLog() { function renderLog() {
const box = $("logView"); box.innerHTML = ""; const box = $("logView"); box.innerHTML = "";
if (!historyName) { box.innerHTML = '<div class="hint">Play a set-list item to see its history — compare BPM &amp; duration across days.</div>'; return; } if (!historyName) { box.innerHTML = '<div class="hint">Play a set-list item to see its history — compare BPM &amp; duration across days.</div>'; return; }
const logs = lsGet(LS.logs, []).filter((e) => e.name === historyName); const entries = lsGet(LS.logs, []).filter((e) => e.name === historyName);
let html = `<div class="log-head" style="margin-bottom:5px">History — ${historyName}</div>`;
if (!logs.length) html += '<div class="hint">No past sessions for this item yet.</div>'; const head = document.createElement("div"); head.className = "log-head";
else html += logs.map((e) => `<div class="log-seg">${new Date(e.at).toLocaleString()} · ${fmtDur(e.durationSec)} @ ${e.bpm}bpm</div>`).join(""); head.innerHTML = `<span class="log-head-nm">History — ${historyName}</span>`;
box.innerHTML = html; if (entries.length) {
const clr = document.createElement("button"); clr.className = "iconbtn"; clr.textContent = "Clear all";
clr.title = "delete all history for this item";
clr.onclick = () => clearItemHistory();
head.appendChild(clr);
}
box.appendChild(head);
if (!entries.length) {
const h = document.createElement("div"); h.className = "hint"; h.textContent = "No past sessions for this item yet.";
box.appendChild(h); return;
}
entries.forEach((e) => {
const row = document.createElement("div"); row.className = "hist-row";
const txt = document.createElement("span"); txt.className = "hist-txt";
txt.textContent = `${new Date(e.at).toLocaleString()} · ${fmtDur(e.durationSec)} @ ${e.bpm}bpm`;
const del = document.createElement("button"); del.className = "hist-del"; del.textContent = "✕";
del.title = "delete this entry";
del.onclick = () => deleteHistoryEntry(e.at);
row.appendChild(txt); row.appendChild(del); box.appendChild(row);
});
}
function deleteHistoryEntry(at) { // remove one session by its timestamp
const logs = lsGet(LS.logs, []).filter((e) => !(e.at === at && e.name === historyName));
lsSet(LS.logs, logs); renderLog();
}
function clearItemHistory() { // clear every session for the current item
if (!historyName) return;
if (!confirm("Clear all history for “" + historyName + "”? (other items, set lists & presets are kept)")) return;
const logs = lsGet(LS.logs, []).filter((e) => e.name !== historyName);
lsSet(LS.logs, logs); renderLog();
} }
function clearLog() { if (confirm("Clear the practice log? (set lists & presets are kept)")) { lsSet(LS.logs, []); renderLog(); } } function clearLog() { if (confirm("Clear the practice log? (set lists & presets are kept)")) { lsSet(LS.logs, []); renderLog(); } }
function resetAll() { function resetAll() {
@ -1078,6 +1112,11 @@ function tapTempo() {
} }
} }
$("tapBtn").addEventListener("click", tapTempo); $("tapBtn").addEventListener("click", tapTempo);
$("saveItemBtn").addEventListener("click", () => {
if (activeItem < 0) return;
updateItem(activeItem);
const b = $("saveItemBtn"), t = b.textContent; b.textContent = "✓ Saved"; setTimeout(() => { b.textContent = t; }, 900);
});
$("bpm").addEventListener("input", (e) => setBpm(+e.target.value)); $("bpm").addEventListener("input", (e) => setBpm(+e.target.value));
$("vol").addEventListener("input", (e) => { $("vol").addEventListener("input", (e) => {
state.volume = +e.target.value / 100; volVal.textContent = e.target.value + "%"; state.volume = +e.target.value / 100; volVal.textContent = e.target.value + "%";