[object Object]

← back to Atomic50 Onboard

auto-save: 2026-07-31T14:58:42 (1 files) — public/index.html

c9b3f524d68fbfc8599deb3e6ddac9866cd7ae2e · 2026-07-31 14:58:59 -0700 · Steve Abrams

Files touched

Diff

commit c9b3f524d68fbfc8599deb3e6ddac9866cd7ae2e
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Jul 31 14:58:59 2026 -0700

    auto-save: 2026-07-31T14:58:42 (1 files) — public/index.html
---
 public/index.html | 110 ++++++++++++++++++++++++++++++++++++++++++++++++------
 1 file changed, 98 insertions(+), 12 deletions(-)

diff --git a/public/index.html b/public/index.html
index 645226d..6e03d6a 100644
--- a/public/index.html
+++ b/public/index.html
@@ -46,6 +46,7 @@ nav.house a:hover{opacity:1;color:var(--accent)}
 select,input[type=text]{background:var(--paper);color:var(--ink);border:1px solid var(--line);border-radius:4px;padding:6px 9px;font-size:13px;font-family:-apple-system,sans-serif}
 input[type=range]{accent-color:var(--accent)}
 .chipf{display:flex;gap:6px;flex-wrap:wrap}
+.chipf a.chip-a{text-decoration:none;display:inline-flex}
 .chipf button{background:var(--paper);border:1px solid var(--line);border-radius:999px;padding:4px 12px;font-size:12px;
  font-family:-apple-system,sans-serif;cursor:pointer;color:#4a463c;letter-spacing:.02em}
 .chipf button:hover{border-color:var(--accent)}
@@ -77,6 +78,9 @@ input[type=range]{accent-color:var(--accent)}
 .card .name{font-size:calc(16px*var(--fs));line-height:1.25}
 .card .sub{font-family:-apple-system,sans-serif;font-size:calc(11px*var(--fs));color:var(--taupe)}
 .card .cta{font-family:-apple-system,sans-serif;font-size:calc(11px*var(--fs));letter-spacing:.08em;text-transform:uppercase;color:var(--accent);margin-top:2px}
+a.drill{color:inherit;text-decoration:none;cursor:pointer}
+a.drill:hover{text-decoration:underline;text-decoration-color:var(--accent);text-underline-offset:2px}
+a.drill.kind:hover{color:var(--gold)}
 #empty{padding:70px;text-align:center;color:var(--taupe);font-family:-apple-system,sans-serif}
 footer.house{background:var(--ink);color:#cfc9bb;padding:34px 30px;font-family:-apple-system,sans-serif;font-size:12.5px;text-align:center;letter-spacing:.04em}
 footer.house a{color:#fff;text-decoration:none;border-bottom:1px solid #ffffff40}
@@ -130,6 +134,47 @@ let ALL=[],CFG=null,FACETS=null,VIEW=[];
 let FTYPE='',FCOLOR='';
 const BUCKET={white:'#f4f4f2',grey:'#9aa0aa',black:'#1c1e24',orange:'#e08a3c',gold:'#c9a75a',green:'#5f8f5c',blue:'#4f7fb5',purple:'#8a6fb5',pink:'#f3a8c0',red:'#d0453e'};
 function esc(s){return String(s==null?'':s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));}
+function esc0(s){return encodeURIComponent(String(s==null?'':s));}
+
+// ── href-drill primitives ──────────────────────────────────────────
+// Every displayed data point (type / collection / color / style / pattern)
+// is an <a href> to its filtered view; filter state is URL-addressable so the
+// grid is shareable, deep-linkable and back-button-friendly.
+// Drill-only facets (not wired to a visible control) live in FIL.
+const FIL={collection:'',style:'',pattern:''};
+// qstr(over): build the query string from the live controls + FIL, then
+// override with the fields in `over`. Empty values are omitted.
+function qstr(over){
+ const st={type:FTYPE,color:FCOLOR,collection:FIL.collection,style:FIL.style,pattern:FIL.pattern,
+   q:$('#q').value.trim(),sort:$('#sort').value};
+ Object.assign(st,over||{});
+ const parts=[];
+ for(const k of ['type','color','collection','style','pattern','q','sort']){
+  let v=st[k];
+  if(k==='sort'&&v==='newest')v='';   // default sort → clean URL
+  if(v)parts.push(k+'='+esc0(v));
+ }
+ return parts.length?('?'+parts.join('&')):location.pathname;
+}
+function drill(field,val,label,cls){
+ return `<a class="drill ${cls||''}" data-filter="${esc(field)}" data-v="${esc(val)}" href="${esc(qstr({[field]:val}))}">${esc(label)}</a>`;
+}
+// readURL: hydrate controls + FIL from location.search (deep-link / back).
+function readURL(){
+ const u=new URLSearchParams(location.search);
+ FTYPE=u.get('type')||'';
+ FCOLOR=u.get('color')||'';
+ FIL.collection=u.get('collection')||'';
+ FIL.style=u.get('style')||'';
+ FIL.pattern=u.get('pattern')||'';
+ $('#q').value=u.get('q')||'';
+ const so=u.get('sort'); if(so)$('#sort').value=so;
+}
+// writeURL: reflect the current filter state into the address bar.
+function writeURL(push){
+ const url=qstr();
+ if(push)history.pushState(null,'',url); else history.replaceState(null,'',url);
+}
 
 // persisted prefs (CLAUDE.md: sort + density persist)
 $('#sort').value=localStorage.getItem('a50.sort')||'newest';
@@ -151,10 +196,22 @@ function finishSet(p){
 function card(p){
  const name=esc(p.display_name||p.sku||'(untitled)');
  const set=finishSet(p);
+ // Each filterable data point is a drill <a>. The "kind" chip drills on the
+ // product type (or collection when it's the Architectural line); color/style
+ // /pattern each drill to their filtered view.
+ const kindLabel=(p.product_type==='Architectural Wallcoverings')?(p.collection||'Pressed Tin'):p.product_type;
+ const kindHTML=(p.product_type==='Architectural Wallcoverings')
+   ? (p.collection?drill('collection',p.collection,kindLabel,'kind'):`<span class="kind">${esc(kindLabel)}</span>`)
+   : (p.product_type?drill('type',p.product_type,kindLabel,'kind'):`<span class="kind">${esc(kindLabel)}</span>`);
+ const facetBits=[];
+ if(p.style)facetBits.push(drill('style',p.style,p.style,'sub'));
+ if(p.pattern)facetBits.push(drill('pattern',p.pattern,p.pattern,'sub'));
+ if(p.color_bucket)facetBits.push(drill('color',p.color_bucket,p.color_bucket,'sub'));
+ const facetHTML=`<span class="sub">${facetBits.join(' · ')||esc(p.finish||'')}</span>`;
  if(!set.length) return `<div class="card" data-href="/product/${encodeURIComponent(p.handle)}">
    <div class="imgwrap noimg"></div><div class="b">
-   <span class="kind">${esc(p.product_type==='Architectural Wallcoverings'?(p.collection||'Pressed Tin'):p.product_type)}</span>
-   <div class="name">${name}</div><span class="sub">${esc(p.finish||'')}</span>
+   ${kindHTML}
+   <div class="name">${name}</div>${facetHTML}
    <span class="cta">Request a Quote →</span></div></div>`;
  const multi=set.length>1;
  const dots=multi?`<div class="dots">${set.map((_,i)=>`<i class="${i===0?'on':''}"></i>`).join('')}</div>`:'';
@@ -164,9 +221,9 @@ function card(p){
      <img loading="lazy" src="${esc(set[0])}" alt="${name}" onerror="this.style.display='none';this.parentNode.classList.add('noimg')">
      ${badge}${dots}
    </div><div class="b">
-   <span class="kind">${esc(p.product_type==='Architectural Wallcoverings'?(p.collection||'Pressed Tin'):p.product_type)}</span>
+   ${kindHTML}
    <div class="name">${name}</div>
-   <span class="sub">${esc(p.finish||'')}</span>
+   ${facetHTML}
    <span class="cta">${multi?'Cycle finishes · ':''}Request a Quote →</span>
  </div></div>`;
 }
@@ -197,18 +254,34 @@ grid.addEventListener('mouseleave',e=>{
  const wrap=e.target&&e.target.closest?e.target.closest('.imgwrap.multi'):null;if(wrap)setSwatch(wrap,0);
 },true);
 grid.addEventListener('click',e=>{
+ // Drill link — filter in place (unless a modifier => open href in new tab).
+ const d=e.target.closest('a.drill');
+ if(d){
+   if(e.metaKey||e.ctrlKey||e.shiftKey||e.button===1)return; // let the href open
+   e.preventDefault();
+   const f=d.dataset.filter,v=d.dataset.v;
+   if(f==='type')FTYPE=FTYPE===v?'':v;
+   else if(f==='color')FCOLOR=FCOLOR===v?'':v;
+   else if(f in FIL)FIL[f]=FIL[f]===v?'':v;
+   writeURL(true);renderChips();apply();return;
+ }
  const wrap=e.target.closest('.imgwrap.multi');
  if(isTouch&&wrap){ // tap-to-advance on touch — do NOT navigate
    e.preventDefault();setSwatch(wrap,(Number(wrap.dataset.i)||0)+1);return;
  }
  const c=e.target.closest('.card[data-href]');if(c)location.href=c.dataset.href;
 });
+// Back/forward button restores the exact filtered set.
+window.addEventListener('popstate',()=>{readURL();renderChips();apply();});
 
 function apply(){
  const q=$('#q').value.trim().toLowerCase();
  VIEW=ALL.filter(p=>{
   if(FTYPE&&String(p.product_type||'')!==FTYPE)return false;
   if(FCOLOR&&String(p.color_bucket||'')!==FCOLOR)return false;
+  if(FIL.collection&&String(p.collection||'')!==FIL.collection)return false;
+  if(FIL.style&&String(p.style||'')!==FIL.style)return false;
+  if(FIL.pattern&&String(p.pattern||'')!==FIL.pattern)return false;
   if(q&&![p.display_name,p.sku,p.pattern,p.style,p.collection].some(v=>v&&String(v).toLowerCase().includes(q)))return false;
   return true;
  });
@@ -226,16 +299,28 @@ function apply(){
  $('#count').textContent=VIEW.length+' of '+ALL.length+' designs';
  $('#empty').style.display=VIEW.length?'none':'block';
 }
-$('#sort').onchange=()=>{localStorage.setItem('a50.sort',$('#sort').value);apply();};
-let t;$('#q').oninput=()=>{clearTimeout(t);t=setTimeout(apply,250);};
+$('#sort').onchange=()=>{localStorage.setItem('a50.sort',$('#sort').value);writeURL(true);apply();};
+let t;$('#q').oninput=()=>{clearTimeout(t);t=setTimeout(()=>{writeURL(false);apply();},250);};
 
+// Chips are real <a href> drill links too — clicking toggles the facet, but
+// they carry a shareable href (the state with that chip applied).
 function renderChips(){
- $('#typef').innerHTML=(FACETS.types||[]).map(o=>
-  `<button data-t="${esc(o.value)}" class="${FTYPE===o.value?'on':''}">${esc(o.value)} · ${o.count}</button>`).join('');
- $('#colorf').innerHTML=(FACETS.colors||[]).map(o=>
-  `<button data-c="${esc(o.value)}" class="${FCOLOR===o.value?'on':''}"><span class="dot" style="background:${BUCKET[o.value]||'#999'}"></span>${esc(o.value)} · ${o.count}</button>`).join('');
- $('#typef').querySelectorAll('button').forEach(b=>b.onclick=()=>{FTYPE=FTYPE===b.dataset.t?'':b.dataset.t;renderChips();apply();});
- $('#colorf').querySelectorAll('button').forEach(b=>b.onclick=()=>{FCOLOR=FCOLOR===b.dataset.c?'':b.dataset.c;renderChips();apply();});
+ $('#typef').innerHTML=(FACETS.types||[]).map(o=>{
+  const on=FTYPE===o.value; const href=esc(qstr({type:on?'':o.value}));
+  return `<a class="chip-a" href="${href}"><button data-t="${esc(o.value)}" class="${on?'on':''}">${esc(o.value)} · ${o.count}</button></a>`;
+ }).join('');
+ $('#colorf').innerHTML=(FACETS.colors||[]).map(o=>{
+  const on=FCOLOR===o.value; const href=esc(qstr({color:on?'':o.value}));
+  return `<a class="chip-a" href="${href}"><button data-c="${esc(o.value)}" class="${on?'on':''}"><span class="dot" style="background:${BUCKET[o.value]||'#999'}"></span>${esc(o.value)} · ${o.count}</button></a>`;
+ }).join('');
+ $('#typef').querySelectorAll('a.chip-a').forEach(a=>a.onclick=e=>{
+  if(e.metaKey||e.ctrlKey||e.shiftKey||e.button===1)return; e.preventDefault();
+  const b=a.querySelector('button'); FTYPE=FTYPE===b.dataset.t?'':b.dataset.t; writeURL(true); renderChips(); apply();
+ });
+ $('#colorf').querySelectorAll('a.chip-a').forEach(a=>a.onclick=e=>{
+  if(e.metaKey||e.ctrlKey||e.shiftKey||e.button===1)return; e.preventDefault();
+  const b=a.querySelector('button'); FCOLOR=FCOLOR===b.dataset.c?'':b.dataset.c; writeURL(true); renderChips(); apply();
+ });
 }
 
 async function boot(){
@@ -259,6 +344,7 @@ async function boot(){
  if(heroP)$('#hero').style.backgroundImage=`url("${heroP.swatch}")`;
  $('#footer').innerHTML=`Atomic 50 — a collection of <a href="${esc(cfg.houseUrl)}">Designer Wallcoverings</a>. `
    +`Pressed-tin ceilings & wall panels, to the trade. Request a quote or order a $${cfg.samplePrice.toFixed(2)} sample on any pattern.`;
+ readURL();          // deep-link: hydrate filter state from the URL first
  renderChips();apply();
 }
 boot();

← 7591234 auto-save: 2026-07-15T08:05:18 (2 files) — cadence/data/late  ·  back to Atomic50 Onboard  ·  viewer standard: add Grid/List toggle + per-field on/off men 412128b →