From 336d1b43bb3bb514b16071dad883f3e57d6613ef Mon Sep 17 00:00:00 2001 From: Me Here Date: Tue, 2 Jun 2026 07:52:23 -0500 Subject: [PATCH] editor: fill the screen + align header to editor width MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Editor #app and .device were capped (1400/1000px) and the shared header at 980px, so on wide screens the logo sat inset from the editor's left edge and content floated narrow in a wide page. Drop the caps (fill to the 24px body padding) and widen .site-head/.site-foot to match — logo now lines up with the editor's left edge and the editor uses the full width. Co-Authored-By: Claude Opus 4.8 (1M context) --- editor-beta.html | 115 ++++++++++++++++++++++++++++++++--------------- editor.html | 59 +++++++++++++++++++----- 2 files changed, 126 insertions(+), 48 deletions(-) diff --git a/editor-beta.html b/editor-beta.html index fa96c46..9c433ae 100644 --- a/editor-beta.html +++ b/editor-beta.html @@ -72,18 +72,28 @@ .kbd-legend { color:var(--muted); font-size:13px; font-family:"Courier New",monospace; text-align:left; line-height:1.75; } .kbd-legend span { white-space:nowrap; } /* wrap only between shortcut groups, never mid-token */ .appheader-ctrls { margin-left:auto; } /* push controls right on wide; narrow media query resets to left */ - #app { display:flex; gap:18px; max-width:1400px; margin:0 auto; align-items:flex-start; justify-content:center; } - .device { flex:1 1 auto; min-width:0; max-width:1000px; background:linear-gradient(180deg, var(--panel), var(--bg)); + /* Editor fills the screen (to the 24px body padding); the header/footer match so the logo lines + up with the editor's left edge. */ + #app { display:flex; gap:18px; max-width:none; margin:0 auto; align-items:flex-start; justify-content:center; } + .site-head, .site-foot { max-width:none; } + .device { flex:1 1 auto; min-width:0; max-width:none; background:linear-gradient(180deg, var(--panel), var(--bg)); border:1px solid var(--edge); border-radius:16px; padding:18px; box-shadow:0 18px 50px rgba(0,0,0,.5); } .row { display:flex; gap:18px; flex-wrap:wrap; } .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; } - .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: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:26px; color:#4dd0e1; margin:6px 0; display:flex; gap:18px; justify-content:center; flex-wrap:wrap; } + /* Top display panel: compact, evenly spaced. A flex column with a small, + uniform gap replaces the old per-element margins (which stacked into big gaps). */ + .display { background:#0a0d11; border:1px solid #000; border-radius:8px; padding:10px 14px; text-align:center; box-shadow:inset 0 2px 10px rgba(0,0,0,.7); + display:flex; flex-direction:column; align-items:center; gap:6px; } + .display .big { font-family:"Courier New",monospace; font-weight:700; font-size:72px; 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:24px; color:#4dd0e1; margin:0; display:flex; gap:8px 16px; justify-content:center; flex-wrap:wrap; line-height:1.1; } .display .dtimers[hidden] { display:none; } - .display .ctx { font-family:"Courier New",monospace; font-size:19px; color:#4dd0e1; min-height:22px; line-height:1.25; } + .display .ctx { font-family:"Courier New",monospace; font-size:18px; color:#4dd0e1; min-height:20px; line-height:1.2; } .display .ctx.muted-cue { color:#ffb454; } + /* Gap-trainer indicator in the display: persistent green "GAP p/m" while armed; + turns amber while the current bars are muted (count-along cue). */ + .display .gap-ind { color:#5fd08a; font-size:20px; } + .display .gap-ind.muting { color:#ffb454; } .knob { margin-bottom:10px; } .knob label { display:flex; justify-content:space-between; font-size:12px; margin-bottom:5px; } .knob label b { color:#fff; font-variant-numeric:tabular-nums; } @@ -266,7 +276,8 @@
120
- 0:00 + 0:00 +
@@ -386,7 +397,18 @@
Alt+↑/↓ reorder · 💾 save settings to an item.
-
+ +
+
+

Practice log

