[object Object]

← 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

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 => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[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 →