[object Object]

← back to Commercialrealestate

feat(index): Amazon-style docked left filter rail + List/Grid toggle + per-field show/hide (34 fields) + density to 20 + new data points (price/unit, price/SF, GRM, year built, days-on-market, ZIP)

c02f5b2d6cd7595a8ee155a065af467a2be1073c · 2026-07-02 09:39:20 -0700 · Steve Abrams

- Left filter drawer now docks as a persistent Amazon-style rail on desktop (>=901px); stays off-canvas burger on mobile
- Grid/List segmented toggle in results bar; List renders a sortable columnar table (sticky Property column), columns driven by field toggles
- Fields show/hide panel: every card field + several list-only columns, All/None/Default, persisted to localStorage
- Density slider max 6 -> 20 cards per row
- New computed data points: price/unit, price/SF, GRM, year built, days-on-market, ZIP

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files touched

Diff

commit c02f5b2d6cd7595a8ee155a065af467a2be1073c
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Jul 2 09:39:20 2026 -0700

    feat(index): Amazon-style docked left filter rail + List/Grid toggle + per-field show/hide (34 fields) + density to 20 + new data points (price/unit, price/SF, GRM, year built, days-on-market, ZIP)
    
    - Left filter drawer now docks as a persistent Amazon-style rail on desktop (>=901px); stays off-canvas burger on mobile
    - Grid/List segmented toggle in results bar; List renders a sortable columnar table (sticky Property column), columns driven by field toggles
    - Fields show/hide panel: every card field + several list-only columns, All/None/Default, persisted to localStorage
    - Density slider max 6 -> 20 cards per row
    - New computed data points: price/unit, price/SF, GRM, year built, days-on-market, ZIP
    
    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
 public/index.html | 200 +++++++++++++++++++++++++++++++++++++++++++++++-------
 1 file changed, 176 insertions(+), 24 deletions(-)

