[object Object]

← 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 →