[object Object]

← back to Hospitalitywallcoverings

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

68aeba2ab9e0b0904db3680527594cfb01509559 · 2026-08-01 11:23:11 -0700 · Steve Abrams

Files touched

Diff

commit 68aeba2ab9e0b0904db3680527594cfb01509559
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sat Aug 1 11:23:11 2026 -0700

    hospitalitywallcoverings: adopt href-to-deeper-data storefront primitives (TK-10093)
---
 public/drill.js   | 36 ++++++++++++++++++++++++++++++
 public/index.html | 65 +++++++++++++++++++++++++++++++++++++++----------------
 2 files changed, 82 insertions(+), 19 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 28ecdcd..789daa6 100644
--- a/public/index.html
+++ b/public/index.html
@@ -538,12 +538,12 @@ textarea:focus-visible,
   </div>
 
   <div class="filters" id="facets" role="tablist" aria-label="Hospitality grade filters">
-    <button class="chip active" data-grade="all">All</button>
-    <button class="chip" data-grade="type-ii">Type-II Vinyl</button>
-    <button class="chip" data-grade="acoustic">Acoustic</button>
-    <button class="chip" data-grade="healthcare">Healthcare-rated</button>
-    <button class="chip" data-grade="class-a">Class A</button>
-    <button class="chip" data-grade="wovens">Wovens</button>
+    <a class="chip active" data-grade="all" href="./">All</a>
+    <a class="chip drill" data-grade="type-ii" data-filter="grade" data-v="type-ii" href="?grade=type-ii">Type-II Vinyl</a>
+    <a class="chip drill" data-grade="acoustic" data-filter="grade" data-v="acoustic" href="?grade=acoustic">Acoustic</a>
+    <a class="chip drill" data-grade="healthcare" data-filter="grade" data-v="healthcare" href="?grade=healthcare">Healthcare-rated</a>
+    <a class="chip drill" data-grade="class-a" data-filter="grade" data-v="class-a" href="?grade=class-a">Class A</a>
+    <a class="chip drill" data-grade="wovens" data-filter="grade" data-v="wovens" href="?grade=wovens">Wovens</a>
   </div>
 
   <div class="density">
@@ -744,6 +744,8 @@ textarea:focus-visible,
 })();
 </script>
 
+<style>a.drill.chip{text-decoration:none}.chip[data-grade]{text-decoration:none}</style>
+<script src="/drill.js"></script>
 <script>
 const state = { q:'', grade:'all', facet:'all', sort:'newest', view:'grid', page:1, pages:1, total:0, loading:false, exhausted:false };
 const TOUCH = window.matchMedia('(hover: none)').matches;
@@ -762,6 +764,34 @@ const HOSTKEY = location.hostname.replace(/\./g, '_');
 })();
 const LABELS = {"all":"All","textured":"Textured & Tactile","beige":"Warm Neutrals","contemporary":"Modern & Contemporary","gray":"Cool Grays","traditional":"Classic & Traditional","white":"Whites & Ivories"};
 
