← back to Permit Radar
permit-radar: adopt href-to-deeper-data primitives (TK-10093)
d596a1b1a243564797f179486a6fcfe4533f7d9d · 2026-08-01 20:16:01 -0700 · Steve Abrams
Files touched
A public/drill.jsM public/index.html
Diff
commit d596a1b1a243564797f179486a6fcfe4533f7d9d
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sat Aug 1 20:16:01 2026 -0700
permit-radar: adopt href-to-deeper-data primitives (TK-10093)
---
public/drill.js | 36 +++++++++++++++++++++++++++++++++
public/index.html | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++-----
2 files changed, 90 insertions(+), 5 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 f9ddce2..bbfed7c 100644
--- a/public/index.html
+++ b/public/index.html
@@ -227,6 +227,12 @@ nav {
footer { background: #0f172a; color: #64748b; padding: 24px 20px; text-align: center; font-size: 13px; margin-top: 32px; }
footer a { color: #94a3b8; text-decoration: none; }
footer a:hover { color: #fff; }
+
+/* ── drill atom: every data point hrefs to its filtered view (deeper data) ── */
+.drill { color: inherit; text-decoration: none; cursor: pointer; border-bottom: 1px dotted transparent; }
+.drill:hover { border-bottom-color: currentColor; opacity: .82; }
+.type-chip .drill, .status-chip .drill { color: inherit; }
+.card-city .drill:hover { color: var(--accent); }
</style>
</head>
<body>
@@ -474,6 +480,7 @@ footer a:hover { color: #fff; }
<p style="margin-top:6px;font-size:11px">⚠️ Sample data only — not sourced from any county permit portal. Real-feed seam: <code>scripts/ingest-stub.js</code></p>
</footer>
+<script src="/drill.js"></script>
<script>
'use strict';
const esc = s => (s||'').replace(/[&<>"]/g, c => ({'&':'&','<':'<','>':'>','"':'"'}[c]));
@@ -488,6 +495,45 @@ const filtJ = document.getElementById('filt-jurisdiction');
const filtT = document.getElementById('filt-type');
const filtS = document.getElementById('filt-status');
+// ── href-to-deeper-data adapter (server-paged select model) ──────────────────
+// Filters live in <select id=filt-jurisdiction|filt-type|filt-status> + <input id=search>.
+// Override the drill primitives to map each facet field to its control, keeping the
+// <a class="drill" data-filter data-v href> atom contract from /drill.js. Only EXISTING
+// on-card values are linkified.
+const DFIELDS=[['q',search],['jurisdiction',filtJ],['permit_type',filtT],['status',filtS]];
+function drillState(over){
+ const s={q:search.value.trim(),jurisdiction:filtJ.value,permit_type:filtT.value,status:filtS.value};
+ if(over)Object.assign(s,over); return s;
+}
+function drillURL(s){
+ const u=new URLSearchParams();
+ for(const [k] of DFIELDS)if(s[k])u.set(k,s[k]);
+ const q=u.toString(); return q?('?'+q):location.pathname;
+}
+// drill(field,value,label,cls) → <a> whose href IS the filtered view for that value
+function drill(field,val,label,cls){
+ if(val==null||val==='')return '';
+ const href=drillURL(drillState({[field]:String(val)}));
+ return `<a class="drill ${cls||''}" href="${href}" data-filter="${field}" data-v="${esc(String(val))}" title="Filter to ${esc(String(label!=null?label:val))}">${esc(String(label!=null?label:val))}</a>`;
+}
+function setF(field,val){
+ const el=DFIELDS.find(([k])=>k===field); if(!el)return;
+ el[1].value=val; drillWriteURL(true); load();
+}
+function drillReadURL(){
+ const u=new URLSearchParams(location.search);
+ search.value=u.get('q')||''; filtJ.value=u.get('jurisdiction')||'';
+ filtT.value=u.get('permit_type')||''; filtS.value=u.get('status')||'';
+}
+function drillWriteURL(push){ history[push?'pushState':'replaceState']({},'',drillURL(drillState())); }
+// delegated: plain click on a drill atom filters in place; modified-click keeps real href
+document.getElementById('grid').addEventListener('click',e=>{
+ const d=e.target.closest('a.drill'); if(!d)return;
+ if(e.metaKey||e.ctrlKey||e.shiftKey||e.altKey||e.button===1)return;
+ e.preventDefault(); e.stopPropagation();
+ setF(d.dataset.filter,d.dataset.v);
+});
+
sortSel.value = localStorage.getItem('pr_sort') || 'date_desc';
dens.value = localStorage.getItem('pr_cols') || '3';
@@ -523,13 +569,13 @@ async function load() {
<div class="card-header">
<div>
<div class="permit-id">${esc(p.id)}</div>
- <span class="type-chip">${esc(p.permit_type_label || p.permit_type)}</span>
+ <span class="type-chip">${drill('permit_type', p.permit_type, p.permit_type_label || p.permit_type)}</span>
</div>
- <span class="status-chip ${statusClass}">${esc(p.status)}</span>
+ <span class="status-chip ${statusClass}">${drill('status', p.status, p.status)}</span>
</div>
<div class="card-body">
<p class="card-address">${esc(p.address)}</p>
- <p class="card-city">${esc(p.city)}, ${esc(p.state)} ${esc(p.zip)} — ${esc(p.jurisdiction)}</p>
+ <p class="card-city">${esc(p.city)}, ${esc(p.state)} ${esc(p.zip)} — ${drill('jurisdiction', p.jurisdiction, p.jurisdiction)}</p>
<div class="card-meta">
<span class="meta-item"><strong>Filed:</strong> ${fmtDate(p.date_filed)}</span>
<span class="valuation">${fmtVal(p.valuation)}</span>
@@ -547,9 +593,12 @@ async function load() {
sortSel.onchange = () => { localStorage.setItem('pr_sort', sortSel.value); load(); };
dens.oninput = applyCols;
-search.oninput = () => { clearTimeout(debounce); debounce = setTimeout(load, 300); };
-filtJ.onchange = filtT.onchange = filtS.onchange = load;
+search.oninput = () => { clearTimeout(debounce); debounce = setTimeout(() => { drillWriteURL(false); load(); }, 300); };
+filtJ.onchange = filtT.onchange = filtS.onchange = () => { drillWriteURL(true); load(); };
+addEventListener('popstate', () => { drillReadURL(); load(); });
+// deep-link: hydrate filter controls from the URL before first load
+drillReadURL();
load();
// ── Subscribe form ────────────────────────────────────────────────────────────
← fdc840b shopify: refresh catalog — keep sample-only as 'by request'
·
back to Permit Radar
·
creds-safe fetch guard: resolve relative fetch vs credential c3b0ab2 →