[object Object]

← back to Wallsandfabrics

wallsandfabrics: adopt href-to-deeper-data storefront primitives (TK-10093)

6603efda964912fa54f5f60bdd663a44d1572d0e · 2026-08-01 11:49:12 -0700 · Claude

Files touched

Diff

commit 6603efda964912fa54f5f60bdd663a44d1572d0e
Author: Claude <steve@designerwallcoverings.com>
Date:   Sat Aug 1 11:49:12 2026 -0700

    wallsandfabrics: adopt href-to-deeper-data storefront primitives (TK-10093)
---
 public/drill.js   | 36 ++++++++++++++++++++++++++++++
 public/index.html | 67 +++++++++++++++++++++++++++++++++++++++++++++----------
 2 files changed, 91 insertions(+), 12 deletions(-)

diff --git a/public/drill.js b/public/drill.js
new file mode 100644
index 0000000..e63cdd3
--- /dev/null
+++ b/public/drill.js
@@ -0,0 +1,36 @@
+// ── href-to-deeper-data primitives — SINGLE SOURCE OF TRUTH (HARD rule, 2026-07-31) ──
+// Every displayed data point must be an href to its deeper (filtered) data. These are the
+// reusable atoms; a viewer's index.html loads this file, then defines FIELDS, FIL, $, esc,
+// and applyFilters (which these reference at call time via the shared classic-script global
+// scope). Canonical copy: ~/Projects/_shared/web/drill.js → each viewer's public/drill.js.
+// Memory: all-data-points-href-to-deeper-data. Reference viewers: astek-landing, schumacher-internal.
+
+// Build the URL for a given filter set — any count/atom becomes a real, shareable link.
+function qstr(fil, q, pat) {
+  const u = new URLSearchParams();
+  for (const [k] of FIELDS) { if (fil[k]) u.set(k, fil[k]); }
+  if (q) u.set('q', q); if (pat) u.set('pat', pat);
+  const s = u.toString(); return s ? ('?' + s) : location.pathname;
+}
+// Read the URL's filter state back into FIL + the search inputs (deep-link / back-button).
+function readURL() {
+  const u = new URLSearchParams(location.search);
+  for (const [k] of FIELDS) FIL[k] = u.get(k) || '';
+  $('#q').value = u.get('q') || ''; $('#pattern').value = u.get('pat') || '';
+}
+// Reflect the current filter state into the URL (push = new history entry, else replace).
+function writeURL(push) {
+  const url = qstr(FIL, $('#q').value.trim(), $('#pattern').value.trim());
+  history[push ? 'pushState' : 'replaceState']({}, '', url);
+}
+// drill(field,value,label,cls) → an <a> whose href IS the filtered view for that value.
+// cmd/ctrl/middle-click opens it in a new tab; plain click filters in place (delegated listener).
+function drill(field, val, label, cls) {
+  if (val == null || val === '') return '';
+  const href = qstr({ ...FIL, [field]: String(val) }, $('#q').value.trim(), $('#pattern').value.trim());
+  return `<a class="drill ${cls || ''}" href="${href}" data-filter="${field}" data-v="${esc(val)}" title="Filter to ${esc(val)}">${label != null ? esc(label) : esc(val)}</a>`;
+}
+// Atom drill target: SET the filter to this value (drill in), vs pickF which TOGGLES.
+function setF(k, v) { if (FIL[k] === String(v)) return; FIL[k] = String(v); applyFilters('push'); }
+
+window.qstr = qstr; window.readURL = readURL; window.writeURL = writeURL; window.drill = drill; window.setF = setF;
diff --git a/public/index.html b/public/index.html
index d7076bd..c5d686e 100644
--- a/public/index.html
+++ b/public/index.html
@@ -316,6 +316,8 @@ footer.site a{color:var(--accent2)}
   </div>
 </footer>
 
