[object Object]

← back to Corkwallcovering

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

ab4df4dc9c9c13087984bdeefff724b2a440af78 · 2026-08-01 11:36:27 -0700 · Steve Abrams

Files touched

Diff

commit ab4df4dc9c9c13087984bdeefff724b2a440af78
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sat Aug 1 11:36:27 2026 -0700

    corkwallcovering: adopt href-to-deeper-data storefront primitives (TK-10093)
---
 public/index.html | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++-----
 1 file changed, 69 insertions(+), 7 deletions(-)

diff --git a/public/index.html b/public/index.html
index b771cb8..9b7922b 100644
--- a/public/index.html
+++ b/public/index.html
@@ -600,6 +600,8 @@ textarea:focus-visible,
 })();
 </script>
 
+<style>a.drill.chip{text-decoration:none}.r-aes .drill,.d-chip.drill,.fchip.drill{cursor:pointer;text-decoration:none}.r-aes .drill{border-bottom:1px dotted transparent}.r-aes .drill:hover,.d-chip.drill:hover{border-bottom-color:currentColor;opacity:.85}</style>
+<script src="/drill.js"></script>
 <script>
 const state = { q:'', facet:'all', sort: localStorage.getItem('cork_sort') || 'newest', view:'grid', page:1, pages:1, total:0, loading:false, exhausted:false };
 const TOUCH = window.matchMedia('(hover: none)').matches;
@@ -636,7 +638,55 @@ function facetParams(){
   return p;
 }
 function persistFacets(){ for (const g of FACET_GROUPS) localStorage.setItem('cork_'+g.key, JSON.stringify([...state[g.key]])); }