+// -- href-to-deeper-data (HARD rule) -- storefront model: `grade` facet + q + sort --
+function sfQstr(){
+  const u=new URLSearchParams();
+  if(state.grade && state.grade!=='all') u.set('grade',state.grade);
+  if(state.q) u.set('q',state.q);
+  if(state.sort && state.sort!=='newest') u.set('sort',state.sort);
+  const s=u.toString(); return s?('?'+s):location.pathname;
+}
+function sfWriteURL(push){ try{ history[push?'pushState':'replaceState']({},'',sfQstr()); }catch(_){} }
+function sfReadURL(){
+  const u=new URLSearchParams(location.search);
+  state.grade=u.get('grade')||'all';
+  state.q=u.get('q')||'';
+  if(u.get('sort')) state.sort=u.get('sort');
+  const si=document.getElementById('searchInput'); if(si) si.value=state.q;
+  const ss=document.getElementById('sortSelect'); if(ss && u.get('sort')) ss.value=state.sort;
+}
+function sfSyncChip(){ document.querySelectorAll('#facets [data-grade]').forEach(b=>b.classList.toggle('active',(b.dataset.grade||'all')===state.grade)); }
+function sfSetGrade(v){ state.grade=v||'all'; sfSyncChip(); resetGrid(); sfWriteURL(true); const shop=document.getElementById('shop'); if(shop) shop.scrollIntoView({behavior:'smooth'}); }
+window.sfSetGrade=sfSetGrade;
+// delegated: plain click on any grade drill atom filters in place; modifier/middle keeps the href
+document.addEventListener('click',function(e){
+  const d=e.target.closest('.drill[data-filter="grade"]'); if(!d) return;
+  if(e.metaKey||e.ctrlKey||e.shiftKey||e.altKey||e.button===1) return;
+  e.preventDefault(); e.stopPropagation(); sfSetGrade(d.dataset.v);
+});
+addEventListener('popstate',function(){ sfReadURL(); sfSyncChip(); resetGrid(); });
+
 function escAttr(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;' }[c])); }
 function cleanSku(s){
   var V=/(?:^|-)(versace|kravet|fentucci|sandberg|harlequin|blithfield|westport|thibaut|koroseal|schumacher|scalamandre|fromental|dedar|carnegie|marburg|fabricut|coordonne|nina-campbell|lee-jofa(?:-modern)?|clarke(?:-and)?-clarke|clarke|brunschwig(?:-and)?(?:-fils)?|designers-guild|graham-(?:and-)?brown|andrew-martin|candice-olson|ronald-redding|jeffrey-stevens|sister-parish|arte-international|wolf-gordon|phillip-jeffries|cole-(?:and-)?son|maya-romanoff|g-p-j-baker|les-ensembliers|breegan(?:-jane)?)(?:-\\d+)?(?=-|$)/gi;
@@ -856,21 +886,21 @@ async function loadFacets() {
 
   const el = document.getElementById('facets');
   // Append count badges to each static chip
-  el.querySelectorAll('button[data-grade]').forEach(btn => {
+  el.querySelectorAll('[data-grade]').forEach(btn => {
     const k = btn.dataset.grade;
     const n = k === 'all' ? g.total : (g.counts[k] || 0);
     if (!btn.querySelector('.cct')) {
       btn.insertAdjacentHTML('beforeend', ' <span class="cct" style="opacity:.55;font-weight:500;margin-left:4px">' + n + '</span>');
     }
   });
+  // dynamic .drill grade anchors are handled by the delegated document click.
+  // this handler covers the static "All" anchor (data-grade=all, not a .drill).
   el.addEventListener('click', e => {
-    const btn = e.target.closest('button[data-grade]');
-    if (!btn) return;
-    document.querySelectorAll('#facets button').forEach(b => b.classList.remove('active'));
-    btn.classList.add('active');
-    state.grade = btn.dataset.grade;
-    resetGrid();
+    const t = e.target.closest('[data-grade]'); if (!t || t.classList.contains('drill')) return;
+    e.preventDefault();
+    state.grade = t.dataset.grade || 'all'; sfSyncChip(); resetGrid(); sfWriteURL(true);
   });
+  sfSyncChip();
   document.getElementById('totalCount').textContent = g.total;
   document.getElementById('footerStat').textContent = g.total + ' patterns · live archive';
 
@@ -879,12 +909,7 @@ async function loadFacets() {
   for (const k of Object.keys(g.counts)) {
     const b = document.createElement('button');
     b.textContent = (LABELS[k] || k) + ' (' + (g.counts[k] || 0) + ')';
-    b.onclick = () => {
-      state.grade = k;
-      document.querySelectorAll('#facets button').forEach(x => x.classList.toggle('active', x.dataset.grade === k));
-      resetGrid();
-      document.getElementById('shop').scrollIntoView({behavior:'smooth'});
-    };
+    b.onclick = () => sfSetGrade(k);
     footerEl.appendChild(b);
   }
 }
@@ -950,6 +975,7 @@ document.getElementById('searchInput').addEventListener('input', e => {
   clearTimeout(window._t);
   window._t = setTimeout(() => {
     resetGrid();
+    sfWriteURL(false);
     // Mobile: results grid sits below the fold under the keyboard, so a search looks
     // like "nothing happened". Bring results into view when a query is entered.
     if (state.q) { const a = document.getElementById('statLine') || document.getElementById('grid'); if (a) a.scrollIntoView({ behavior: 'smooth', block: 'start' }); }
@@ -1019,6 +1045,7 @@ function setTheme(t){ document.documentElement.dataset.theme = t; try { localSto
 setTheme(document.documentElement.dataset.theme || (function(){try{return localStorage.getItem('hosp_theme')||'dark'}catch(e){return 'dark'}})());
 tb.addEventListener('click', () => setTheme(document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark'));
 
+sfReadURL();
 loadFacets();
 loadGridPage();
 </script>

← 19f205a search: scroll results into view on mobile (grid was below t  ·  back to Hospitalitywallcoverings  ·  Wire DW Meta Pixel 1431180262113856 + DW-network ad slot [ad 4e6088d →