[object Object]

← back to Nationalrealestate

USRE grid: active-filter summary bar + one-click clear-all; rail now renders from filter state (v0.5.0)

55b0bc697cca13ca053767d0eda63dad91983195 · 2026-07-26 16:37:50 -0700 · Steve

Files touched

Diff

commit 55b0bc697cca13ca053767d0eda63dad91983195
Author: Steve <steve@designerwallcoverings.com>
Date:   Sun Jul 26 16:37:50 2026 -0700

    USRE grid: active-filter summary bar + one-click clear-all; rail now renders from filter state (v0.5.0)
---
 package-lock.json    |  4 ++--
 package.json         |  2 +-
 public/crcp-grid.css | 13 ++++++++++++
 public/crcp-grid.js  | 60 ++++++++++++++++++++++++++++++++++++++++++++++------
 4 files changed, 70 insertions(+), 9 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 26d32e4..1426d1c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "nationalrealestate",
-  "version": "0.4.0",
+  "version": "0.5.0",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "nationalrealestate",
-      "version": "0.4.0",
+      "version": "0.5.0",
       "dependencies": {
         "better-sqlite3": "^13.0.1",
         "dotenv": "^16.4.5",
diff --git a/package.json b/package.json
index 36241d1..c73e00a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "nationalrealestate",
-  "version": "0.4.0",
+  "version": "0.5.0",
   "private": true,
   "type": "module",
   "description": "USRealEstate — national U.S. residential market explorer. Free-data v1 (Redfin Data Center, Zillow Research, Census ACS, FHFA). Internal analyst tool behind basic auth.",
diff --git a/public/crcp-grid.css b/public/crcp-grid.css
index b8078be..b6fe097 100644
--- a/public/crcp-grid.css
+++ b/public/crcp-grid.css
@@ -25,6 +25,19 @@
 .cg-mini button{background:var(--cg-card,#161b22);color:var(--cg-mut,#8b949e);border:1px solid var(--cg-line,#2a313c);border-radius:7px;padding:4px 9px;font-size:11px;cursor:pointer;}
 .cg-mini button:hover{border-color:var(--cg-acc,#58a6ff);color:var(--cg-ink,#e6edf3);}
 .cg-top{display:flex;align-items:center;gap:12px;flex-wrap:wrap;padding:13px 18px;border-bottom:1px solid var(--cg-line,#2a313c);position:sticky;top:0;background:var(--cg-topbg,rgba(14,17,22,.94));backdrop-filter:blur(6px);z-index:6;}
+/* active-filter summary bar (removable chips + clear all) */
+.cg-active{display:flex;flex-wrap:wrap;align-items:center;gap:8px;padding:12px 18px 0;}
+.cg-active:empty{display:none;}
+.cg-af-lbl{font-size:11px;text-transform:uppercase;letter-spacing:.5px;color:var(--cg-mut,#8b949e);}
+.cg-af{display:inline-flex;align-items:center;gap:6px;font-size:12px;cursor:pointer;
+  background:var(--cg-accbg,rgba(200,162,75,.12));color:var(--cg-ink,#e6edf3);
+  border:1px solid var(--cg-acc,#58a6ff);border-radius:14px;padding:3px 10px;white-space:nowrap;}
+.cg-af b{color:var(--cg-acc,#58a6ff);font-weight:700;}
+.cg-af:hover{background:var(--cg-acc,#58a6ff);color:var(--cg-railbg,#0b0e13);}
+.cg-af:hover b{color:var(--cg-railbg,#0b0e13);}
+.cg-af-clear{font-size:12px;background:none;border:0;cursor:pointer;padding:3px 6px;
+  color:var(--cg-mut,#8b949e);text-decoration:underline;}
+.cg-af-clear:hover{color:var(--cg-acc,#58a6ff);}
 .cg-q{min-width:240px;background:var(--cg-card,#161b22);color:var(--cg-ink,#e6edf3);border:1px solid var(--cg-line,#2a313c);border-radius:8px;padding:8px 11px;font-size:13px;}
 .cg-seg{display:inline-flex;border:1px solid var(--cg-line,#2a313c);border-radius:8px;overflow:hidden;}
 .cg-seg button{background:var(--cg-card,#161b22);color:var(--cg-mut,#8b949e);border:0;border-right:1px solid var(--cg-line,#2a313c);padding:7px 12px;font-size:12px;cursor:pointer;}
diff --git a/public/crcp-grid.js b/public/crcp-grid.js
index a303a90..2bd9488 100644
--- a/public/crcp-grid.js
+++ b/public/crcp-grid.js
@@ -110,12 +110,20 @@
           data.forEach(r => { const v = gv(r, fc); if (v != null && v !== '') counts[v] = (counts[v] || 0) + 1; });
           const ent = Object.entries(counts).sort((a, b) => b[1] - a[1]).slice(0, fc.max || 20);
           if (!ent.length) return;
-          html += `<div class="cg-sec collapsed"><h4>${esc(fc.l)}</h4><div class="cg-chips" data-facet="${esc(fc.k)}">` +
-            ent.map(([k, c]) => `<span class="cg-chip" data-v="${esc(k)}">${esc(k)}<span class="cg-ct">${c}</span></span>`).join('') + `</div></div>`;
+          // render FROM F: mark active chips + auto-expand a section that has a live filter
+          const set = F.chips[fc.k];
+          const secCls = (set && set.size) ? 'cg-sec' : 'cg-sec collapsed';
+          html += `<div class="${secCls}"><h4>${esc(fc.l)}</h4><div class="cg-chips" data-facet="${esc(fc.k)}">` +
+            ent.map(([k, c]) => `<span class="cg-chip${set && set.has(String(k)) ? ' active' : ''}" data-v="${esc(k)}">${esc(k)}<span class="cg-ct">${c}</span></span>`).join('') + `</div></div>`;
         } else {
-          html += `<div class="cg-sec collapsed"><h4>${esc(fc.l)}</h4><div class="cg-row2" data-facet="${esc(fc.k)}">` +
-            `<div class="cg-fld"><label>Min</label><input type="number" class="cg-min" placeholder="any"></div>` +
-            `<div class="cg-fld"><label>Max</label><input type="number" class="cg-max" placeholder="any"></div></div></div>`;
+          const rg = F.ranges[fc.k] || {};
+          const active = rg.min != null || rg.max != null;
+          const secCls = active ? 'cg-sec' : 'cg-sec collapsed';
+          const mn = rg.min != null ? ` value="${esc(rg.min)}"` : '';
+          const mx = rg.max != null ? ` value="${esc(rg.max)}"` : '';
+          html += `<div class="${secCls}"><h4>${esc(fc.l)}</h4><div class="cg-row2" data-facet="${esc(fc.k)}">` +
+            `<div class="cg-fld"><label>Min</label><input type="number" class="cg-min" placeholder="any"${mn}></div>` +
+            `<div class="cg-fld"><label>Max</label><input type="number" class="cg-max" placeholder="any"${mx}></div></div></div>`;
         }
       });
       html += `<div class="cg-sec"><h4>Fields — show / hide</h4><div class="cg-ftogs">` +
@@ -178,6 +186,42 @@
       return `<div class="cg-listwrap"><table class="cg-tbl"><thead><tr>${head}</tr></thead><tbody>${body}</tbody></table></div>`;
     }
 
+    // ---- active-filter summary bar (removable chips + clear all) ----
+    function activeFilterHTML() {
+      const parts = [];
+      facets.forEach(fc => {
+        if (fc.type === 'chip') {
+          const set = F.chips[fc.k];
+          if (set) set.forEach(v => parts.push(
+            `<span class="cg-af" data-fk="${esc(fc.k)}" data-aft="chip" data-v="${esc(v)}">${esc(fc.l)}: ${esc(v)} <b>✕</b></span>`));
+        } else {
+          const r = F.ranges[fc.k];
+          if (r && (r.min != null || r.max != null)) {
+            const txt = (r.min != null && r.max != null) ? esc(r.min) + '–' + esc(r.max)
+              : (r.min != null) ? '≥ ' + esc(r.min) : '≤ ' + esc(r.max);
+            parts.push(`<span class="cg-af" data-fk="${esc(fc.k)}" data-aft="range">${esc(fc.l)}: ${txt} <b>✕</b></span>`);
+          }
+        }
+      });
+      if (F.q) parts.push(`<span class="cg-af" data-aft="q">“${esc(F.q)}” <b>✕</b></span>`);
+      if (!parts.length) return '';
+      return `<span class="cg-af-lbl">Filters</span>` + parts.join('') +
+        `<button class="cg-af-clear" data-clearall="1">Clear all</button>`;
+    }
+    function clearFilter(el) {
+      if (el.dataset.clearall != null) {
+        facets.forEach(fc => { if (fc.type === 'chip') F.chips[fc.k].clear(); else F.ranges[fc.k] = { min: null, max: null }; });
+        F.q = ''; const q = $(O.mount + ' .cg-q'); if (q) q.value = '';
+      } else if (el.dataset.aft === 'q') {
+        F.q = ''; const q = $(O.mount + ' .cg-q'); if (q) q.value = '';
+      } else if (el.dataset.aft === 'chip') {
+        F.chips[el.dataset.fk].delete(el.dataset.v);
+      } else if (el.dataset.aft === 'range') {
+        F.ranges[el.dataset.fk] = { min: null, max: null };
+      }
+      buildRail(); render(); // buildRail re-renders the rail from F so it stays in sync
+    }
+
     let VIEW = localStorage.getItem(KEY + 'View') || O.view || 'grid';
     const CAP = O.cap || 1500; // DOM render cap for perf; count still reflects the full match set
     let lastShown = [];
@@ -193,6 +237,8 @@
       if (cnt) cnt.innerHTML = `<b>${rows.length.toLocaleString()}</b> of ${data.length.toLocaleString()}` + (rows.length > CAP ? ` <span style="opacity:.7">(first ${CAP.toLocaleString()} shown)</span>` : '');
       const sel = $(O.mount + ' .cg-sort');
       if (sel) sel.value = SORT && SORT.k ? SORT.k + ':' + (SORT.dir < 0 ? 'desc' : 'asc') : '';
+      const ab = $(O.mount + ' .cg-active');
+      if (ab) ab.innerHTML = activeFilterHTML();
     }
 
     // ---- topbar ----
@@ -207,7 +253,7 @@
         `<select class="cg-sort" title="Sort">${sortOpts}</select>` +
         `<span class="cg-seg"><button data-view="grid" class="${VIEW === 'grid' ? 'active' : ''}">▦ Grid</button><button data-view="list" class="${VIEW === 'list' ? 'active' : ''}">☰ List</button></span>` +
         `<label class="cg-dens">Density <input type="range" class="cg-cols" min="1" max="12" step="1" value="${O.cols || 3}"></label>` +
-        `<span class="cg-count"></span></div><div class="cg-grid"></div>`);
+        `<span class="cg-count"></span></div><div class="cg-active"></div><div class="cg-grid"></div>`);
       const q = m.querySelector('.cg-q');
       q.addEventListener('input', () => { clearTimeout(q._t); q._t = setTimeout(() => { F.q = q.value.trim().toLowerCase(); render(); }, 160); });
       m.querySelector('.cg-sort').addEventListener('change', e => {
@@ -224,6 +270,8 @@
       setCols(cols.value);
       // th-click sorting + whole-row click (delegated; grid re-renders under us)
       m.addEventListener('click', e => {
+        const af = e.target.closest('.cg-af, .cg-af-clear');
+        if (af) { clearFilter(af); return; }
         const th = e.target.closest('.cg-sortth');
         if (th && th.dataset.k) {
           const k = th.dataset.k;

← 5f9ea95 chore: v0.3.0 → v0.4.0 (session close — all-data-point filte  ·  back to Nationalrealestate  ·  Refine: add situs addresses to Sonoma/Maricopa/Thurston deed 9ca6907 →