[object Object]

← back to Glitterwalls

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

b7c9aabb0fb907caa096104fe4791a7a0132323b · 2026-08-01 11:23:11 -0700 · Steve Abrams

Files touched

Diff

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

    glitterwalls: adopt href-to-deeper-data storefront primitives (TK-10093)
---
 public/index.html | 68 ++++++++++++++++++++++++++++++++++++++++++++++---------
 1 file changed, 57 insertions(+), 11 deletions(-)

diff --git a/public/index.html b/public/index.html
index 78aba0e..4e4daab 100644
--- a/public/index.html
+++ b/public/index.html
@@ -707,6 +707,8 @@ textarea:focus-visible,
 })();
 </script>
 
+<style>a.drill.chip{text-decoration:none}.r-aes .drill,.d-chip.drill{cursor:pointer;text-decoration:none;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('gw_sort') || 'newest'), view:'grid', page:1, pages:1, total:0, loading:false, exhausted:false };
 const TOUCH = window.matchMedia('(hover: none)').matches;
@@ -723,6 +725,45 @@ const HOSTKEY = 'gw';
 })();
 const LABELS = {"all":"All","gold":"Gold Glitter","silver":"Silver Glitter","metallic":"Metallic Shimmer"};
 
+// ── href-to-deeper-data (HARD rule) — storefront model: single `aesthetic` facet + q + sort ──
+// Mirror the filter state into the URL so any filtered view is a real shareable/deep-linkable
+// href; aesthetic values become .drill atoms. drill.js is loaded for the fleet atom contract +
+// the enforcer canary; the server-paged logic below is storefront-specific.
+function sfQstr(){
+  const u=new URLSearchParams();
+  if(state.facet && state.facet!=='all') u.set('aesthetic',state.facet);
+  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.facet=u.get('aesthetic')||'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-facet]').forEach(b=>b.classList.toggle('active',(b.dataset.facet||'all')===state.facet)); }
+function sfSetAesthetic(v){ state.facet=v||'all'; sfSyncChip(); resetGrid(); sfWriteURL(true); const shop=document.getElementById('shop'); if(shop) shop.scrollIntoView({behavior:'smooth'}); }
+window.sfSetAesthetic=sfSetAesthetic;
+// a .drill atom whose href IS the filtered view for that aesthetic (tag='span' inside the card <a>)
+function sfDrill(value,label,tag){
+  if(value==null||value==='') return escAttr(label==null?'':label);
+  const t=tag||'a';
+  const u=new URLSearchParams(location.search); u.set('aesthetic',String(value));
+  const hattr=(t==='a')?(' href="?'+escAttr(u.toString())+'"'):'';
+  return '<'+t+' class="drill" data-filter="aesthetic" data-v="'+escAttr(value)+'"'+hattr+' title="See all '+escAttr(String(label))+'">'+escAttr(label)+'</'+t+'>';
+}
+// delegated: plain click on any aesthetic drill atom filters in place; modifier/middle keeps the href
+document.addEventListener('click',function(e){
+  const d=e.target.closest('.drill[data-filter="aesthetic"]'); if(!d) return;
+  if(e.metaKey||e.ctrlKey||e.shiftKey||e.altKey||e.button===1) return;
+  e.preventDefault(); e.stopPropagation(); sfSetAesthetic(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;
@@ -757,7 +798,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.aesthetic ? sfDrill(p.aesthetic, String(aes).replace(/-/g, ' '), 'span') : escAttr(String(aes).replace(/-/g, ' '))) + '</div>'
     + '<div class="r-col r-price">' + price + '</div>';
 }
 
@@ -788,9 +829,11 @@ 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.filter = 'aesthetic'; c.dataset.v = a;
+    c.href = '?aesthetic=' + encodeURIComponent(a);
     c.textContent = ((typeof LABELS !== 'undefined' && LABELS[a]) || a).replace(/-/g, ' ');
+    c.addEventListener('click', ev => { if (ev.metaKey||ev.ctrlKey||ev.shiftKey||ev.button===1) return; ev.preventDefault(); ev.stopPropagation(); dwmClose('Details'); sfSetAesthetic(a); });
     chipsEl.appendChild(c);
   }
   const specsEl = m.querySelector('[data-d-specs]');
@@ -816,18 +859,19 @@ async function loadFacets() {
   } catch (e) { console.error('loadFacets failed:', e); return; }
   const el = document.getElementById('facets');
   for (const [k, v] of Object.entries(f.aesthetics).sort((a,b) => b[1] - a[1])) {
-    const b = document.createElement('button');
-    b.className = 'chip'; b.dataset.facet = k;
+    const b = document.createElement('a');
+    b.className = 'chip drill'; b.dataset.facet = k; b.dataset.filter = 'aesthetic'; b.dataset.v = k;
+    b.href = '?aesthetic=' + encodeURIComponent(k);
     b.innerHTML = (LABELS[k] || k) + ' <span style="opacity:.55;font-weight:500;margin-left:4px">' + v + '</span>';
     el.appendChild(b);
   }
+  // dynamic aesthetic chips are .drill anchors (handled by the delegated document click).
+  // this handler covers the static "All" button (data-facet=all, not a .drill).
   el.addEventListener('click', e => {
-    if (e.target.tagName !== 'BUTTON') return;
-    document.querySelectorAll('#facets button').forEach(b => b.classList.remove('active'));
-    e.target.classList.add('active');
-    state.facet = e.target.dataset.facet;
-    resetGrid();
+    const t = e.target.closest('[data-facet]'); if (!t || t.classList.contains('drill')) return;
+    state.facet = t.dataset.facet || 'all'; sfSyncChip(); resetGrid(); sfWriteURL(true);
   });
+  sfSyncChip();
   document.getElementById('totalCount').textContent = f.total;
   document.getElementById('footerStat').textContent = f.total + ' patterns · live archive';
   // Footer aesthetic links
@@ -835,7 +879,7 @@ async function loadFacets() {
   for (const [k] of Object.entries(f.aesthetics)) {
     const b = document.createElement('button');
     b.textContent = LABELS[k] || k;
-    b.onclick = () => { state.facet = k; resetGrid(); document.querySelectorAll('#facets button').forEach(x => x.classList.toggle('active', x.dataset.facet === k)); document.getElementById('shop').scrollIntoView({behavior:'smooth'}); };
+    b.onclick = () => sfSetAesthetic(k);
     footerEl.appendChild(b);
   }
 }
@@ -900,6 +944,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' }); }
@@ -969,6 +1014,7 @@ function setTheme(t){ document.documentElement.dataset.theme = t; try { localSto
 setTheme(document.documentElement.dataset.theme || 'light');
 tb.addEventListener('click', () => setTheme(document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark'));
 
+sfReadURL();
 loadFacets();
 loadGridPage();
 </script>

← b61ffba auto-save: 2026-07-31T12:57:59 (1 files) — public/drill.js  ·  back to Glitterwalls  ·  GA4: propagate site gtag to untagged pages (fleet coverage) 44b207a →