-function applyFilterChange(){ persistFacets(); loadFacets(); resetGrid(); }
+function applyFilterChange(){ persistFacets(); sfWriteURL(true); loadFacets(); resetGrid(); }
+
+// ── href-to-deeper-data (HARD rule) — multi-facet model: mirror facetParams() into the URL so
+// every filtered view is a real shareable/deep-linkable href. drill.js is loaded for the fleet
+// atom contract + the enforcer canary; the left-nav facet logic below is storefront-specific.
+function sfWriteURL(push){
+  try {
+    const qs = facetParams();
+    if (state.sort && state.sort !== 'newest') qs.set('sort', state.sort);
+    const s = qs.toString();
+    history[push ? 'pushState' : 'replaceState']({}, '', s ? ('?' + s) : location.pathname);
+  } catch(_){}
+}
+function sfReadURL(){
+  const u = new URLSearchParams(location.search);
+  state.q = u.get('q') || '';
+  if (u.get('sort')) state.sort = u.get('sort');
+  for (const g of FACET_GROUPS) {
+    const raw = u.get(g.key);
+    state[g.key] = new Set(raw ? raw.split(',').filter(Boolean) : []);
+  }
+  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;
+}
+// Build the href for a facet value toggled ON (drill in) against the current filter state.
+function sfFacetHref(gkey, val){
+  const u = new URLSearchParams(location.search);
+  const cur = new Set((u.get(gkey) || '').split(',').filter(Boolean));
+  cur.add(String(val)); u.set(gkey, [...cur].join(','));
+  return '?' + u.toString();
+}
+// a color_family drill atom (row/card) — href IS the filtered view; tag='span' inside the card <a>.
+function sfDrill(gkey, value, label, tag){
+  if (value == null || value === '') return escAttr(label == null ? '' : label);
+  const t = tag || 'a';
+  const hattr = (t === 'a') ? (' href="' + escAttr(sfFacetHref(gkey, value)) + '"') : '';
+  return '<' + t + ' class="drill" data-fgroup="' + escAttr(gkey) + '" data-v="' + escAttr(value) + '"' + hattr + ' title="See all ' + escAttr(String(label)) + '">' + escAttr(label) + '</' + t + '>';
+}
+// delegated: plain click on any facet drill atom toggles that value ON in-place; modifier/middle keeps href
+document.addEventListener('click', function(e){
+  const d = e.target.closest('.drill[data-fgroup]'); if (!d) return;
+  if (e.metaKey||e.ctrlKey||e.shiftKey||e.altKey||e.button===1) return;
+  e.preventDefault(); e.stopPropagation();
+  const gkey = d.dataset.fgroup, val = d.dataset.v;
+  if (state[gkey] && !state[gkey].has(val)) { state[gkey].add(val); applyFilterChange(); }
+  dwmClose('Details');
+  const shop = document.getElementById('shop'); if (shop) shop.scrollIntoView({behavior:'smooth'});
+});
+addEventListener('popstate', function(){ sfReadURL(); loadFacets(); resetGrid(); });
 
 function escAttr(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;' }[c])); }
 function cleanSku(s){
@@ -671,7 +721,7 @@ function rowHTML(p) {
   return '<img loading="lazy" src="' + escAttr(safeImg(p.image_url)) + '" alt="' + escAttr(p.title) + '">'
     + '<div class="r-pat">' + escAttr(p.pattern_name || p.title) + '</div>'
     + '<div class="r-col r-sku">' + escAttr(cleanSku(p.sku || p.handle_display || p.handle) || '—') + '</div>'
-    + '<div class="r-col r-aes">' + escAttr(String(aes).replace(/-/g, ' ')) + '</div>'
+    + '<div class="r-col r-aes">' + (p.color_family ? sfDrill('colors', p.color_family, String(p.color_family).replace(/-/g, ' '), 'span') : escAttr(String(aes).replace(/-/g, ' '))) + '</div>'
     + '<div class="r-col r-price">' + price + '</div>';
 }
 
@@ -701,8 +751,9 @@ function openDetails(p) {
   chipsEl.innerHTML = '';
   for (const a of safeP.aesthetic) {
     if (!a) continue;
-    const c = document.createElement('span');
-    c.className = 'd-chip';
+    const c = document.createElement('a');
+    c.className = 'd-chip drill'; c.dataset.fgroup = 'colors'; c.dataset.v = a;
+    c.href = sfFacetHref('colors', a);
     c.textContent = (LABELS[a] || a).replace(/-/g, ' ');
     chipsEl.appendChild(c);
   }
@@ -744,10 +795,17 @@ async function loadFacets() {
     const chips = document.createElement('div'); chips.className = 'fchips';
     for (const [val, ct] of entries) {
       const on = set.has(val);
-      const el = document.createElement('span');
-      el.className = 'fchip' + (on ? ' on' : '') + (ct ? '' : ' zero');
+      const el = document.createElement('a');
+      el.className = 'fchip drill' + (on ? ' on' : '') + (ct ? '' : ' zero');
+      el.dataset.fgroup = g.key; el.dataset.v = val;
+      el.href = sfFacetHref(g.key, val);
       el.innerHTML = (g.color ? '<span class="dot" style="background:' + (FAMILY_DOT[val] || '#888') + '"></span>' : '') + val + ' <span class="ct">' + ct + '</span>';
-      el.onclick = () => { on ? set.delete(val) : set.add(val); applyFilterChange(); };
+      // toggle in place (delegated handler only ADDS; here we support toggle-off for an active chip)
+      el.addEventListener('click', ev => {
+        if (ev.metaKey||ev.ctrlKey||ev.shiftKey||ev.altKey||ev.button===1) return;
+        ev.preventDefault(); ev.stopPropagation();
+        on ? set.delete(val) : set.add(val); applyFilterChange();
+      });
       chips.appendChild(el);
     }
     wrap.appendChild(chips); groups.appendChild(wrap);
@@ -840,6 +898,7 @@ document.getElementById('searchInput').addEventListener('input', e => {
   clearTimeout(window._t);
   window._t = setTimeout(() => {
     resetGrid();
+    sfWriteURL(false);
     // On mobile the results grid sits below the fold (under the keyboard), so a search
     // looks like "nothing happened" even though it filtered. Bring results into view.
     if (state.q) {
@@ -941,6 +1000,7 @@ if (sortSel) {
     state.sort = e.target.value;
     try { localStorage.setItem('cork_sort', state.sort); } catch(e){}
     if (typeof syncColHeaders === 'function') syncColHeaders();
+    sfWriteURL(true);
     resetGrid();
   });
 }
@@ -964,6 +1024,8 @@ const railClose = document.getElementById('railClose'); if (railClose) railClose
 if (location.hash === '#filters') { setFilters(true); try { document.getElementById('shop').scrollIntoView(); } catch(e){} }
 else setFilters(false); // collapsed on load by default
 
+// Deep-link: if the URL carries filter params, hydrate from it (overrides the localStorage sets).
+if (location.search && /[?&](q|sort|colors|patterns|styles|materials|types|prices)=/.test(location.search)) sfReadURL();
 loadFacets();
 loadGridPage();
 </script>

← 851d6de auto-save: 2026-07-31T12:57:59 (1 files) — public/drill.js  ·  back to Corkwallcovering  ·  Revert "corkwallcovering: adopt href-to-deeper-data storefro c50cace →