[object Object]

← back to Dw Staged Active Viewer

dw-staged-active-viewer: adopt href-to-deeper-data primitives (TK-10093)

68085d62f45453f034f1d0a0eab0041afcfcfc64 · 2026-08-01 20:15:49 -0700 · Steve Studio

Files touched

Diff

commit 68085d62f45453f034f1d0a0eab0041afcfcfc64
Author: Steve Studio <steve@designerwallcoverings.com>
Date:   Sat Aug 1 20:15:49 2026 -0700

    dw-staged-active-viewer: adopt href-to-deeper-data primitives (TK-10093)
---
 public/drill.js   | 36 +++++++++++++++++++++++++++++
 public/index.html | 68 +++++++++++++++++++++++++++++++++++++++++++++++++------
 2 files changed, 97 insertions(+), 7 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 e0bacb7..b808c1d 100644
--- a/public/index.html
+++ b/public/index.html
@@ -36,6 +36,9 @@
   .price .smp { color:var(--mut); }
   .when { color:#9aa3b5; font-size:10.5px; display:flex; align-items:center; gap:4px; }
   .line-badge { display:inline-block; font-size:10px; color:#cdb6ff; background:#241b38; border:1px solid #3a2c5c; border-radius:4px; padding:0 5px; }
+  a.drill { color:inherit; text-decoration:none; cursor:pointer; border-bottom:1px dotted rgba(110,168,254,.5); }
+  a.drill:hover { color:var(--accent); border-bottom-color:var(--accent); }
+  .line-badge a.drill { border-bottom:none; }
   .more { text-align:center; margin:22px 0; }
   .more button { background:#22304a; color:#fff; border:1px solid var(--accent); border-radius:8px; padding:9px 22px; cursor:pointer; font-size:13px; }
   .loading { color:var(--mut); text-align:center; padding:30px; }
@@ -146,6 +149,7 @@
   </div>
 </main>
 
+<script src="/drill.js"></script>
 <script>
 const LS = {
   get sort(){ return localStorage.getItem('dwsav.sort') || 'newest'; },
@@ -162,6 +166,41 @@ let state = { vendor: LS.vendor, offset: 0, rows: [], done: false };
 const $ = s => document.querySelector(s);
 const grid = $('#grid'), totalsEl = $('#totals'), barEl = $('#vendorbar');
 
+// ── href-to-deeper-data adapter (HARD rule) ──────────────────────────────────
+// Hybrid model: `vendor` is a SERVER-paged facet (existing vendor bar); `collection`
+// and `line` are CLIENT-side drill filters over the already-loaded rows. drill.js's
+// drill()/setF() reference FIELDS/FIL/$/esc + qstr/readURL/writeURL — we supply them
+// all here, overriding the URL machinery to reflect BOTH vendor (server) + client fils.
+const FIELDS = [['vendor','Vendor'],['collection','Collection'],['line','Line']];
+const FIL = {}; FIELDS.forEach(([k])=>FIL[k]='');
+// drill.js's qstr/readURL/writeURL are module-closures that reference $('#q')/$('#pattern');
+// this viewer has no search inputs, so we inject two HIDDEN inputs to satisfy them (they
+// stay empty → no extra URL params). FIELDS/FIL/$/esc/applyFilters are the other globals.
+(function(){
+  if (!document.getElementById('q')){ const i=document.createElement('input'); i.type='hidden'; i.id='q'; document.body.appendChild(i); }
+  if (!document.getElementById('pattern')){ const i=document.createElement('input'); i.type='hidden'; i.id='pattern'; document.body.appendChild(i); }
+})();
+// applyFilters: vendor change → server reload; collection/line → client re-render.
+let _lastVendor = null;
+function applyFilters(nav){
+  if (nav !== 'none') writeURL(nav === 'push');
+  if (FIL.vendor !== _lastVendor){
+    _lastVendor = FIL.vendor;
+    state.vendor = FIL.vendor; LS.vendor = FIL.vendor;
+    state.offset = 0; state.rows = []; state.done = false;
+    if (barEl) barEl.querySelectorAll('.vchip').forEach(c=>c.classList.toggle('active', (c.dataset.v||'')===FIL.vendor));
+    grid.innerHTML=''; loadPage();
+  } else {
+    render();
+  }
+}
+// client-side visibility predicate for the non-vendor fils
+function passClientFil(r){
+  if (FIL.collection && String(r.collection||'') !== FIL.collection) return false;
+  if (FIL.line && String(r.line||'') !== FIL.line) return false;
+  return true;
+}
+
 // ---- created date+time chip (Steve admin rule) ----
 function fmtWhen(iso){
   if (!iso) return { txt:'no date', title:'' };
@@ -221,7 +260,10 @@ function sortRows(rows, mode){
 
 function card(r){
   const w = fmtWhen(r.staged_at);
-  const lineBadge = r.line && r.line !== r.vendor ? `<span class="line-badge">${esc(r.line)}</span>` : '';
+  // in-card drill atoms — card is NOT an <a>, so these are real <a href> (drill.js)
+  const lineBadge = r.line && r.line !== r.vendor ? `<span class="line-badge">${drill('line', r.line, r.line)}</span>` : '';
+  const vendorA = r.vendor ? drill('vendor', r.vendor, r.vendor) : '';
+  const collA = r.collection ? drill('collection', r.collection, r.collection) : '';
   const thumb = r.image_url
     ? `<div class="thumb" style="background-image:url('${esc(r.image_url)}')"></div>`
     : `<div class="thumb empty">no image</div>`;
@@ -229,7 +271,7 @@ function card(r){
     ${thumb}
     <div class="body">
       <div class="ttl">${esc(titleOf(r))}</div>
-      <div class="sub">${esc(r.vendor)}${lineBadge?' · ':''}${lineBadge}${r.collection?` · ${esc(r.collection)}`:''}</div>
+      <div class="sub">${vendorA}${lineBadge?' · ':''}${lineBadge}${collA?` · ${collA}`:''}</div>
       <div class="sku">${esc(r.dw_sku)}</div>
       <div class="price">cost ${r.cost!=null?'$'+r.cost.toFixed(2):'—'} → <b>$${r.retail!=null?r.retail.toFixed(2):'—'}</b>${r.priced_at_map?'<span class="map">MAP</span>':''} <span class="smp">· smpl $${r.sample_price}</span></div>
       <div class="when" title="${esc(w.title)}">🕓 ${esc(w.txt)}</div>
@@ -239,7 +281,8 @@ function card(r){
 function esc(s){ return String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
 
 function render(){
-  grid.innerHTML = sortRows(state.rows, LS.sort).map(card).join('');
+  const rows = sortRows(state.rows.filter(passClientFil), LS.sort);
+  grid.innerHTML = rows.map(card).join('');
 }
 
 async function loadSummary(){
@@ -255,10 +298,9 @@ async function loadSummary(){
   barEl.innerHTML = `<span class="vchip ${state.vendor===''?'active':''}" data-v="">All<span class="n">${j.grandTotal.toLocaleString()}</span></span>`
     + j.vendors.map(v=>`<span class="vchip ${state.vendor===v.vendor?'active':''}" data-v="${esc(v.vendor)}">${esc(v.vendor)}<span class="n">${v.count.toLocaleString()}</span></span>`).join('');
   barEl.querySelectorAll('.vchip').forEach(el=>el.onclick=()=>{
-    state.vendor = el.dataset.v; LS.vendor = state.vendor;
-    state.offset = 0; state.rows = []; state.done = false;
-    barEl.querySelectorAll('.vchip').forEach(c=>c.classList.toggle('active', c.dataset.v===state.vendor));
-    grid.innerHTML=''; loadPage();
+    // route through setF so the URL is addressable + back-button works (drill.js)
+    const v = el.dataset.v || '';
+    FIL.vendor = v; applyFilters('push');
   });
 }
 
@@ -453,6 +495,18 @@ function setTab(tab){
 document.getElementById('tab-grid').onclick=()=>setTab('grid');
 document.getElementById('tab-cal').onclick=()=>setTab('cal');
 
+// ── drill wiring: delegated in-card <a class="drill"> click + back/forward ──
+grid.addEventListener('click', e=>{
+  const a = e.target.closest('a.drill'); if (!a) return;
+  e.preventDefault();
+  setF(a.dataset.filter, a.dataset.v);   // drill.js: sets FIL + applyFilters('push')
+});
+addEventListener('popstate', ()=>{ readURL(); applyFilters('none'); });
+
+// deep-link: read URL → seed vendor state → load
+readURL();
+_lastVendor = FIL.vendor;
+state.vendor = FIL.vendor; LS.vendor = FIL.vendor;
 loadSummary().then(loadPage);
 setTab(localStorage.getItem('dwsav.tab') || 'grid');
 </script>

← 7dfec84 chore: macstudio3 migration — reconcile from mac2 + repoint  ·  back to Dw Staged Active Viewer  ·  fix: take infinite-scroll .loading indicator out of flow (ki d211839 →