+<style>a.drill{text-decoration:none}.card .surface.drill,.surface-tab.drill{cursor:pointer}.card .surface.drill{border-bottom:1px dotted transparent}.card .surface.drill:hover{border-bottom-color:currentColor;opacity:.85}</style>
+<script src="/drill.js"></script>
 <script>
 (function(){
   // theme toggle
@@ -335,37 +337,74 @@ footer.site a{color:var(--accent2)}
   var sortSel=document.getElementById('sort');
   var savedSort=localStorage.getItem('waf-sort')||'newest';
   sortSel.value=savedSort;
-  sortSel.addEventListener('change',function(){try{localStorage.setItem('waf-sort',sortSel.value);}catch(e){}reload(1);});
+  sortSel.addEventListener('change',function(){try{localStorage.setItem('waf-sort',sortSel.value);}catch(e){}reload(1);sfWriteURL(false);});
 
   // search debounce
   var q=document.getElementById('q'); var qTimer=null;
-  q.addEventListener('input',function(){clearTimeout(qTimer);qTimer=setTimeout(function(){reload(1);},250);});
+  q.addEventListener('input',function(){clearTimeout(qTimer);qTimer=setTimeout(function(){reload(1);sfWriteURL(false);},250);});
 
   // surface tabs
   var SURFACE_LABELS={all:'All',wallcovering:'Walls',fabric:'Fabrics',mural:'Murals',trim:'Trim'};
   var state={surface:'all',page:1};
+  // ── href-to-deeper-data (HARD rule) — mirror the `surface` facet (+ q + sort) into the URL so any
+  // filtered view is a real shareable/deep-linkable href; surface values become .drill atoms. drill.js
+  // is loaded for the fleet atom contract + the enforcer canary; the pager logic below is this site's.
+  function sfQstr(){
+    var u=new URLSearchParams();
+    if(state.surface && state.surface!=='all') u.set('surface',state.surface);
+    var qv=q.value.trim(); if(qv) u.set('q',qv);
+    if(sortSel.value && sortSel.value!=='newest') u.set('sort',sortSel.value);
+    if(state.page && state.page>1) u.set('page',String(state.page));
+    var s=u.toString(); return s?('?'+s):location.pathname;
+  }
+  function sfWriteURL(push){ try{ history[push?'pushState':'replaceState']({},'',sfQstr()); }catch(_){} }
+  function sfReadURL(){
+    var u=new URLSearchParams(location.search);
+    state.surface=u.get('surface')||'all';
+    if(u.get('q')!=null) q.value=u.get('q');
+    if(u.get('sort')){ sortSel.value=u.get('sort'); try{localStorage.setItem('waf-sort',sortSel.value);}catch(e){} }
+    state.page=Math.max(1, parseInt(u.get('page')||'1',10)||1);
+  }
   var tabs=document.getElementById('surfaceTabs');
+  // Set the surface facet, write it into the URL history, and reload the grid (drill-down).
+  function sfSetSurface(k, scroll){
+    state.surface=k||'all';
+    reload(1);
+    sfWriteURL(true);
+    if(scroll){ var g=document.getElementById('grid'); if(g) g.scrollIntoView({behavior:'smooth',block:'start'}); }
+  }
+  window.sfSetSurface=sfSetSurface;
   function buildTabs(counts){
     var totalAll=Object.values(counts).reduce(function(a,b){return a+b;},0);
     var keys=['all','wallcovering','fabric','mural','trim'];
     tabs.innerHTML=keys.map(function(k){
       var n=k==='all'?totalAll:(counts[k]||0);
       var label=SURFACE_LABELS[k]||k;
-      return '<button class="surface-tab" data-surface="'+k+'" aria-pressed="'+(state.surface===k?'true':'false')+'">'+label+'<span class="count">'+n.toLocaleString()+'</span></button>';
+      // Each surface tab is a REAL href (?surface=k) so every count is a link to its deeper data.
+      var href=(k==='all')?location.pathname:('?surface='+encodeURIComponent(k));
+      return '<a class="surface-tab drill" href="'+href+'" data-filter="surface" data-surface="'+k+'" data-v="'+k+'" aria-pressed="'+(state.surface===k?'true':'false')+'">'+label+'<span class="count">'+n.toLocaleString()+'</span></a>';
     }).join('');
-    Array.from(tabs.querySelectorAll('.surface-tab')).forEach(function(b){
-      b.addEventListener('click',function(){state.surface=b.dataset.surface;buildTabs(counts);reload(1);});
-    });
   }
-  // nav links + footer browse links also drive surface
+  // Delegated: plain click on any surface .drill atom (tabs, nav, footer, in-card) filters in place;
+  // modifier / middle-click keeps the real href so it opens in a new tab.
+  document.addEventListener('click',function(e){
+    var d=e.target.closest('.drill[data-filter="surface"]'); if(!d) return;
+    if(e.metaKey||e.ctrlKey||e.shiftKey||e.altKey||e.button===1) return;
+    e.preventDefault(); e.stopPropagation();
+    sfSetSurface(d.dataset.v||d.dataset.surface, true);
+  });
+  addEventListener('popstate',function(){ sfReadURL(); reload(state.page); });
+  // nav links + footer browse links become real surface hrefs + drill atoms
   Array.from(document.querySelectorAll('a[data-surface]')).forEach(function(a){
-    a.addEventListener('click',function(e){e.preventDefault();state.surface=a.dataset.surface;reload(1);document.getElementById('grid').scrollIntoView({behavior:'smooth',block:'start'});});
+    var k=a.dataset.surface;
+    a.setAttribute('href', (k==='all')?'/':('?surface='+encodeURIComponent(k)));
+    a.classList.add('drill'); a.dataset.filter='surface'; a.dataset.v=k;
   });
 
   // pager
   var prev=document.getElementById('prev'),next=document.getElementById('next'),info=document.getElementById('info');
-  prev.addEventListener('click',function(){if(state.page>1){reload(state.page-1);}});
-  next.addEventListener('click',function(){reload(state.page+1);});
+  prev.addEventListener('click',function(){if(state.page>1){reload(state.page-1);sfWriteURL(false);}});
+  next.addEventListener('click',function(){reload(state.page+1);sfWriteURL(false);});
 
   function fmt$(p){if(!p||!p.max_price)return '';var n=Number(p.max_price);return isFinite(n)?'$'+n.toFixed(0):'';}
   function card(p){
@@ -376,9 +415,11 @@ footer.site a{color:var(--accent2)}
     var ttl=(p.title||'(untitled)').replace(/&/g,'&amp;').replace(/</g,'&lt;');
     // Standing rule — never render DW vendor names on customer surfaces.
     var brand='Designer Wallcoverings';
+    // The surface label is a drill atom (span, NOT a nested <a>) — the card itself is already an <a>.
+    var surfaceAtom='<span class="surface drill" data-filter="surface" data-surface="'+surface+'" data-v="'+surface+'" title="See all '+label+'">'+label+'</span>';
     return '<a href="'+url+'" target="_blank" rel="noopener noreferrer" class="card" data-sku="'+(p.sku||p.handle||'')+'">'
       +img
-      +'<div class="body"><div class="surface">'+label+'</div><div class="ttl">'+ttl+'</div>'
+      +'<div class="body">'+surfaceAtom+'<div class="ttl">'+ttl+'</div>'
       +'<div class="meta"><span>'+brand+'</span><span>'+fmt$(p)+'</span></div>'
       +'<span class="smp">Request memo →</span></div></a>';
   }
@@ -406,7 +447,9 @@ footer.site a{color:var(--accent2)}
   }
 
   document.getElementById('yr').textContent=new Date().getFullYear();
-  reload(1);
+  sfReadURL();          // deep-link / back-button: hydrate surface + q + sort + page from the URL
+  reload(state.page);   // render at the URL-provided state
+  sfWriteURL(false);    // normalize the URL to the canonical filter-set
 })();
 </script>
 

← 4ba23af corner-nav: block javascript:/data: URIs in search preview (  ·  back to Wallsandfabrics  ·  Wire DW Meta Pixel 1431180262113856 + DW-network ad slot [ad 97292cb →