← back to Commercialrealestate
feat(condos): surface parity (DTD verdict C, 2/3 Codex+Claude; Qwen dissent B=export-compare) — collapsible rail tabs (all collapsed on load) + aggregate stats strip (count/median price/$-SF/HOA/year) + ⭐shortlist stars sharing the SAME cre_shortlist key/ids as Explorer+MLS (shortlist now unified across ALL 4 surfaces) + a11y focus-visible/reduced-motion. Contrarian-gated, 0 errors.
6d8e18280cf19e27da3ec02bc10b87b3313d7d99 · 2026-07-02 14:48:07 -0700 · Steve Abrams
Officer: vp-engineering
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Files touched
Diff
commit 6d8e18280cf19e27da3ec02bc10b87b3313d7d99
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Jul 2 14:48:07 2026 -0700
feat(condos): surface parity (DTD verdict C, 2/3 Codex+Claude; Qwen dissent B=export-compare) — collapsible rail tabs (all collapsed on load) + aggregate stats strip (count/median price/$-SF/HOA/year) + ⭐shortlist stars sharing the SAME cre_shortlist key/ids as Explorer+MLS (shortlist now unified across ALL 4 surfaces) + a11y focus-visible/reduced-motion. Contrarian-gated, 0 errors.
Officer: vp-engineering
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
public/condos.html | 46 +++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 43 insertions(+), 3 deletions(-)
diff --git a/public/condos.html b/public/condos.html
index 8b40078..2c4fb59 100644
--- a/public/condos.html
+++ b/public/condos.html
@@ -62,6 +62,21 @@
.listtbl tr:hover td{background:rgba(88,166,255,.06);}
.listtbl .la{font-weight:600;color:var(--ink);} .listtbl .lc{color:var(--mut);font-size:11px;}
@media(max-width:820px){ .rail{display:none;} }
+ :focus-visible { outline:2px solid var(--blue); outline-offset:2px; }
+ @media (prefers-reduced-motion: reduce){ *,*::before,*::after { transition-duration:.001ms !important; animation-duration:.001ms !important; } }
+ .rail .rsec > h4 { cursor:pointer; display:flex; align-items:center; justify-content:space-between; user-select:none; }
+ .rail .rsec > h4::after { content:'▾'; color:var(--mut); font-size:11px; margin-left:8px; transition:transform .15s; }
+ .rail .rsec.collapsed > h4::after { transform:rotate(-90deg); }
+ .rail .rsec.collapsed > *:not(h4) { display:none !important; }
+ .starbtn { background:none; border:0; color:var(--mut); font-size:16px; line-height:1; cursor:pointer; padding:0; }
+ .starbtn:hover, .starbtn.on { color:var(--gold); }
+ .condo .chead { display:flex; justify-content:space-between; gap:8px; align-items:flex-start; }
+ #shortbtn { background:var(--card); color:var(--mut); border:1px solid var(--line); border-radius:8px; padding:7px 12px; font-size:12px; cursor:pointer; }
+ #shortbtn.shorton { border-color:var(--gold); color:var(--gold); }
+ .statsbar { display:flex; flex-wrap:wrap; gap:8px; align-items:center; padding:9px 24px; border-bottom:1px solid var(--line); }
+ .statsbar:empty { display:none; }
+ .statpill { font-size:12px; color:var(--ink); background:var(--card); border:1px solid var(--line); border-radius:8px; padding:4px 10px; }
+ .statpill .sl { color:var(--mut); }
</style>
</head>
<body>
@@ -92,9 +107,11 @@
<div class="topbar">
<input type="text" id="q" placeholder="search project / city / zip / address / broker / firm…">
<span class="viewseg" id="viewseg"><button data-view="grid" class="active">▦ Grid</button><button data-view="list">☰ List</button></span>
+ <button id="shortbtn" title="Show only your shortlisted (starred) listings — shared with the Explorer & MLS">⭐ Shortlist (0)</button>
<label class="dens">Density <input type="range" id="cols" min="1" max="20" step="1" value="3"></label>
<span class="count" id="count">…</span>
</div>
+ <div class="statsbar" id="statsbar"></div>
<div class="gridwrap"><div class="grid" id="grid"><div class="empty">loading…</div></div></div>
</main>
</div>
@@ -146,7 +163,21 @@ function fcell(c,f){ const v=fval(c,f);
}
// ---- facet state ----
-const F={ warr:'all', cities:new Set(), firms:new Set(), bedsMin:0, bathsMin:0,
+// unified shortlist — same localStorage key + listing ids as the Explorer & MLS
+const SHORT=new Set((function(){ try{ return JSON.parse(localStorage.getItem('cre_shortlist')||'[]'); }catch(e){ return []; } })());
+function saveShort(){ try{ localStorage.setItem('cre_shortlist', JSON.stringify([...SHORT])); }catch(e){} }
+function toggleStar(id){ SHORT.has(id)?SHORT.delete(id):SHORT.add(id); saveShort(); }
+function updateShortBtn(){ const sb=$('#shortbtn'); if(sb){ sb.textContent='⭐ Shortlist ('+SHORT.size+')'; sb.classList.toggle('shorton',F.shortlist); } }
+function median(a){ if(!a.length) return null; const s=a.slice().sort((x,y)=>x-y); const m=Math.floor(s.length/2); return s.length%2?s[m]:(s[m-1]+s[m])/2; }
+function renderStats(rows){ const sb=$('#statsbar'); if(!sb) return; if(!rows.length){ sb.innerHTML=''; return; }
+ const prices=rows.map(c=>+c.price).filter(v=>v>0);
+ const pps=rows.map(c=>c.sqft?+c.price/c.sqft:null).filter(v=>v>0);
+ const hoas=rows.map(c=>+c.hoa).filter(v=>v>0);
+ const yrs=rows.map(c=>+c.year_built).filter(v=>v>1800);
+ const pill=(l,v)=>v==null?'':`<span class="statpill"><span class="sl">${l}</span> <b>${v}</b></span>`;
+ sb.innerHTML=pill('Count',rows.length)+pill('Median price',prices.length?'$'+Math.round(median(prices)).toLocaleString():null)+pill('Median $/SF',pps.length?'$'+Math.round(median(pps)).toLocaleString():null)+pill('Median HOA',hoas.length?'$'+Math.round(median(hoas)).toLocaleString()+'/mo':null)+pill('Median year',yrs.length?Math.round(median(yrs)):null);
+}
+const F={ warr:'all', cities:new Set(), firms:new Set(), bedsMin:0, bathsMin:0, shortlist:false,
pmin:null,pmax:null, hoamin:null,hoamax:null, sqftMin:null,sqftMax:null, yearMin:null,yearMax:null, ppsfMin:null,ppsfMax:null };
function rng(v,mn,mx){ if(mn!=null){ if(v==null||!(+v>=mn)) return false; } if(mx!=null){ if(v==null||!(+v<=mx)) return false; } return true; }
function passFacets(c){
@@ -161,6 +192,7 @@ function passFacets(c){
if(!rng(c.sqft,F.sqftMin,F.sqftMax)) return false;
if(!rng(c.year_built,F.yearMin,F.yearMax)) return false;
if(!rng(c.sqft?c.price/c.sqft:null,F.ppsfMin,F.ppsfMax)) return false;
+ if(F.shortlist && !SHORT.has(c.id)) return false;
return true;
}
@@ -190,7 +222,7 @@ const CARD_SKIP=new Set(['warrantable_status','city','zip','project_name','warra
function dbCards(rows){
const vis=FIELDS.filter(f=>fvis(f.k)&&!CARD_SKIP.has(f.k));
return rows.map(c=>`<div class="condo">
- <div class="name">${esc(c.project_name||c.address||'Condo')}</div>
+ <div class="chead"><div class="name">${esc(c.project_name||c.address||'Condo')}</div><button class="starbtn ${SHORT.has(c.id)?'on':''}" data-star="${c.id}" aria-label="${SHORT.has(c.id)?'Remove from':'Add to'} shortlist" aria-pressed="${SHORT.has(c.id)}" title="Shortlist">★</button></div>
<div class="addr">${esc(c.address||'')}${c.city?', '+esc(c.city):''} ${esc(c.zip||'')}</div>
${fvis('warrantable_status')?statusBadge(c.warrantable_status||c.warrant_signal):''}
${vis.map(f=>`<div class="row"><span class="k">${esc(f.l)}</span><span>${fcell(c,f)}</span></div>`).join('')}
@@ -201,7 +233,7 @@ function dbCards(rows){
function dbList(rows){
const cols=FIELDS.filter(f=>f.col&&fvis(f.k));
const head='<th class="stick">Project</th>'+cols.map(f=>`<th>${esc(f.l)}</th>`).join('');
- const body=rows.map(c=>`<tr><td class="stick"><div class="la">${esc(c.project_name||c.address||'Condo')}</div><div class="lc">${esc(c.address||'')}${c.city?', '+esc(c.city):''}</div></td>`
+ const body=rows.map(c=>`<tr><td class="stick"><button class="starbtn ${SHORT.has(c.id)?'on':''}" data-star="${c.id}" aria-label="${SHORT.has(c.id)?'Remove from':'Add to'} shortlist" aria-pressed="${SHORT.has(c.id)}" title="Shortlist">★</button> <span class="la">${esc(c.project_name||c.address||'Condo')}</span><div class="lc">${esc(c.address||'')}${c.city?', '+esc(c.city):''}</div></td>`
+cols.map(f=>`<td>${fcell(c,f)}</td>`).join('')+'</tr>').join('');
return `<div class="listwrap"><table class="listtbl"><thead><tr>${head}</tr></thead><tbody>${body}</tbody></table></div>`;
}
@@ -216,6 +248,7 @@ function apply(){
let rows=DATA.filter(passFacets);
const ql=($('#q').value||'').trim().toLowerCase();
if(ql) rows=rows.filter(c=>((c.project_name||'')+' '+(c.city||'')+' '+(c.zip||'')+' '+(c.address||'')+' '+(c.broker_name||'')+' '+(c.firm_name||'')).toLowerCase().includes(ql));
+ renderStats(rows); updateShortBtn();
const unw=F.warr==='unwarrantable';
$('#count').innerHTML=`<b>${rows.length}</b> of ${DATA.length} ${unw?'unwarrantable (non-FHA) condos':(MODE==='fha'?'FHA-list projects':'condos')}`;
$('#banner').textContent = unw
@@ -258,6 +291,13 @@ $('#cols').oninput=e=>{ document.documentElement.style.setProperty('--cols',e.ta
const savedCols=localStorage.getItem('condoCols'); if(savedCols){ $('#cols').value=savedCols; document.documentElement.style.setProperty('--cols',savedCols); }
$$('#viewseg button').forEach(b=>b.classList.toggle('active',b.dataset.view===VIEW));
$('#viewseg').onclick=e=>{ const b=e.target.closest('button[data-view]'); if(!b) return; VIEW=b.dataset.view; localStorage.setItem('condoView',VIEW); $$('#viewseg button').forEach(x=>x.classList.toggle('active',x.dataset.view===VIEW)); apply(); };
+// shortlist stars (unified) + toggle
+$('#grid').addEventListener('click',e=>{ const st=e.target.closest('[data-star]'); if(st){ toggleStar(st.dataset.star); st.classList.toggle('on',SHORT.has(st.dataset.star)); updateShortBtn(); if(F.shortlist) apply(); } });
+$('#shortbtn').addEventListener('click',()=>{ F.shortlist=!F.shortlist; apply(); });
+// collapsible rail tabs — all collapsed on load
+const railEl=document.querySelector('.rail');
+if(railEl){ railEl.addEventListener('click',e=>{ const h=e.target.closest('h4'); if(h && h.parentElement.classList.contains('rsec')) h.parentElement.classList.toggle('collapsed'); });
+ document.querySelectorAll('.rail .rsec').forEach(s=>s.classList.add('collapsed')); }
boot();
</script>
</body>
← 589fdb5 a11y(index): accessibility pass (DTD verdict A, 2/3 Codex+Cl
·
back to Commercialrealestate
·
feat(index): Compare -> deliverable (DTD verdict A, 2/3 Code 6c08b42 →