[object Object]

← back to Commercialrealestate

mls.html: add compact-list view (3rd mode) — grid/list/table + all-col sort; property-history modal + shortlist + facets/search/pagination preserved

b36efec4c49f106958f16b6358a3081180458daf · 2026-08-13 08:17:46 -0700 · steve

Files touched

Diff

commit b36efec4c49f106958f16b6358a3081180458daf
Author: steve <steve@designerwallcoverings.com>
Date:   Thu Aug 13 08:17:46 2026 -0700

    mls.html: add compact-list view (3rd mode) — grid/list/table + all-col sort; property-history modal + shortlist + facets/search/pagination preserved
---
 public/mls.html                | 23 ++++++++++++++-----
 public/nav-agent/nav-agent.css |  7 ++++++
 public/nav-agent/nav-agent.js  | 51 +++++++++++++++++++++++++++++++++++++++---
 3 files changed, 73 insertions(+), 8 deletions(-)

diff --git a/public/mls.html b/public/mls.html
index a31f7f9..1a1a118 100644
--- a/public/mls.html
+++ b/public/mls.html
@@ -109,6 +109,7 @@
   .heatleg .hb { width:52px; height:8px; border-radius:4px; background:linear-gradient(90deg,hsl(0,70%,45%),hsl(60,70%,45%),hsl(120,70%,45%)); }
 </style>
 <link rel="stylesheet" href="/crcp-theme.css">
+  <link rel="stylesheet" href="/three-view.css">
   <link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
 </head>
 <body>
@@ -117,7 +118,7 @@
   <div class="nav"><a href="/">← Explorer</a><a href="/deals-flow.html" title="Closed deal flow — public-record sales + FHA + SEC REIT">💰 Deal Flow</a><a href="/map.html">🗺️ Map</a><a href="/licensed-agents.html" title="Real-estate agents &amp; brokers verified against the CA DRE public license roll">🎫 Licensed Agents</a><a href="/crcp.html">🛰️ CRCP</a></div>
   <span class="spacer"></span>
   <input class="search" id="q" type="text" placeholder="search all fields — address, city, firm, type, agent, source, year… (space = AND)">
-  <div class="toggle" id="view"><button data-v="table" class="on">▦ Table</button><button data-v="grid">▤ Grid</button></div>
+  <div class="toggle tv-seg" id="view"><button data-v="grid">▦ Grid</button><button data-v="list">☰ List</button><button data-v="table" class="on">▤ Table</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>
@@ -146,6 +147,7 @@
   <div class="statsbar" id="statsbar"></div>
   <div id="tableView" class="tblwrap"><table class="mls" data-no-sort><thead id="thead"></thead><tbody id="tbody"></tbody></table></div>
   <div id="gridView" class="grid hide"></div>
+  <div id="listView" class="hide"></div>
   <div id="moreWrap" style="text-align:center;padding:16px 0"></div>
 </div>
 </div>
@@ -227,20 +229,31 @@ 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); } }
 // pagination (perf, parity with the Explorer)
 let _rows=[], _cols=[], _shown=0;
