← 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
A public/drill.jsM public/index.html
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=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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 →