← back to Commercialrealestate
5x sweep 1: add sort select + density slider to mls.html grid (Steve grid rule); persisted, 0 JS errors
bb93e9cb223b24d8c7781c94ec6336a26bbe1c0f · 2026-07-10 11:12:22 -0700 · Steve Abrams
Files touched
M 5x/sweep-1.mdM public/mls.html
Diff
commit bb93e9cb223b24d8c7781c94ec6336a26bbe1c0f
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Jul 10 11:12:22 2026 -0700
5x sweep 1: add sort select + density slider to mls.html grid (Steve grid rule); persisted, 0 JS errors
---
5x/sweep-1.md | 11 +++++++++++
public/mls.html | 37 ++++++++++++++++++++++++++++++++++---
2 files changed, 45 insertions(+), 3 deletions(-)
diff --git a/5x/sweep-1.md b/5x/sweep-1.md
index b28f655..b379786 100644
--- a/5x/sweep-1.md
+++ b/5x/sweep-1.md
@@ -21,3 +21,14 @@ Clickthrough + console pass only ran on mls.html. The two pages changed most
recently — map.html (popup upgrade) and index.html (Explorer grid/slider) —
only got the M1 HTTP check via --paths. Sweep 1 FIX action: run full M1/M2/M3
on those two directly (below).
+
+## FIX applied (sweep 1)
+Real defect: mls.html renders a card grid but the Grid view had NO reachable
+sort control (table-header sort vanishes in grid) and NO density slider —
+violates Steve's hard rule (every grid = sort + density, side-by-side,
+localStorage-persisted).
+Fix: added #sortsel (built from COLS) + #density range slider (1–10 → card-min
+380→140px via --card-min var) beside the view toggle. Sort/density/view all
+persist to localStorage; table-header sort stays in sync with the select.
+Verified via Playwright: density visible in grid, 353px@2 vs 167px@9, both
+persist across reload, 0 JS errors.
diff --git a/public/mls.html b/public/mls.html
index 70bc35a..2210983 100644
--- a/public/mls.html
+++ b/public/mls.html
@@ -33,7 +33,10 @@
.pill.active{background:rgba(63,185,80,.16);color:var(--acc);} .pill.other{background:rgba(210,153,34,.16);color:var(--warn);}
.mut{color:var(--mut);}
/* GRID */
- .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:20px;}
+ .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(var(--card-min,260px),1fr));gap:20px;}
+ #sortsel{background:var(--card);color:var(--ink);border:1px solid var(--line);border-radius:7px;padding:5px 8px;font-size:12px}
+ #denswrap{display:none;align-items:center;gap:6px;font-size:13px;color:var(--mut)}
+ #denswrap input[type=range]{width:92px;accent-color:var(--acc)}
.gc{background:var(--card);border:2px solid #fff;border-radius:11px;padding:18px 20px;}
.gc .addr{font-weight:600;} .gc .sub{color:var(--mut);font-size:12px;margin-bottom:6px;}
.gc .row{display:flex;justify-content:space-between;font-size:12.5px;padding:2px 0;border-top:1px solid #20262f;}
@@ -92,6 +95,8 @@
<span class="spacer"></span>
<input class="search" id="q" type="text" placeholder="search anything — address, city, firm, type, year…">
<div class="toggle" id="view"><button data-v="table" class="on">▦ Table</button><button data-v="grid">▤ Grid</button></div>
+ <select id="sortsel" title="Sort listings"></select>
+ <label id="denswrap" title="Card density — grid view">⤢<input type="range" id="density" min="1" max="10" step="1"></label>
<button class="csvbtn" id="shortbtn" title="Show only your shortlisted (starred) listings — shared with the Explorer">⭐ Shortlist (0)</button>
<select id="heat" title="Color rows red (low) → green (high) by a metric"><option value="none">Heat: off</option><option value="cap">Heat: Cap rate</option><option value="score">Heat: Score</option></select>
<span id="heatleg" class="heatleg">low <span class="hb"></span> high · <span class="hl"></span></span>
@@ -321,7 +326,8 @@ function render(){
paintBody();
}
$('#thead').addEventListener('click',e=>{ const th=e.target.closest('th'); if(!th) return; const k=th.dataset.k; if(!k) return;
- if(sortKey===k) sortDir*=-1; else { sortKey=k; sortDir=(k==='address'||k==='city'||k==='firm'||k==='type')?1:-1; } render(); });
+ if(sortKey===k) sortDir*=-1; else { sortKey=k; sortDir=(k==='address'||k==='city'||k==='firm'||k==='type')?1:-1; }
+ localStorage.setItem('mlsSortKey',sortKey); localStorage.setItem('mlsSortDir',sortDir); if(window.__syncSortSel) window.__syncSortSel(); render(); });
// click a property → 100-yr history popup (address-only, no names)
async function propHistory(address,city){
const ov=$('#ov'), mb=$('#mbody'); ov.classList.add('on'); mb.innerHTML='<div class="ph-mut">loading history for '+esc(address)+'…</div>';
@@ -351,7 +357,32 @@ $('#shortbtn').addEventListener('click',()=>{ F.shortlist=!F.shortlist; render()
$('#moreWrap').addEventListener('click',e=>{ if(e.target.closest('#showmore')){ _shown=Math.min(_shown+PAGE(), _rows.length); paintBody(); } });
$('#heat').value=heat; $('#heat').addEventListener('change',()=>{ heat=$('#heat').value; localStorage.setItem('cre_heat_mls',heat); render(); });
$('#view').addEventListener('click',e=>{ const b=e.target.closest('button'); if(!b) return;
- document.querySelectorAll('#view button').forEach(x=>x.classList.toggle('on',x===b)); view=b.dataset.v; render(); });
+ document.querySelectorAll('#view button').forEach(x=>x.classList.toggle('on',x===b)); view=b.dataset.v; localStorage.setItem('mlsView',view); $('#denswrap').style.display=(view==='grid')?'inline-flex':'none'; render(); });
+// ── sort <select> + density slider (Steve rule: every grid gets sort + density) ──
+(function(){
+ const sel=$('#sortsel'); const dz=$('#density');
+ // build sort options from COLS (skip the source link column)
+ sel.innerHTML=COLS.filter(c=>c.t!=='link').map(c=>`<option value="${c.k}">Sort: ${esc(c.l==='#'?'Default (rank)':c.l)}</option>`).join('');
+ // restore persisted sort
+ const sk=localStorage.getItem('mlsSortKey'); const sd=localStorage.getItem('mlsSortDir');
+ if(sk && COLS.some(c=>c.k===sk)) sortKey=sk;
+ if(sd==='1'||sd==='-1') sortDir=+sd;
+ sel.value=sortKey;
+ sel.addEventListener('change',()=>{ sortKey=sel.value; const c=COLS.find(x=>x.k===sortKey);
+ sortDir=(c&&(c.t==='s'||c.t==='status'))?1:-1; // text asc, numbers/money desc
+ localStorage.setItem('mlsSortKey',sortKey); localStorage.setItem('mlsSortDir',sortDir); render(); });
+ // density 1..10 → card-min width (1 = big/sparse, 10 = small/dense)
+ const widthFor=v=>Math.round(380-(Math.max(1,Math.min(10,v))-1)*(380-140)/9);
+ function applyDensity(v){ document.documentElement.style.setProperty('--card-min', widthFor(v)+'px'); }
+ const dv=parseInt(localStorage.getItem('mlsDensity'),10); const dval=(dv>=1&&dv<=10)?dv:5;
+ dz.value=dval; applyDensity(dval);
+ dz.addEventListener('input',()=>{ const v=+dz.value; applyDensity(v); localStorage.setItem('mlsDensity',v); });
+ // restore persisted view (and show density only in grid view)
+ const pv=localStorage.getItem('mlsView'); if(pv==='grid'||pv==='table'){ view=pv; document.querySelectorAll('#view button').forEach(x=>x.classList.toggle('on',x.dataset.v===view)); }
+ $('#denswrap').style.display=(view==='grid')?'inline-flex':'none';
+ // keep the select in sync when the user sorts via a table-column header
+ window.__syncSortSel=()=>{ if(sel.value!==sortKey) sel.value=sortKey; };
+})();
// left-rail facets + column toggles
$('#fType').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;tog(F.types,c.dataset.ty);c.classList.toggle('active');render();});
$('#fStatus').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;tog(F.status,c.dataset.st);c.classList.toggle('active');render();});
← 69e2760 5x sweep 1: triage — clickthrough FAILs are phantom (wrong-a
·
back to Commercialrealestate
·
5x: sweeps 2-3 clean + REPORT (clean twice, done) 9e62037 →