+ +
+
+
@@ -747,7 +769,7 @@ function renderLaneStrip(m) { /* ========================================================================= PRESETS (localStorage) ========================================================================= */ -const LS = { presets: "metronome.presets", setlists: "metronome.setlists", logs: "metronome.logs", seeded: "metronome.seeded", continue: "metronome.continue", timers: "metronome.timers" }; +const LS = { presets: "metronome.presets", setlists: "metronome.setlists", logs: "metronome.logs", seeded: "metronome.seeded", continue: "metronome.continue", timers: "metronome.timers", logging: "metronome.logging" }; function lsGet(k, fb) { try { const v = localStorage.getItem(k); return v ? JSON.parse(v) : fb; } catch (e) { return fb; } } function lsSet(k, v) { try { localStorage.setItem(k, JSON.stringify(v)); return true; } catch (e) { console.warn("localStorage unavailable", e); return false; } } @@ -780,6 +802,7 @@ let nowPlaying = null; // { at, name } for duration logging let historyName = null; // item whose past-session history is shown let continueMode = lsGet(LS.continue, false); // auto-advance to next item when countdown ends let timersOn = lsGet(LS.timers, true); // master switch for the elapsed/countdown timers +let loggingOn = lsGet(LS.logging, true); // master switch for recording practice-log sessions (default ON) function currentSetup() { return { bpm: state.bpm, lanes: snapshotLanes(), trainer: { ...trainer }, ramp: { ...ramp }, countMs: timers.totalMs, bars: segBars, rep: state.rep, end: state.end }; } function applySetup(s) { @@ -828,7 +851,7 @@ function refreshFeatureBoxes() { function fmtDur(sec) { sec = Math.round(sec); const m = Math.floor(sec / 60); return m + ":" + String(sec % 60).padStart(2, "0"); } function getSL() { return setlists[activeSL]; } // the VIEWED list function loadedItem() { const sl = setlists[loadedSL]; return (sl && activeItem >= 0) ? sl.items[activeItem] : null; } -function saveSetlists() { lsSet(LS.setlists, setlists); } +function saveSetlists() { lsSet(LS.setlists, setlists); if (typeof syncSetlistsSoon === "function") syncSetlistsSoon(); } // mirror content to a connected device (live-sync §8) // --- set list CRUD --- function newSetlist() { @@ -1007,9 +1030,12 @@ function renderItems() { // --- practice log (flat entries, one per played item) --- function logFinalize() { if (!nowPlaying) return; + if (!loggingOn) { nowPlaying = null; return; } // logging off → discard this session, keep existing history const logs = lsGet(LS.logs, []); - logs.unshift({ at: nowPlaying.at, name: nowPlaying.name, durationSec: (Date.now() - nowPlaying.at) / 1000, bpm: state.bpm, lanes: snapshotLanes() }); + const entry = { at: nowPlaying.at, name: nowPlaying.name, durationSec: (Date.now() - nowPlaying.at) / 1000, bpm: state.bpm, lanes: snapshotLanes() }; + logs.unshift(entry); lsSet(LS.logs, logs); nowPlaying = null; renderLog(); + if (typeof syncLog === "function") syncLog(entry); // mirror this session to a connected device (live-sync §9) } // Show history for the item being (or last) played, so the user can compare // today's BPM/duration against previous days for that specific task. @@ -1432,27 +1458,42 @@ function tickTimers() { renderTimers(); } function renderTimers() { - $("dtimers").hidden = !timersOn; - if (!timersOn) return; - $("elapsedVal").textContent = fmtClock(timers.elapsedMs); - const off = timers.totalMs <= 0; - $("countWrap").hidden = off; // hide time countdown when off - if (!off) { - const cd = $("countVal"); - cd.textContent = fmtClock(timers.remainingMs); - cd.classList.toggle("over", timers.remainingMs <= 0); // overtime - cd.classList.toggle("low", timers.remainingMs > 0 && timers.remainingMs <= 10000); // almost up - } - // bar countdown — bars remaining in the current segment (audible bar from lane 1, not the look-ahead master clock) - const showBars = state.running && segBars > 0; - $("barWrap").hidden = !showBars; - if (showBars) { - const elapsed = meters.length ? meters[0].currentBar : segBarCount; - const remaining = Math.max(0, segBars - elapsed); - const bv = $("barVal"); - bv.textContent = remaining; - bv.classList.toggle("low", remaining <= 1); - } + // Gap-trainer indicator is shown whenever gap mode is armed, even if the timers + // master switch is off — so the dtimers row stays visible for it alone. + const gapArmed = trainer.on && trainer.muteBars > 0; + $("dtimers").hidden = !timersOn && !gapArmed; + // timer-specific spans only when the timers master switch is on + $("elapsedWrap").hidden = !timersOn; + if (timersOn) { + $("elapsedVal").textContent = fmtClock(timers.elapsedMs); + const off = timers.totalMs <= 0; + $("countWrap").hidden = off; // hide time countdown when off + if (!off) { + const cd = $("countVal"); + cd.textContent = fmtClock(timers.remainingMs); + cd.classList.toggle("over", timers.remainingMs <= 0); // overtime + cd.classList.toggle("low", timers.remainingMs > 0 && timers.remainingMs <= 10000); // almost up + } + // bar countdown — bars remaining in the current segment (audible bar from lane 1, not the look-ahead master clock) + const showBars = state.running && segBars > 0; + $("barWrap").hidden = !showBars; + if (showBars) { + const elapsed = meters.length ? meters[0].currentBar : segBarCount; + const remaining = Math.max(0, segBars - elapsed); + const bv = $("barVal"); + bv.textContent = remaining; + bv.classList.toggle("low", remaining <= 1); + } + } else { $("countWrap").hidden = true; $("barWrap").hidden = true; } + // Gap-trainer indicator: persistent "GAP /" whenever gap mode is armed + // (not just while bars are muted). Goes amber while a muted window is active. + const gw = $("gapWrap"); + if (gapArmed) { + gw.hidden = false; + gw.textContent = "GAP " + trainer.playBars + "/" + trainer.muteBars; + const muting = state.running && typeof isMutedAt === "function" && audioCtx && isMutedAt(audioCtx.currentTime); + gw.classList.toggle("muting", !!muting); + } else gw.hidden = true; } // Status shows in the display, under the BPM. Stopped → meter count; running → @@ -1555,9 +1596,9 @@ $("vol").addEventListener("input", (e) => { if (masterGain) masterGain.gain.setTargetAtTime(state.volume, audioCtx.currentTime, 0.01); if (typeof syncVol === "function") syncVol(); }); -$("trainerOn").addEventListener("change", (e) => { trainer.on = e.target.checked; refreshFeatureBoxes(); syncPatchSoon(); }); -$("playBars").addEventListener("input", (e) => { trainer.playBars = +e.target.value; syncPatchSoon(); }); -$("muteBars").addEventListener("input", (e) => { trainer.muteBars = +e.target.value; syncPatchSoon(); }); +$("trainerOn").addEventListener("change", (e) => { trainer.on = e.target.checked; refreshFeatureBoxes(); renderTimers(); syncPatchSoon(); }); +$("playBars").addEventListener("input", (e) => { trainer.playBars = +e.target.value; renderTimers(); syncPatchSoon(); }); +$("muteBars").addEventListener("input", (e) => { trainer.muteBars = +e.target.value; renderTimers(); syncPatchSoon(); }); $("rampOn").addEventListener("change", (e) => { ramp.on = e.target.checked; refreshFeatureBoxes(); syncPatchSoon(); }); $("rampStart").addEventListener("input", (e) => { ramp.startBpm = +e.target.value; syncPatchSoon(); }); $("rampAmt").addEventListener("input", (e) => { ramp.amount = +e.target.value; syncPatchSoon(); }); @@ -1572,6 +1613,7 @@ $("endGoto").addEventListener("input", readEndActionUI); $("endRep").addEventListener("input", readEndActionUI); $("continueMode").addEventListener("change", (e) => { continueMode = e.target.checked; lsSet(LS.continue, continueMode); }); $("timersOn").addEventListener("change", (e) => { timersOn = e.target.checked; lsSet(LS.timers, timersOn); refreshFeatureBoxes(); renderTimers(); }); +$("logSessions").addEventListener("change", (e) => { loggingOn = e.target.checked; lsSet(LS.logging, loggingOn); }); // practice-log on/off (persisted) $("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; }); $("delSetlistBtn").addEventListener("click", deleteSetlist); @@ -1656,7 +1698,7 @@ window.addEventListener("keydown", (e) => { // Seed the demo set lists. Versioned + additive: a newer SEED_VERSION adds any // seed list whose title isn't already present, without clobbering the user's lists // (and won't re-add one they've deleted at the same version). -const SEED_VERSION = 3; +const SEED_VERSION = 4; if ((lsGet(LS.seeded, 0) | 0) < SEED_VERSION) { for (const s of SEED_SETLISTS) { if (!setlists.some((x) => x.title === s.title)) { @@ -1678,6 +1720,7 @@ updateCtx(); refreshFeatureBoxes(); $("continueMode").checked = continueMode; $("timersOn").checked = timersOn; +$("logSessions").checked = loggingOn; requestAnimationFrame(drawLoop); /*@BUILD:include:src/chrome.js@*/ diff --git a/editor.html b/editor.html index cfd5bab..c7adc9d 100644 --- a/editor.html +++ b/editor.html @@ -72,18 +72,28 @@ .kbd-legend { color:var(--muted); font-size:13px; font-family:"Courier New",monospace; text-align:left; line-height:1.75; } .kbd-legend span { white-space:nowrap; } /* wrap only between shortcut groups, never mid-token */ .appheader-ctrls { margin-left:auto; } /* push controls right on wide; narrow media query resets to left */ - #app { display:flex; gap:18px; max-width:1400px; margin:0 auto; align-items:flex-start; justify-content:center; } - .device { flex:1 1 auto; min-width:0; max-width:1000px; background:linear-gradient(180deg, var(--panel), var(--bg)); + /* Editor fills the screen (to the 24px body padding); the header/footer match so the logo lines + up with the editor's left edge. */ + #app { display:flex; gap:18px; max-width:none; margin:0 auto; align-items:flex-start; justify-content:center; } + .site-head, .site-foot { max-width:none; } + .device { flex:1 1 auto; min-width:0; max-width:none; background:linear-gradient(180deg, var(--panel), var(--bg)); border:1px solid var(--edge); border-radius:16px; padding:18px; box-shadow:0 18px 50px rgba(0,0,0,.5); } .row { display:flex; gap:18px; flex-wrap:wrap; } .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; } - .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: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:26px; color:#4dd0e1; margin:6px 0; display:flex; gap:18px; justify-content:center; flex-wrap:wrap; } + /* Top display panel: compact, evenly spaced. A flex column with a small, + uniform gap replaces the old per-element margins (which stacked into big gaps). */ + .display { background:#0a0d11; border:1px solid #000; border-radius:8px; padding:10px 14px; text-align:center; box-shadow:inset 0 2px 10px rgba(0,0,0,.7); + display:flex; flex-direction:column; align-items:center; gap:6px; } + .display .big { font-family:"Courier New",monospace; font-weight:700; font-size:72px; 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:24px; color:#4dd0e1; margin:0; display:flex; gap:8px 16px; justify-content:center; flex-wrap:wrap; line-height:1.1; } .display .dtimers[hidden] { display:none; } - .display .ctx { font-family:"Courier New",monospace; font-size:19px; color:#4dd0e1; min-height:22px; line-height:1.25; } + .display .ctx { font-family:"Courier New",monospace; font-size:18px; color:#4dd0e1; min-height:20px; line-height:1.2; } .display .ctx.muted-cue { color:#ffb454; } + /* Gap-trainer indicator in the display: persistent green "GAP p/m" while armed; + turns amber while the current bars are muted (count-along cue). */ + .display .gap-ind { color:#5fd08a; font-size:20px; } + .display .gap-ind.muting { color:#ffb454; } .knob { margin-bottom:10px; } .knob label { display:flex; justify-content:space-between; font-size:12px; margin-bottom:5px; } .knob label b { color:#fff; font-variant-numeric:tabular-nums; } @@ -269,6 +279,7 @@
0:00 +
@@ -388,7 +399,18 @@
Alt+↑/↓ reorder · 💾 save settings to an item.
-
+ +
+
+

Practice log

+ +
+
+
@@ -747,7 +769,7 @@ function renderLaneStrip(m) { /* ========================================================================= PRESETS (localStorage) ========================================================================= */ -const LS = { presets: "metronome.presets", setlists: "metronome.setlists", logs: "metronome.logs", seeded: "metronome.seeded", continue: "metronome.continue", timers: "metronome.timers" }; +const LS = { presets: "metronome.presets", setlists: "metronome.setlists", logs: "metronome.logs", seeded: "metronome.seeded", continue: "metronome.continue", timers: "metronome.timers", logging: "metronome.logging" }; function lsGet(k, fb) { try { const v = localStorage.getItem(k); return v ? JSON.parse(v) : fb; } catch (e) { return fb; } } function lsSet(k, v) { try { localStorage.setItem(k, JSON.stringify(v)); return true; } catch (e) { console.warn("localStorage unavailable", e); return false; } } @@ -780,6 +802,7 @@ let nowPlaying = null; // { at, name } for duration logging let historyName = null; // item whose past-session history is shown let continueMode = lsGet(LS.continue, false); // auto-advance to next item when countdown ends let timersOn = lsGet(LS.timers, true); // master switch for the elapsed/countdown timers +let loggingOn = lsGet(LS.logging, true); // master switch for recording practice-log sessions (default ON) function currentSetup() { return { bpm: state.bpm, lanes: snapshotLanes(), trainer: { ...trainer }, ramp: { ...ramp }, countMs: timers.totalMs, bars: segBars, rep: state.rep, end: state.end }; } function applySetup(s) { @@ -1005,6 +1028,7 @@ function renderItems() { // --- practice log (flat entries, one per played item) --- function logFinalize() { if (!nowPlaying) return; + if (!loggingOn) { nowPlaying = null; return; } // logging off → discard this session, keep existing history const logs = lsGet(LS.logs, []); logs.unshift({ at: nowPlaying.at, name: nowPlaying.name, durationSec: (Date.now() - nowPlaying.at) / 1000, bpm: state.bpm, lanes: snapshotLanes() }); lsSet(LS.logs, logs); nowPlaying = null; renderLog(); @@ -1434,6 +1458,15 @@ function renderTimers() { const rw = $("rampWrap"); // tempo-ramp indicator (matches the device's ramp arrow) if (ramp.on) { rw.hidden = false; rw.textContent = (ramp.amount < 0 ? "↘ " : "↗ ") + (ramp.amount >= 0 ? "+" : "") + ramp.amount + "/" + ramp.everyBars + "b"; } else rw.hidden = true; + // Gap-trainer indicator: persistent "GAP /" whenever gap mode is armed + // (not just while bars are muted). Goes amber while a muted window is active. + const gw = $("gapWrap"); + if (trainer.on && trainer.muteBars > 0) { + gw.hidden = false; + gw.textContent = "GAP " + trainer.playBars + "/" + trainer.muteBars; + const muting = state.running && typeof isMutedAt === "function" && audioCtx && isMutedAt(audioCtx.currentTime); + gw.classList.toggle("muting", !!muting); + } else gw.hidden = true; const off = !timersOn || timers.totalMs <= 0; $("countWrap").hidden = off; // hide time countdown when off if (!off) { @@ -1552,9 +1585,9 @@ $("vol").addEventListener("input", (e) => { state.volume = +e.target.value / 100; volVal.textContent = e.target.value + "%"; if (masterGain) masterGain.gain.setTargetAtTime(state.volume, audioCtx.currentTime, 0.01); }); -$("trainerOn").addEventListener("change", (e) => { trainer.on = e.target.checked; refreshFeatureBoxes(); }); -$("playBars").addEventListener("input", (e) => trainer.playBars = +e.target.value); -$("muteBars").addEventListener("input", (e) => trainer.muteBars = +e.target.value); +$("trainerOn").addEventListener("change", (e) => { trainer.on = e.target.checked; refreshFeatureBoxes(); renderTimers(); }); +$("playBars").addEventListener("input", (e) => { trainer.playBars = +e.target.value; renderTimers(); }); +$("muteBars").addEventListener("input", (e) => { trainer.muteBars = +e.target.value; renderTimers(); }); $("rampOn").addEventListener("change", (e) => { ramp.on = e.target.checked; refreshFeatureBoxes(); }); $("rampStart").addEventListener("input", (e) => ramp.startBpm = +e.target.value); $("rampAmt").addEventListener("input", (e) => ramp.amount = +e.target.value); @@ -1569,6 +1602,7 @@ $("endGoto").addEventListener("input", readEndActionUI); $("endRep").addEventListener("input", readEndActionUI); $("continueMode").addEventListener("change", (e) => { continueMode = e.target.checked; lsSet(LS.continue, continueMode); }); $("timersOn").addEventListener("change", (e) => { timersOn = e.target.checked; lsSet(LS.timers, timersOn); refreshFeatureBoxes(); renderTimers(); }); +$("logSessions").addEventListener("change", (e) => { loggingOn = e.target.checked; lsSet(LS.logging, loggingOn); }); // practice-log on/off (persisted) $("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; }); $("delSetlistBtn").addEventListener("click", deleteSetlist); @@ -1653,7 +1687,7 @@ window.addEventListener("keydown", (e) => { // Seed the demo set lists. Versioned + additive: a newer SEED_VERSION adds any // seed list whose title isn't already present, without clobbering the user's lists // (and won't re-add one they've deleted at the same version). -const SEED_VERSION = 3; +const SEED_VERSION = 4; if ((lsGet(LS.seeded, 0) | 0) < SEED_VERSION) { for (const s of SEED_SETLISTS) { if (!setlists.some((x) => x.title === s.title)) { @@ -1675,6 +1709,7 @@ updateCtx(); refreshFeatureBoxes(); $("continueMode").checked = continueMode; $("timersOn").checked = timersOn; +$("logSessions").checked = loggingOn; // Connect to a PM_K-1 / PM_X-1 over USB-MIDI on load so the header badge reflects the link // (Chrome remembers the grant; first visit prompts once). updateDevBadge runs via _wireMidi. if (navigator.requestMIDIAccess) _ensureMidi().then(updateDevBadge).catch(() => updateDevBadge());