diff --git a/public/index.html b/public/index.html
index bc2e681..a829ffa 100644
--- a/public/index.html
+++ b/public/index.html
@@ -130,6 +130,42 @@
   #send:disabled { opacity:.5; cursor:default; }
   .examples { display:flex; flex-wrap:wrap; gap:6px; margin-top:8px; }
   .ex { font-size:12px; color:var(--blue); border:1px dashed #284b7a; border-radius:16px; padding:4px 10px; cursor:pointer; background:transparent; }
+
+  /* ---- view toggle (grid/list) ---- */
+  .viewseg { display:inline-flex; border:1px solid var(--line); border-radius:8px; overflow:hidden; }
+  .viewseg button { background:var(--card); color:var(--mut); border:0; border-right:1px solid var(--line); padding:7px 12px; font-size:12px; cursor:pointer; }
+  .viewseg button:last-child { border-right:0; }
+  .viewseg button.active { background:#1f6feb; color:#fff; }
+  /* ---- field show/hide toggles ---- */
+  .fieldtoggles { display:flex; flex-direction:column; gap:1px; max-height:300px; overflow-y:auto; padding-right:4px; }
+  .ftog { display:flex; align-items:center; gap:8px; font-size:12.5px; color:var(--ink); cursor:pointer; padding:3px 5px; border-radius:6px; }
+  .ftog:hover { background:rgba(88,166,255,.06); }
+  .ftog input { accent-color:var(--blue); }
+  .ftog .colbadge { margin-left:auto; color:var(--mut); font-size:11px; }
+  .minibtn { background:var(--card); color:var(--mut); border:1px solid var(--line); border-radius:7px; padding:4px 9px; font-size:11px; cursor:pointer; margin-right:5px; }
+  .minibtn:hover { border-color:var(--blue); color:var(--ink); }
+  /* ---- list view ---- */
+  .listhost { display:block; padding:20px 20px 30px; }
+  .listwrap { overflow-x:auto; border:2px solid #fff; border-radius:12px; }
+  .listtbl { width:100%; border-collapse:collapse; font-size:12.5px; white-space:nowrap; }
+  .listtbl th, .listtbl td { text-align:right; padding:9px 13px; border-top:1px solid var(--line); }
+  .listtbl th { position:sticky; top:0; background:#0b0e13; color:var(--mut); font-size:10px; text-transform:uppercase; letter-spacing:.5px; border-top:0; z-index:1; }
+  .listtbl td.stick, .listtbl th.stick { text-align:left; position:sticky; left:0; background:var(--card); }
+  .listtbl th.stick { background:#0b0e13; z-index:2; }
+  .listtbl tr.lrow:hover td { background:rgba(88,166,255,.06); }
+  .listtbl tr.lrow:hover td.stick { background:#1b222c; }
+  .listtbl .la { font-weight:600; color:var(--ink); text-decoration:none; }
+  .listtbl .la:hover { color:var(--blue); }
+  .listtbl .lc { color:var(--mut); font-size:11px; }
+  /* ---- Amazon-style docked left filter rail (desktop) ---- */
+  .shell { display:flex; align-items:flex-start; }
+  main { flex:1 1 auto; min-width:0; }
+  @media (min-width:901px){
+    #burger { display:none; }
+    #scrim { display:none !important; }
+    #drawer { position:sticky; top:57px; height:calc(100vh - 57px); transform:none; flex:0 0 var(--drawerW); box-shadow:none; }
+    #drawer .dhead .x { display:none; }
+  }
 </style>
 </head>
 <body>
@@ -160,6 +196,7 @@
 </header>
 
 <div id="scrim"></div>
+<div class="shell">
 <aside id="drawer">
   <div class="dhead"><b>Filters</b><button class="x" id="dclose" aria-label="Close">✕</button></div>
 
@@ -202,8 +239,13 @@
       <option value="unitsDesc">Units ↓</option>
       <option value="rentRoi">Pro-forma Rent ROI ↓</option>
     </select>
-    <div style="margin-top:10px"><label class="small">Cards per row <span class="rangeval" id="densVal">3</span></label>
-      <input type="range" id="density" min="1" max="6" step="1" value="3"></div>
+    <div style="margin-top:10px"><label class="small">Cards per row (grid) <span class="rangeval" id="densVal">3</span></label>
+      <input type="range" id="density" min="1" max="20" step="1" value="3"></div>
+  </div>
+  <div class="fsec"><h4>Fields — show / hide</h4>
+    <div class="fieldtoggles" id="fFields"></div>
+    <div style="margin-top:8px"><button class="minibtn" id="fAll">All</button><button class="minibtn" id="fNone">None</button><button class="minibtn" id="fDef">Default</button></div>
+    <div class="small" style="margin-top:6px">▤ = also a column in List view</div>
   </div>
   <button class="resetbtn" id="reset">Reset all filters</button>
 </aside>
@@ -360,6 +402,10 @@
   </script>
   <div class="bar">
     <span class="count" id="count">…</span>
+    <span class="viewseg" id="viewseg">
+      <button data-view="grid" class="active">▦ Grid</button>
+      <button data-view="list">☰ List</button>
+    </span>
     <div class="right">
       <label>Sort</label>
       <select id="sortTop" style="width:auto;min-width:180px"></select>
@@ -368,6 +414,7 @@
   <div class="grid" id="grid"></div>
   <div class="foot" id="foot"></div>
 </main>
+</div><!-- /shell -->
 
 <div id="chatwrap">
   <h2>💬 Ask the analyst &nbsp;<span class="small">— grounded in this dataset · free (Claude CLI)</span></h2>
@@ -460,6 +507,100 @@ function rentLine(p){
 // For ranking, treat implausible (>18%) pro-forma yields as low-confidence so bad-units outliers don't top the list.
 function rentRoiScore(p){ const r=rentEstimate(p); if(!r||!r.grossYieldPro) return -1e9; return r.grossYieldPro>18 ? -1 : r.grossYieldPro; }
 
+// ===== Field show/hide + list/grid view + extra data points =====
+const esc = s => String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/"/g,'&quot;');
+// Every data field the UI can render. card=shows on the grid card · col=available as a List column · def=default-on.
+const FIELDS = [
+  {k:'score',      label:'Investment score',  card:1, col:1, def:1},
+  {k:'deal',       label:'Deal score',        card:1, col:1, def:1},
+  {k:'firm',       label:'Listing firm',      card:1, col:1, def:1},
+  {k:'type',       label:'Asset type',        card:1, col:1, def:1},
+  {k:'units',      label:'Units',             card:1, col:1, def:1},
+  {k:'price',      label:'Price',             card:1, col:1, def:1},
+  {k:'ppu',        label:'Price / unit',      card:1, col:1, def:1},
+  {k:'pricePerSf', label:'Price / SF',        card:1, col:1, def:0},
+  {k:'cap',        label:'Cap rate',          card:1, col:1, def:1},
+  {k:'verified',   label:'Cap-verified flag', card:1, col:0, def:1},
+  {k:'yield',      label:'Rent yield',        card:1, col:1, def:1},
+  {k:'grm',        label:'GRM (rent mult.)',  card:1, col:1, def:0},
+  {k:'yearBuilt',  label:'Year built',        card:1, col:1, def:0},
+  {k:'dom',        label:'Days on market',    card:1, col:1, def:0},
+  {k:'zip',        label:'ZIP',               card:0, col:1, def:0},
+  {k:'status',     label:'Status badge',      card:1, col:1, def:1},
+  {k:'afford',     label:'Affordability',     card:1, col:1, def:1},
+  {k:'rec',        label:'AI recommendation', card:1, col:1, def:0},
+  {k:'statusline', label:'Listing-date line', card:1, col:0, def:1},
+  {k:'brokerflag', label:'Prior-broker flag', card:1, col:0, def:1},
+  {k:'txhist',     label:'Transaction history',card:1,col:0, def:0},
+  {k:'brokerline', label:'Current broker',    card:1, col:1, def:1},
+  {k:'coc',        label:'Cash-on-cash',      card:1, col:1, def:1},
+  {k:'dscr',       label:'DSCR',              card:1, col:1, def:1},
+  {k:'noi',        label:'Est. NOI',          card:1, col:1, def:1},
+  {k:'debt',       label:'Debt service /yr',  card:1, col:1, def:1},
+  {k:'cashNeeded', label:'Cash needed',       card:1, col:1, def:1},
+  {k:'cashFlow',   label:'Cash flow /yr',     card:1, col:1, def:1},
+  {k:'thesis',     label:'AI thesis',         card:1, col:0, def:1},
+  {k:'risks',      label:'Risks',             card:1, col:0, def:0},
+  {k:'upside',     label:'Upside note',       card:1, col:0, def:1},
+  {k:'demo',       label:'Demographics',      card:1, col:0, def:1},
+  {k:'rent',       label:'Rent estimate',     card:1, col:1, def:1},
+  {k:'links',      label:'Listing links',     card:1, col:0, def:1},
+];
+const VIS = (function(){ let s={}; try{ s=JSON.parse(localStorage.getItem('cre_fields')||'{}'); }catch(e){} const o={}; FIELDS.forEach(f=>o[f.k]=(f.k in s)?!!s[f.k]:!!f.def); return o; })();
+function vis(k){ return !!VIS[k]; }
+function saveVis(){ try{ localStorage.setItem('cre_fields', JSON.stringify(VIS)); }catch(e){} }
+let view = localStorage.getItem('cre_view') || 'grid';
+function buildFieldToggles(){
+  const host=$('#fFields'); if(!host) return;
+  host.innerHTML = FIELDS.map(f=>`<label class="ftog"><input type="checkbox" data-fk="${f.k}" ${vis(f.k)?'checked':''}><span>${f.label}</span>${f.col?'<span class="colbadge" title="Also shows as a column in List view">▤</span>':''}</label>`).join('');
+}
+// ---- extra computed data points ----
+function ppu(p){ return (p.price&&p.units)?Math.round(p.price/p.units):null; }
+function ppsf(p){ return (p.price&&p.sqft)?Math.round(p.price/p.sqft):null; }
+function grm(p){ const r=rentEstimate(p); if(!r||!r.std) return null; const annual=r.std*12; return annual?+(p.price/annual).toFixed(1):null; }
+function daysOnMarket(p){ const ld=listingDate(p); if(!ld||!ld.exact) return null; const d=Math.round((Date.now()-ld.d.getTime())/86400000); return d>=0?d:null; }
+function shortStatus(p){ if(/Under Contract|Pending|Escrow/i.test(p.status)) return p.status; if(/Sold|Closed/i.test(p.status)) return 'Sold'; if(/OUT OF BUDGET/i.test(p.status)) return 'Over budget'; return 'Active'; }
+// ---- list-view renderer ----
+function listCols(){ return FIELDS.filter(f=>f.col && vis(f.k)); }
+function colCell(k,p){ const f=p.finance||{};
+  switch(k){
+    case 'score': return p.composite??'—';
+    case 'deal': return (p.deal&&p.deal.score!=null)?p.deal.score:'—';
+    case 'firm': return esc(firmLabel(p));
+    case 'type': return esc(p.type||'—');
+    case 'units': return p.units||'—';
+    case 'price': return fmt(p.price);
+    case 'ppu': { const v=ppu(p); return v?'$'+v.toLocaleString():'—'; }
+    case 'pricePerSf': { const v=ppsf(p); return v?'$'+v.toLocaleString():'—'; }
+    case 'cap': return p.cap_rate!=null?p.cap_rate+'%':'—';
+    case 'yield': return f.grossYield?f.grossYield+'%':'—';
+    case 'grm': { const v=grm(p); return v!=null?v:'—'; }
+    case 'yearBuilt': return p.year_built||'—';
+    case 'dom': { const v=daysOnMarket(p); return v!=null?v+'d':'—'; }
+    case 'zip': return p.zip||'—';
+    case 'status': return esc(shortStatus(p));
+    case 'afford': return f.affordable?'✓ in budget':'✗ over';
+    case 'rec': return (p.qwen&&p.qwen.recommendation)||'—';
+    case 'brokerline': { const bh=window.BROKER_HIST[p.id]||window.BROKER_HIST_ADDR[(p.address||'').toLowerCase().trim()]; return (bh&&bh.current_broker)?esc(bh.current_broker.name||bh.current_broker.firm||'—'):'—'; }
+    case 'coc': return f.coc!=null?f.coc+'%':'—';
+    case 'dscr': return f.dscr??'—';
+    case 'noi': return fmt(f.noi);
+    case 'debt': return fmt(f.annualDebtService);
+    case 'cashNeeded': return fmt(f.cashNeeded);
+    case 'cashFlow': return fmt(f.cashFlow);
+    case 'rent': { const r=rentEstimate(p); return r?'$'+r.std.toLocaleString():'—'; }
+    default: return '—';
+  }
+}
+function renderList(rows){
+  const cols=listCols();
+  const head='<th class="stick">Property</th>'+cols.map(c=>`<th>${esc(c.label)}</th>`).join('');
+  const body=rows.map(p=>`<tr class="lrow" data-id="${p.id}">`
+    +`<td class="stick"><a class="la" href="${p.source}" target="_blank" rel="noopener noreferrer">${esc(p.address)}</a><div class="lc">${esc(p.city)}, CA ${p.zip||''}</div></td>`
+    +cols.map(c=>`<td>${colCell(c.k,p)}</td>`).join('')+`</tr>`).join('');
+  return `<div class="listwrap"><table class="listtbl"><thead><tr>${head}</tr></thead><tbody>${body}</tbody></table></div>`;
+}
+
 // ---- Unified Deal Score (0-100) — DealScore.compute() over the signals we already have. ----
 // Gathers: pro-forma gross yield (Census rent estimate), cap rate, in-place DSCR (finance.js),
 // a PROXY DSCR when no cap is disclosed (est NOI from pro-forma rent vs the active-scenario debt
@@ -576,27 +717,27 @@ 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 class="score">${p.composite}<small>fin ${p.financeScore}${p.qwenScore!=null?' · ai '+p.qwenScore:''}</small></div>
+      ${vis('score')?`<div class="score">${p.composite}<small>fin ${p.financeScore}${p.qwenScore!=null?' · ai '+p.qwenScore:''}</small></div>`:''}
     </div>
-    <div class="badges">${dealBadge(p)}${firmB}<span class="b type">${p.type}</span><span class="b">${p.units} unit${p.units>1?'s':''}</span><span class="b">${fmt(p.price)}</span>${capB}${verB}${f.grossYield?`<span class="b ok" title="Annual gross rent ÷ price">${f.grossYield}% rent yld</span>`:''}${statusB} ${aff} ${rec}</div>
-    ${brokerFlagBadge}
-    ${statusLine(p)}
-    ${txHistHTML(bh)}
-    ${brokerLineHTML(bh)}
-    <div class="metrics">
-      <div><span class="k">Cash-on-cash</span><span class="v ${cls(f.coc,6,3)}">${pct(f.coc)}</span></div>
-      <div><span class="k">DSCR</span><span class="v ${cls(f.dscr,1.25,1.0)}">${f.dscr??'—'}</span></div>
-      <div><span class="k">Est. NOI</span><span class="v">${fmt(f.noi)}</span></div>
-      <div><span class="k">${scenario==='cash'?'Debt svc':'Debt svc/yr'}</span><span class="v">${fmt(f.annualDebtService)}</span></div>
-      <div><span class="k">Cash needed</span><span class="v ${f.affordable?'pos':'neg'}">${fmt(f.cashNeeded)}</span></div>
-      <div><span class="k">Cash flow/yr</span><span class="v ${cls(f.cashFlow,1,0)}">${fmt(f.cashFlow)}</span></div>
-    </div>
-    <div class="thesis">${p.qwen?.thesis||''}</div>
-    ${p.qwen?.risks?.length?`<div><span class="small">Risks</span><ul class="lst">${p.qwen.risks.map(r=>`<li>${r}</li>`).join('')}</ul></div>`:''}
-    ${p.upside_note?`<div class="small">💡 ${p.upside_note}</div>`:''}
-    ${demoLine(p)}
-    ${rentLine(p)}
-    <div class="src"><a href="${p.source}" target="_blank" rel="noopener noreferrer">View listing ↗</a> · ${loopnetLink(p)} · <button class="lcbtn" data-id="${p.id}" title="Pull fresh comps from LoopNet via a cloud browser (~$0.03)">🔄 Live comps</button> · <button class="histbtn" data-id="${p.id}" data-address="${(p.address||'').replace(/"/g,'&quot;')}" data-city="${(p.city||'').replace(/"/g,'&quot;')}" data-zip="${p.zip||''}" title="LA County assessor + permit + film history (free)">📜 Property history</button></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)?`<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('status')?statusB:''} ${vis('afford')?aff:''} ${vis('rec')?rec:''}</div>
+    ${vis('brokerflag')?brokerFlagBadge:''}
+    ${vis('statusline')?statusLine(p):''}
+    ${vis('txhist')?txHistHTML(bh):''}
+    ${vis('brokerline')?brokerLineHTML(bh):''}
+    ${(vis('coc')||vis('dscr')||vis('noi')||vis('debt')||vis('cashNeeded')||vis('cashFlow'))?`<div class="metrics">
+      ${vis('coc')?`<div><span class="k">Cash-on-cash</span><span class="v ${cls(f.coc,6,3)}">${pct(f.coc)}</span></div>`:''}
+      ${vis('dscr')?`<div><span class="k">DSCR</span><span class="v ${cls(f.dscr,1.25,1.0)}">${f.dscr??'—'}</span></div>`:''}
+      ${vis('noi')?`<div><span class="k">Est. NOI</span><span class="v">${fmt(f.noi)}</span></div>`:''}
+      ${vis('debt')?`<div><span class="k">${scenario==='cash'?'Debt svc':'Debt svc/yr'}</span><span class="v">${fmt(f.annualDebtService)}</span></div>`:''}
+      ${vis('cashNeeded')?`<div><span class="k">Cash needed</span><span class="v ${f.affordable?'pos':'neg'}">${fmt(f.cashNeeded)}</span></div>`:''}
+      ${vis('cashFlow')?`<div><span class="k">Cash flow/yr</span><span class="v ${cls(f.cashFlow,1,0)}">${fmt(f.cashFlow)}</span></div>`:''}
+    </div>`:''}
+    ${vis('thesis')&&p.qwen?.thesis?`<div class="thesis">${p.qwen.thesis}</div>`:''}
+    ${vis('risks')&&p.qwen?.risks?.length?`<div><span class="small">Risks</span><ul class="lst">${p.qwen.risks.map(r=>`<li>${r}</li>`).join('')}</ul></div>`:''}
+    ${vis('upside')&&p.upside_note?`<div class="small">💡 ${p.upside_note}</div>`:''}
+    ${vis('demo')?demoLine(p):''}
+    ${vis('rent')?rentLine(p):''}
+    ${vis('links')?`<div class="src"><a href="${p.source}" target="_blank" rel="noopener noreferrer">View listing ↗</a> · ${loopnetLink(p)} · <button class="lcbtn" data-id="${p.id}" title="Pull fresh comps from LoopNet via a cloud browser (~$0.03)">🔄 Live comps</button> · <button class="histbtn" data-id="${p.id}" data-address="${(p.address||'').replace(/"/g,'&quot;')}" data-city="${(p.city||'').replace(/"/g,'&quot;')}" data-zip="${p.zip||''}" title="LA County assessor + permit + film history (free)">📜 Property history</button></div>`:''}
     <div class="lcout" id="lc-${p.id}"></div>
     <div class="histout" id="hist-${p.id}"></div>
   </div>`;
@@ -639,8 +780,10 @@ const dealNum=p=>(p.deal && p.deal.score!=null) ? p.deal.score : -1e9;  // unsco
 function render(){
   let r = applyFilters(DATA.ranked.slice());
   r.sort(SORTS[$('#sort').value]||SORTS.composite);
-  $('#grid').innerHTML = r.length ? r.map(card).join('')
-    : `<div class="empty">No properties match these filters.<br><span class="small">Loosen a filter or hit “Reset all”.</span></div>`;
+  const g=$('#grid');
+  if(!r.length){ g.className='grid'; g.innerHTML=`<div class="empty">No properties match these filters.<br><span class="small">Loosen a filter or hit “Reset all”.</span></div>`; }
+  else if(view==='list'){ g.className='listhost'; g.innerHTML=renderList(r); }
+  else { g.className='grid'; g.innerHTML=r.map(card).join(''); }
   const fc=activeFilterCount();
   $('#count').innerHTML = `<b>${r.length}</b> of ${DATA.ranked.length} properties${fc?` · ${fc} filter${fc>1?'s':''} active`:''}`;
   $('#fcount').textContent=fc; $('#fcount').classList.toggle('on',fc>0);
@@ -844,6 +987,15 @@ fetch('data/ranked.json').then(r=>r.json()).then(d=>{
   const dv=localStorage.getItem('cre_density')||'3'; $('#density').value=dv; $('#densVal').textContent=dv; document.documentElement.style.setProperty('--cols',dv);
   $('#density').oninput=()=>{ document.documentElement.style.setProperty('--cols',$('#density').value); $('#densVal').textContent=$('#density').value; localStorage.setItem('cre_density',$('#density').value); };
 
+  // field show/hide toggles + list/grid view
+  buildFieldToggles();
+  $('#fFields').onchange=e=>{ const cb=e.target.closest('input[data-fk]'); if(!cb) return; VIS[cb.dataset.fk]=cb.checked; saveVis(); render(); };
+  $('#fAll').onclick=()=>{ FIELDS.forEach(f=>VIS[f.k]=true); saveVis(); buildFieldToggles(); render(); };
+  $('#fNone').onclick=()=>{ FIELDS.forEach(f=>VIS[f.k]=false); saveVis(); buildFieldToggles(); render(); };
+  $('#fDef').onclick=()=>{ FIELDS.forEach(f=>VIS[f.k]=!!f.def); saveVis(); buildFieldToggles(); render(); };
+  $$('#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('cre_view',view); $$('#viewseg button').forEach(x=>x.classList.toggle('active',x.dataset.view===view)); render(); };
+
   // chat
   const EX=['Show only multifamily under $1M, sorted by cash-on-cash','Which deals actually cash-flow at 30% down?','Office and industrial in Pasadena','Take me to the Reseda mixed-use','Best in-budget deals'];
   $('#examples').innerHTML = EX.map(x=>`<button class="ex">${x}</button>`).join('');

← 177e1b0 style: roomier collection grids — bigger card+container padd  ·  back to Commercialrealestate  ·  feat(condos): List/Grid toggle (both DB-listing & FHA-direct 5a3ee55 →