-const PAGE=()=> view==='table'?200:60;
+const PAGE=()=> view==='table'?200:view==='list'?120:60;
 const star=r=>`<button class="starbtn ${SHORT.has(r.id)?'on':''}" data-star="${r.id}" aria-label="${SHORT.has(r.id)?'Remove from':'Add to'} shortlist" aria-pressed="${SHORT.has(r.id)}" title="Shortlist">★</button><button class="copybtn" data-copy="${r.id}" aria-label="Copy summary" title="Copy summary">📋</button>`;
 function paintBody(){
   const rows=_rows.slice(0,_shown), cols=_cols;
+  const show=id=>['#tableView','#gridView','#listView'].forEach(s=>$(s).classList.toggle('hide',s!==id));
   if(view==='table'){
-    $('#tableView').classList.remove('hide'); $('#gridView').classList.add('hide');
+    show('#tableView');
     $('#tbody').innerHTML=rows.map(r=>`<tr class="prow" data-addr="${esc(r.address||'')}" data-city="${esc(r.city||'')}" style="cursor:pointer"><td class="startd">${star(r)}</td>`+cols.map(c=>{
       const cls=(c.t==='money'||c.t==='n'||c.t==='pct')?' class="n"':''; return `<td${cls}>${cellHTML(r,c)}</td>`;
     }).join('')+'</tr>').join('');
     const csig=cols.map(c=>c.k).join(',');
     if(csig!==lastColSig){ lastColSig=csig; reinitResize(); }
     else if(window.ColResize && window.ColResize.reattach){ window.ColResize.reattach(document.querySelector('#tableView table.mls')); } // resize handles survive sort/search re-render (TK-10089)
+  } else if(view==='list'){
+    // COMPACT LIST — one scannable row per property, every VISIBLE column as inline
+    // key/value pairs (reuses cellHTML so all formatting matches table + cards).
+    // Rows keep the .prow + data-addr/data-city so the existing property-history
+    // click delegation on document just works. TK-10526 additive 3rd view.
+    show('#listView');
+    const metaCols=cols.filter(c=>!['address','city','zip','type'].includes(c.k));
+    $('#listView').innerHTML='<div class="tv-rows">'+rows.map(r=>`<div class="tv-lrow prow" data-addr="${esc(r.address||'')}" data-city="${esc(r.city||'')}">`
+      +`<div class="tv-lmain">${star(r)} <span class="tv-la">${esc(r.address||'—')}</span><span class="tv-lsub">${esc(r.city||'')} ${esc(r.zip||'')}${r.type?' · '+esc(r.type):''}</span></div>`
+      +`<div class="tv-lmeta">`+metaCols.map(c=>`<span class="tv-lm"><span class="tv-lmk">${esc(c.l)}</span>${cellHTML(r,c)}</span>`).join('')+`</div></div>`).join('')+'</div>';
   } else {
-    $('#tableView').classList.add('hide'); $('#gridView').classList.remove('hide');
+    show('#gridView');
     const bodyCols=cols.filter(c=>!['address','city','zip','type'].includes(c.k));
     $('#gridView').innerHTML=rows.map(r=>`<div class="gc prow" data-addr="${esc(r.address||'')}" data-city="${esc(r.city||'')}" style="cursor:pointer">
       <div style="display:flex;justify-content:space-between;gap:8px"><div class="addr">${esc(r.address||'—')}</div>${star(r)}</div><div class="sub">${esc(r.city||'')} ${esc(r.zip||'')} · ${esc(r.type||'')}</div>${r.first_seen?`<div class="when" title="${esc(String(r.first_seen))}" style="font-size:10px;color:var(--mut,#8b949e);margin-top:4px">🕓 First seen ${esc(new Date(r.first_seen).toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'}))}</div>`:''}`
@@ -546,7 +559,7 @@ $('#view').addEventListener('click',e=>{ const b=e.target.closest('button'); if(
   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)); }
+  const pv=localStorage.getItem('mlsView'); if(pv==='grid'||pv==='table'||pv==='list'){ 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; };
diff --git a/public/nav-agent/nav-agent.css b/public/nav-agent/nav-agent.css
index 702094c..844d787 100644
--- a/public/nav-agent/nav-agent.css
+++ b/public/nav-agent/nav-agent.css
@@ -90,4 +90,11 @@ table .na-group-h td{padding:8px 6px;color:var(--na-acc);font-weight:600;border-
   border-radius:6px;padding:6px 10px;font-size:12px;cursor:pointer}
 .na-restorebar button:hover{border-color:var(--na-acc)}
 
+/* ---- merged mode: folded INTO an existing house panel ----------------- */
+.na-merge{border-top:1px dashed var(--na-line);margin-top:12px;padding-top:10px}
+.na-merge .na-mh{display:flex;align-items:center;gap:6px;font-weight:600;color:var(--na-acc);
+  text-transform:uppercase;font-size:11px;letter-spacing:.6px;margin-bottom:8px}
+.na-merge .na-row label{color:var(--na-mut);min-width:48px}
+.na-merge select{background:var(--na-bg2);border:1px solid var(--na-line);color:var(--na-fg);border-radius:6px;padding:6px 8px;font-size:13px;flex:1}
+
 @media (max-width:640px){.na-panel{width:86vw}}
diff --git a/public/nav-agent/nav-agent.js b/public/nav-agent/nav-agent.js
index 89119e2..991a82f 100644
--- a/public/nav-agent/nav-agent.js
+++ b/public/nav-agent/nav-agent.js
@@ -364,6 +364,47 @@
     if (g.removed) { wrap.classList.add('na-removed'); addRestore(); }
   }
 
+  /* -- merged mode: fold into an existing house panel ("2 into 1") ------- */
+  function findHousePanel(target) {
+    var asides = [].slice.call(document.querySelectorAll('aside, .sidebar, .panel-left, [data-house-panel]'));
+    for (var i = 0; i < asides.length; i++) {
+      var a = asides[i];
+      if (a.classList.contains('na-panel')) continue;
+      if (a.contains(target)) continue;            // a real control sidebar won't contain the grid
+      if (a.querySelector('input, select, details, .toggle, .chip')) return a; // has controls
+    }
+    return null;
+  }
+
+  function buildMergeSection(agent, house, opt) {
+    var M = agent.model, st = agent.state;
+    var old = house.querySelector('.na-merge'); if (old) old.remove();
+    var box = el('div', 'na-merge');
+    box.appendChild(el('div', 'na-mh', '⚙ Nav Agent'));
+    if (M.fields && M.fields.length) {
+      // sort ANY field (superset of the house's fixed sort options)
+      var srow = el('div', 'na-row', '<label>Sort</label>');
+      var ssel = el('select'); ssel.appendChild(new Option('— none —', ''));
+      M.fields.forEach(function (f) { ssel.appendChild(new Option(f.label, f.key)); });
+      ssel.value = st.sort || ''; srow.appendChild(ssel); box.appendChild(srow);
+      var dir = el('div', 'na-mini', '<button data-asc>▲ Asc</button><button data-desc>▼ Desc</button>');
+      function markDir() { dir.querySelector('[data-asc]').classList.toggle('on', st.dir !== 'desc'); dir.querySelector('[data-desc]').classList.toggle('on', st.dir === 'desc'); }
+      ssel.onchange = function () { st.sort = ssel.value; agent.save(); agent.applySort(); };
+      dir.querySelector('[data-asc]').onclick = function () { st.dir = 'asc'; agent.save(); agent.applySort(); markDir(); };
+      dir.querySelector('[data-desc]').onclick = function () { st.dir = 'desc'; agent.save(); agent.applySort(); markDir(); };
+      markDir(); box.appendChild(dir);
+      // group ANY field
+      var grow = el('div', 'na-row', '<label>Group</label>');
+      var gsel = el('select'); gsel.appendChild(new Option('— none —', ''));
+      M.fields.forEach(function (f) { gsel.appendChild(new Option(f.label, f.key)); });
+      gsel.value = st.group || ''; gsel.onchange = function () { st.group = gsel.value; agent.save(); agent.applyGroup(); };
+      grow.appendChild(gsel); box.appendChild(grow);
+    }
+    box.appendChild(el('div', 'na-note', 'Drag the grid’s ▤ bar to float · ▁ collapse · ⤢ maximize · ✕ remove.'));
+    house.appendChild(box);
+    return box;
+  }
+
   /* -- public mount ------------------------------------------------------ */
   var API = {
     instances: [],
@@ -378,20 +419,24 @@
         else target.setAttribute('data-na-sel', target.id ? '#' + target.id : '.' + (target.className.split(/\s+/)[0] || 'grid'));
         var agent = Agent(target, opt);
         target.__naAgent = agent;
-        // (re)build the panel — called again by the observer when fields change
+        // "2 into 1": if the page already has a house control panel, fold into it
+        var house = opt.merge === false ? null : findHousePanel(target);
+        // (re)build the controls — called again by the observer when fields change
         function makePanel() {
+          if (house) { agent._merge = buildMergeSection(agent, house, opt); return; }
           var wasOpen = agent._panel && agent._panel.classList.contains('open');
           if (agent._panel) agent._panel.remove();
           agent._panel = buildPanel(agent, opt);
           if (wasOpen) agent._panel.classList.add('open');
         }
         agent.onFields = makePanel;
+        agent.merged = !!house;
         makePanel();
         chrome(agent, opt);
         agent.applyAll();
 
-        // single launcher toggles every panel on the page
-        if (!document.querySelector('.na-launch')) {
+        // standalone launcher only when NOT merged into a house panel
+        if (!house && !document.querySelector('.na-launch')) {
           var b = el('button', 'na-launch', '⚙'); b.title = 'View controls';
           b.onclick = function () {
             var anyOpen = document.querySelector('.na-panel.open');

← ca654ac broker-grid.html: add compact-list view (3rd mode) — grid/li  ·  back to Commercialrealestate  ·  nav-agent: v1.1 merge-mode asset refresh 242d569 →