diff --git a/mobile.html b/mobile.html
index a5e7fe4..ba563df 100644
--- a/mobile.html
+++ b/mobile.html
@@ -50,13 +50,18 @@
font-family:"Segoe UI", Roboto, Helvetica, Arial, sans-serif;
-webkit-user-select:none; user-select:none; -webkit-tap-highlight-color:transparent;
touch-action:manipulation; overscroll-behavior:none; }
- #app{ position:fixed; inset:0; overflow:hidden; display:flex; flex-direction:column;
- padding:max(8px,env(safe-area-inset-top)) max(12px,env(safe-area-inset-right))
- max(8px,env(safe-area-inset-bottom)) max(12px,env(safe-area-inset-left)); }
+ /* Content is capped to --maxw and centered, so phone→tablet is the SAME layout,
+ just larger (no flex re-flow). Generous margins; even more in full-screen. */
+ #app{ position:fixed; inset:0; overflow:hidden; display:flex; flex-direction:column; align-items:center; --maxw:600px; --pad:14px;
+ padding:max(12px,env(safe-area-inset-top)) max(var(--pad),env(safe-area-inset-right))
+ max(12px,env(safe-area-inset-bottom)) max(var(--pad),env(safe-area-inset-left)); }
+ #top, #mid, #sessbar{ width:100%; max-width:var(--maxw); }
+ :fullscreen #app, html:fullscreen #app{ --pad:30px; padding-top:max(24px,env(safe-area-inset-top)); padding-bottom:max(18px,env(safe-area-inset-bottom)); }
+ @media (display-mode:standalone){ #app{ --pad:26px; padding-top:max(20px,env(safe-area-inset-top)); padding-bottom:max(16px,env(safe-area-inset-bottom)); } }
/* ---- top ---- */
- #top{ flex:0 0 auto; display:flex; flex-direction:column; gap:8px; }
- .sels{ display:flex; gap:8px; }
+ #top{ flex:0 0 auto; display:flex; flex-direction:column; gap:10px; }
+ .sels{ display:flex; gap:8px; align-items:flex-end; }
.sel{ flex:1 1 0; min-width:0; display:flex; flex-direction:column; gap:3px; }
.sel > span{ font-size:10px; letter-spacing:.14em; text-transform:uppercase; color:var(--muted); padding-left:3px; }
.sel select{ width:100%; background:var(--field-bg); color:var(--txt); border:1px solid var(--field-bd); border-radius:10px; padding:10px 8px; font-size:15px; }
@@ -85,19 +90,19 @@
#meterline{ font-size:clamp(12px,2.1vmin,16px); color:var(--muted); text-align:center; min-height:1.2em; letter-spacing:.02em; }
/* ---- track panel (repeat/end/ramp/gap/string) + editable lanes ---- */
- #detail{ flex:0 1 auto; width:100%; max-width:560px; max-height:32vh; overflow-y:auto; display:flex; flex-direction:column; gap:8px; padding:2px 0; }
+ #detail{ flex:0 1 auto; width:100%; max-height:32vh; overflow-y:auto; display:flex; flex-direction:column; gap:8px; padding:2px 0; }
#lanes{ display:flex; flex-direction:column; gap:6px; }
- #trackpanel{ width:100%; background:var(--chip-bg); border:1px solid var(--chip-bd); border-radius:10px; padding:8px 10px; display:flex; flex-direction:column; gap:7px; font-size:12px; color:var(--muted); }
- #trackpanel .tp-grid{ display:flex; flex-wrap:wrap; align-items:center; gap:8px 14px; }
- #trackpanel label{ display:flex; align-items:center; gap:6px; }
+ #trackpanel{ width:100%; background:var(--chip-bg); border:1px solid var(--chip-bd); border-radius:10px; padding:8px 11px; display:flex; flex-direction:column; gap:8px; font-size:12px; color:var(--muted); }
+ #trackpanel .tp-row{ display:flex; align-items:center; gap:8px 18px; flex-wrap:nowrap; }
+ #trackpanel label{ display:flex; align-items:center; gap:6px; white-space:nowrap; }
#trackpanel .tp-chk{ color:var(--txt); }
- #trackpanel .tp-chk input{ width:18px; height:18px; accent-color:var(--cyan); }
- #trackpanel input[type=number]{ width:48px; background:var(--field-bg); color:var(--txt); border:1px solid var(--field-bd); border-radius:7px; padding:5px 6px; font-size:13px; -moz-appearance:textfield; }
+ #trackpanel .tp-chk input{ width:18px; height:18px; accent-color:var(--cyan); flex:0 0 auto; }
+ #trackpanel .tp-loop{ color:var(--muted); }
+ #trackpanel input[type=number]{ width:46px; background:var(--field-bg); color:var(--txt); border:1px solid var(--field-bd); border-radius:7px; padding:5px 6px; font-size:13px; -moz-appearance:textfield; }
#trackpanel input[type=number]::-webkit-outer-spin-button, #trackpanel input[type=number]::-webkit-inner-spin-button{ -webkit-appearance:none; margin:0; }
#trackpanel select{ background:var(--field-bg); color:var(--txt); border:1px solid var(--field-bd); border-radius:7px; padding:5px 6px; font-size:13px; }
- #trackpanel .tp-sub{ display:flex; align-items:center; gap:6px; flex-wrap:wrap; }
+ #trackpanel .tp-sub{ display:flex; align-items:center; gap:6px; flex-wrap:nowrap; white-space:nowrap; color:var(--muted); }
#trackpanel .tp-sub.off{ display:none; }
- #trackpanel .tp-sub b{ color:var(--txt); font-weight:600; }
#trackpanel .tp-str{ display:flex; gap:6px; }
#trackpanel .tp-str input{ flex:1 1 auto; min-width:0; background:var(--field-bg); color:var(--txt); border:1px solid var(--field-bd); border-radius:7px; padding:6px 8px; font-family:"Courier New",monospace; font-size:11px; }
#trackpanel .tp-btn{ flex:0 0 auto; background:linear-gradient(180deg,var(--btn1),var(--btn2)); color:var(--txt); border:1px solid var(--btn-bd); border-radius:7px; padding:0 12px; font-size:12px; cursor:pointer; }
@@ -112,6 +117,15 @@
.lmeta .rhythm{ flex:0 0 auto; color:var(--txt); display:block; }
.lmeta .polybadge{ flex:0 0 auto; color:var(--poly); font-weight:700; letter-spacing:.01em; }
.lane.poly .lmeta{ border-left:3px solid var(--poly); padding-left:6px; }
+ .lmeta .rh-host{ flex:0 0 auto; display:flex; align-items:center; padding:2px 3px; margin:-2px -1px; border-radius:5px; cursor:pointer; }
+ .lmeta .rh-host:active{ background:rgba(127,139,154,.22); }
+ /* graphic note-value picker */
+ .noterow{ display:flex; gap:8px; flex-wrap:wrap; }
+ .noterow .notebtn{ flex:0 0 auto; display:flex; flex-direction:column; align-items:center; gap:4px; min-width:56px; padding:9px 6px;
+ background:var(--field-bg); border:1px solid var(--field-bd); border-radius:10px; color:var(--txt); cursor:pointer; }
+ .noterow .notebtn.active{ border-color:var(--cyan); box-shadow:0 0 0 1px var(--cyan); }
+ .noterow .notebtn .rhythm{ height:22px; width:auto; }
+ .noterow .notebtn small{ font-size:10px; color:var(--muted); letter-spacing:.02em; }
/* beats line up in columns across lanes; sub-beats sit inside the beat cell, smaller */
.pads{ flex:1 1 auto; display:flex; gap:7px; overflow-x:auto; padding-bottom:2px; min-width:0; align-items:center; }
.beatcell{ flex:1 1 0; min-width:0; display:flex; gap:2px; align-items:center; }
@@ -132,7 +146,7 @@
/* ---- transport ---- */
#transport{ flex:0 0 auto; display:flex; justify-content:center; padding-top:2px; }
- .tgrid{ display:grid; width:100%; max-width:560px; gap:clamp(6px,1.6vmin,13px);
+ .tgrid{ display:grid; width:100%; gap:clamp(6px,1.6vmin,13px);
grid-template-columns:1fr 1.5fr 1.5fr 1fr; grid-template-areas:"dn10 prev next up10" "dn play prac up"; }
.tbtn{ background:linear-gradient(180deg,var(--btn1),var(--btn2)); color:var(--txt); border:1px solid var(--btn-bd);
border-radius:14px; height:clamp(46px,11vmin,72px); font-size:clamp(18px,4.4vmin,28px); cursor:pointer;
@@ -146,9 +160,11 @@
.tbtn.prac{ background:linear-gradient(180deg,#1c87b8,#136488); border-color:#1f9bd0; color:#eaf8ff; }
.tbtn.prac.on{ background:linear-gradient(180deg,#c8922a,#9c6f12); border-color:#e0a93a; color:#fff; }
- /* landscape phones: 2-column grid — pulse + transport on the left, panel + lanes on the right */
- @media (orientation:landscape) and (max-height:600px){
- #top{ flex-direction:row; align-items:flex-end; gap:12px; }
+ /* landscape (phone AND tablet — same layout, just scaled): 2-column grid —
+ pulse + transport on the left, panel + lanes on the right */
+ @media (orientation:landscape){
+ #app{ --maxw:1060px; }
+ #top{ flex-direction:row-reverse; align-items:flex-end; gap:14px; }
#top .sels{ flex:3 1 0; min-width:0; }
#top .trow{ flex:2 1 0; min-width:0; }
#mid{ display:grid; align-items:center; gap:8px 4vw;
@@ -157,33 +173,38 @@
#stage{ grid-area:stage; align-self:center; }
#detail{ grid-area:detail; align-self:stretch; width:auto; max-width:none; max-height:none; min-height:0; overflow-y:auto; }
#transport{ grid-area:transport; align-self:end; }
- #pulse{ width:clamp(100px,30vmin,240px); height:clamp(100px,30vmin,240px); }
- #bpmlab{ display:none; }
- .tbtn{ height:clamp(34px,11vmin,54px); }
+ #pulse{ width:clamp(110px,40vmin,360px); height:clamp(110px,40vmin,360px); }
+ .tbtn{ height:clamp(40px,13vmin,68px); }
}
/* ---- session bar ---- */
- #sessbar{ flex:0 0 auto; display:flex; align-items:center; gap:8px; width:100%; margin-top:6px; padding:9px 12px; border-radius:11px;
- cursor:pointer; text-decoration:none; background:rgba(127,139,154,.10); border:1px solid var(--panel-bd); color:var(--muted); font-size:13px; }
- #sessbar.rec{ background:rgba(192,57,43,.12); border-color:#c0392b; color:var(--txt); cursor:default; }
+ #sessbar{ flex:0 0 auto; display:flex; align-items:center; gap:10px; width:100%; margin-top:6px; padding:9px 12px; border-radius:11px;
+ background:rgba(127,139,154,.10); border:1px solid var(--panel-bd); color:var(--muted); font-size:13px; }
+ #sessbar.rec{ background:rgba(192,57,43,.12); border-color:#c0392b; color:var(--txt); }
+ #sessLink{ flex:1 1 auto; min-width:0; display:flex; align-items:center; gap:8px; text-decoration:none; color:inherit; cursor:pointer; }
#sessbar .dotrec{ width:9px; height:9px; border-radius:50%; background:#e0493a; box-shadow:0 0 8px #e0493a; flex:0 0 auto; display:none; }
- #sessbar.rec .dotrec{ display:block; } #sessText{ flex:1 1 auto; }
+ #sessbar.rec .dotrec{ display:block; }
+ #sessText{ flex:1 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
+ #repoLink{ flex:0 0 auto; display:inline-flex; align-items:center; opacity:.38; }
+ #repoLink:hover{ opacity:.85; }
+ #repoLink .rlogo{ height:13px; width:auto; display:block; }
+ [data-theme="light"] .logo-dark{ display:none; } [data-theme="dark"] .logo-light{ display:none; }
/* ---- bottom sheet (lane editor) ---- */
#scrim{ position:fixed; inset:0; background:rgba(0,0,0,.55); opacity:0; pointer-events:none; transition:opacity .2s; z-index:40; }
#scrim.open{ opacity:1; pointer-events:auto; }
- #laneSheet, #trackSheet, #saveSheet{ position:fixed; left:0; right:0; bottom:0; z-index:50; max-height:88vh; overflow-y:auto;
+ #laneSheet, #trackSheet, #saveSheet, #noteSheet, #shareSheet{ position:fixed; left:0; right:0; bottom:0; z-index:50; max-height:88vh; overflow-y:auto;
background:var(--panel-bg); border-top:1px solid var(--panel-bd); border-radius:18px 18px 0 0; transform:translateY(110%);
transition:transform .26s cubic-bezier(.2,.8,.2,1);
padding:12px max(16px,env(safe-area-inset-right)) max(18px,env(safe-area-inset-bottom)) max(16px,env(safe-area-inset-left)); }
- #laneSheet.open, #trackSheet.open, #saveSheet.open{ transform:none; }
- #laneSheet .grab, #trackSheet .grab, #saveSheet .grab{ width:42px; height:5px; border-radius:3px; background:var(--panel-bd); margin:0 auto 12px; }
- #laneSheet h2, #trackSheet h2, #saveSheet h2{ margin:0 0 10px; font-size:16px; }
- #laneSheet label, #trackSheet label, #saveSheet label{ display:block; font-size:12px; color:var(--muted); margin:10px 0 5px; }
+ #laneSheet.open, #trackSheet.open, #saveSheet.open, #noteSheet.open, #shareSheet.open{ transform:none; }
+ #laneSheet .grab, #trackSheet .grab, #saveSheet .grab, #noteSheet .grab, #shareSheet .grab{ width:42px; height:5px; border-radius:3px; background:var(--panel-bd); margin:0 auto 12px; }
+ #laneSheet h2, #trackSheet h2, #saveSheet h2, #noteSheet h2, #shareSheet h2{ margin:0 0 10px; font-size:16px; }
+ #laneSheet label, #trackSheet label, #saveSheet label, #shareSheet label{ display:block; font-size:12px; color:var(--muted); margin:10px 0 5px; }
#laneSheet select, #trackSheet select, #saveSheet select,
- #laneSheet input[type=text], #trackSheet input[type=text], #trackSheet input[type=number], #saveSheet input[type=text]{
+ #laneSheet input[type=text], #trackSheet input[type=text], #trackSheet input[type=number], #saveSheet input[type=text], #shareSheet input[type=text]{
width:100%; background:var(--field-bg); color:var(--txt); border:1px solid var(--field-bd); border-radius:10px; padding:11px; font-size:15px; }
- #laneSheet .lrow, #trackSheet .lrow, #saveSheet .lrow{ display:flex; gap:12px; margin-top:10px; flex-wrap:wrap; align-items:center; }
+ #laneSheet .lrow, #trackSheet .lrow, #saveSheet .lrow, #shareSheet .lrow{ display:flex; gap:12px; margin-top:10px; flex-wrap:wrap; align-items:center; }
#laneSheet .half, #trackSheet .half{ display:block; flex:1 1 120px; margin:0; }
#laneSheet .chk, #trackSheet .chk{ display:flex; align-items:center; gap:8px; font-size:14px; color:var(--txt); margin-top:16px; }
#laneSheet .chk input, #trackSheet .chk input{ width:20px; height:20px; accent-color:var(--cyan); flex:0 0 auto; }
@@ -191,6 +212,9 @@
.lfoot{ display:flex; justify-content:space-between; align-items:center; margin-top:18px; }
.lbtn{ cursor:pointer; color:var(--txt); background:linear-gradient(180deg,var(--btn1),var(--btn2)); border:1px solid var(--btn-bd); border-radius:10px; padding:10px 16px; font-size:14px; }
.lbtn.danger{ background:transparent; color:#ff7a7a; border-color:#ff7a7a; }
+ .seg{ display:flex; gap:8px; margin-bottom:6px; }
+ .seg button{ flex:1 1 0; padding:9px; border:1px solid var(--field-bd); background:var(--field-bg); color:var(--muted); border-radius:9px; font-size:14px; cursor:pointer; }
+ .seg button.active{ border-color:var(--cyan); color:var(--txt); }
.seclbl{ font-size:11px; letter-spacing:.1em; text-transform:uppercase; color:var(--muted); margin:4px 0 8px; }
.savemsg{ font-size:12px; color:#5fd08a; align-self:center; }
.liblbl{ font-size:12px; color:var(--muted); margin:14px 0 4px; }
@@ -216,17 +240,18 @@
@@ -277,10 +306,7 @@
Edit lane
-
-
-
-
+
@@ -290,6 +316,26 @@
+
+
+
+
+
+
+
Share
+
+
+
+
+
+
+
+
+
@@ -363,9 +409,9 @@ function scheduler(){
for(const m of meters){ while(m.nextTime
'';
const stem=(sx)=>'';
const beam=(x0,x1,y)=>'';
- const wrap=(W,g)=>'';
- if(swing && n===2){ // dotted-eighth + sixteenth (shuffle)
- const LEFT=3, U=4.6, cx0=LEFT, cx1=LEFT+2*U, s0=cx0+2, s1=cx1+2, W=Math.round(cx1+8);
- let g=head(cx0)+head(cx1)+stem(s0)+stem(s1)+beam(s0-0.45,s1+0.45,topY)+beam(s1-3.4,s1+0.45,topY+2.6)
- +'';
- return wrap(W,g);
- }
const beams = n===1?0 : (n<=3?1:2), tup = (n===3||n>=5)?n:null;
const LEFT=3, GAP = n<=2?8 : n<=4?6.5 : 5.5, W=Math.round(LEFT*2 + (n-1)*GAP + 4);
let g="", first=0, last=0;
@@ -487,17 +525,19 @@ function rhythmSVG(n, swing){
if(beams>=1) g+=beam(first-0.45,last+0.45,topY);
if(beams>=2) g+=beam(first-0.45,last+0.45,topY+2.6);
if(tup) g+=''+tup+'';
- return wrap(W,g);
+ return '';
}
-function laneMetaHTML(m){ const eff=laneNoteValue(m), swGlyph=m.swing&&eff===2;
+function laneMetaHTML(m){ const eff=laneNoteValue(m);
const ref=(meters[0]?meters[0].beatsPerBar:m.beatsPerBar);
const poly=m.poly?"↻"+m.beatsPerBar+":"+ref+"":"";
- return ""+esc(m.sound)+""+rhythmSVG(eff,m.swing)+""+esc(m.groupsStr)+(m.swing&&!swGlyph?" sw":"")+""+poly; }
+ return ""+esc(m.sound)+""+rhythmSVG(eff)+""+esc(m.groupsStr)+""+poly; }
+function setLaneMeta(m){ if(!m._meta) return; m._meta.innerHTML=laneMetaHTML(m);
+ const rh=m._meta.querySelector(".rh-host"); if(rh) rh.onclick=(e)=>{ e.stopPropagation(); openNotePicker(m._idx); }; }
function buildLanes(){
const box=$("lanes"); box.innerHTML="";
meters.forEach((m,i)=>{
const lane=document.createElement("div"); lane.className="lane"+(m.enabled?"":" off")+(m.poly?" poly":"");
- const meta=document.createElement("button"); meta.className="lmeta"; meta.innerHTML=laneMetaHTML(m); m._meta=meta;
+ const meta=document.createElement("button"); meta.className="lmeta"; m._meta=meta; m._idx=i; setLaneMeta(m);
meta.onclick=()=>openLaneSheet(i);
const pads=document.createElement("div"); pads.className="pads";
const spb=m.stepsPerBeat; m._padEls=new Array(m.beatsPerBar*spb); m._lastPad=-1;
@@ -512,8 +552,22 @@ function buildLanes(){
function renderPadLevels(){ meters.forEach(m=>{ if(!m._padEls) return;
m._padEls.forEach((p,k)=>{ if(p) p.className=padClass(m,k); }); }); }
function cyclePad(m,k,p){ m.beatsOn[k]=((m.beatsOn[k]|0)+1)%4; if(m.orns) m.orns[k]=0; p.className=padClass(m,k);
- if(m._meta) m._meta.innerHTML=laneMetaHTML(m); // note value can change as hits are added/removed
+ setLaneMeta(m); // note value can change as hits are added/removed
saveState(); }
+
+/* ---- graphic note-value picker (tap a lane's rhythm icon, or use it in the lane sheet) ---- */
+const NOTE_OPTS=[1,2,3,4,6];
+function noteName(n){ return {1:"quarter",2:"eighth",3:"triplet",4:"sixteenth",6:"sextuplet"}[n]||(n+"-tuplet"); }
+function renderNoteOpts(box, cur, pick){ if(!box) return; box.innerHTML="";
+ NOTE_OPTS.forEach(n=>{ const b=el("button","notebtn"+(n===cur?" active":"")); b.innerHTML=rhythmSVG(n)+""+noteName(n)+""; b.onclick=()=>pick(n); box.appendChild(b); }); }
+function setLaneSub(i,n){ const m=meters[i]; if(!m) return;
+ rebuildLane(i,{groupsStr:m.groupsStr,stepsPerBeat:n,swing:m.swing,poly:m.poly,enabled:m.enabled,sound:m.sound,gainDb:m.gainDb,beatsOn:m.beatsOn.slice(),orns:(m.orns||[]).slice()});
+ laneSig=null; renderAll(); saveState(); }
+function openNotePicker(i){ const m=meters[i]; if(!m) return; editLaneIdx=i;
+ renderNoteOpts($("noteOpts"), m.stepsPerBeat, (n)=>{ setLaneSub(i,n); closeSheets(); });
+ $("laneSheet").classList.remove("open"); $("saveSheet").classList.remove("open"); $("shareSheet").classList.remove("open"); $("scrim").classList.add("open"); $("noteSheet").classList.add("open"); }
+function refreshLaneSheetNotes(){ const m=meters[editLaneIdx]; if(!m) return;
+ renderNoteOpts($("lsNotes"), m.stepsPerBeat, (n)=>{ setLaneSub(editLaneIdx,n); refreshLaneSheetNotes(); }); }
function renderPadPlayheads(){ meters.forEach(m=>{ if(!m._padEls) return; const cur=state.running?m.currentStep:-1;
if(m._lastPad!==cur){ if(m._lastPad>=0&&m._padEls[m._lastPad]) m._padEls[m._lastPad].classList.remove("cur"); if(cur>=0&&m._padEls[cur]) m._padEls[cur].classList.add("cur"); m._lastPad=cur; } }); }
function rebuildLane(i,cfg){
@@ -531,17 +585,17 @@ function addLane(){ meters.push(buildMeters([{groupsStr:"4",stepsPerBeat:1,sound
(function(){ const sel=$("lsSound"); VOICES.forEach(([k,lab])=>{ const o=document.createElement("option"); o.value=k; o.textContent=lab; sel.appendChild(o); }); })();
function gainLabel(db){ return (db>0?"+":"")+db+" dB"; }
function openLaneSheet(i){ editLaneIdx=i; const m=meters[i]; if(!m) return;
- $("lsSound").value=m.sound; $("lsGroup").value=m.groupsStr; $("lsSub").value=String(m.stepsPerBeat); $("lsSwing").checked=!!m.swing; $("lsPoly").checked=!!m.poly; $("lsMute").checked=!m.enabled;
- $("lsGain").value=m.gainDb||0; $("lsGainVal").textContent=gainLabel(m.gainDb||0);
- $("saveSheet").classList.remove("open"); $("scrim").classList.add("open"); $("laneSheet").classList.add("open"); }
-function closeSheets(){ $("scrim").classList.remove("open"); $("laneSheet").classList.remove("open"); $("saveSheet").classList.remove("open"); }
+ $("lsSound").value=m.sound; $("lsGroup").value=m.groupsStr; $("lsPoly").checked=!!m.poly; $("lsMute").checked=!m.enabled;
+ $("lsGain").value=m.gainDb||0; $("lsGainVal").textContent=gainLabel(m.gainDb||0); refreshLaneSheetNotes();
+ $("saveSheet").classList.remove("open"); $("noteSheet").classList.remove("open"); $("shareSheet").classList.remove("open"); $("scrim").classList.add("open"); $("laneSheet").classList.add("open"); }
+function closeSheets(){ ["laneSheet","saveSheet","noteSheet","shareSheet","scrim"].forEach(id=>$(id).classList.remove("open")); }
const closeLaneSheet=closeSheets;
function applyLane(){ const m=meters[editLaneIdx]; if(!m) return;
let grp=($("lsGroup").value||"").trim()||"4";
- rebuildLane(editLaneIdx,{groupsStr:grp,stepsPerBeat:Math.max(1,Math.min(4,parseInt($("lsSub").value,10)||1)),swing:$("lsSwing").checked,
+ rebuildLane(editLaneIdx,{groupsStr:grp,stepsPerBeat:m.stepsPerBeat,swing:m.swing,
poly:$("lsPoly").checked,enabled:!$("lsMute").checked,sound:$("lsSound").value,gainDb:parseInt($("lsGain").value,10)||0,beatsOn:m.beatsOn.slice(),orns:(m.orns||[]).slice()});
- laneSig=null; renderAll(); saveState(); }
-["lsSound","lsSub","lsSwing","lsPoly","lsMute"].forEach(id=>$(id).addEventListener("change",applyLane));
+ laneSig=null; renderAll(); saveState(); refreshLaneSheetNotes(); }
+["lsSound","lsPoly","lsMute"].forEach(id=>$(id).addEventListener("change",applyLane));
$("lsGain").addEventListener("input",()=>{ const m=meters[editLaneIdx]; if(!m) return; m.gainDb=parseInt($("lsGain").value,10)||0; $("lsGainVal").textContent=gainLabel(m.gainDb); saveState(); }); // live, no rebuild
$("lsGroup").addEventListener("change",applyLane);
$("lsDone").onclick=closeLaneSheet;
@@ -553,28 +607,25 @@ function clampInt(v,lo,hi,def){ v=parseInt(v,10); if(isNaN(v)) return def; retur
function flashIp(msg){ $("ipMsg").textContent=msg; setTimeout(()=>{ if($("ipMsg").textContent===msg) $("ipMsg").textContent=""; },1600); }
function buildTrackPanel(){
if(document.activeElement&&document.activeElement.closest&&document.activeElement.closest("#trackpanel")) return; // don't fight the user mid-edit
+ const hasEnd=segBars>0;
$("ipBars").value=segBars||0;
- $("ipEnd").value = curEnd==null?"loop":(curEnd==="stop"?"stop":"next");
+ $("ipThen").style.display=hasEnd?"":"none"; $("ipLoop").style.display=hasEnd?"none":"";
+ $("ipEnd").value = curEnd==="stop"?"stop":(curEnd===-1?"prev":"next");
$("ipRamp").checked=ramp.on; $("ipGap").checked=trainer.on;
$("ipRampStart").value=ramp.startBpm||80; $("ipRampAmt").value=ramp.amount||5; $("ipRampEvery").value=ramp.everyBars||4;
$("ipGapPlay").value=trainer.playBars||2; $("ipGapMute").value=trainer.muteBars||2;
$("ipRampRow").classList.toggle("off",!ramp.on); $("ipGapRow").classList.toggle("off",!trainer.on);
- $("ipStr").value=currentPatch();
}
function applyTrackPanel(){
segBars=Math.max(0,parseInt($("ipBars").value,10)||0);
- const e=$("ipEnd").value; curEnd = e==="loop"?null:(e==="stop"?"stop":1);
+ if(segBars>0){ const e=$("ipEnd").value; curEnd = e==="stop"?"stop":(e==="prev"?-1:1); } else { curEnd=null; } // 0 bars = loop forever
+ $("ipThen").style.display=segBars>0?"":"none"; $("ipLoop").style.display=segBars>0?"none":"";
ramp.on=$("ipRamp").checked; ramp.startBpm=clampInt($("ipRampStart").value,30,300,80); ramp.amount=clampInt($("ipRampAmt").value,1,50,5); ramp.everyBars=clampInt($("ipRampEvery").value,1,64,4);
trainer.on=$("ipGap").checked; trainer.playBars=clampInt($("ipGapPlay").value,1,32,2); trainer.muteBars=clampInt($("ipGapMute").value,1,32,2);
$("ipRampRow").classList.toggle("off",!ramp.on); $("ipGapRow").classList.toggle("off",!trainer.on);
- $("ipStr").value=currentPatch(); saveState();
+ saveState();
}
["ipBars","ipEnd","ipRamp","ipGap","ipRampStart","ipRampAmt","ipRampEvery","ipGapPlay","ipGapMute"].forEach(id=>$(id).addEventListener("change",applyTrackPanel));
-$("ipCopy").onclick=()=>{ const s=currentPatch();
- if(navigator.clipboard&&navigator.clipboard.writeText){ navigator.clipboard.writeText(s).then(()=>flashIp("Copied ✓"),()=>{ $("ipStr").select(); }); } else { $("ipStr").select(); try{ document.execCommand("copy"); flashIp("Copied ✓"); }catch(e){} } };
-$("ipPaste").onclick=()=>{ const txt=($("ipStr").value||"").trim().replace(/^[#?&]*p=/,"");
- try{ const setup=patchToSetup(txt); if(!setup.lanes.length) throw 0; loadSetup(setup); laneSig=null; renderAll(); saveState(); flashIp("Applied ✓"); }
- catch(e){ flashIp("✗ invalid string"); } };
/* ---- save & library: user set lists/tracks (same store + format as the editor) ---- */
function userSetlists(){ return lsGet(LS_SETLISTS,[]); }
@@ -649,9 +700,28 @@ function openSaveSheet(){
$("saveName").value=currentName()||"My track"; buildSaveTo();
const upd=$("saveUpd"); if(slKey[0]==="s"){ upd.style.display=""; upd.textContent='Update “'+(currentName()||"track")+'”'; } else upd.style.display="none";
$("saveMsg").textContent=""; renderLibrary();
- $("laneSheet").classList.remove("open"); $("scrim").classList.add("open"); $("saveSheet").classList.add("open"); }
+ $("laneSheet").classList.remove("open"); $("noteSheet").classList.remove("open"); $("shareSheet").classList.remove("open"); $("scrim").classList.add("open"); $("saveSheet").classList.add("open"); }
$("saveNew").onclick=doSaveAsNew; $("saveUpd").onclick=doUpdate; $("saveDone").onclick=closeSheets; $("saveBtn").onclick=openSaveSheet;
+/* ---- share: a track or set list as a link, or paste a string to load ---- */
+function setlistToCode(sl){ return b64u(JSON.stringify({t:sl.title||"",d:sl.description||"",i:(sl.items||[]).map(it=>({n:it.name,p:setupToPatch(it)}))})); }
+function shareSetlistObj(){ return {title:setlist?setlist.title:"Set list", description:"", items:(setlist?setlist.items:[]).map((it,i)=> i===idx?{name:it.name, ...curSetupObj()}:it)}; }
+let shareKind="p";
+function shareText(){ return shareKind==="p" ? currentPatch() : setlistToCode(shareSetlistObj()); }
+function shareUrl(){ return location.origin+location.pathname+"#"+shareKind+"="+(shareKind==="p"?encodeURIComponent(currentPatch()):setlistToCode(shareSetlistObj())); }
+function refreshShare(){ $("shareLink").value=shareUrl(); $("shareSeg").querySelectorAll("button").forEach(b=>b.classList.toggle("active",b.dataset.k===shareKind)); }
+function flashShare(m){ $("shareMsg").textContent=m; setTimeout(()=>{ if($("shareMsg").textContent===m) $("shareMsg").textContent=""; },1600); }
+function copyText(s, ok){ if(navigator.clipboard&&navigator.clipboard.writeText){ navigator.clipboard.writeText(s).then(ok,()=>{}); } else { const t=$("shareLink"); t.value=s; t.select(); try{ document.execCommand("copy"); ok(); }catch(e){} refreshShare(); } }
+function openShareSheet(){ shareKind="p"; refreshShare(); $("sharePaste").value=""; $("shareMsg").textContent="";
+ ["laneSheet","saveSheet","noteSheet"].forEach(id=>$(id).classList.remove("open")); $("scrim").classList.add("open"); $("shareSheet").classList.add("open"); }
+$("shareSeg").querySelectorAll("button").forEach(b=>b.onclick=()=>{ shareKind=b.dataset.k; refreshShare(); });
+$("shareCopy").onclick=()=>copyText(shareUrl(),()=>flashShare("Link copied ✓"));
+$("shareCopyT").onclick=()=>copyText(shareText(),()=>flashShare("Copied ✓"));
+$("shareLoad").onclick=()=>{ const v=($("sharePaste").value||"").trim(); if(!v){ flashShare("Paste a string or link"); return; }
+ if(loadFromHash(/[#?&](p|sl)=/.test(v)?v:("#p="+v))){ closeSheets(); } else flashShare("✗ not valid"); };
+$("shareDone").onclick=closeSheets;
+$("shareBtn").onclick=openShareSheet;
+
/* ========================= SET-LIST / TRACK DROPDOWNS ======================== */
function opt(v,t){ const o=document.createElement("option"); o.value=v; o.textContent=t; return o; }
function og(label){ const g=document.createElement("optgroup"); g.label=label; return g; }
@@ -760,13 +830,13 @@ function loadFromHash(text){
/* ========================= HELP TOUR ========================================= */
const TOUR=[
+ {sel:"#utilrow", title:"Controls", text:"Volume (soft p → loud f), ◐ light/dark theme, ⛶ full screen, the ↑ share menu, and ? to replay this tour anytime."},
{sel:".sels", title:"Pick what to play", text:"Choose a set list and the track within it. Tracks are your practice items — name them for whatever you're working on, even if two share the same beat."},
- {sel:"#pulse", title:"Tempo", text:"Tap the BPM to tap-tempo, press-and-hold to type an exact value, or drag up/down to scrub."},
- {sel:"#bDn10", title:"Nudge tempo", text:"−10 / +10 for big jumps, − / + for fine adjustments."},
- {sel:"#lanes", title:"Edit the beat", text:"Each lane is a row of pads that blink on the beat — tap a pad to cycle rest → beat → accent → ghost. Tap a lane's label to change its sound, grouping, subdivision, mute or polymeter. “+ Add lane” for more."},
- {sel:"#bPlay", title:"Play", text:"Just runs the metronome — nothing is added to your practice log."},
- {sel:"#bPrac", title:"Practice = a timed session", text:"Practice times your playing and logs it to your practice log (it does NOT record audio). It starts a session clock; Play turns into Stop. Use Practice to start/pause each track, then tap Stop when you're done to save the session."},
- {sel:"#trackpanel", title:"Track settings", text:"Right here: bar count, what happens at the end (loop / stop / next), a tempo ramp and practice gaps — plus copy or paste this track's share string."},
+ {sel:"#saveBtn", title:"Save & library", text:"Save the current track — “Save as new”, or “Update” one of yours. The same sheet is your library: make set lists and rename / reorder / delete tracks. It all lives with the full editor too."},
+ {sel:"#pulse", title:"Tempo", text:"Tap the BPM to tap-tempo, press-and-hold to type an exact value, or drag up/down to scrub. ±10 / ±1 buttons nudge it."},
+ {sel:"#lanes", title:"Edit the beat", text:"Each lane is a row of pads that blink on the beat — tap a pad to cycle rest → beat → accent → ghost. Tap the little note icon to pick the note value (eighths, triplets, sixteenths…); tap the lane name for sound, grouping, mute or polymeter. “+ Add lane” for more."},
+ {sel:"#trackpanel", title:"Track settings", text:"How many bars to play and what happens then (loop, stop, or jump to the next/previous track), plus a tempo ramp and practice gaps."},
+ {sel:"#bPrac", title:"Practice = a timed session", text:"Play just runs the metronome. Practice times your playing and logs it (not audio): it starts a session clock and Play becomes Stop — start/pause each track, then Stop to save the session."},
{sel:"#sessbar", title:"Your practice log", text:"Review past sessions, add notes, and compare a track across days."},
];
let tstep=0;
@@ -797,7 +867,7 @@ $("bPrev").onclick=()=>gotoItem(idx-1,state.running); $("bNext").onclick=()=>got
$("bUp").onclick=()=>nudge(+1); $("bDown").onclick=()=>nudge(-1);
$("bUp10").onclick=()=>nudge(+10); $("bDn10").onclick=()=>nudge(-10);
$("vol").oninput=(e)=>{ state.volume=(+e.target.value)/100; if(masterGain) masterGain.gain.value=state.volume; saveState(); };
-$("sessbar").addEventListener("click",(e)=>{ if(sessionActive) e.preventDefault(); });
+$("sessLink").addEventListener("click",(e)=>{ if(sessionActive) e.preventDefault(); }); // don't lose an in-progress session
/*@BUILD:include:src/chrome.js@*/