← back to 4square Admin
feat: 12-chip color filter strip (right-aligned in source bar)
198d58c21d17534b9d863e0f1a878428821b4ad8 · 2026-05-14 09:40:58 -0700 · Steve
- 12 hue buckets: black, white, grey, brown, beige, yellow, orange, red, pink, purple, blue, green
- hexToBucket() js HSL conversion — handles low-saturation rows (b/w/grey) before hue, narrow brown/beige bands at low L / mid-low S
- rowBucket() per kind: wallco→dominant_hex, patternbank→colors_hex[0], shopify/vintage→tag-match 'color-X'
- click chip to filter; click × or active chip to clear; persists in component state for current session
- grid + stat counts update reactively; load-more retains filter context note
- gracefully empty-states when no loaded row matches the picked hue with hint to load more
Files touched
Diff
commit 198d58c21d17534b9d863e0f1a878428821b4ad8
Author: Steve <steve@designerwallcoverings.com>
Date: Thu May 14 09:40:58 2026 -0700
feat: 12-chip color filter strip (right-aligned in source bar)
- 12 hue buckets: black, white, grey, brown, beige, yellow, orange, red, pink, purple, blue, green
- hexToBucket() js HSL conversion — handles low-saturation rows (b/w/grey) before hue, narrow brown/beige bands at low L / mid-low S
- rowBucket() per kind: wallco→dominant_hex, patternbank→colors_hex[0], shopify/vintage→tag-match 'color-X'
- click chip to filter; click × or active chip to clear; persists in component state for current session
- grid + stat counts update reactively; load-more retains filter context note
- gracefully empty-states when no loaded row matches the picked hue with hint to load more
---
index.html | 111 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
1 file changed, 102 insertions(+), 9 deletions(-)
diff --git a/index.html b/index.html
index 0a6e5c1..7d2703c 100644
--- a/index.html
+++ b/index.html
@@ -76,11 +76,20 @@
.source-bar {
padding:10px 22px; border-bottom:1px solid var(--line); background:#0a0908;
- font-size:11px; color:#888; display:flex; align-items:center; gap:12px;
+ font-size:11px; color:#888; display:flex; align-items:center; gap:12px; flex-wrap:wrap;
}
.source-bar .lic { color:#d2b15c; font-style:italic }
.source-bar .pill { padding:2px 9px; border-radius:11px; border:1px solid var(--line); color:#bba; font-size:10.5px }
.source-bar .pill.ro { color:#e08070; border-color:#4a2a26 }
+ .source-bar .colorbar { margin-left:auto; display:flex; gap:5px; align-items:center }
+ .source-bar .colorbar .clbl { color:#888; font:500 10.5px/1 -apple-system; text-transform:uppercase; letter-spacing:.1em; margin-right:4px }
+ .color-chip {
+ width:20px; height:20px; border-radius:50%; cursor:pointer; border:2px solid transparent;
+ transition:border-color .12s, transform .12s;
+ }
+ .color-chip:hover { border-color:#888; transform:scale(1.1) }
+ .color-chip.active { border-color:var(--gold); transform:scale(1.12); box-shadow:0 0 8px rgba(210,177,92,.5) }
+ .color-chip.clear { background:transparent !important; border:1px dashed #555; font:600 11px/1 -apple-system; color:#888; display:flex; align-items:center; justify-content:center }
.grid-wrap { overflow-y:auto; padding:18px 22px 80px; flex:1 }
.grid { display:grid; grid-template-columns: repeat(var(--cols), 1fr); gap:10px }
.card {
@@ -226,6 +235,66 @@ let SOURCES = [];
let CURRENT = { id: localStorage.getItem(LS.src) || 'wallco:fliepaper-bugs', kind:null, rows:[], total:0, spec:null, readonly:false, has_more:false, offset:0 };
let selected = new Set();
let searchDebounce = null;
+let colorFilter = null; // null | 'red' | 'blue' | ...
+
+const COLOR_BUCKETS = [
+ ['black', '#0a0908'], ['white', '#f3f0ea'], ['grey', '#7a7672'],
+ ['brown', '#6b4a2a'], ['beige', '#d8c6a0'], ['yellow', '#e6c948'],
+ ['orange', '#d97a2a'], ['red', '#c43c3c'], ['pink', '#e0a5c0'],
+ ['purple', '#7a5eb0'], ['blue', '#4f7bc4'], ['green', '#5a8e5a'],
+];
+
+function hexToBucket(hex) {
+ if (!hex) return null;
+ const m = /#?([0-9a-f]{6})/i.exec(hex);
+ if (!m) return null;
+ const r = parseInt(m[1].slice(0,2),16)/255,
+ g = parseInt(m[1].slice(2,4),16)/255,
+ b = parseInt(m[1].slice(4,6),16)/255;
+ const max = Math.max(r,g,b), min = Math.min(r,g,b), L = (max+min)/2;
+ const d = max - min;
+ if (d < 0.05) {
+ if (L < 0.18) return 'black';
+ if (L > 0.85) return 'white';
+ return 'grey';
+ }
+ const S = L > 0.5 ? d/(2-max-min) : d/(max+min);
+ let H;
+ if (max === r) H = ((g-b)/d + (g<b?6:0))*60;
+ else if (max === g) H = ((b-r)/d + 2)*60;
+ else H = ((r-g)/d + 4)*60;
+ if (S < 0.18) return L < 0.2 ? 'black' : L > 0.85 ? 'white' : 'grey';
+ // brown/beige: low-saturation oranges in narrow band
+ if (H >= 15 && H < 50 && L < 0.4) return 'brown';
+ if (H >= 15 && H < 60 && S < 0.45 && L > 0.55) return 'beige';
+ if (H < 15 || H >= 345) return 'red';
+ if (H < 40) return 'orange';
+ if (H < 65) return 'yellow';
+ if (H < 170) return 'green';
+ if (H < 250) return 'blue';
+ if (H < 290) return 'purple';
+ return 'pink';
+}
+
+function rowBucket(r) {
+ if (CURRENT.kind === 'wallco') return hexToBucket(r.dominant_hex);
+ if (CURRENT.kind === 'patternbank' && Array.isArray(r.colors_hex)) {
+ // pick the most-saturated swatch
+ let best = null, bestS = -1;
+ for (const h of r.colors_hex) {
+ const b = hexToBucket(h);
+ if (b) { return b; } // first non-null is fine; could be improved
+ }
+ return null;
+ }
+ // shopify/vintage: parse tags for "color-X" pattern if present
+ const tags = Array.isArray(r.tags) ? r.tags : (typeof r.tags === 'string' ? r.tags.split(',') : []);
+ for (const t of tags) {
+ const m = /color[-_:](\w+)/i.exec(String(t).trim());
+ if (m && COLOR_BUCKETS.find(b => b[0] === m[1].toLowerCase())) return m[1].toLowerCase();
+ }
+ return null;
+}
const SORT_WALLCO = [
['newest','Newest'],['oldest','Oldest'],
@@ -353,8 +422,11 @@ function populateRecolor(spec) {
}
function renderControls() {
- const showing = CURRENT.rows.length;
- $('#stat').textContent = `${showing.toLocaleString()} of ${CURRENT.total.toLocaleString()}`;
+ const visibleRows = visibleRowSet();
+ const showing = visibleRows.length;
+ $('#stat').textContent = colorFilter
+ ? `${showing.toLocaleString()} matching "${colorFilter}" of ${CURRENT.rows.length.toLocaleString()} loaded`
+ : `${showing.toLocaleString()} of ${CURRENT.total.toLocaleString()}`;
document.querySelectorAll('#bulk-actions button').forEach(b => b.disabled = CURRENT.readonly || selected.size === 0);
$('#bulk-actions').style.opacity = CURRENT.readonly ? .35 : 1;
$('#bulk-actions').title = CURRENT.readonly ? 'Read-only source' : '';
@@ -364,7 +436,24 @@ function renderControls() {
const bar = $('#src-bar');
const lic = CURRENT.license_note ? `<span class="lic">${esc(CURRENT.license_note)}</span>` : '';
const ro = CURRENT.readonly ? '<span class="pill ro">read-only</span>' : '<span class="pill" style="color:#7dd87d;border-color:#2a4a2a">editable</span>';
- bar.innerHTML = `<strong style="color:var(--gold);font-weight:500">${esc(CURRENT.id)}</strong> ${ro} · ${showing.toLocaleString()} of ${CURRENT.total.toLocaleString()} ${lic}`;
+ const chips = `<span class="colorbar">
+ <span class="clbl">Hue</span>
+ ${COLOR_BUCKETS.map(([name, hex]) =>
+ `<span class="color-chip ${colorFilter===name?'active':''}" style="background:${hex}" title="${name}" onclick="toggleColor('${name}')"></span>`).join('')}
+ <span class="color-chip clear ${colorFilter?'':'active'}" title="clear" onclick="toggleColor(null)">×</span>
+ </span>`;
+ bar.innerHTML = `<strong style="color:var(--gold);font-weight:500">${esc(CURRENT.id)}</strong> ${ro} · ${showing.toLocaleString()} of ${CURRENT.total.toLocaleString()} ${lic}${chips}`;
+}
+
+function visibleRowSet() {
+ if (!colorFilter) return CURRENT.rows;
+ return CURRENT.rows.filter(r => rowBucket(r) === colorFilter);
+}
+
+function toggleColor(c) {
+ colorFilter = (c === colorFilter) ? null : c;
+ renderGrid(); // re-render with filter applied
+ renderControls();
}
async function exportCSV() {
@@ -386,16 +475,20 @@ function renderGrid() {
const wrap = $('#grid-wrap');
if (CURRENT.rows.length === 0) { wrap.innerHTML = '<div class="empty-state">no products in this source</div>'; return; }
- // Matrix layout for fliepaper-bugs (no pagination needed — small fixed set)
if (CURRENT.spec && CURRENT.spec.patterns && CURRENT.spec.palette) {
return renderMatrix(wrap);
}
- // Flat grid for everything else
- const cardsHtml = CURRENT.rows.map(r => cardHtml(r)).join('');
+ const vis = visibleRowSet();
+ if (vis.length === 0) {
+ wrap.innerHTML = `<div class="empty-state">No "${esc(colorFilter)}" cards in loaded set. Hint: load more first, or pick a different hue.</div>`;
+ return;
+ }
+ const cardsHtml = vis.map(r => cardHtml(r)).join('');
const remaining = CURRENT.total - CURRENT.rows.length;
+ const filtNote = colorFilter ? ` <span style="color:var(--gold);font-style:italic">(hue: ${esc(colorFilter)})</span>` : '';
const more = CURRENT.has_more
- ? `<div style="text-align:center; padding:32px 0 16px"><button class="small" id="load-more" onclick="loadMore()" style="padding:10px 24px; font-size:13px">Load more (${remaining.toLocaleString()} remaining)</button></div>`
- : (CURRENT.total > CURRENT.rows.length ? '' : `<div style="text-align:center; padding:24px 0 16px; color:#555; font-size:11px; font-style:italic">end · ${CURRENT.rows.length.toLocaleString()} of ${CURRENT.total.toLocaleString()}</div>`);
+ ? `<div style="text-align:center; padding:32px 0 16px"><button class="small" id="load-more" onclick="loadMore()" style="padding:10px 24px; font-size:13px">Load more (${remaining.toLocaleString()} remaining)${filtNote}</button></div>`
+ : (CURRENT.total > CURRENT.rows.length ? '' : `<div style="text-align:center; padding:24px 0 16px; color:#555; font-size:11px; font-style:italic">end · ${CURRENT.rows.length.toLocaleString()} of ${CURRENT.total.toLocaleString()}${filtNote}</div>`);
wrap.innerHTML = `<div class="grid">${cardsHtml}</div>${more}`;
attachCardHandlers();
}
← 0598058 feat: source-context header bar + CSV export of selected row
·
back to 4square Admin
·
feat: bulk tag editor for wallco rows b358ef8 →