[object Object]

← back to Scarlet Riverboat Masquerade

riverboat: rebuild year browse per UX review — tappable 44px year-tile grid (no more 11px bars), Source selector (Grateful Dead + Dead & Company), heat-colored tiles, loading/error states, mobile one-tap player, bigger transport controls

6b0db05ea8c0c0ff60d9ab789628b64790ee4c96 · 2026-09-02 08:40:01 -0700 · Steve Abrams

Files touched

Diff

commit 6b0db05ea8c0c0ff60d9ab789628b64790ee4c96
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Sep 2 08:40:01 2026 -0700

    riverboat: rebuild year browse per UX review — tappable 44px year-tile grid (no more 11px bars), Source selector (Grateful Dead + Dead & Company), heat-colored tiles, loading/error states, mobile one-tap player, bigger transport controls
---
 index.html | 151 +++++++++++++++++++++++++++++++++++++++----------------------
 1 file changed, 98 insertions(+), 53 deletions(-)

diff --git a/index.html b/index.html
index d08161d..fa6af65 100644
--- a/index.html
+++ b/index.html
@@ -154,18 +154,29 @@
   .mr-search{width:100%;background:rgba(40,12,16,.8);color:var(--ink);border:1px solid var(--panel-edge);border-radius:9px;padding:8px 10px;font-size:12px;margin-bottom:8px}
   .mr-search::placeholder{color:var(--ink-dim)}
   .mr-year{width:100%;background:rgba(40,12,16,.8);color:var(--ink);border:1px solid var(--panel-edge);border-radius:9px;padding:7px 9px;font-size:12px;margin-bottom:8px;cursor:pointer}
