[object Object]

← back to Sample Box

viewer standard: add Grid/List toggle + per-field on/off menu (Fields) to product grid

f3ccf2ef053b25c5e248ed739a3f233cb2c19d19 · 2026-08-12 08:33:43 -0700 · Steve

Files touched

Diff

commit f3ccf2ef053b25c5e248ed739a3f233cb2c19d19
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 | 96 +++++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 93 insertions(+), 3 deletions(-)

diff --git a/public/index.html b/public/index.html
index cdf2400..b29e6a9 100644
--- a/public/index.html
+++ b/public/index.html
@@ -101,6 +101,35 @@ select,input[type=range]{font:inherit}
 /* search bar */
 .search-bar{padding:10px 14px;border:1px solid var(--border);border-radius:6px;font:inherit;font-size:14px;width:220px;background:#fff}
 
+/* ── Grid / List view toggle ─────────────────────────────────────────── */
+.view-seg{display:flex;border:1px solid var(--border);border-radius:6px;overflow:hidden}
+.view-seg button{padding:7px 13px;background:#fff;border:none;font:inherit;font-size:12px;letter-spacing:.08em;text-transform:uppercase;cursor:pointer;color:var(--muted);transition:.15s}
+.view-seg button.on{background:var(--accent);color:#fff}
+
+/* list mode — swatch-card becomes a horizontal row */
+#swatch-grid.list-view{display:flex;flex-direction:column;gap:8px}
+#swatch-grid.list-view .swatch-card{display:grid;grid-template-columns:72px 1fr auto;align-items:center;gap:12px;border-radius:var(--radius);padding:8px 12px}
+#swatch-grid.list-view .swatch-card img{width:72px;height:72px;border-radius:6px}
+#swatch-grid.list-view .check-badge{position:static;width:22px;height:22px;font-size:12px;flex:0 0 auto}
+
+/* ── Fields on/off popover ─────────────────────────────────────────── */
+.sb-fieldmenu{position:relative}
+.sb-fbtn{padding:8px 12px;border:1px solid var(--border);border-radius:6px;background:#fff;font:inherit;font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);cursor:pointer;transition:.15s;white-space:nowrap}
+.sb-fbtn:hover{border-color:var(--accent);color:var(--accent)}
+.sb-fpop{position:absolute;top:calc(100% + 8px);right:0;background:#fff;border:1px solid var(--border);border-radius:var(--radius);box-shadow:0 14px 34px rgba(0,0,0,.14);padding:8px 4px;z-index:60;min-width:176px}
+.sb-fpop[hidden]{display:none}
+.sb-fpop label{display:flex;align-items:center;gap:10px;padding:7px 14px;font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:#4b463d;cursor:pointer;white-space:nowrap}
+.sb-fpop label:hover{background:#faf9f6}
+.sb-fpop input{accent-color:var(--gold)}
+
+/* per-field hide rules keyed on body.sbhf-<field> */
+body.sbhf-image  .swatch-card img,
+body.sbhf-image  #swatch-grid.list-view .swatch-card img{display:none}
+body.sbhf-title  [data-f="title"]  {display:none}
+body.sbhf-vendor [data-f="vendor"] {display:none}
+body.sbhf-price  [data-f="price"]  {display:none}
+body.sbhf-sku    [data-f="sku"]    {display:none}
+
 @media(max-width:640px){
   .swatch-grid{--cols:2!important}
   .vibe-grid{grid-template-columns:repeat(2,1fr)}
@@ -159,6 +188,14 @@ select,input[type=range]{font:inherit}
       </select>
     </label>
     <label class="ctrl">Density <input id="density" type="range" min="2" max="6" value="4"></label>
+    <div class="view-seg" role="group" aria-label="View">
+      <button id="sb-vGrid" class="on" type="button" title="Grid view">Grid</button>
+      <button id="sb-vList" type="button" title="List view">List</button>
+    </div>
+    <div class="sb-fieldmenu">
+      <button class="sb-fbtn" id="sb-fieldsBtn" type="button" aria-haspopup="true" aria-expanded="false">Fields ▾</button>
+      <div class="sb-fpop" id="sb-fpop" hidden role="menu"></div>
+    </div>
     <span class="sel-count" id="sel-count">0 selected</span>
   </div>
   <div class="swatch-grid" id="swatch-grid"><div class="empty">Loading…</div></div>
@@ -386,9 +423,10 @@ async function loadSwatches(){
       <img src="${esc(p.image||'')}" alt="${esc(p.title||'')}" loading="lazy"
            onerror="this.style.background='#f5f0ea'">
       <div class="swatch-meta">
-        <div class="swatch-title">${esc(p.title||'Untitled')}</div>
-        <div class="swatch-vendor">${esc(p.vendor||'')}</div>
-        ${priceDisplay?`<div class="swatch-price">${esc(priceDisplay)}</div>`:''}
+        <div class="swatch-title" data-f="title">${esc(p.title||'Untitled')}</div>
+        <div class="swatch-vendor" data-f="vendor">${esc(p.vendor||'')}</div>
+        ${priceDisplay?`<div class="swatch-price" data-f="price">${esc(priceDisplay)}</div>`:''}
+        ${p.sku?`<div class="swatch-vendor" data-f="sku" style="font-size:10px;opacity:.65">${esc(p.sku)}</div>`:''}
         ${sampleOnlyBadge}
       </div>
       <div class="check-badge">&#x2713;</div>
@@ -542,6 +580,58 @@ Promise.all([loadVibes(), loadMeta()]).then(()=>{
   // Meta ready; update CTA if already on checkout panel
   updateShopifyCTA();
 });
+
+// ── Grid / List view toggle ────────────────────────────────────────────────────
+const SB_FIELDS = [
+  ['image',  'Image',  true],
+  ['title',  'Title',  true],
+  ['vendor', 'Vendor', true],
+  ['price',  'Price',  true],
+  ['sku',    'SKU',    false],
+];
+function sbLoadFields(){
+  const def={}; SB_FIELDS.forEach(([k,,d])=>def[k]=d);
+  try{return Object.assign(def,JSON.parse(localStorage.getItem('sb_fields')||'{}'));}
+  catch(e){return def;}
+}
+const sbState={view:localStorage.getItem('sb_view')||'grid', fields:sbLoadFields()};
+
+function sbSetView(v){
+  sbState.view=v;
+  localStorage.setItem('sb_view',v);
+  const sg=$('#swatch-grid');
+  if(sg)sg.classList.toggle('list-view',v==='list');
+  const g=$('#sb-vGrid'),l=$('#sb-vList');
+  if(g)g.classList.toggle('on',v==='grid');
+  if(l)l.classList.toggle('on',v==='list');
+}
+function sbApplyFields(){
+  SB_FIELDS.forEach(([k])=>document.body.classList.toggle('sbhf-'+k,!sbState.fields[k]));
+}
+function sbBuildFieldMenu(){
+  const pop=$('#sb-fpop'); if(!pop)return;
+  pop.innerHTML=SB_FIELDS.map(([k,label])=>
+    `<label><input type="checkbox" data-field="${k}"${sbState.fields[k]?' checked':''}> ${label}</label>`).join('');
+  pop.querySelectorAll('input').forEach(cb=>cb.addEventListener('change',()=>{
+    sbState.fields[cb.dataset.field]=cb.checked;
+    localStorage.setItem('sb_fields',JSON.stringify(sbState.fields));
+    sbApplyFields();
+  }));
+}
+
+// init on DOMContentLoaded (script is at end of body so DOM is ready)
+(function sbInit(){
+  sbSetView(sbState.view);
+  sbApplyFields();
+  sbBuildFieldMenu();
+  const vg=$('#sb-vGrid'),vl=$('#sb-vList'),fb=$('#sb-fieldsBtn'),fp=$('#sb-fpop');
+  if(vg)vg.onclick=()=>sbSetView('grid');
+  if(vl)vl.onclick=()=>sbSetView('list');
+  if(fb&&fp){
+    fb.addEventListener('click',e=>{e.stopPropagation();const open=fp.hidden;fp.hidden=!open;fb.setAttribute('aria-expanded',String(open));});
+    document.addEventListener('click',e=>{if(!fp.hidden&&!e.target.closest('.sb-fieldmenu')){fp.hidden=true;fb.setAttribute('aria-expanded','false');}});
+  }
+})();
 </script>
 </body>
 </html>

← cecac17 shopify: refresh catalog — keep sample-only as 'by request'  ·  back to Sample Box  ·  creds-safe fetch guard: resolve relative fetch vs credential f6685d3 →