diff --git a/mobile.html b/mobile.html
index cd20f97..c24a78b 100644
--- a/mobile.html
+++ b/mobile.html
@@ -145,18 +145,18 @@
/* ---- 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{ position:fixed; left:0; right:0; bottom:0; z-index:50; max-height:88vh; overflow-y:auto;
+ #laneSheet, #trackSheet, #saveSheet{ 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{ transform:none; }
- #laneSheet .grab, #trackSheet .grab{ width:42px; height:5px; border-radius:3px; background:var(--panel-bd); margin:0 auto 12px; }
- #laneSheet h2, #trackSheet h2{ margin:0 0 10px; font-size:16px; }
- #laneSheet label, #trackSheet label{ display:block; font-size:12px; color:var(--muted); margin:10px 0 5px; }
- #laneSheet select, #trackSheet select,
- #laneSheet input[type=text], #trackSheet input[type=text], #trackSheet input[type=number]{
+ #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 select, #trackSheet select, #saveSheet select,
+ #laneSheet input[type=text], #trackSheet input[type=text], #trackSheet input[type=number], #saveSheet 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{ display:flex; gap:12px; margin-top:10px; flex-wrap:wrap; }
+ #laneSheet .lrow, #trackSheet .lrow, #saveSheet .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; }
@@ -164,6 +164,15 @@
.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; }
+ .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; }
+ .libhint{ font-size:12px; color:var(--muted); padding:6px 2px; line-height:1.4; }
+ .librow{ display:flex; align-items:center; gap:4px; padding:4px 0; border-bottom:1px solid var(--panel-bd); }
+ .librow.active .libname{ color:var(--cyan); font-weight:600; }
+ .libname{ flex:1 1 auto; min-width:0; text-align:left; background:transparent; border:none; color:var(--txt); font-size:14px; padding:7px 2px; cursor:pointer; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
+ .ibtn{ flex:0 0 auto; width:32px; height:32px; border-radius:7px; background:var(--chip-bg); border:1px solid var(--chip-bd); color:var(--txt); font-size:13px; cursor:pointer; }
+ .ibtn:disabled{ opacity:.3; }
/* ---- help tour (coachmarks) ---- */
#tour{ position:fixed; inset:0; z-index:200; display:none; }
@@ -186,6 +195,7 @@
@@ -448,17 +480,20 @@ function addLane(){ meters.push(buildMeters([{groupsStr:"4",stepsPerBeat:1,sound
/* lane settings sheet */
(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;
- $("scrim").classList.add("open"); $("laneSheet").classList.add("open"); }
-function closeSheets(){ $("scrim").classList.remove("open"); $("laneSheet").classList.remove("open"); $("trackSheet").classList.remove("open"); }
+ $("lsGain").value=m.gainDb||0; $("lsGainVal").textContent=gainLabel(m.gainDb||0);
+ $("trackSheet").classList.remove("open"); $("saveSheet").classList.remove("open"); $("scrim").classList.add("open"); $("laneSheet").classList.add("open"); }
+function closeSheets(){ $("scrim").classList.remove("open"); $("laneSheet").classList.remove("open"); $("trackSheet").classList.remove("open"); $("saveSheet").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,
- poly:$("lsPoly").checked,enabled:!$("lsMute").checked,sound:$("lsSound").value,gainDb:m.gainDb||0,beatsOn:m.beatsOn.slice(),orns:(m.orns||[]).slice()});
+ 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));
+$("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;
$("lsDel").onclick=()=>{ if(meters.length<=1){ closeLaneSheet(); return; } meters.splice(editLaneIdx,1); laneSig=null; renderAll(); saveState(); closeLaneSheet(); };
@@ -473,7 +508,7 @@ function openTrackSheet(){
$("tsGap").checked=trainer.on; $("tsGapPlay").value=trainer.playBars||2; $("tsGapMute").value=trainer.muteBars||2;
$("tsRampRow").classList.toggle("off",!ramp.on); $("tsGapRow").classList.toggle("off",!trainer.on);
$("tsStr").value=currentPatch(); $("tsMsg").textContent="";
- $("laneSheet").classList.remove("open"); $("scrim").classList.add("open"); $("trackSheet").classList.add("open");
+ $("laneSheet").classList.remove("open"); $("saveSheet").classList.remove("open"); $("scrim").classList.add("open"); $("trackSheet").classList.add("open");
}
function applyTrackSettings(){
segBars=Math.max(0,parseInt($("tsBars").value,10)||0);
@@ -493,6 +528,82 @@ $("tsApply").onclick=()=>{ const txt=($("tsStr").value||"").trim().replace(/^[#?
$("tsDone").onclick=closeSheets;
$("trackBtn").onclick=openTrackSheet;
+/* ---- save & library: user set lists/tracks (same store + format as the editor) ---- */
+function userSetlists(){ return lsGet(LS_SETLISTS,[]); }
+function saveUserSetlists(a){ lsSet(LS_SETLISTS,a); savedLists=a; }
+function curSetupObj(){ return { bpm:state.bpm, lanes:snapshotLanes(), trainer:{...trainer}, ramp:{...ramp}, countMs:0, bars:segBars, rep:curRep, end:curEnd }; }
+function flashSave(msg){ $("saveMsg").textContent=msg; setTimeout(()=>{ if($("saveMsg").textContent===msg) $("saveMsg").textContent=""; },1800); }
+function el(tag,cls,txt){ const e=document.createElement(tag); if(cls) e.className=cls; if(txt!=null) e.textContent=txt; return e; }
+function ibtn(label,fn,dis){ const b=el("button","ibtn",label); b.disabled=!!dis; b.onclick=(e)=>{ e.stopPropagation(); fn(); }; return b; }
+
+function selectUserList(i){ const arr=userSetlists(); if(!arr[i]) return; slKey="s"+i; transientTitle=null;
+ loadSetlistObj({title:arr[i].title,items:(arr[i].items||[]).map(it=>({...it}))}); renderLibrary(); }
+function selectUserTrack(i,j){ slKey="s"+i; transientTitle=null; savedLists=userSetlists();
+ setlist={title:savedLists[i].title,items:savedLists[i].items.map(it=>({...it}))}; idx=Math.max(0,Math.min(j,setlist.items.length-1));
+ loadSetup(setlist.items[idx]); buildSetlistOptions(); buildTrackOptions(); renderAll(); saveState(); }
+
+function doSaveAsNew(){
+ const name=($("saveName").value||"My track").trim(); const arr=userSetlists(); let i;
+ if($("saveTo").value==="__new"){ const t=($("saveNewName").value||"My set list").trim(); arr.push({title:t,description:"",items:[]}); i=arr.length-1; }
+ else { i=+$("saveTo").value.slice(1); if(!arr[i]) return; }
+ arr[i].items.push({name, ...curSetupObj()}); saveUserSetlists(arr);
+ selectUserTrack(i, arr[i].items.length-1); $("saveNewName").value=""; buildSaveTo(); renderLibrary(); flashSave("Saved β");
+}
+function doUpdate(){
+ if(slKey[0]!=="s") return; const arr=userSetlists(), i=+slKey.slice(1); if(!arr[i]||!arr[i].items[idx]) return;
+ const oldName=arr[i].items[idx].name||"this track"; const nm=($("saveName").value||oldName).trim();
+ if(!confirm('Overwrite "'+oldName+'" with the current settings?')) return;
+ arr[i].items[idx]={name:nm, ...curSetupObj()}; saveUserSetlists(arr);
+ setlist.items[idx]={name:nm, ...curSetupObj()}; lastCur=null; buildTrackOptions(); renderInfo(); renderLibrary(); flashSave("Updated β");
+}
+function moveList(i,dir){ const arr=userSetlists(), j=i+dir; if(j<0||j>=arr.length) return; const t=arr[i]; arr[i]=arr[j]; arr[j]=t; saveUserSetlists(arr);
+ if(slKey==="s"+i) slKey="s"+j; else if(slKey==="s"+j) slKey="s"+i; buildSetlistOptions(); renderLibrary(); saveState(); }
+function moveTrack(i,j,dir){ const arr=userSetlists(), sl=arr[i], k=j+dir; if(!sl||k<0||k>=sl.items.length) return; const t=sl.items[j]; sl.items[j]=sl.items[k]; sl.items[k]=t; saveUserSetlists(arr);
+ if(slKey==="s"+i){ if(idx===j) idx=k; else if(idx===k) idx=j; setlist.items=sl.items.map(it=>({...it})); buildTrackOptions(); renderInfo(); } renderLibrary(); saveState(); }
+function renameList(i){ const arr=userSetlists(); if(!arr[i]) return; const n=prompt("Rename set list:",arr[i].title||""); if(n==null) return; arr[i].title=n.trim()||arr[i].title; saveUserSetlists(arr);
+ if(slKey==="s"+i&&setlist) setlist.title=arr[i].title; buildSetlistOptions(); renderLibrary(); saveState(); }
+function renameTrack(i,j){ const arr=userSetlists(); if(!arr[i]||!arr[i].items[j]) return; const n=prompt("Rename track:",arr[i].items[j].name||""); if(n==null) return; arr[i].items[j].name=n.trim()||arr[i].items[j].name; saveUserSetlists(arr);
+ if(slKey==="s"+i){ setlist.items[j].name=arr[i].items[j].name; if(idx===j){ lastCur=null; } buildTrackOptions(); renderInfo(); } renderLibrary(); saveState(); }
+function deleteTrack(i,j){ const arr=userSetlists(), sl=arr[i]; if(!sl||!sl.items[j]) return; if(!confirm('Delete track "'+(sl.items[j].name||"")+'"?')) return; sl.items.splice(j,1); saveUserSetlists(arr);
+ if(slKey==="s"+i){ if(!sl.items.length){ deleteListResolved(i); return; } if(idx>=sl.items.length) idx=sl.items.length-1; else if(idx>j) idx--; selectUserTrack(i,idx); } renderLibrary(); }
+function deleteList(i){ const arr=userSetlists(); if(!arr[i]) return; if(!confirm('Delete set list "'+(arr[i].title||"")+'" and all its tracks?')) return; deleteListResolved(i); }
+function deleteListResolved(i){ const arr=userSetlists(); arr.splice(i,1); saveUserSetlists(arr);
+ if(slKey==="s"+i){ slKey="b0"; setlist=BUILTIN[0]; idx=0; loadSetup(setlist.items[0]); buildSetlistOptions(); buildTrackOptions(); renderAll(); saveState(); }
+ else if(slKey[0]==="s"){ const k=+slKey.slice(1); if(k>i) slKey="s"+(k-1); buildSetlistOptions(); }
+ buildSaveTo(); renderLibrary(); }
+function newList(){ const n=prompt("New set list name:","My set list"); if(n==null) return; const arr=userSetlists(); arr.push({title:n.trim()||"My set list",description:"",items:[]}); saveUserSetlists(arr); buildSaveTo(); renderLibrary(); }
+
+function buildSaveTo(){ savedLists=userSetlists(); const sel=$("saveTo"); sel.innerHTML="";
+ savedLists.forEach((sl,i)=>sel.appendChild(opt("s"+i,(sl.title||("Set list "+(i+1)))+" ("+(sl.items?sl.items.length:0)+")")));
+ sel.appendChild(opt("__new","β New set listβ¦"));
+ sel.value = slKey[0]==="s" ? slKey : (savedLists.length?"s0":"__new");
+ $("saveNewName").style.display = sel.value==="__new" ? "block":"none"; }
+$("saveTo").onchange=()=>{ $("saveNewName").style.display = $("saveTo").value==="__new" ? "block":"none"; };
+
+function renderLibrary(){ savedLists=userSetlists(); const box=$("libBody"); box.innerHTML="";
+ box.appendChild(el("div","liblbl","Your set lists"));
+ if(!savedLists.length) box.appendChild(el("div","libhint","None yet β βSave as new trackβ creates one."));
+ savedLists.forEach((sl,i)=>{ const row=el("div","librow"+(slKey==="s"+i?" active":""));
+ const nm=el("button","libname",(sl.title||"set list")+" ("+(sl.items?sl.items.length:0)+")"); nm.onclick=()=>selectUserList(i); row.appendChild(nm);
+ row.appendChild(ibtn("β",()=>moveList(i,-1),i===0)); row.appendChild(ibtn("β",()=>moveList(i,1),i===savedLists.length-1));
+ row.appendChild(ibtn("β",()=>renameList(i))); row.appendChild(ibtn("π",()=>deleteList(i))); box.appendChild(row); });
+ const addL=el("button","addlane","β New set list"); addL.onclick=newList; box.appendChild(addL);
+ if(slKey[0]==="s"){ const i=+slKey.slice(1), sl=savedLists[i]; if(sl){
+ box.appendChild(el("div","liblbl","Tracks in β"+(sl.title||"set list")+"β"));
+ sl.items.forEach((it,j)=>{ const row=el("div","librow"+(idx===j?" active":""));
+ const nm=el("button","libname",(j+1)+". "+(it.name||"track")); nm.onclick=()=>{ gotoItem(j,state.running); renderLibrary(); }; row.appendChild(nm);
+ row.appendChild(ibtn("β",()=>moveTrack(i,j,-1),j===0)); row.appendChild(ibtn("β",()=>moveTrack(i,j,1),j===sl.items.length-1));
+ row.appendChild(ibtn("β",()=>renameTrack(i,j))); row.appendChild(ibtn("π",()=>deleteTrack(i,j))); box.appendChild(row); });
+ const addT=el("button","addlane","β Add current track here"); addT.onclick=()=>{ $("saveTo").value="s"+i; doSaveAsNew(); }; box.appendChild(addT);
+ }} else { box.appendChild(el("div","libhint","This set list is built-in (read-only). βSave as new trackβ copies your edits into one of your own set lists.")); }
+}
+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"); $("trackSheet").classList.remove("open"); $("scrim").classList.add("open"); $("saveSheet").classList.add("open"); }
+$("saveNew").onclick=doSaveAsNew; $("saveUpd").onclick=doUpdate; $("saveDone").onclick=closeSheets; $("saveBtn").onclick=openSaveSheet;
+
/* ========================= 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; }