-  /* interactive touring-history year map */
-  .mr-map-head{display:flex;justify-content:space-between;align-items:center;font-size:10px;color:var(--ink-dim);margin:2px 0 3px;gap:8px}
+  /* source selector — which free Dead archive to browse */
+  .mr-src{display:flex;gap:6px;margin-bottom:9px}
+  .mr-src button{flex:1;background:rgba(40,12,16,.7);color:var(--ink-dim);border:1px solid var(--panel-edge);border-radius:9px;padding:8px 8px;cursor:pointer;line-height:1.25;text-align:center;font-family:var(--sans)}
+  .mr-src button b{display:block;font-size:12px;color:var(--ink);font-weight:600}
+  .mr-src button .yr{display:block;font-size:9px;color:var(--ink-dim);letter-spacing:.02em;margin-top:1px}
+  .mr-src button:hover{background:rgba(70,20,26,.85)}
+  .mr-src button.on{background:linear-gradient(180deg,#ffd97a,var(--gold));border-color:var(--gold)}
+  .mr-src button.on b,.mr-src button.on .yr{color:#2a0d10}
+  /* tappable touring-history year map — wrapping tile grid (WCAG-sized targets) */
+  .mr-map-head{display:flex;justify-content:space-between;align-items:center;font-size:11px;color:var(--ink-dim);margin:2px 0 5px;gap:8px}
   #mrMapLabel{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
-  .mr-allbtn{flex:none;background:rgba(70,20,26,.6);color:var(--ink);border:1px solid var(--panel-edge);border-radius:6px;font-size:10px;padding:2px 8px;cursor:pointer}
+  .mr-allbtn{flex:none;background:rgba(70,20,26,.6);color:var(--ink);border:1px solid var(--panel-edge);border-radius:8px;font-size:11px;padding:6px 12px;min-height:32px;cursor:pointer}
   .mr-allbtn.on{background:var(--gold);color:#2a0d10;border-color:var(--gold)}
-  .mr-map{display:flex;align-items:flex-end;gap:2px;height:60px;overflow-x:auto;overflow-y:hidden;padding:3px 2px 0;margin-bottom:8px;border-bottom:1px solid var(--panel-edge);scrollbar-width:thin}
-  .mr-yr{flex:0 0 auto;width:11px;display:flex;flex-direction:column;align-items:center;justify-content:flex-end;height:100%;cursor:pointer;background:none;border:0;padding:0}
-  .mr-yr .bar{width:8px;min-height:3px;border-radius:2px 2px 0 0;background:linear-gradient(180deg,var(--gold),#7a2a1e);opacity:.5;transition:opacity .15s,box-shadow .15s}
-  .mr-yr:hover .bar{opacity:.9}
-  .mr-yr.on .bar{opacity:1;background:linear-gradient(180deg,#ffd97a,var(--gold));box-shadow:0 0 8px rgba(240,190,90,.85)}
-  .mr-yr .yl{font-size:7px;line-height:1;color:var(--ink-dim);margin-top:2px;writing-mode:vertical-rl;transform:rotate(180deg);font-variant-numeric:tabular-nums;letter-spacing:-.5px;height:14px}
-  .mr-yr.on .yl{color:var(--gold)}
+  .mr-map{display:flex;flex-wrap:wrap;gap:5px;margin-bottom:9px}
+  .mr-yr{flex:1 1 46px;min-width:46px;min-height:44px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1px;padding:5px 3px;cursor:pointer;color:var(--ink);border:1px solid var(--panel-edge);border-radius:8px;background:rgba(90,30,20,calc(.12 + var(--heat,0) * .55));transition:transform .1s,box-shadow .15s,background .15s}
+  .mr-yr:hover{transform:translateY(-1px);box-shadow:0 3px 8px rgba(0,0,0,.35)}
+  .mr-yr.on{background:linear-gradient(180deg,#ffd97a,var(--gold));border-color:var(--gold);color:#2a0d10;box-shadow:0 0 10px rgba(240,190,90,.6)}
+  .mr-yr .yl{font-size:12px;font-weight:600;line-height:1;font-variant-numeric:tabular-nums}
+  .mr-yr .yc{font-size:8.5px;color:var(--ink-dim);letter-spacing:.02em;line-height:1}
+  .mr-yr.on .yc{color:#6a2c12}
+  .mr-yr.loading{animation:mrPulse 1s ease-in-out infinite}
+  @keyframes mrPulse{0%,100%{opacity:.4}50%{opacity:.8}}
+  .mr-map-err{font-size:11px;color:#e2a08a;padding:6px 4px;font-style:italic}
   .mr-results{max-height:150px;overflow:auto;margin-bottom:8px;display:flex;flex-direction:column;gap:2px}
   .mr-results:empty{display:none}
   .mr-result{display:flex;gap:9px;align-items:baseline;width:100%;text-align:left;background:none;border:0;color:var(--ink-dim);font-family:var(--sans);font-size:11.5px;padding:5px 6px;cursor:pointer;border-radius:6px}
@@ -179,14 +190,14 @@
   .mr-now{margin:12px 0 3px;color:var(--ink);font-family:var(--serif);font-style:italic;font-size:13px;min-height:18px;
     overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
   .mr-sub{color:var(--ink-dim);font-size:10.5px;letter-spacing:.03em;margin-bottom:9px}
-  .mr-transport{display:flex;align-items:center;gap:8px;margin:6px 0}
-  .mr-transport .btn{padding:7px 10px}
-  .mr-transport .play{min-width:46px;justify-content:center;font-size:13px}
-  .mr-tracks-toggle{margin-left:auto;font-size:10px;letter-spacing:.06em;text-transform:uppercase;color:var(--ink-dim);
-    background:none;border:0;cursor:pointer;padding:4px 2px}
+  .mr-transport{display:flex;align-items:center;gap:8px;margin:8px 0}
+  .mr-transport .btn{padding:9px 12px;min-height:44px;min-width:44px;justify-content:center}
+  .mr-transport .play{min-width:54px;justify-content:center;font-size:14px}
+  .mr-tracks-toggle{margin-left:auto;font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--ink-dim);
+    background:none;border:0;cursor:pointer;padding:10px 6px;min-height:44px}
   .mr-tracks-toggle:hover{color:var(--gold)}
-  .mr-seek{display:flex;align-items:center;margin:9px 0 4px}
-  .mr-seek input[type=range]{width:100%}
+  .mr-seek{display:flex;align-items:center;margin:6px 0 2px;padding:9px 0}
+  .mr-seek input[type=range]{width:100%;height:7px}
   .mr-time{font-variant-numeric:tabular-nums;color:var(--ink-dim);font-size:10px;text-align:right}
   .mr-tracks{max-height:148px;overflow:auto;margin-top:9px;border-top:1px solid rgba(242,182,74,.16);padding-top:8px;display:none}
   .musicroom.show-tracks .mr-tracks{display:block}
@@ -254,8 +265,9 @@
       <span class="chev" aria-hidden="true">▾</span>
     </header>
     <div class="mrbody" id="mrbody">
-      <input class="mr-search" id="mrSearch" type="search" placeholder="🔍 Search the whole Dead archive — date, city, song…" aria-label="Search the Grateful Dead archive">
-      <div class="mr-map-head"><span id="mrMapLabel">Touring history — tap a year</span><button type="button" class="mr-allbtn on" id="mrAll">All years</button></div>
+      <input class="mr-search" id="mrSearch" type="search" placeholder="🔍 Search the archive — date, city, song…" aria-label="Search the Grateful Dead archive">
+      <div class="mr-src" id="mrSrc" role="tablist" aria-label="Choose a music source"></div>
+      <div class="mr-map-head"><span id="mrMapLabel">Tap a year</span><button type="button" class="mr-allbtn on" id="mrAll">All years</button></div>
       <div class="mr-map" id="mrMap" role="listbox" aria-label="Browse shows by year"></div>
       <input type="hidden" id="mrYear" value="">
       <div class="mr-results" id="mrResults" role="list"></div>
@@ -1521,8 +1533,9 @@ async function searchArchive(q){
   const res=$('mrResults'); if(!res) return;
   q=(q||'').trim();
   const yr=($('mrYear')&&$('mrYear').value)||'';
+  const coll='collection:'+((window.MR_SOURCES&&window.MR_SOURCES[window.MR_SRC||'gd'])||{id:'GratefulDead'}).id;
   const filters=[]; if(q) filters.push('('+q+')'); if(yr) filters.push('year:'+yr);
-  const query = filters.length ? 'collection:GratefulDead AND '+filters.join(' AND ') : 'collection:GratefulDead';
+  const query = filters.length ? coll+' AND '+filters.join(' AND ') : coll;
   res.innerHTML='<div class="mr-hint">'+(filters.length?'Searching the archive…':'Loading the most-played shows…')+'</div>';
   const sort = (q||yr) ? 'downloads+desc' : 'downloads+desc';
   try{
@@ -1658,47 +1671,81 @@ seek.addEventListener('change',()=>{ const d=audio.duration||0; if(d>0) audio.cu
 
 sel.addEventListener('change',()=>loadShow(+sel.value,{play:true}));
 
-// archive search wiring — type anything, debounced; Enter searches immediately
+// archive browse wiring — source selector + tappable year-tile grid + debounced search
 const searchEl=$('mrSearch'), yearEl=$('mrYear');
-// Interactive touring-history map: one bar per year, height ∝ shows surviving that year.
-const mapEl=$('mrMap'), mapLabel=$('mrMapLabel'), allBtn=$('mrAll');
-if(mapEl && yearEl){
-  const YEARS=[]; for(let y=1965;y<=1995;y++) YEARS.push(y);
-  const counts={};
-  function setYear(y){
-    yearEl.value=y?String(y):'';
-    [...mapEl.children].forEach(c=>c.classList.toggle('on', c.dataset.y===String(y)));
-    allBtn.classList.toggle('on', !y);
-    mapLabel.textContent = y ? `${y} · ${(counts[y]||0).toLocaleString()} shows` : 'Touring history — tap a year';
-    searchArchive(searchEl?searchEl.value:'');
-  }
+const mapEl=$('mrMap'), mapLabel=$('mrMapLabel'), allBtn=$('mrAll'), srcEl=$('mrSrc');
+
+// Two FREE, band-sanctioned Dead archives on archive.org (CORS-open, streamable).
+window.MR_SOURCES={
+  gd:{id:'GratefulDead',   label:'Grateful Dead', years:'1965–1995', y0:1965, y1:1995},
+  dc:{id:'DeadAndCompany', label:'Dead & Company', years:'2015–2023', y0:2015, y1:2023}
+};
+window.MR_SRC='gd';
+const counts={};                              // per (source|year) show-count cache
+const ck=(s,y)=>s+':'+y;
+
+function setYear(y){
+  yearEl.value=y?String(y):'';
+  [...mapEl.children].forEach(c=>c.classList&&c.classList.toggle('on', c.dataset.y===String(y)));
+  allBtn.classList.toggle('on', !y);
+  const c=counts[ck(window.MR_SRC,y)];
+  mapLabel.textContent = y ? `${y}${c!=null?' · '+c.toLocaleString()+' shows':''}` : 'Tap a year';
+  searchArchive(searchEl?searchEl.value:'');
+}
+
+function buildMap(){
+  const src=window.MR_SOURCES[window.MR_SRC], mySrc=window.MR_SRC;
+  const YEARS=[]; for(let y=src.y0;y<=src.y1;y++) YEARS.push(y);
+  mapEl.innerHTML='';
   YEARS.forEach(y=>{
-    const b=document.createElement('button'); b.type='button'; b.className='mr-yr'; b.dataset.y=String(y); b.setAttribute('role','option');
-    b.title=y+' — counting…';
-    b.innerHTML=`<span class="bar" style="height:3px"></span><span class="yl">${y%5===0?y:''}</span>`;
+    const b=document.createElement('button'); b.type='button'; b.className='mr-yr loading';
+    b.dataset.y=String(y); b.setAttribute('role','option'); b.title=y+' — counting…';
+    b.innerHTML=`<span class="yl">${y}</span><span class="yc">…</span>`;
     b.addEventListener('click',()=>setYear(y));
-    b.addEventListener('mouseenter',()=>{ if(counts[y]!=null) mapLabel.textContent=`${y} · ${counts[y].toLocaleString()} shows`; });
+    b.addEventListener('mouseenter',()=>{ const c=counts[ck(window.MR_SRC,y)]; if(c!=null) mapLabel.textContent=`${y} · ${c.toLocaleString()} shows`; });
     mapEl.appendChild(b);
   });
-  mapEl.addEventListener('mouseleave',()=>{ const on=yearEl.value; mapLabel.textContent = on?`${on} · ${(counts[on]||0).toLocaleString()} shows`:'Touring history — tap a year'; });
-  allBtn.addEventListener('click',()=>setYear(0));
-  // Fetch per-year show counts in parallel, then scale bars (sqrt to tame the '77–'90 spikes).
+  // per-year show counts → heat-color the tiles (sqrt to tame the '77/'89 spikes)
   Promise.all(YEARS.map(y=>
-    fetch('https://archive.org/advancedsearch.php?q='+encodeURIComponent('collection:GratefulDead AND year:'+y)+'&rows=0&output=json',{mode:'cors'})
-      .then(r=>r.json()).then(j=>({y,n:(j.response&&j.response.numFound)||0})).catch(()=>({y,n:0}))
+    fetch('https://archive.org/advancedsearch.php?q='+encodeURIComponent('collection:'+src.id+' AND year:'+y)+'&rows=0&output=json',{mode:'cors'})
+      .then(r=>r.json()).then(j=>({y,n:(j.response&&j.response.numFound)||0})).catch(()=>({y,n:-1}))
   )).then(rows=>{
-    const max=Math.max(1,...rows.map(r=>Math.sqrt(r.n)));
-    rows.forEach(({y,n})=>{ counts[y]=n; const el=mapEl.querySelector('[data-y="'+y+'"]'); if(!el) return;
-      el.querySelector('.bar').style.height=Math.round(3+(Math.sqrt(n)/max)*40)+'px';
-      el.title=y+' — '+n.toLocaleString()+' shows';
+    if(mySrc!==window.MR_SRC) return;                          // source switched mid-flight
+    if(!rows.some(r=>r.n>=0)){ mapEl.innerHTML='<div class="mr-map-err">Couldn’t load the tour history — archive.org may be unreachable. Search still works.</div>'; return; }
+    const max=Math.max(1,...rows.filter(r=>r.n>0).map(r=>Math.sqrt(r.n)));
+    rows.forEach(({y,n})=>{ const el=mapEl.querySelector('[data-y="'+y+'"]'); if(!el) return;
+      el.classList.remove('loading'); const nn=Math.max(0,n); counts[ck(mySrc,y)]=nn;
+      el.style.setProperty('--heat',(Math.sqrt(nn)/max).toFixed(3));
+      el.querySelector('.yc').textContent=nn.toLocaleString();
+      el.title=y+' — '+nn.toLocaleString()+' shows';
     });
-    const cur=yearEl.value; if(cur) mapLabel.textContent=`${cur} · ${(counts[cur]||0).toLocaleString()} shows`;
-  }).catch(()=>{});
+    const cur=yearEl.value, c=cur?counts[ck(mySrc,cur)]:null;
+    if(cur&&c!=null) mapLabel.textContent=`${cur} · ${c.toLocaleString()} shows`;
+  });
 }
+
+if(srcEl){
+  srcEl.innerHTML=Object.entries(window.MR_SOURCES).map(([k,s])=>
+    `<button type="button" data-src="${k}" class="${k===window.MR_SRC?'on':''}" role="tab" aria-selected="${k===window.MR_SRC}"><b>${s.label}</b><span class="yr">${s.years}</span></button>`).join('');
+  srcEl.addEventListener('click',e=>{
+    const btn=e.target.closest('button[data-src]'); if(!btn||btn.dataset.src===window.MR_SRC) return;
+    window.MR_SRC=btn.dataset.src;
+    [...srcEl.children].forEach(c=>{ const on=c.dataset.src===window.MR_SRC; c.classList.toggle('on',on); c.setAttribute('aria-selected',String(on)); });
+    yearEl.value=''; allBtn.classList.add('on'); mapLabel.textContent='Tap a year';
+    buildMap(); searchArchive(searchEl?searchEl.value:'');
+  });
+}
+
+if(mapEl && yearEl){
+  mapEl.addEventListener('mouseleave',()=>{ const on=yearEl.value, c=on?counts[ck(window.MR_SRC,on)]:null; mapLabel.textContent = on?`${on}${c!=null?' · '+c.toLocaleString()+' shows':''}`:'Tap a year'; });
+  allBtn.addEventListener('click',()=>setYear(0));
+  buildMap();
+}
+
 if(searchEl){
   searchEl.addEventListener('input',()=>{ clearTimeout(searchTimer); searchTimer=setTimeout(()=>searchArchive(searchEl.value),450); });
   searchEl.addEventListener('keydown',e=>{ if(e.key==='Enter'){ e.preventDefault(); clearTimeout(searchTimer); searchArchive(searchEl.value); } });
-  searchArchive('');   // populate the most-played shows from the whole archive on load
+  searchArchive('');   // populate the most-played shows on load
 }
 
 $('mrTracksToggle').addEventListener('click',()=>{
@@ -1715,10 +1762,8 @@ function toggleRoom(){
 mrToggle.addEventListener('click',toggleRoom);
 mrToggle.addEventListener('keydown',e=>{ if(e.key==='Enter'||e.key===' '){ e.preventDefault(); toggleRoom(); } });
 
-// small screens: start collapsed so the art leads
-if(window.matchMedia && matchMedia('(max-width:820px)').matches){
-  room.setAttribute('data-open','false'); mrToggle.setAttribute('aria-expanded','false');
-}
+// Music Room stays EXPANDED (incl. small screens) so revealing the chrome shows a
+// working player in one tap — no hidden second collapse (CTA/screenrecord CRITICAL #1).
 
 // cue the default night (no autoplay — the source policy is user-initiated playback)
 loadShow(DEFAULT_SHOW);

← 6ceafcc riverboat: interactive touring-history year map (bars sized  ·  back to Scarlet Riverboat Masquerade  ·  5x sweep 2: abort-timeout on archive.org fetches so a hung/d d2d1341 →