[object Object]

← back to Patterndesignlab

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

00a367879f6fc515eccd5acaa7ab97402a2f2406 · 2026-08-12 08:33:44 -0700 · Steve

Files touched

Diff

commit 00a367879f6fc515eccd5acaa7ab97402a2f2406
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Aug 12 08:33:44 2026 -0700

    viewer standard: add Grid/List toggle + per-field on/off menu (Fields) to product grid
---
 public/index.html | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++-----
 public/styles.css | 35 +++++++++++++++++++++++++++
 2 files changed, 100 insertions(+), 6 deletions(-)

diff --git a/public/index.html b/public/index.html
index 395b710..b55fd38 100644
--- a/public/index.html
+++ b/public/index.html
@@ -42,6 +42,14 @@
         <div><label for="density">Density</label>
           <input id="density" type="range" min="2" max="6" step="1" value="4" oninput="setDensity(this.value)">
         </div>
+        <div class="pdl-view-seg" role="group" aria-label="View">
+          <button id="pdlVGrid" class="on" type="button" title="Grid view">Grid</button>
+          <button id="pdlVList" type="button" title="List view">List</button>
+        </div>
+        <div class="pdl-fieldmenu">
+          <button class="pdl-fbtn" id="pdlFieldsBtn" type="button" aria-haspopup="true" aria-expanded="false">Fields ▾</button>
+          <div class="pdl-fpop" id="pdlFpop" hidden role="menu"></div>
+        </div>
       </div>
       <div class="grid" id="grid"></div>
       <div class="empty" id="empty" hidden>No patterns match those filters. <a href="#" onclick="clearAll();return false">Clear filters</a></div>
