Phase C — per-page plain/info split + per-device program box

Each form-factor page is now plain by default: title + summary + the front view +
a program I/O box (shared src/progbox.* — paste a patch OR a base64 set-list code,
decoded + linted + loaded; copy; reflects the device state and posts it to an
embedding parent). All technical content — description, BOM, dimensions, top/side
views, embedding — hidden behind a "Show technical info" checkbox; ?info=1 opens it
checked. Teacher's top-edge view + dimensions are marked .tech so they hide too.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Me Here 2026-05-28 11:22:23 -05:00
parent 271abfe43a
commit f1146e720a
8 changed files with 172 additions and 13 deletions

View file

@ -127,6 +127,9 @@
/*@BUILD:include:src/header.html@*/ /*@BUILD:include:src/header.html@*/
<h1 class="ff-title">PMµ Micro</h1>
<p class="ff-sum">Inline practice bar — patch your instrument through it (in one end, amp/headphones out the other) with the click mixed in. One clickable thumbroller, an amber 14segment display.</p>
<div class="device"> <div class="device">
<!-- LEFT END: instrument / aux in --> <!-- LEFT END: instrument / aux in -->
<div class="endcap left"> <div class="endcap left">
@ -168,8 +171,9 @@
</div> </div>
</div> </div>
<div class="hint">Roll = <b>tempo</b> · press = <b>start / stop</b> · hold &amp; roll = <b>switch track</b>. <div class="hint">Roll = <b>tempo</b> · press = <b>start / stop</b> · hold &amp; roll = <b>switch track</b>.</div>
Instrument in one end, amp/headphones out the other — the click is mixed into your signal in the analog domain.</div>
/*@BUILD:include:src/progbox.html@*/
<script> <script>
const APP_VERSION = "v0.0.1-dev"; const APP_VERSION = "v0.0.1-dev";
@ -268,6 +272,7 @@ function render(){
$("indBpm").classList.toggle("on", displayMode==="bpm"); $("indBpm").classList.toggle("on", displayMode==="bpm");
$("indTrk").classList.toggle("on", displayMode==="track"); $("indTrk").classList.toggle("on", displayMode==="track");
$("indPlay").classList.toggle("on", state.running); $("indPlay").classList.toggle("on", state.running);
if(window.progRefresh) progRefresh();
} }
/* ========================= ROLLER (the only control) ========================= */ /* ========================= ROLLER (the only control) ========================= */
@ -310,9 +315,15 @@ addEventListener("keydown",(e)=>{
{ const ht=tracksFromHash(); if(ht) tracks=ht; } // a #p=/#sl= link (or embed config) overrides the built-ins { const ht=tracksFromHash(); if(ht) tracks=ht; } // a #p=/#sl= link (or embed config) overrides the built-ins
loadTrack(0); loadTrack(0);
render(); render();
window.currentProgramString = function(){ var t=tracks[trackIdx]||{}; return setupToPatch({bpm:state.bpm, volume:state.volume, lanes:t.lanes||[]}); };
window.loadProgramString = function(plain){ var s=patchToSetup(plain); tracks=[{name:"Program", ...s}]; trackIdx=0; loadTrack(0); };
/*@BUILD:include:src/progbox.js@*/
</script> </script>
<section class="about pageonly"> <label class="info-toggle pageonly"><input type="checkbox" id="infoToggle"> Show technical info (dimensions, BOM, embedding)</label>
<div id="techinfo" class="pageonly" hidden>
<section class="about">
<h2>PMµ — Micro</h2> <h2>PMµ — Micro</h2>
<div class="ff-tags"><span class="hw">Hardware</span><span>Inline practice bar</span><span>~$35 oneoff</span></div> <div class="ff-tags"><span class="hw">Hardware</span><span>Inline practice bar</span><span>~$35 oneoff</span></div>
<p>A long, narrow practice bar you patch <i>into</i> your signal: instrument in one end, amp or headphones out <p>A long, narrow practice bar you patch <i>into</i> your signal: instrument in one end, amp or headphones out
@ -353,6 +364,10 @@ render();
</div> </div>
</details> </details>
<p class="sub" style="max-width:760px;margin:14px auto 0">Embed this widget elsewhere with one <code>&lt;div&gt;</code> + a script —
see <a href="/embed.html">the embed docs</a>.</p>
</div><!-- /#techinfo -->
/*@BUILD:include:src/footer.html@*/ /*@BUILD:include:src/footer.html@*/
</body> </body>
</html> </html>

View file

@ -211,6 +211,9 @@
/*@BUILD:include:src/header.html@*/ /*@BUILD:include:src/header.html@*/
<h1 class="ff-title">PM1 Initial</h1>
<p class="ff-sum">The idealized concept render — a clean screenfirst player with a colour beat display, setlist navigation, theming and a fullscreen landscape view.</p>
<!-- fullscreen "stage mode" toggle — floats over the page (was a header button before the shared header) --> <!-- fullscreen "stage mode" toggle — floats over the page (was a header button before the shared header) -->
<button id="fsBtn" class="fs-float" title="Full screen (landscape)"></button> <button id="fsBtn" class="fs-float" title="Full screen (landscape)"></button>
@ -487,8 +490,12 @@ if(location.hash && /(p|sl)=/.test(location.hash)) loadConfig(location.hash, tru
if(!setlist){ setlist=BUILTIN[0]; idx=0; loadSetup(setlist.items[0]); } if(!setlist){ setlist=BUILTIN[0]; idx=0; loadSetup(setlist.items[0]); }
renderAll(); renderAll();
requestAnimationFrame(draw); requestAnimationFrame(draw);
/*@BUILD:include:src/progbox.js@*/
</script> </script>
<section class="about pageonly">
<label class="info-toggle pageonly"><input type="checkbox" id="infoToggle"> Show technical info</label>
<div id="techinfo" class="pageonly" hidden>
<section class="about">
<h2>PM1 — Initial</h2> <h2>PM1 — Initial</h2>
<div class="ff-tags"><span>Concept</span><span>Idealized device</span><span>Not buildable as drawn</span></div> <div class="ff-tags"><span>Concept</span><span>Idealized device</span><span>Not buildable as drawn</span></div>
<p>The idealized PM1: the player as a clean, screenfirst device with no concession to mechanical parts yet. <p>The idealized PM1: the player as a clean, screenfirst device with no concession to mechanical parts yet.
@ -500,6 +507,7 @@ requestAnimationFrame(draw);
buildable realization of this idea is the <a href="/teacher.html">PM1 Teacher</a> (full priced BOM there); buildable realization of this idea is the <a href="/teacher.html">PM1 Teacher</a> (full priced BOM there);
for the smallest practical unit, see the <a href="/micro.html">PMµ Micro</a>.</p> for the smallest practical unit, see the <a href="/micro.html">PMµ Micro</a>.</p>
</section> </section>
</div><!-- /#techinfo -->
/*@BUILD:include:src/footer.html@*/ /*@BUILD:include:src/footer.html@*/
</body> </body>

View file

@ -59,6 +59,9 @@
/*@BUILD:include:src/header.html@*/ /*@BUILD:include:src/header.html@*/
<main> <main>
<h1 class="ff-title">PMS Showcase</h1>
<p class="ff-sum">A displaypiece metronome — the pendulum is an RGB light bar that combines every lane's subdivisions &amp; accents; a printed tempo scale with a sliding weight sets the tempo.</p>
<div class="device"><canvas id="stage" width="300" height="470" aria-label="RGB pendulum metronome"></canvas></div> <div class="device"><canvas id="stage" width="300" height="470" aria-label="RGB pendulum metronome"></canvas></div>
<div class="ctrls"> <div class="ctrls">
@ -70,7 +73,12 @@
moving RGB light. Drag the <b>weight</b> up/down (or scroll) to set tempo — the scale is printed on the bar, moving RGB light. Drag the <b>weight</b> up/down (or scroll) to set tempo — the scale is printed on the bar,
just like a windup metronome. (No power switch: the real one starts when you lift it from its holder.)</div> just like a windup metronome. (No power switch: the real one starts when you lift it from its holder.)</div>
<section class="about pageonly"> /*@BUILD:include:src/progbox.html@*/
<label class="info-toggle pageonly"><input type="checkbox" id="infoToggle"> Show technical info (dimensions, BOM, embedding)</label>
<div id="techinfo" class="pageonly" hidden>
<section class="about">
<h2>PMS — Showcase</h2> <h2>PMS — Showcase</h2>
<div class="ff-tags"><span class="hw">Hardware</span><span>Display piece</span><span>~$41 oneoff</span></div> <div class="ff-tags"><span class="hw">Hardware</span><span>Display piece</span><span>~$41 oneoff</span></div>
<p>A metronome as an object: the silhouette of a classic pyramid windup unit, but the swinging pendulum is <p>A metronome as an object: the silhouette of a classic pyramid windup unit, but the swinging pendulum is
@ -110,6 +118,10 @@
</table> </table>
</div> </div>
</details> </details>
<p class="sub" style="max-width:760px;margin:14px auto 0">Embed this widget elsewhere with one <code>&lt;div&gt;</code> + a script —
see <a href="/embed.html">the embed docs</a>.</p>
</div><!-- /#techinfo -->
</main> </main>
/*@BUILD:include:src/footer.html@*/ /*@BUILD:include:src/footer.html@*/
@ -125,7 +137,7 @@ const SAMPLES = {};
const state = { bpm:120, volume:0.85, running:false }; const state = { bpm:120, volume:0.85, running:false };
let meters = [], muteWindows = []; let meters = [], muteWindows = [];
function setBpm(v){ state.bpm = Math.max(30, Math.min(300, Math.round(v))); } function setBpm(v){ state.bpm = Math.max(30, Math.min(300, Math.round(v))); if(window.progRefresh) progRefresh(); }
function scheduler(){ function scheduler(){
const ahead = audioCtx.currentTime + SCHEDULE_AHEAD; const ahead = audioCtx.currentTime + SCHEDULE_AHEAD;
for(const m of meters){ while(m.nextTime < ahead){ scheduleMeterTick(m, m.nextTime); m.nextTime += laneStepDur(m, m.tick); m.tick++; } } for(const m of meters){ while(m.nextTime < ahead){ scheduleMeterTick(m, m.nextTime); m.nextTime += laneStepDur(m, m.tick); m.tick++; } }
@ -286,6 +298,9 @@ addEventListener("keydown",(e)=>{
{ const ht=tracksFromHash(); if(ht) tracks=ht; } { const ht=tracksFromHash(); if(ht) tracks=ht; }
loadTrack(0); syncBtns(); loadTrack(0); syncBtns();
requestAnimationFrame(draw); requestAnimationFrame(draw);
window.currentProgramString = function(){ var t=tracks[trackIdx]||{}; return setupToPatch({bpm:state.bpm, volume:state.volume, lanes:t.lanes||[]}); };
window.loadProgramString = function(plain){ var s=patchToSetup(plain); tracks=[{name:"Program", ...s}]; trackIdx=0; loadTrack(0); syncBtns(); };
/*@BUILD:include:src/progbox.js@*/
/*@BUILD:include:src/chrome.js@*/ /*@BUILD:include:src/chrome.js@*/
</script> </script>
</body> </body>

View file

@ -73,3 +73,30 @@ details.spec .spec-body { padding:2px 16px 16px; }
.about p { margin:0 0 10px; max-width:64ch; } .about p { margin:0 0 10px; max-width:64ch; }
/* page-only chrome (description, dimensioned views, loading panel) — hidden when embedded */ /* page-only chrome (description, dimensioned views, loading panel) — hidden when embedded */
[data-embed] .pageonly { display:none !important; } [data-embed] .pageonly { display:none !important; }
/* ---- per-form-factor page: visible title + summary (plain view) ---- */
.ff-title { font-size:20px; margin:6px 0 2px; text-align:center; color:var(--txt,#c7d0db); }
.ff-sum { max-width:60ch; margin:0 auto; text-align:center; color:var(--muted,#7f8b9a); font-size:13.5px; line-height:1.55; }
[data-embed] .ff-title, [data-embed] .ff-sum { display:none !important; }
/* ---- per-device program I/O box (plain view) ---- */
.progbox { width:100%; max-width:620px; margin:14px auto 0; display:flex; align-items:center; gap:9px; flex-wrap:wrap;
padding:9px 12px; border:1px solid var(--panel-bd,#2a313c); border-radius:11px; background:var(--panel-bg,#161b22); }
.progbox > label { flex:0 0 auto; font-size:10px; text-transform:uppercase; letter-spacing:.09em; color:var(--muted,#7f8b9a); }
.progbox input { flex:1; min-width:160px; background:var(--field-bg,#0e1116); color:var(--txt,#c7d0db);
border:1px solid var(--field-bd,#2a313c); border-radius:8px; padding:8px 10px; font-family:"Courier New",monospace; font-size:12.5px; }
.progbox input.err { border-color:#c0392b; }
.progbox button { flex:0 0 auto; background:var(--field-bg,#0e1116); color:var(--txt,#c7d0db); border:1px solid var(--field-bd,#2a313c);
border-radius:8px; padding:8px 12px; font-size:13px; cursor:pointer; }
.progbox button.primary { background:linear-gradient(180deg,#34c6ff,var(--cyan)); color:#04121b; border-color:transparent; font-weight:600; }
.progbox button:hover { border-color:var(--cyan); }
.progbox-msg { flex:1 1 100%; font-size:11.5px; color:var(--muted,#7f8b9a); min-height:1em; }
.progbox-msg.ok { color:#5fd08a; } .progbox-msg.bad { color:#ff8a7a; }
[data-embed] .progbox { display:none !important; }
/* ---- "Show info" toggle + technical section ---- */
.info-toggle { display:flex; align-items:center; justify-content:center; gap:8px; margin:18px auto 0; max-width:620px;
font-size:13px; color:var(--muted,#7f8b9a); cursor:pointer; }
.info-toggle input { width:15px; height:15px; cursor:pointer; }
[data-embed] .info-toggle { display:none !important; }
#techinfo[hidden] { display:none; }

11
src/progbox.html Normal file
View file

@ -0,0 +1,11 @@
<!-- Per-device program I/O — assembled into each form-factor page by build.sh.
Hidden in embed mode (the landing supplies its own box). The host page defines
window.currentProgramString() and window.loadProgramString(plain); progbox.js wires this. -->
<div class="progbox" title="The program for what's loaded — paste a patch or a base64 set-list code, then Load">
<label for="dProg">program</label>
<input id="dProg" spellcheck="false" autocomplete="off" autocapitalize="off"
placeholder="v1;t120;kick:4;snare:4=.X.X;hat:4/2 — or a base64 set-list code">
<span id="dProgMsg" class="progbox-msg"></span>
<button id="dProgLoad" class="primary">Load ▸</button>
<button id="dProgCopy">Copy</button>
</div>

60
src/progbox.js Normal file
View file

@ -0,0 +1,60 @@
/* Per-device program I/O + "Show info" toggle assembled into each form-factor page.
Uses the engine codec (patchToSetup / setupToPatch / codeToSetlist) for decode + lint.
Host page provides: window.currentProgramString() and window.loadProgramString(plain).
Exposes window.progRefresh() call it after the device's program changes. When the
page is embedded it posts {type:'varasys-prog'} to the parent instead of touching the box. */
(function () {
var box = document.getElementById("dProg");
var msg = document.getElementById("dProgMsg");
var embedded = document.documentElement.dataset.embed === "1";
function setMsg(t, ok) { if (!msg) return; msg.textContent = t || ""; msg.classList.toggle("ok", !!ok && !!t); msg.classList.toggle("bad", !ok && !!t); }
function lint(text) {
text = (text || "").trim(); if (!text) return { ok: false, msg: "empty" };
var m = text.match(/[#?&](p|sl)=([^&\s]+)/), kind = null, payload = text;
if (m) { kind = m[1]; try { payload = decodeURIComponent(m[2]); } catch (e) { payload = m[2]; } }
var b64 = /^[A-Za-z0-9_-]{12,}$/.test(payload) && !/[;:]/.test(payload);
try {
if (kind === "sl" || (kind !== "p" && b64)) {
var sl = codeToSetlist(payload); if (!sl.items || !sl.items.length) throw new Error("set-list code has no items");
return { ok: true, plain: setupToPatch(sl.items[0]), msg: "decoded set list “" + sl.title + "” — item 1" };
}
var s = patchToSetup(payload); if (!s.lanes.length) throw new Error("no lanes — try e.g. kick:4");
return { ok: true, plain: setupToPatch(s), msg: s.lanes.length + " lane" + (s.lanes.length > 1 ? "s" : "") + " · " + s.bpm + " BPM" };
} catch (e) { return { ok: false, msg: "✗ " + e.message }; }
}
var editing = false;
// report the current program: to the parent when embedded, else into the box.
// No-op on pages without a program hook (e.g. panel-based Teacher/Player).
window.progRefresh = function () {
if (!window.currentProgramString) return;
var p = ""; try { p = window.currentProgramString(); } catch (e) {}
if (embedded) { try { parent.postMessage({ type: "varasys-prog", patch: p }, "*"); } catch (e) {} return; }
if (box && !editing) { box.value = p; setMsg("", true); }
};
if (box) {
box.addEventListener("focus", function () { editing = true; });
box.addEventListener("blur", function () { editing = false; });
box.addEventListener("input", function () { box.classList.remove("err"); });
box.addEventListener("keydown", function (e) { if (e.key === "Enter") { e.preventDefault(); doLoad(); } });
var loadBtn = document.getElementById("dProgLoad"); if (loadBtn) loadBtn.addEventListener("click", doLoad);
var copyBtn = document.getElementById("dProgCopy"); if (copyBtn) copyBtn.addEventListener("click", function () {
try { navigator.clipboard.writeText(box.value); copyBtn.textContent = "Copied!"; setTimeout(function () { copyBtn.textContent = "Copy"; }, 1200); } catch (e) { box.select(); }
});
}
function doLoad() {
var r = lint(box.value);
if (!r.ok) { box.classList.add("err"); setMsg(r.msg, false); return; }
box.classList.remove("err"); box.value = r.plain; setMsg("✓ " + r.msg, true);
try { if (window.loadProgramString) window.loadProgramString(r.plain); } catch (e) { setMsg("✗ " + e.message, false); }
}
// "Show info" toggle — reveals the technical section; ?info=1 opens it checked
var tog = document.getElementById("infoToggle"), tech = document.getElementById("techinfo");
if (tog) {
var extra = document.querySelectorAll(".tech"); // e.g. dimensioned views outside #techinfo
var apply = function (on) { if (tech) tech.hidden = !on; for (var i = 0; i < extra.length; i++) extra[i].hidden = !on; };
var open = /[?&]info=1/.test(location.search);
tog.checked = open; apply(open);
tog.addEventListener("change", function () { apply(tog.checked); });
}
window.progRefresh();
})();

View file

@ -111,6 +111,9 @@
/*@BUILD:include:src/header.html@*/ /*@BUILD:include:src/header.html@*/
<h1 class="ff-title">PM1 Stage</h1>
<p class="ff-sum">Footpedal stompbox for the stage — handsfree with two footswitches and an expression pedal, a big floorreadable RGB beat light, instrument passthrough with the click mixed in.</p>
<div class="device"> <div class="device">
<!-- top edge: all jacks, including dual USB-C daisy-chain power --> <!-- top edge: all jacks, including dual USB-C daisy-chain power -->
<div class="edge"> <div class="edge">
@ -154,6 +157,8 @@
<div class="hint">Stomp <b>Tap</b> to set tempo (hold = start/stop) · <b>Next</b> to change item (hold = previous) · <div class="hint">Stomp <b>Tap</b> to set tempo (hold = start/stop) · <b>Next</b> to change item (hold = previous) ·
an expression pedal sweeps tempo. Instrument passes through with the click mixed in (analog).</div> an expression pedal sweeps tempo. Instrument passes through with the click mixed in (analog).</div>
/*@BUILD:include:src/progbox.html@*/
<script> <script>
const APP_VERSION = "v0.0.1-dev"; const APP_VERSION = "v0.0.1-dev";
const $ = (id) => document.getElementById(id); const $ = (id) => document.getElementById(id);
@ -165,7 +170,7 @@ const SAMPLES = {};
const state = { bpm:120, volume:0.85, running:false }; const state = { bpm:120, volume:0.85, running:false };
let meters = [], muteWindows = []; let meters = [], muteWindows = [];
function setBpm(v){ state.bpm = Math.max(30, Math.min(300, Math.round(v))); } function setBpm(v){ state.bpm = Math.max(30, Math.min(300, Math.round(v))); if(window.progRefresh) progRefresh(); }
function scheduler(){ function scheduler(){
const ahead = audioCtx.currentTime + SCHEDULE_AHEAD; const ahead = audioCtx.currentTime + SCHEDULE_AHEAD;
for(const m of meters){ while(m.nextTime < ahead){ scheduleMeterTick(m, m.nextTime); m.nextTime += laneStepDur(m, m.tick); m.tick++; } } for(const m of meters){ while(m.nextTime < ahead){ scheduleMeterTick(m, m.nextTime); m.nextTime += laneStepDur(m, m.tick); m.tick++; } }
@ -296,9 +301,15 @@ addEventListener("keydown",(e)=>{
{ const ht=tracksFromHash(); if(ht) tracks=ht; } { const ht=tracksFromHash(); if(ht) tracks=ht; }
loadTrack(0); loadTrack(0);
requestAnimationFrame(draw); requestAnimationFrame(draw);
window.currentProgramString = function(){ var t=tracks[trackIdx]||{}; return setupToPatch({bpm:state.bpm, volume:state.volume, lanes:t.lanes||[]}); };
window.loadProgramString = function(plain){ var s=patchToSetup(plain); tracks=[{name:"Program", ...s}]; trackIdx=0; loadTrack(0); };
/*@BUILD:include:src/progbox.js@*/
</script> </script>
<section class="about pageonly"> <label class="info-toggle pageonly"><input type="checkbox" id="infoToggle"> Show technical info (dimensions, BOM, embedding)</label>
<div id="techinfo" class="pageonly" hidden>
<section class="about">
<h2>PM1 — Stage</h2> <h2>PM1 — Stage</h2>
<div class="ff-tags"><span class="hw">Hardware</span><span>Footpedal stompbox</span><span>~$52 oneoff</span></div> <div class="ff-tags"><span class="hw">Hardware</span><span>Footpedal stompbox</span><span>~$52 oneoff</span></div>
<p>A footoperated polymeter stompbox for the stage: drive it handsfree with two heavy footswitches and an <p>A footoperated polymeter stompbox for the stage: drive it handsfree with two heavy footswitches and an
@ -346,6 +357,10 @@ requestAnimationFrame(draw);
</div> </div>
</details> </details>
<p class="sub" style="max-width:760px;margin:14px auto 0">Embed this widget elsewhere with one <code>&lt;div&gt;</code> + a script —
see <a href="/embed.html">the embed docs</a>.</p>
</div><!-- /#techinfo -->
/*@BUILD:include:src/footer.html@*/ /*@BUILD:include:src/footer.html@*/
</body> </body>
</html> </html>

View file

@ -204,11 +204,14 @@
/*@BUILD:include:src/header.html@*/ /*@BUILD:include:src/header.html@*/
<h1 class="ff-title">PM1 Teacher</h1>
<p class="ff-sum">Fullfeature studio / lesson desk console — a colour TFT showing every lane, arcade buttons and a thumbroller, with your instrument running through and the click mixed in.</p>
<div class="cols"> <div class="cols">
<div class="col-left"> <div class="col-left">
<!-- ===================== TOP EDGE (connectors) ===================== --> <!-- ===================== TOP EDGE (connectors) — technical view, hidden until "Show info" ===================== -->
<div class="topview"> <div class="topview tech" hidden>
<div class="tv-cap">Top edge — all connectors (cables exit upward; pedalboard-friendly)</div> <div class="tv-cap">Top edge — all connectors (cables exit upward; pedalboard-friendly)</div>
<div class="dim-row"> <div class="dim-row">
<div class="dim-y">↕ 1.8 in (45 mm)</div> <div class="dim-y">↕ 1.8 in (45 mm)</div>
@ -224,7 +227,7 @@
<!-- ===================== THE DEVICE (front view) ===================== --> <!-- ===================== THE DEVICE (front view) ===================== -->
<div class="dim-row"> <div class="dim-row">
<div class="dim-y">↕ 5.5 in (140 mm)</div> <div class="dim-y tech" hidden>↕ 5.5 in (140 mm)</div>
<div class="device"> <div class="device">
<div class="brandrow"> <div class="brandrow">
@ -254,7 +257,7 @@
<div class="grille"></div> <div class="grille"></div>
</div><!-- /device --> </div><!-- /device -->
</div><!-- /dim-row --> </div><!-- /dim-row -->
<div class="dim-x">↔ 4.7 in (120 mm) wide</div> <div class="dim-x tech" hidden>↔ 4.7 in (120 mm) wide</div>
<!-- ===================== LOAD CONFIG ===================== --> <!-- ===================== LOAD CONFIG ===================== -->
<div class="panel"> <div class="panel">
@ -275,7 +278,10 @@
</div><!-- /col-left --> </div><!-- /col-left -->
</div><!-- /cols --> </div><!-- /cols -->
<section class="about pageonly"> <label class="info-toggle pageonly"><input type="checkbox" id="infoToggle"> Show technical info (front/top views, dimensions, BOM, embedding)</label>
<div id="techinfo" class="pageonly" hidden>
<section class="about">
<h2>PM1 — Teacher</h2> <h2>PM1 — Teacher</h2>
<div class="ff-tags"><span class="hw">Hardware</span><span>Studio / lesson console</span><span>~$59 oneoff</span></div> <div class="ff-tags"><span class="hw">Hardware</span><span>Studio / lesson console</span><span>~$59 oneoff</span></div>
<p>The fullfeature desktop console: a colour readout of every lane, fast setlist navigation, and your <p>The fullfeature desktop console: a colour readout of every lane, fast setlist navigation, and your
@ -324,6 +330,7 @@
monitor amp — so your instrument is never redigitised (no added latency).</p> monitor amp — so your instrument is never redigitised (no added latency).</p>
</div> </div>
</details> </details>
</div><!-- /#techinfo -->
<script> <script>
const APP_VERSION = "v0.0.1-dev"; const APP_VERSION = "v0.0.1-dev";
@ -553,6 +560,7 @@ if(location.hash && /(p|sl)=/.test(location.hash)) loadConfig(location.hash, tru
if(!setlist){ setlist=BUILTIN[0]; idx=0; loadSetup(setlist.items[0]); } if(!setlist){ setlist=BUILTIN[0]; idx=0; loadSetup(setlist.items[0]); }
renderAll(); renderAll();
requestAnimationFrame(draw); requestAnimationFrame(draw);
/*@BUILD:include:src/progbox.js@*/
</script> </script>
/*@BUILD:include:src/footer.html@*/ /*@BUILD:include:src/footer.html@*/
</body> </body>