← back to Commercialrealestate
commercialrealestate: adopt href-to-deeper-data primitives (TK-10093)
9cea4043a0a4a571fbe8d3706c04927fe9bad948 · 2026-08-01 20:27:12 -0700 · Steve Abrams
Files touched
A public/drill.jsM public/index.html
Diff
commit 9cea4043a0a4a571fbe8d3706c04927fe9bad948
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sat Aug 1 20:27:12 2026 -0700
commercialrealestate: adopt href-to-deeper-data primitives (TK-10093)
---
public/drill.js | 36 ++++++++++++++++++++++++++++++++++++
public/index.html | 28 +++++++++++++++++++++++-----
2 files changed, 59 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 6a73eea..03d1994 100644
--- a/public/index.html
+++ b/public/index.html
@@ -280,6 +280,12 @@
.flink { cursor:pointer; }
.listtbl td .flink { border-bottom:1px dotted #3a4453; }
.listtbl td .flink:hover { color:var(--blue); border-bottom-color:var(--blue); }
+ /* ── drill atom: every displayed data point filters to its deeper view ── */
+ .drill { cursor:pointer; border-bottom:1px dotted transparent; }
+ .drill:hover { border-bottom-color:currentColor; filter:brightness(1.15); }
+ .badges .b.drill:hover { border-bottom-color:currentColor; }
+ .city .drill { color:inherit; }
+ .city .drill:hover { color:var(--blue); border-bottom-color:var(--blue); }
.copyb { background:transparent; border:0; color:var(--mut); cursor:pointer; font-size:11px; padding:0 3px; border-radius:4px; }
.copyb:hover { color:var(--blue); }
.crm-note, .lexpbody .crm-note { margin-top:6px; font-size:12px; color:var(--mut); white-space:pre-wrap; background:#0c1017; border:1px solid #232b36; border-radius:8px; padding:6px 9px; text-align:left; }
@@ -644,6 +650,11 @@
<div id="cmpov"><div id="cmpmodal"><button class="cmpx" aria-label="Close" onclick="document.getElementById('cmpov').classList.remove('on')">✕</button><h3 style="margin:0 0 10px">⚖ Compare shortlist</h3><div id="cmpbody"></div></div></div>
<script src="finance.js"></script>
<script src="deal-score.js"></script>
+<!-- href-to-deeper-data primitives (shared atoms). CRCP drives drilling through its
+ own native Set-based facet model (data-firmfilter / data-typefilter / data-cityfilter
+ → F.firms/F.types/F.cityQuery → render()→stateToURL()); this shared module is loaded
+ so the atom contract + URL-addressable filter state are the single source of truth. -->
+<script src="/drill.js"></script>
<script>
const $ = s => document.querySelector(s);
const $$ = s => Array.from(document.querySelectorAll(s));
@@ -1119,7 +1130,11 @@ function dealBadge(p){
}
function card(p){
const f=p.finance, uc=/Under Contract|Pending/i.test(p.status);
- const firmB = `<span class="b" title="Listing source / brokerage firm">🏢 ${firmLabel(p)}</span>`;
+ // firm badge is now a DRILL atom → filters the grid to this firm (reuses data-firmfilter handler)
+ const _fn=firmLabel(p);
+ const firmB = (_fn && _fn!=='Unknown')
+ ? `<span class="b drill" data-firmfilter="${esc(_fn).replace(/"/g,'"')}" data-filter="firm" data-v="${esc(_fn).replace(/"/g,'"')}" title="Filter to ${esc(_fn)} listings">🏢 ${esc(_fn)}</span>`
+ : `<span class="b" title="Listing source / brokerage firm">🏢 ${esc(_fn||'—')}</span>`;
const aff = f.affordable ? `<span class="b ok">✓ $${(f.cashNeeded/1000).toFixed(0)}k in budget</span>`
: `<span class="b bad">✗ needs $${(f.cashNeeded/1000).toFixed(0)}k</span>`;
const rec = p.qwen && p.qwen.recommendation ? `<span class="b rec-${p.qwen.recommendation}">${p.qwen.recommendation}</span>` : '';
@@ -1161,14 +1176,14 @@ function card(p){
}
return `<div class="card ${uc?'uc':''}" id="card-${p.id}">
<div class="top">
- <div><div class="rank">#${p.rank} · score</div><div class="addr">${p.address}</div><div class="city">${p.city}, CA ${p.zip||''}</div></div>
+ <div><div class="rank">#${p.rank} · score</div><div class="addr">${esc(p.address||'')}</div><div class="city">${p.city?`<span class="drill" data-cityfilter="${esc(p.city).replace(/"/g,'"')}" data-filter="city" data-v="${esc(p.city).replace(/"/g,'"')}" title="Filter to ${esc(p.city)}">${esc(p.city)}</span>`:''}, CA ${esc(p.zip||'')}</div></div>
<div style="display:flex;align-items:flex-start;gap:8px">
<button class="copybtn" data-copy="${p.id}" aria-label="Copy listing summary" title="Copy summary to clipboard">📋</button>
<button class="starbtn ${SHORT.has(p.id)?'on':''}" data-star="${p.id}" aria-label="${SHORT.has(p.id)?'Remove from':'Add to'} shortlist" aria-pressed="${SHORT.has(p.id)}" title="Shortlist this listing">★</button>
${vis('score')?`<div class="score">${p.composite}<small>fin ${p.financeScore}${p.qwenScore!=null?' · ai '+p.qwenScore:''}</small></div>`:''}
</div>
</div>
- <div class="badges">${vis('deal')?dealBadge(p):''}${vis('firm')?firmB:''}${vis('type')?`<span class="b type">${p.type}</span>`:''}${vis('units')?`<span class="b">${p.units} unit${p.units>1?'s':''}</span>`:''}${vis('price')?`<span class="b">${fmt(p.price)}</span>`:''}${vis('ppu')&&ppu(p)&&!p.isCondo?`<span class="b" title="Price per unit">$${ppu(p).toLocaleString()}/unit</span>`:''}${vis('pricePerSf')&&ppsf(p)?`<span class="b" title="Price per building SF">$${ppsf(p).toLocaleString()}/sf</span>`:''}${vis('cap')?capB:''}${vis('verified')?verB:''}${vis('yield')&&f.grossYield?`<span class="b ok" title="Annual gross rent ÷ price">${f.grossYield}% rent yld</span>`:''}${vis('grm')&&grm(p)!=null?`<span class="b" title="Gross rent multiplier — price ÷ annual gross rent (lower = cheaper vs rent)">GRM ${grm(p)}</span>`:''}${vis('yearBuilt')&&p.year_built?`<span class="b" title="Year built">🏗 ${p.year_built}</span>`:''}${vis('dom')&&daysOnMarket(p)!=null?`<span class="b" title="Days since listed">${daysOnMarket(p)}d on mkt</span>`:''}${vis('beds')&&p.beds!=null?`<span class="b">${p.beds} bd</span>`:''}${vis('baths')&&p.baths!=null?`<span class="b">${p.baths} ba</span>`:''}${vis('sqft')&&p.sqft?`<span class="b">${(+p.sqft).toLocaleString()} sf</span>`:''}${vis('hoa')&&p.hoa?`<span class="b" title="HOA dues">$${(+p.hoa).toLocaleString()}/mo</span>`:''}${vis('warr')&&p.warrantable_status?`<span class="b ${p.warrantable_status==='fha_approved'?'ok':'warn'}" title="Condo warrantability (FHA-approval proxy)">${p.warrantable_status==='fha_approved'?'✓ warrantable':'⚑ '+p.warrantable_status}</span>`:''}${vis('broker')&&p.broker_name?`<span class="b" title="Listing broker">🧑💼 ${esc(p.broker_name)}</span>`:''}${vis('status')?statusB:''} ${vis('afford')?aff:''} ${vis('rec')?rec:''}</div>
+ <div class="badges">${vis('deal')?dealBadge(p):''}${vis('firm')?firmB:''}${vis('type')?(p.type?`<span class="b type drill" data-typefilter="${esc(p.type).replace(/"/g,'"')}" data-filter="type" data-v="${esc(p.type).replace(/"/g,'"')}" title="Filter to ${esc(p.type)}">${esc(p.type)}</span>`:`<span class="b type">—</span>`):''}${vis('units')?`<span class="b">${p.units} unit${p.units>1?'s':''}</span>`:''}${vis('price')?`<span class="b">${fmt(p.price)}</span>`:''}${vis('ppu')&&ppu(p)&&!p.isCondo?`<span class="b" title="Price per unit">$${ppu(p).toLocaleString()}/unit</span>`:''}${vis('pricePerSf')&&ppsf(p)?`<span class="b" title="Price per building SF">$${ppsf(p).toLocaleString()}/sf</span>`:''}${vis('cap')?capB:''}${vis('verified')?verB:''}${vis('yield')&&f.grossYield?`<span class="b ok" title="Annual gross rent ÷ price">${f.grossYield}% rent yld</span>`:''}${vis('grm')&&grm(p)!=null?`<span class="b" title="Gross rent multiplier — price ÷ annual gross rent (lower = cheaper vs rent)">GRM ${grm(p)}</span>`:''}${vis('yearBuilt')&&p.year_built?`<span class="b" title="Year built">🏗 ${p.year_built}</span>`:''}${vis('dom')&&daysOnMarket(p)!=null?`<span class="b" title="Days since listed">${daysOnMarket(p)}d on mkt</span>`:''}${vis('beds')&&p.beds!=null?`<span class="b">${p.beds} bd</span>`:''}${vis('baths')&&p.baths!=null?`<span class="b">${p.baths} ba</span>`:''}${vis('sqft')&&p.sqft?`<span class="b">${(+p.sqft).toLocaleString()} sf</span>`:''}${vis('hoa')&&p.hoa?`<span class="b" title="HOA dues">$${(+p.hoa).toLocaleString()}/mo</span>`:''}${vis('warr')&&p.warrantable_status?`<span class="b ${p.warrantable_status==='fha_approved'?'ok':'warn'}" title="Condo warrantability (FHA-approval proxy)">${p.warrantable_status==='fha_approved'?'✓ warrantable':'⚑ '+p.warrantable_status}</span>`:''}${vis('broker')&&p.broker_name?`<span class="b" title="Listing broker">🧑💼 ${esc(p.broker_name)}</span>`:''}${vis('status')?statusB:''} ${vis('afford')?aff:''} ${vis('rec')?rec:''}</div>
${vis('brokerflag')?brokerFlagBadge:''}
${vis('statusline')?statusLine(p):''}
${vis('txhist')?txHistHTML(bh):''}
@@ -1954,12 +1969,15 @@ fetch('data/ranked.json').then(r=>r.json()).then(async d=>{
$('#chatform').onsubmit=e=>{ e.preventDefault(); const t=$('#q').value.trim(); if(!t) return; $('#q').value=''; ask(t); };
$('#grid').onclick=e=>{ const cp=e.target.closest('[data-copy]'); if(cp){ copySummary(cp.dataset.copy, cp); return; } const qk=e.target.closest('[data-quick]'); if(qk){ applyQuick(qk.dataset.quick); return; } const more=e.target.closest('#showmore'); if(more){ _shown=Math.min(_shown+PAGE(), _rows.length); paintPage(); return; } const st=e.target.closest('[data-star]'); if(st){ toggleStar(st.dataset.star); st.classList.toggle('on',SHORT.has(st.dataset.star)); const sb=$('#shortbtn'); if(sb) sb.textContent='⭐ Shortlist ('+SHORT.size+')'; if(F.shortlist) render(); return; } const th=e.target.closest('.listtbl th[data-sortk]'); if(th){ const k=th.dataset.sortk; if(colSort&&colSort.key===k) colSort.dir*=-1; else colSort={key:k,dir:['type','firm','warr','broker','status','zip','addr','rec'].includes(k)?1:-1}; render(); return; } const b=e.target.closest('.lcbtn'); if(b){ fetchLiveComps(b.dataset.id); return; } const h=e.target.closest('.histbtn'); if(h){ fetchHistory(h); return; }
const cv=e.target.closest('[data-copyval]'); if(cv){ navigator.clipboard.writeText(cv.dataset.copyval).then(()=>toast('📋 copied')).catch(()=>toast('⚠ copy failed')); return; }
- const ff=e.target.closest('[data-firmfilter]'); if(ff){ F.firms=new Set([ff.dataset.firmfilter]); syncChips(); render(); return; }
+ const ff=e.target.closest('[data-firmfilter]'); if(ff){ if(e.metaKey||e.ctrlKey||e.shiftKey) F.firms.add(ff.dataset.firmfilter); else F.firms=new Set([ff.dataset.firmfilter]); syncChips(); render(); return; }
+ // drill atoms: card asset-type + city → filter the grid to that value (URL updates via render→stateToURL)
+ const tf=e.target.closest('[data-typefilter]'); if(tf){ if(e.metaKey||e.ctrlKey||e.shiftKey) F.types.add(tf.dataset.typefilter); else F.types=new Set([tf.dataset.typefilter]); syncChips(); render(); return; }
+ const cf=e.target.closest('[data-cityfilter]'); if(cf){ const cy=cf.dataset.cityfilter.toLowerCase(); F.cityQuery=(F.cityQuery===cy)?'':cy; if($('#fCity'))$('#fCity').value=(F.cityQuery?cf.dataset.cityfilter:''); syncChips(); render(); return; }
const ct=e.target.closest('[data-contact]'); if(ct){ openCompose(ct.dataset.contact); return; }
const mk=e.target.closest('[data-mark]'); if(mk){ markContacted(mk.dataset.mark); return; }
const si=e.target.closest('[data-saveinfo]'); if(si){ saveAgentInfo(si.dataset.saveinfo); return; }
const ld=e.target.closest('[data-letdel]'); if(ld){ deleteLetter(ld.dataset.letdel, ld.dataset.idx); return; }
- const row=e.target.closest('.listtbl tr.lrow'); if(row && !e.target.closest('a,button,input,textarea,select,[data-firmfilter],[data-copyval]')){ openContact(row.dataset.id); return; } };
+ const row=e.target.closest('.listtbl tr.lrow'); if(row && !e.target.closest('a,button,input,textarea,select,[data-firmfilter],[data-typefilter],[data-cityfilter],[data-copyval]')){ openContact(row.dataset.id); return; } };
// ── drag-to-reorder PRIMARY List columns (Steve list-build rule 2) ──
// Delegated on #grid since the list table is re-rendered each render(). Property (addr) is a
// fixed anchor (no data-k) so it never participates as a drag source or drop target.
← 5413425 crcp: M&M firm-direct DEAD-END — API caps at 100 results ser
·
back to Commercialrealestate
·
condos: normalize junk broker/firm placeholders (N/A etc) to a241df1 →