Editor: subtle live program-string bar (editable, copy, paste)

A faded one-line monospace bar under the app shows the current patch via
currentPatch(); it brightens on hover/focus. Press Enter or blur to apply it
(applyPatch) — paste a program string and it loads; the field renormalises to
the canonical string and flashes on apply, tints red on parse failure. A focus
guard keeps refreshUI from overwriting the field while you're typing in it.
Hidden in embed mode. Refreshed from the existing updateCtx() path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Me Here 2026-05-26 12:08:11 -05:00
parent 1b5e430b2a
commit 884bbcb244

View file

@ -135,6 +135,20 @@
.x:hover { color:#ff9a8a; border-color:#c0392b; } .x:hover { color:#ff9a8a; border-color:#c0392b; }
.hint { font-size:11px; color:var(--muted); margin-top:8px; } .hint { font-size:11px; color:var(--muted); margin-top:8px; }
code { background:#0d1014; padding:1px 5px; border-radius:4px; color:#cfe3ff; } code { background:#0d1014; padding:1px 5px; border-radius:4px; color:#cfe3ff; }
/* subtle live program string: shows the current patch; editable + copy/paste.
Faded by default so it doesn't compete with the editor; lights up on hover/focus. */
.patchbar { display:flex; align-items:center; gap:9px; max-width:1100px; margin:16px auto 0;
padding:6px 11px; border:1px solid var(--edge); border-radius:9px; background:var(--panel);
opacity:.55; transition:opacity .15s, box-shadow .25s; }
.patchbar:hover, .patchbar:focus-within { opacity:1; }
.patchbar > label { flex:0 0 auto; font-size:10px; text-transform:uppercase; letter-spacing:.09em; color:var(--muted); }
.patchbar input { flex:1; min-width:0; background:transparent; border:0; outline:none; color:var(--txt);
font-family:"Courier New",monospace; font-size:12px; padding:3px 0; }
.patchbar button { flex:0 0 auto; font-size:11px; padding:4px 10px; }
.patchbar.applied { box-shadow:0 0 0 2px var(--cyan) inset; }
.patchbar.err input { color:#ff8a7a; }
[data-embed] .patchbar { display:none !important; }
.ex-item { display:flex; gap:8px; align-items:center; padding:7px 9px; border:1px solid var(--edge); border-radius:8px; margin-bottom:6px; font-size:13px; background:var(--panel); cursor:pointer; } .ex-item { display:flex; gap:8px; align-items:center; padding:7px 9px; border:1px solid var(--edge); border-radius:8px; margin-bottom:6px; font-size:13px; background:var(--panel); cursor:pointer; }
.ex-item:hover { border-color:var(--muted); } .ex-item:hover { border-color:var(--muted); }
.ex-item.active { border-color:#2e7d32; box-shadow:inset 3px 0 0 #2e7d32; } /* loaded / playing */ .ex-item.active { border-color:#2e7d32; box-shadow:inset 3px 0 0 #2e7d32; } /* loaded / playing */
@ -359,6 +373,14 @@
</aside> </aside>
</div><!-- /#app --> </div><!-- /#app -->
<!-- Subtle live program string for what's loaded: editable, copy & paste. -->
<div class="patchbar" id="patchBar" title="Program string for what's loaded — edit it and press Enter (or paste one) to apply">
<label for="patchField">program</label>
<input id="patchField" spellcheck="false" autocomplete="off" autocapitalize="off"
placeholder="v1;t120;kick:4;snare:4=.X.X;hatClosed:4/2" aria-label="program string (editable)">
<button id="patchCopy" title="Copy the program string">Copy</button>
</div>
<div id="shortcutsOverlay" class="overlay" hidden> <div id="shortcutsOverlay" class="overlay" hidden>
<div class="overlay-box"> <div class="overlay-box">
<div class="tray-head"><h2 style="margin:0">Keyboard shortcuts</h2><button class="x" id="shortcutsClose" style="margin-left:0"></button></div> <div class="tray-head"><h2 style="margin:0">Keyboard shortcuts</h2><button class="x" id="shortcutsClose" style="margin-left:0"></button></div>
@ -1142,7 +1164,24 @@ function updateStatus() {
ctxDisplay.textContent = s; ctxDisplay.textContent = s;
ctxDisplay.classList.toggle("muted-cue", muted || !!pendingSwitch); ctxDisplay.classList.toggle("muted-cue", muted || !!pendingSwitch);
} }
function updateCtx() { updateStatus(); } function updateCtx() { updateStatus(); refreshPatchField(); }
/* keep the subtle program-string bar in sync with what's loaded, unless the
user is mid-edit in it (don't yank text out from under them). */
function refreshPatchField() {
const el = $("patchField"); if (!el || document.activeElement === el) return;
try { el.value = currentPatch(); } catch (e) {}
$("patchBar").classList.remove("err");
}
function commitPatchField() {
const el = $("patchField"), bar = $("patchBar");
try {
applyPatch(el.value.trim()); // parse + apply (throws on garbage)
bar.classList.remove("err");
el.blur(); refreshPatchField(); // normalise to the canonical string
bar.classList.add("applied"); setTimeout(() => bar.classList.remove("applied"), 450);
} catch (e) { bar.classList.add("err"); }
}
/* ========================================================================= /* =========================================================================
UI WIRING UI WIRING
@ -1222,6 +1261,14 @@ $("shareSetlistBtn").addEventListener("click", () => { $("trayMenu").hidden = tr
$("shareClose").addEventListener("click", () => $("shareOverlay").hidden = true); $("shareClose").addEventListener("click", () => $("shareOverlay").hidden = true);
$("shareOverlay").addEventListener("click", (e) => { if (e.target.id === "shareOverlay") $("shareOverlay").hidden = true; }); $("shareOverlay").addEventListener("click", (e) => { if (e.target.id === "shareOverlay") $("shareOverlay").hidden = true; });
$("shareCopy").addEventListener("click", async () => { try { await navigator.clipboard.writeText($("shareUrl").value); const b = $("shareCopy"); b.textContent = "Copied!"; setTimeout(() => b.textContent = "Copy link", 1200); } catch (e) { $("shareUrl").select(); } }); $("shareCopy").addEventListener("click", async () => { try { await navigator.clipboard.writeText($("shareUrl").value); const b = $("shareCopy"); b.textContent = "Copied!"; setTimeout(() => b.textContent = "Copy link", 1200); } catch (e) { $("shareUrl").select(); } });
/* program-string bar: apply on Enter / blur, copy button, drop the error tint while typing */
$("patchField").addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); commitPatchField(); } });
$("patchField").addEventListener("change", commitPatchField);
$("patchField").addEventListener("input", () => $("patchBar").classList.remove("err"));
$("patchCopy").addEventListener("click", async () => {
const el = $("patchField"); try { await navigator.clipboard.writeText(el.value); const b = $("patchCopy"); b.textContent = "Copied!"; setTimeout(() => b.textContent = "Copy", 1200); } catch (e) { el.select(); }
});
$("shareOpen").addEventListener("click", () => window.open($("shareUrl").value, "_blank")); $("shareOpen").addEventListener("click", () => window.open($("shareUrl").value, "_blank"));
window.addEventListener("keydown", (e) => { window.addEventListener("keydown", (e) => {
const t = e.target, tag = t ? t.tagName : "", type = (t && t.type ? String(t.type) : "").toLowerCase(); const t = e.target, tag = t ? t.tagName : "", type = (t && t.type ? String(t.type) : "").toLowerCase();