← back to Trending Dw
viewer standard: add Grid/List toggle + per-field on/off menu (Fields) to product grid
2d2d84118f9bdc6ace0903ebda15b31c0d6efc5d · 2026-08-12 08:33:43 -0700 · Steve
Files touched
Diff
commit 2d2d84118f9bdc6ace0903ebda15b31c0d6efc5d
Author: Steve <steve@designerwallcoverings.com>
Date: Wed Aug 12 08:33:43 2026 -0700
viewer standard: add Grid/List toggle + per-field on/off menu (Fields) to product grid
---
public/index.html | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++-----
1 file changed, 67 insertions(+), 6 deletions(-)
diff --git a/public/index.html b/public/index.html
index 6020ddb..4e9636d 100644
--- a/public/index.html
+++ b/public/index.html
@@ -112,6 +112,18 @@
.bars{ display:inline-block; height:8px; border-radius:4px; background:var(--accent); vertical-align:middle; }
#sentinel{ height:40px; } .loading{ text-align:center; color:var(--muted); font-size:12px; padding:20px; position:fixed;left:50%;bottom:20px;transform:translateX(-50%);width:auto;margin:0;padding:10px 20px;background:rgba(255,255,255,.92);border:1px solid var(--line,#e5e2dc);border-radius:999px;box-shadow:0 2px 12px rgba(0,0,0,.1);z-index:60;pointer-events:none }
@media(max-width:820px){ .layout{ grid-template-columns:1fr; } aside{ position:static; height:auto; border-right:none; border-bottom:1px solid var(--line); } }
+ /* ── Fields ▾ field-hide rules (html.hf-<field>) ── */
+ html.hf-image .card .swatch { display:none; }
+ html.hf-title [data-f="title"] { display:none; }
+ html.hf-company [data-f="company"] { display:none; }
+ html.hf-chips [data-f="chips"] { display:none; }
+ html.hf-signal [data-f="signal"] { display:none; }
+ html.hf-spotted [data-f="spotted"] { display:none; }
+ html.hf-tier [data-f="tier"] { display:none; }
+ /* ── Fields popover labels ── */
+ #tdwFpop label { display:flex; align-items:center; gap:10px; padding:7px 14px; font-size:11px; letter-spacing:.1em; text-transform:uppercase; color:var(--ink); cursor:pointer; white-space:nowrap; font-family:inherit; }
+ #tdwFpop label:hover { background:var(--chip); border-radius:4px; }
+ #tdwFpop input[type=checkbox] { accent-color:var(--accent); width:13px; height:13px; cursor:pointer; }
</style>
</head>
<body>
@@ -140,9 +152,17 @@
<option value="titleAZ">Title A–Z</option>
<option value="company">Company</option>
<option value="marketplace">Marketplace</option>
+ <option value="color">Color</option>
+ <option value="style">Style</option>
+ <option value="priceBand">Price Band</option>
</select>
</div>
<div class="dens"><label>Density</label><input type="range" id="dens" min="2" max="6" step="1" value="4"/></div>
+ <!-- Fields ▾ menu -->
+ <div class="tdw-fieldmenu" style="position:relative;">
+ <button id="tdwFieldsBtn" style="padding:6px 12px;border:1px solid var(--line);background:#fff;border-radius:7px;font-size:12px;color:var(--muted);cursor:pointer;font-family:inherit;" aria-haspopup="true" aria-expanded="false">Fields ▾</button>
+ <div id="tdwFpop" hidden style="position:absolute;top:calc(100% + 8px);right:0;background:#fff;border:1px solid var(--line);box-shadow:0 10px 28px rgba(0,0,0,.13);padding:6px 4px;z-index:40;min-width:190px;border-radius:8px;" role="menu"></div>
+ </div>
</div>
<div class="chipsbar" id="chipsbar"></div>
<div id="errbar"></div>
@@ -293,12 +313,12 @@ function card(it){
+'<div class="cov '+it.ourCoverage+'">'+(it.ourCoverage==='gap'?'🎯 GAP':'✓ HAVE')+'</div>'
+(it.signalRank?'<div class="rank"'+(prov?' style="bottom:34px"':'')+'>▲ '+it.signalRank+'</div>':'')
+prov+oursInset+oursPill+'</div>'
- +'<div class="body"><div class="title">'+esc(it.title)+'</div>'
- +'<div class="company">by <b>'+dspan('company',it.company,it.company)+'</b></div>'
- +'<div class="chips"><span class="chip mk">'+dspan('marketplace',it.marketplace,it.marketplace)+'</span><span class="chip">'+dspan('style',it.style,it.style)+'</span><span class="chip">'+dspan('color',it.color,it.color)+'</span><span class="chip">'+dspan('priceBand',it.priceBand,it.priceBand)+'</span></div>'
- +(it.spottedAt?'<div class="spotted" title="Spotted '+esc(it.spottedAt)+(it.realImageFetchedAt?' · real image fetched '+esc(it.realImageFetchedAt):'')+'">🕓 spotted '+esc(fmtSpotted(it.spottedAt))+(it.realImageFetchedAt?' · 📷 '+esc(fmtWhen(it.realImageFetchedAt)):'')+'</div>':'')
- +'<div class="signal"><span class="dot">●</span> '+esc(it.signal)+'</div>'
- +'<div class="tier">DW tier → '+esc(it.dwTier)+(it.url?' · <a class="src" href="'+esc(it.url)+'" target="_blank" rel="noopener noreferrer">source ↗</a>':'')+'</div>'
+ +'<div class="body"><div class="title" data-f="title">'+esc(it.title)+'</div>'
+ +'<div class="company" data-f="company">by <b>'+dspan('company',it.company,it.company)+'</b></div>'
+ +'<div class="chips" data-f="chips"><span class="chip mk">'+dspan('marketplace',it.marketplace,it.marketplace)+'</span><span class="chip">'+dspan('style',it.style,it.style)+'</span><span class="chip">'+dspan('color',it.color,it.color)+'</span><span class="chip">'+dspan('priceBand',it.priceBand,it.priceBand)+'</span></div>'
+ +(it.spottedAt?'<div class="spotted" data-f="spotted" title="Spotted '+esc(it.spottedAt)+(it.realImageFetchedAt?' · real image fetched '+esc(it.realImageFetchedAt):'')+'">🕓 spotted '+esc(fmtSpotted(it.spottedAt))+(it.realImageFetchedAt?' · 📷 '+esc(fmtWhen(it.realImageFetchedAt)):'')+'</div>':'')
+ +'<div class="signal" data-f="signal"><span class="dot">●</span> '+esc(it.signal)+'</div>'
+ +'<div class="tier" data-f="tier">DW tier → '+esc(it.dwTier)+(it.url?' · <a class="src" href="'+esc(it.url)+'" target="_blank" rel="noopener noreferrer">source ↗</a>':'')+'</div>'
+(it.isOurOriginal?'<button class="shopbtn" data-id="'+esc(it.id)+'">+ Put on Shopify</button>':'')
+'</div>';
c.style.cursor="pointer"; c.title="Open detail + match against our catalog"; c.addEventListener("click",function(e){
@@ -403,6 +423,47 @@ tdwReadURL();
addEventListener('popstate', ()=>{ tdwReadURL(); const se=document.getElementById('search'); if(se) se.value=state.q; sortSel.value=state.sort; loadFacets(); chips(); setView(state.view); });
loadFacets(); chips(); setView(state.view); refreshQueueCount(); loadMeta();
+// ── Fields ▾ menu ─────────────────────────────────────────────────────────
+const TDW_FIELDS = [
+ ['image', 'Image', true],
+ ['title', 'Title', true],
+ ['company', 'Company', true],
+ ['chips', 'Tags', true],
+ ['signal', 'Signal', true],
+ ['spotted', 'Spotted', true],
+ ['tier', 'DW Tier', false],
+];
+function tdwLoadFields(){
+ const def = {}; TDW_FIELDS.forEach(([k,,d]) => def[k] = d);
+ try { return Object.assign(def, JSON.parse(localStorage.getItem('tdw_fields') || '{}')); } catch(e){ return def; }
+}
+const tdwFieldState = tdwLoadFields();
+function tdwApplyFields(){
+ TDW_FIELDS.forEach(([k]) => document.documentElement.classList.toggle('hf-' + k, !tdwFieldState[k]));
+}
+function tdwBuildFieldMenu(){
+ const pop = document.getElementById('tdwFpop'); if(!pop) return;
+ pop.innerHTML = TDW_FIELDS.map(([k, label]) =>
+ `<label><input type="checkbox" data-field="${k}"${tdwFieldState[k] ? ' checked' : ''}> ${label}</label>`).join('');
+ pop.querySelectorAll('input').forEach(cb => cb.addEventListener('change', () => {
+ tdwFieldState[cb.dataset.field] = cb.checked;
+ localStorage.setItem('tdw_fields', JSON.stringify(tdwFieldState));
+ tdwApplyFields();
+ }));
+}
+const tdwFBtn = document.getElementById('tdwFieldsBtn'), tdwFpop = document.getElementById('tdwFpop');
+if(tdwFBtn && tdwFpop){
+ tdwFBtn.addEventListener('click', e => {
+ e.stopPropagation(); const open = tdwFpop.hidden; tdwFpop.hidden = !open;
+ tdwFBtn.setAttribute('aria-expanded', String(open));
+ });
+ document.addEventListener('click', e => {
+ if(!tdwFpop.hidden && !e.target.closest('.tdw-fieldmenu')){ tdwFpop.hidden = true; tdwFBtn.setAttribute('aria-expanded','false'); }
+ });
+}
+tdwBuildFieldMenu();
+tdwApplyFields();
+
/* ---- Put on Shopify: stage our-own design as a DW product (sequential DWPV SKU) ---- */
function putOnShopify(btn){
var id=btn.getAttribute("data-id"); btn.disabled=true; var old=btn.textContent; btn.textContent="Staging…";
← aa3b65e fix: take infinite-scroll .loading indicator out of flow (ki
·
back to Trending Dw
·
creds-safe fetch guard: resolve relative fetch vs credential 4bcd26f →