← 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">✓</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 →