← 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 →