← 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
M package-lock.jsonM package.jsonM public/crcp-grid.cssM public/crcp-grid.js
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 →