[object Object]

← back to Sublease Agentabrams

sublease-agentabrams: adopt href-to-deeper-data primitives (TK-10093)

77183f2e08fd081fec075f51384b552e672d8059 · 2026-08-01 20:21:45 -0700 · Steve Abrams

Files touched

Diff

commit 77183f2e08fd081fec075f51384b552e672d8059
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sat Aug 1 20:21:45 2026 -0700

    sublease-agentabrams: adopt href-to-deeper-data primitives (TK-10093)
---
 public/drill.js   | 36 +++++++++++++++++++++++
 public/index.html | 86 +++++++++++++++++++++++++++++++++++++++++++++++++------
 2 files changed, 113 insertions(+), 9 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 6040089..6f89b51 100644
--- a/public/index.html
+++ b/public/index.html
@@ -55,6 +55,9 @@
   .bk .n{font-weight:700}.bk .f{color:var(--mut);font-size:12.5px;margin:2px 0 6px}
   .bk .row{font-size:12.5px;color:#41505f}.bk a{color:var(--accent)}
   footer{border-top:1px solid var(--line);color:var(--mut);font-size:12px;padding:24px 0;background:#fff}
+  /* ── drill atom: every data point filters to its deeper view (in-card atoms are <span>) ── */
+  .drill{color:inherit;cursor:pointer;border-bottom:1px dotted transparent}
+  .drill:hover{border-bottom-color:currentColor;opacity:.85}
 </style>
 </head>
 <body>
@@ -108,9 +111,70 @@
 
 <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
 <script src="https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js"></script>
+<script src="/drill.js"></script>
 <script>
 const $=s=>document.querySelector(s), fmtSF=n=>n?Number(n).toLocaleString()+' SF':'—';
 const RENDER_CAP=300; let ALL=[], SPON=[], subOnly=false;
+// drill-only client filter dimensions (already-displayed values; no server round-trip)
+let fAssetVal='', fKindVal='';
+
+// ── href-to-deeper-data adapter (client-side filter model, VIEWER style) ─────
+// Listings are loaded once and filtered in-memory. Filters live in controls
+// (#q,#fType,#fSponsor) + the subOnly toggle + two drill-only dims (asset_class,
+// listing_kind). Override the drill primitives to map each facet field to that
+// client state, keeping the <span class="drill" data-filter data-v> atom contract
+// from /drill.js. Cards are <div> (only .foot is an external <a>), so in-card
+// atoms are <span>. Only EXISTING on-card values are linkified.
+function esc(s){return String(s==null?'':s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));}
+const DFIELDS=['q','type','sponsor','asset_class','kind','sub'];
+function drillState(over){
+  const s={q:$('#q').value.trim(),type:$('#fType').value,sponsor:$('#fSponsor').value,
+    asset_class:fAssetVal,kind:fKindVal,sub:subOnly?'1':''};
+  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 so=$('#sort').value; if(so&&so!=='featured')u.set('sort',so);
+  const q=u.toString(); return q?('?'+q):location.pathname;
+}
+// drill(field,value,label,cls) → in-card <span>; delegated click filters in place.
+function drill(field,val,label,cls){
+  if(val==null||val==='')return esc(label!=null?label:val);
+  return `<span class="drill ${cls||''}" role="link" tabindex="0" data-filter="${field}" data-v="${esc(String(val))}" title="Filter to ${esc(String(label!=null?label:val))}">${esc(String(label!=null?label:val))}</span>`;
+}
+function setF(field,val){
+  val=String(val);
+  if(field==='q')$('#q').value=val;
+  else if(field==='type')$('#fType').value=val;
+  else if(field==='sponsor')$('#fSponsor').value=val;
+  else if(field==='asset_class')fAssetVal=(fAssetVal===val?'':val);
+  else if(field==='kind')fKindVal=(fKindVal===val?'':val);
+  else if(field==='sub'){subOnly=(val==='1');$('#subtog').classList.toggle('on',subOnly);}
+  writeURL(true); renderGrid();
+}
+function readURL(){
+  const u=new URLSearchParams(location.search);
+  $('#q').value=u.get('q')||''; $('#fType').value=u.get('type')||''; $('#fSponsor').value=u.get('sponsor')||'';
+  fAssetVal=u.get('asset_class')||''; fKindVal=u.get('kind')||'';
+  subOnly=u.get('sub')==='1'; $('#subtog').classList.toggle('on',subOnly);
+  if(u.get('sort'))$('#sort').value=u.get('sort');
+}
+function writeURL(push){ history[push?'pushState':'replaceState']({},'',drillURL(drillState())); }
+// delegated: plain click / Enter on a drill atom filters in place
+document.addEventListener('click',e=>{
+  const d=e.target.closest('#grid span.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);
+});
+document.addEventListener('keydown',e=>{
+  if(e.key!=='Enter'&&e.key!==' ')return;
+  const d=e.target.closest('#grid span.drill'); if(!d)return;
+  e.preventDefault(); e.stopPropagation();
+  setF(d.dataset.filter,d.dataset.v);
+});
+addEventListener('popstate',()=>{ readURL(); renderGrid(); });
 
 async function boot(){
   fetch('/api/me').then(r=>r.json()).then(m=>{if(m.role==='admin')$('#adminLink').style.display='';}).catch(()=>{});
@@ -121,7 +185,9 @@ async function boot(){
     fetch('/api/map-points').then(r=>r.json())
   ]);
   renderStats(st); SPON=sp.sponsors; renderSponsors(SPON); ALL=ls.listings;
-  fillFilters(); renderGrid(); renderMap(mp.points); loadBrokers('');
+  fillFilters();
+  readURL();               // deep-link: hydrate filter state from URL after controls are populated
+  renderGrid(); renderMap(mp.points); loadBrokers('');
 }
 function renderStats(s){
   $('#stats').innerHTML=`
@@ -144,8 +210,8 @@ function fillFilters(){
   $('#fType').innerHTML='<option value="">All space types</option>'+types.map(t=>`<option>${t}</option>`).join('');
   const brk=SPON.filter(s=>s.role==='broker');
   $('#fSponsor').innerHTML='<option value="">All sponsors</option>'+brk.map(s=>`<option value="${s.slug}">${s.name}</option>`).join('');
-  ['q','fType','fSponsor','sort'].forEach(id=>$('#'+id).addEventListener('input',renderGrid));
-  $('#subtog').addEventListener('click',()=>{subOnly=!subOnly;$('#subtog').classList.toggle('on',subOnly);renderGrid();});
+  ['q','fType','fSponsor','sort'].forEach(id=>$('#'+id).addEventListener('input',()=>{writeURL(id==='q'?false:true);renderGrid();}));
+  $('#subtog').addEventListener('click',()=>{subOnly=!subOnly;$('#subtog').classList.toggle('on',subOnly);writeURL(true);renderGrid();});
   $('#dens').addEventListener('input',e=>document.documentElement.style.setProperty('--cols',e.target.value));
   const sd=localStorage.getItem('sub_dens'); if(sd){$('#dens').value=sd;document.documentElement.style.setProperty('--cols',sd);}
   $('#dens').addEventListener('change',e=>localStorage.setItem('sub_dens',e.target.value));
@@ -158,6 +224,8 @@ function renderGrid(){
     if(subOnly&&!l.is_sublease)return false;
     if(ty&&l.space_type!==ty)return false;
     if(sp&&l.sponsor_slug!==sp)return false;
+    if(fAssetVal&&l.asset_class!==fAssetVal)return false;
+    if(fKindVal&&l.listing_kind!==fKindVal)return false;
     if(qv&&!(`${l.title} ${l.address} ${l.city}`.toLowerCase().includes(qv)))return false;
     return true;
   });
@@ -173,12 +241,12 @@ function cardHTML(l){
   const price=l.price_amount?`$${Number(l.price_amount).toLocaleString()}${l.listing_kind==='sale'?'':' '+(l.price_unit||'')}`:(l.price_unit||'Call');
   const extra=l.listing_kind==='sale'?[l.units?l.units+' units':null,l.cap_rate?l.cap_rate+'% cap':null].filter(Boolean):[];
   return `<div class="card">
-    <div class="top"><span class="kind ${l.listing_kind}">${l.listing_kind}</span>
-      ${l.asset_class?`<span class="chip">${l.asset_class}</span>`:''}</div>
-    <div class="b"><div class="t">${l.title||'Listing'}</div>
-      <div class="a">${[l.address,l.city].filter(Boolean).join(', ')||l.city||''}</div>
-      <div class="meta"><span class="chip">${fmtSF(l.size_sf)}</span><span class="chip">${price}</span>
-        ${extra.map(x=>`<span class="chip">${x}</span>`).join('')}</div></div>
+    <div class="top"><span class="kind ${l.listing_kind}">${drill('kind',l.listing_kind,l.listing_kind)}</span>
+      ${l.asset_class?`<span class="chip">${drill('asset_class',l.asset_class,l.asset_class)}</span>`:''}</div>
+    <div class="b"><div class="t">${esc(l.title||'Listing')}</div>
+      <div class="a">${esc([l.address,l.city].filter(Boolean).join(', ')||l.city||'')}</div>
+      <div class="meta"><span class="chip">${fmtSF(l.size_sf)}</span><span class="chip">${esc(price)}</span>
+        ${extra.map(x=>`<span class="chip">${esc(x)}</span>`).join('')}</div></div>
     <a class="foot" href="${l.source_url||'#'}" target="_blank" rel="noopener noreferrer">
       ${l.logo_path?`<img src="${l.logo_path}" onerror="this.style.display='none'">`:''}
       <span>${l.sponsor_name||'Market listing'}</span></a>

← 7b74ac2 auto-save: 2026-07-21T14:38:01 (1 files) — tmp-shot-tx.js  ·  back to Sublease Agentabrams  ·  creds-safe fetch guard: resolve relative fetch vs credential f30ad79 →