← 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
A public/drill.jsM public/index.html
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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
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 →