@@ -75,14 +83,14 @@ function fmtPrice(lo,hi){ if(lo==null) return 'Inquire'; return hi&&hi!=lo ? '$'
 function cardEl(d){
   const el = document.createElement('a'); el.className='card'; el.href='/design/'+d.slug;
   el.innerHTML = `
-    <div class="thumb"><div class="tile" style="background-image:url('${d.img}')"></div>
-      ${d.seamless?'<span class="badge">Seamless</span>':'<span class="badge">Mural</span>'}</div>
+    <div class="thumb" data-f="image"><div class="tile" style="background-image:url('${d.img}')"></div>
+      <span class="badge" data-f="badge">${d.seamless?'Seamless':'Mural'}</span></div>
     <div class="cardbody">
-      <h3>${esc(d.title)}</h3>
-      <div class="meta"><span class="swatch" style="background:${d.dominant_hex||'#ccc'}"></span>
+      <h3 data-f="title">${esc(d.title)}</h3>
+      <div class="meta" data-f="style"><span class="swatch" style="background:${d.dominant_hex||'#ccc'}"></span>
         <span>${esc(d.style||'')}</span><span>·</span><span>${esc(d.colorway||'')}</span></div>
-      <div class="byline">by ${esc(d.designer_name)}</div>
-      <div class="price">${fmtPrice(d.price_min,d.price_max)}</div>
+      <div class="byline" data-f="designer">by ${esc(d.designer_name)}</div>
+      <div class="price" data-f="price">${fmtPrice(d.price_min,d.price_max)}</div>
     </div>`;
   return el;
 }
@@ -166,6 +174,57 @@ function setClass(v){ state.class=v; load(); }
 // honor an inbound ?q= search (e.g. from the detail-page search bar)
 const urlq = new URLSearchParams(location.search).get('q');
 if(urlq){ state.q = urlq; const qi = document.getElementById('q'); if(qi) qi.value = urlq; }
+
+// ── Grid / List view toggle + Fields menu ──────────────────────────────────────
+const PDL_FIELDS = [
+  ['image',    'Image',    true],
+  ['title',    'Title',    true],
+  ['style',    'Style',    true],
+  ['price',    'Price',    true],
+  ['designer', 'Designer', true],
+  ['badge',    'Type Badge', true],
+];
+function pdlLoadFields(){
+  const def={}; PDL_FIELDS.forEach(([k,,d])=>def[k]=d);
+  try{return Object.assign(def,JSON.parse(localStorage.getItem('pdl.fields')||'{}'));}
+  catch(e){return def;}
+}
+const pdlViewState={view:localStorage.getItem('pdl.view')||'grid', fields:pdlLoadFields()};
+
+function pdlSetView(v){
+  pdlViewState.view=v; localStorage.setItem('pdl.view',v);
+  const g=document.getElementById('grid'); if(g) g.classList.toggle('pdl-list',v==='list');
+  const vg=document.getElementById('pdlVGrid'),vl=document.getElementById('pdlVList');
+  if(vg)vg.classList.toggle('on',v==='grid');
+  if(vl)vl.classList.toggle('on',v==='list');
+}
+function pdlApplyFields(){
+  PDL_FIELDS.forEach(([k])=>document.documentElement.classList.toggle('pdlhf-'+k,!pdlViewState.fields[k]));
+}
+function pdlBuildFieldMenu(){
+  const pop=document.getElementById('pdlFpop'); if(!pop)return;
+  pop.innerHTML=PDL_FIELDS.map(([k,label])=>
+    `<label><input type="checkbox" data-field="${k}"${pdlViewState.fields[k]?' checked':''}> ${label}</label>`).join('');
+  pop.querySelectorAll('input').forEach(cb=>cb.addEventListener('change',()=>{
+    pdlViewState.fields[cb.dataset.field]=cb.checked;
+    localStorage.setItem('pdl.fields',JSON.stringify(pdlViewState.fields));
+    pdlApplyFields();
+  }));
+}
+(function pdlInit(){
+  pdlSetView(pdlViewState.view);
+  pdlApplyFields();
+  pdlBuildFieldMenu();
+  const vg=document.getElementById('pdlVGrid'),vl=document.getElementById('pdlVList');
+  const fb=document.getElementById('pdlFieldsBtn'),fp=document.getElementById('pdlFpop');
+  if(vg)vg.addEventListener('click',()=>pdlSetView('grid'));
+  if(vl)vl.addEventListener('click',()=>pdlSetView('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('.pdl-fieldmenu')){fp.hidden=true;fb.setAttribute('aria-expanded','false');}});
+  }
+})();
+
 loadControls(); loadFacets(); load();
 </script>
 </body>
diff --git a/public/styles.css b/public/styles.css
index 423c779..7768065 100644
--- a/public/styles.css
+++ b/public/styles.css
@@ -185,6 +185,41 @@ header.top{position:sticky;top:0;z-index:20;background:rgba(250,249,247,.86);bac
 
 footer{border-top:1px solid var(--line);padding:24px 0;color:var(--muted);font-size:13px}
 
+/* ── Grid / List view toggle ─────────────────────────────────── */
+.pdl-view-seg{display:flex;border:1px solid var(--line);border-radius:10px;overflow:hidden;flex-shrink:0}
+.pdl-view-seg button{padding:0 13px;height:38px;background:var(--panel);border:none;font:inherit;font-size:12px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;cursor:pointer;color:var(--muted);transition:background .15s,color .15s}
+.pdl-view-seg button.on{background:var(--accent);color:#fff}
+
+/* List mode — card becomes a horizontal row */
+#grid.pdl-list{display:flex;flex-direction:column;gap:6px}
+#grid.pdl-list .card{display:grid;grid-template-columns:80px 1fr auto;align-items:center;gap:14px;border-radius:10px;padding:8px 14px 8px 8px;box-shadow:none;border:1px solid var(--line)}
+#grid.pdl-list .card:hover{transform:none;box-shadow:0 2px 8px rgba(0,0,0,.07)}
+#grid.pdl-list .thumb{width:80px;height:80px;min-width:80px;border-radius:8px;aspect-ratio:1/1}
+#grid.pdl-list .thumb .tile{background-size:20% 20%}
+#grid.pdl-list .cardbody{padding:4px 0}
+#grid.pdl-list .cardbody h3{font-size:14px;margin-bottom:2px}
+#grid.pdl-list .meta{font-size:11.5px}
+#grid.pdl-list .byline{font-size:11px}
+#grid.pdl-list .price{margin-top:2px;font-size:12px}
+
+/* ── Fields on/off popover ───────────────────────────────────── */
+.pdl-fieldmenu{position:relative;flex-shrink:0}
+.pdl-fbtn{height:38px;padding:0 14px;border:1px solid var(--line);border-radius:10px;background:var(--panel);font:inherit;font-size:12px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--muted);cursor:pointer;transition:border-color .15s,color .15s;white-space:nowrap}
+.pdl-fbtn:hover{border-color:var(--accent);color:var(--ink)}
+.pdl-fpop{position:absolute;top:calc(100% + 8px);right:0;background:var(--panel);border:1px solid var(--line);border-radius:12px;box-shadow:0 14px 34px rgba(0,0,0,.14);padding:6px 4px;z-index:50;min-width:180px}
+.pdl-fpop[hidden]{display:none}
+.pdl-fpop label{display:flex;align-items:center;gap:10px;padding:7px 14px;font-size:12px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--muted);cursor:pointer;white-space:nowrap}
+.pdl-fpop label:hover{background:var(--chip);border-radius:8px}
+.pdl-fpop input[type=checkbox]{accent-color:var(--accent);width:13px;height:13px}
+
+/* Per-field hide keyed on html.pdlhf-<field> */
+html.pdlhf-image  [data-f="image"]   {display:none!important}
+html.pdlhf-title  [data-f="title"]   {display:none!important}
+html.pdlhf-style  [data-f="style"]   {display:none!important}
+html.pdlhf-price  [data-f="price"]   {display:none!important}
+html.pdlhf-designer [data-f="designer"]{display:none!important}
+html.pdlhf-badge  [data-f="badge"]   {display:none!important}
+
 @media(max-width:900px){
   .shell{grid-template-columns:1fr}
   .filters{position:static;max-height:none;border:1px solid var(--line);border-radius:12px;padding:12px;margin-bottom:8px}

← 22fb969 chore: lint+refactor hardening (open-redirect guard, image p  ·  back to Patterndesignlab  ·  GA4+Meta pixel: inject gtag + fbq into homepage 170be93 →