[object Object]

← 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

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 →