← back to Bleachresistantfabrics
bleachresistantfabrics: adopt href-to-deeper-data primitives (TK-10093)
b537e577be875cffeefbc9f8c7f02313d0acad35 · 2026-08-01 20:35:00 -0700 · Steve Abrams
Files touched
A public/drill.jsM public/index.html
Diff
commit b537e577be875cffeefbc9f8c7f02313d0acad35
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sat Aug 1 20:35:00 2026 -0700
bleachresistantfabrics: adopt href-to-deeper-data primitives (TK-10093)
---
public/drill.js | 36 +++++++++++++++++++++
public/index.html | 94 +++++++++++++++++++++++++++++++++++++++++++++----------
2 files changed, 114 insertions(+), 16 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 d26bb1c..d210aca 100644
--- a/public/index.html
+++ b/public/index.html
@@ -92,6 +92,7 @@ h1.headline .accent-italic { color:var(--accent); font-style:italic }
.chip { padding:8px 14px; font-size:11px; letter-spacing:0.2em; text-transform:uppercase; font-weight:700; color:var(--paper); background:transparent; border:1px solid var(--line); cursor:pointer; transition:all .2s; font-family:inherit }
.chip:hover { border-color:var(--paper) }
.chip.active { background:var(--paper); color:var(--bg); border-color:var(--paper) }
+a.chip.drill { text-decoration:none; display:inline-block }
.search { flex:1 1 240px; max-width:320px; display:flex; align-items:center; gap:8px; border-bottom:1px solid var(--line); padding:6px 0 }
.search input { flex:1; background:transparent; border:0; color:var(--paper); font-family:inherit; font-size:13px; outline:none }
.search input::placeholder { color:var(--muted) }
@@ -298,14 +299,17 @@ a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible
</div>
</div>
+ <!-- Rail facet = the filter model. Each option is a real <a href> to its
+ filtered (deeper) view — cmd/ctrl/middle-click opens in a new tab; plain
+ click filters in place. (href-to-deeper-data primitives, TK-10093) -->
<div class="controls" id="rails">
- <button class="chip active" data-rail="all">All</button>
- <button class="chip" data-rail="crypton">Crypton</button>
- <button class="chip" data-rail="vinyl-coated">Vinyl-Coated</button>
- <button class="chip" data-rail="performance">Performance</button>
- <button class="chip" data-rail="healthcare">Healthcare</button>
- <button class="chip" data-rail="moisture-barrier">Moisture-Barrier</button>
- <button class="chip" data-rail="antimicrobial">Antimicrobial</button>
+ <a class="chip drill active" data-filter="rail" data-v="all" href="?rail=all">All</a>
+ <a class="chip drill" data-filter="rail" data-v="crypton" href="?rail=crypton">Crypton</a>
+ <a class="chip drill" data-filter="rail" data-v="vinyl-coated" href="?rail=vinyl-coated">Vinyl-Coated</a>
+ <a class="chip drill" data-filter="rail" data-v="performance" href="?rail=performance">Performance</a>
+ <a class="chip drill" data-filter="rail" data-v="healthcare" href="?rail=healthcare">Healthcare</a>
+ <a class="chip drill" data-filter="rail" data-v="moisture-barrier" href="?rail=moisture-barrier">Moisture-Barrier</a>
+ <a class="chip drill" data-filter="rail" data-v="antimicrobial" href="?rail=antimicrobial">Antimicrobial</a>
</div>
<div class="toolbar">
@@ -416,6 +420,8 @@ a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible
</div>
</footer>
+<!-- href-to-deeper-data primitives (shared atoms; app-specific adapter is inline below) -->
+<script src="/drill.js"></script>
<script>
document.getElementById('yr').textContent = new Date().getFullYear();
@@ -461,23 +467,75 @@ document.getElementById('sort').addEventListener('change', e => {
state.sort = e.target.value;
state.page = 1;
try { localStorage.setItem('brf_sort', state.sort); } catch(_) {}
- load(true);
+ writeURL(false); load(true);
});
document.getElementById('q').addEventListener('input', e => {
state.q = e.target.value.trim();
state.page = 1;
clearTimeout(window.__qt);
- window.__qt = setTimeout(() => load(true), 220);
+ window.__qt = setTimeout(() => { writeURL(false); load(true); }, 220);
});
-document.querySelectorAll('#rails .chip').forEach(c => c.addEventListener('click', () => {
- document.querySelectorAll('#rails .chip').forEach(x => x.classList.remove('active'));
- c.classList.add('active');
- state.rail = c.dataset.rail;
- state.page = 1;
+// ── href-to-deeper-data (TK-10093) ──────────────────────────────────────────
+// This storefront is server-paginated with a single rail facet + q + sort. The
+// shared /drill.js atoms want classic-script globals (FIELDS/FIL/$/esc/applyFilters
+// + qstr/readURL/writeURL); we provide a THIN adapter mapping them onto this app's
+// `state` object so the rail chips are real, shareable <a href> deep-links and the
+// URL always reflects rail/q/sort (deep-link + back-button restore filtered state).
+const $ = s => document.querySelector(s);
+const esc = s => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[c]));
+const FIELDS = [['rail','Cleanability']];
+const FIL = { rail: '' }; // '' / 'all' both mean "no rail"
+// qstr/readURL/writeURL overrides (the shared ones key off #q/#pattern which don't
+// exist here) — build the URL from rail + q + sort.
+function qstr(fil, q, sort) {
+ const u = new URLSearchParams();
+ const r = (fil && fil.rail) || '';
+ if (r && r !== 'all') u.set('rail', r);
+ if (q) u.set('q', q);
+ if (sort && sort !== 'newest') u.set('sort', sort);
+ const s = u.toString();
+ return s ? ('?' + s) : location.pathname;
+}
+function drill(field, val, label, cls) {
+ if (val == null || val === '') return '';
+ const href = qstr({ ...FIL, [field]: String(val) }, state.q, state.sort);
+ 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>`;
+}
+function readURL() {
+ const u = new URLSearchParams(location.search);
+ state.rail = u.get('rail') || 'all'; FIL.rail = state.rail;
+ state.q = u.get('q') || '';
+ state.sort = u.get('sort') || state.sort || 'newest';
+}
+function writeURL(push) {
+ const url = qstr(FIL, state.q, state.sort);
+ history[push ? 'pushState' : 'replaceState']({}, '', url);
+}
+// applyFilters(nav) — reflect FIL/state → UI + URL, then reload page 1.
+function applyFilters(nav) {
+ FIL.rail = state.rail || 'all';
+ document.querySelectorAll('#rails a.drill').forEach(a =>
+ a.classList.toggle('active', (a.dataset.v || 'all') === (state.rail || 'all')));
+ const sortEl = document.getElementById('sort'); if (sortEl) sortEl.value = state.sort;
+ const qEl = document.getElementById('q'); if (qEl && qEl.value !== state.q) qEl.value = state.q;
+ if (nav !== 'none') writeURL(nav === 'push');
load(true);
-}));
+}
+window.qstr = qstr; window.drill = drill; window.readURL = readURL; window.writeURL = writeURL;
+window.esc = esc; window.applyFilters = applyFilters; window.FIELDS = FIELDS; window.FIL = FIL;
+
+// Delegated drill: plain click filters in place; modifier/middle-click keeps the real href.
+document.getElementById('rails').addEventListener('click', e => {
+ const a = e.target.closest('a.drill'); if (!a) return;
+ if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button === 1) return;
+ e.preventDefault();
+ state.rail = a.dataset.v || 'all';
+ state.page = 1;
+ applyFilters('push');
+});
+addEventListener('popstate', () => { readURL(); applyFilters('none'); });
function card(p) {
const handle = encodeURIComponent(p.handle_display || p.handle || p.sku || '');
@@ -535,7 +593,11 @@ const io = new IntersectionObserver(entries => {
}, { rootMargin: '600px 0px' });
io.observe($more);
-load(true);
+// URL wins over localStorage for rail/q/sort so a shared deep-link restores the
+// exact filtered view. readURL() sets state.rail/q/sort from the query string;
+// applyFilters('none') paints the chips/sort/search + loads without a new history push.
+readURL();
+applyFilters('none');
</script>
← 4505950 deploy.conf: smoke-check root / (was a /health path these ap
·
back to Bleachresistantfabrics
·
Wire DW Meta Pixel 1431180262113856 + DW-network ad slot [ad 72bd90c →