[object Object]

← back to Muralsource Curator

viewer standard: add Grid/List toggle + per-field on/off menu to internal record-grid viewer

da8d484215078ef8a7243eccc6907701baf3d58b · 2026-08-12 08:53:18 -0700 · Steve

Files touched

Diff

commit da8d484215078ef8a7243eccc6907701baf3d58b
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Aug 12 08:53:18 2026 -0700

    viewer standard: add Grid/List toggle + per-field on/off menu to internal record-grid viewer
---
 index.html | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
 1 file changed, 77 insertions(+), 8 deletions(-)

diff --git a/index.html b/index.html
index 672bd78..d9d1449 100644
--- a/index.html
+++ b/index.html
@@ -16,7 +16,36 @@
   button.ghost{background:transparent}
   .controls{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
   .count{font-weight:700}
+  /* Grid / List toggle */
+  .vbtn{font-size:12px;padding:5px 10px;border-radius:6px;border:1px solid #2c313b;background:#1c1f26;color:#8b90a0;cursor:pointer}
+  .vbtn.on{background:#2f6df6;border-color:#2f6df6;color:#fff}
+  /* Fields popover */
+  .fieldmenu{position:relative}
+  .fbtn{font-size:12px;padding:5px 10px;border-radius:6px;border:1px solid #2c313b;background:#1c1f26;color:#8b90a0;cursor:pointer}
+  .fbtn:hover{border-color:#5a9af0;color:#e7e7ea}
+  .fpop{position:absolute;top:calc(100% + 6px);right:0;background:#1c1f26;border:1px solid #2c313b;border-radius:8px;box-shadow:0 10px 28px rgba(0,0,0,.5);padding:8px 4px;z-index:30;min-width:170px}
+  .fpop[hidden]{display:none}
+  .fpop label{display:flex;align-items:center;gap:8px;padding:6px 12px;font-size:12px;color:#b8bec9;cursor:pointer;white-space:nowrap}
+  .fpop label:hover{background:#21252e;color:#e7e7ea}
+  .fpop input{accent-color:#2f6df6;width:14px;height:14px;margin:0;padding:0}
+  /* Grid view */
   #grid{display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:12px;padding:16px}
+  /* List view */
+  #grid.list{display:flex;flex-direction:column;gap:6px;padding:16px}
+  #grid.list .card{display:flex;flex-direction:row;align-items:stretch;border-radius:8px}
+  #grid.list .card .imgwrap{aspect-ratio:1/1;width:72px;min-width:72px;height:72px;border-radius:6px 0 0 6px}
+  #grid.list .card .meta{flex:1;display:flex;flex-direction:row;align-items:center;gap:16px;padding:8px 12px;flex-wrap:wrap}
+  #grid.list .card .price{position:static;border:0;background:none;font-weight:700;font-size:12px;white-space:nowrap}
+  #grid.list .card .chk{position:static;width:18px;height:18px}
+  #grid.list .card .sku{position:static;font-size:10px;color:#6f7688}
+  /* Field visibility — body class hides tagged elements */
+  body.hf-image [data-f="image"]{display:none!important}
+  body.hf-title [data-f="title"]{display:none!important}
+  body.hf-sub [data-f="sub"]{display:none!important}
+  body.hf-tags [data-f="tags"]{display:none!important}
+  body.hf-price [data-f="price"]{display:none!important}
+  body.hf-sku [data-f="sku"]{display:none!important}
+  body.hf-when [data-f="when"]{display:none!important}
   .card{background:#15171c;border:1px solid #262a32;border-radius:12px;overflow:hidden;position:relative;transition:border-color .15s}
   .card.sel{border-color:#2f6df6;box-shadow:0 0 0 2px #2f6df6 inset}
   .card .imgwrap{aspect-ratio:1/1.1;background:#0a0b0d;overflow:hidden;display:flex;align-items:center;justify-content:center}
@@ -49,6 +78,12 @@
       <option value="sku">SKU</option>
     </select>
     <label class="muted">density <input id="dens" type="range" min="3" max="8" value="5"></label>
+    <button class="vbtn on" id="vGrid" title="Grid view">⊞ Grid</button>
+    <button class="vbtn" id="vList" title="List view">☰ List</button>
+    <div class="fieldmenu">
+      <button class="fbtn" id="fieldsBtn" aria-haspopup="true" aria-expanded="false">Fields ▾</button>
+      <div class="fpop" id="fpop" hidden role="menu"></div>
+    </div>
     <button class="ghost" id="selAll">select all shown</button>
     <button class="ghost" id="selNone">clear</button>
     <button class="primary" id="exp">Export selection ▸</button>
@@ -64,12 +99,46 @@
   <button class="ghost" id="close">×</button>
 </div>
 <script>
-const LS='ms_curator_sel', LSD='ms_curator_dens', LSS='ms_curator_sort';
+const LS='ms_curator_sel', LSD='ms_curator_dens', LSS='ms_curator_sort',
+      LSV='ms_curator_view', LSF='ms_curator_fields';
 let items=[], sel=new Set(JSON.parse(localStorage.getItem(LS)||'[]'));
 const grid=document.getElementById('grid'), q=document.getElementById('q'),
       sortEl=document.getElementById('sort'), dens=document.getElementById('dens');
 dens.value=localStorage.getItem(LSD)||5; document.documentElement.style.setProperty('--cols',dens.value);
 sortEl.value=localStorage.getItem(LSS)||'pattern';
+
+// ── Grid / List toggle ──
+const MS_FIELDS=[['image','Image'],['title','Title'],['sub','Color / Type'],['tags','Tags'],['price','Price'],['sku','SKU'],['when','Date / Link']];
+const MS_FIELDS_DEF=Object.fromEntries(MS_FIELDS.map(([k])=>[k,true]));
+let msFieldVis=Object.assign({},MS_FIELDS_DEF,JSON.parse(localStorage.getItem(LSF)||'{}'));
+function applyMsFields(){
+  MS_FIELDS.forEach(([k])=>document.body.classList.toggle('hf-'+k,!msFieldVis[k]));
+  document.querySelectorAll('#fpop input[data-field]').forEach(cb=>cb.checked=!!msFieldVis[cb.dataset.field]);
+  localStorage.setItem(LSF,JSON.stringify(msFieldVis));
+}
+function buildMsFieldMenu(){
+  const fpop=document.getElementById('fpop');
+  fpop.innerHTML=MS_FIELDS.map(([k,lbl])=>`<label><input type="checkbox" data-field="${k}"${msFieldVis[k]?' checked':''}> ${lbl}</label>`).join('');
+  fpop.querySelectorAll('input').forEach(cb=>cb.addEventListener('change',()=>{msFieldVis[cb.dataset.field]=cb.checked;localStorage.setItem(LSF,JSON.stringify(msFieldVis));applyMsFields();}));
+}
+function setMsView(v){
+  grid.classList.toggle('list',v==='list');
+  document.getElementById('vGrid').classList.toggle('on',v==='grid');
+  document.getElementById('vList').classList.toggle('on',v==='list');
+  localStorage.setItem(LSV,v);
+}
+document.getElementById('vGrid').onclick=()=>setMsView('grid');
+document.getElementById('vList').onclick=()=>setMsView('list');
+setMsView(localStorage.getItem(LSV)||'grid');
+buildMsFieldMenu();
+applyMsFields();
+// Fields popover open/close
+(function(){
+  const btn=document.getElementById('fieldsBtn'), pop=document.getElementById('fpop');
+  btn.addEventListener('click',e=>{e.stopPropagation();const h=pop.hidden;pop.hidden=!h;btn.setAttribute('aria-expanded',String(h));});
+  document.addEventListener('click',()=>{pop.hidden=true;btn.setAttribute('aria-expanded','false');});
+  pop.addEventListener('click',e=>e.stopPropagation());
+})();
 function tagList(s){try{return JSON.parse(s)}catch{return[]}}
 function render(){
   const term=q.value.trim().toLowerCase();
@@ -84,15 +153,15 @@ function render(){
   for(const it of list){
     const c=document.createElement('div'); c.className='card'+(sel.has(it.dw_sku)?' sel':'');
     const tags=tagList(it.ai_tags).slice(0,6).join(' · ');
-    c.innerHTML=`<div class="imgwrap"><img loading="lazy" src="${it.image_url}" alt="${it.pattern_name}"
+    c.innerHTML=`<div class="imgwrap" data-f="image"><img loading="lazy" src="${it.image_url}" alt="${it.pattern_name}"
         onerror="this.style.opacity=.2;this.alt='no image'"></div>
       <input type="checkbox" class="chk" ${sel.has(it.dw_sku)?'checked':''}>
-      <span class="price">$${Number(it.price_retail).toFixed(0)}</span>
-      <span class="sku">${it.dw_sku}</span>
-      <div class="meta"><div class="pat">${it.pattern_name}</div>
-      <div class="sub">${it.color_name||''} · ${it.product_type||''}</div>
-      <div class="tags">${tags}</div>
-      <div class="when" title="scraped ${it.refreshed_at}">🕓 ${it.created_at} · <a href="${it.product_url}" target="_blank" rel="noopener noreferrer">vendor ↗</a></div></div>`;
+      <span class="price" data-f="price">$${Number(it.price_retail).toFixed(0)}</span>
+      <span class="sku" data-f="sku">${it.dw_sku}</span>
+      <div class="meta"><div class="pat" data-f="title">${it.pattern_name}</div>
+      <div class="sub" data-f="sub">${it.color_name||''} · ${it.product_type||''}</div>
+      <div class="tags" data-f="tags">${tags}</div>
+      <div class="when" data-f="when" title="scraped ${it.refreshed_at}">🕓 ${it.created_at} · <a href="${it.product_url}" target="_blank" rel="noopener noreferrer">vendor ↗</a></div></div>`;
     const toggle=()=>{ sel.has(it.dw_sku)?sel.delete(it.dw_sku):sel.add(it.dw_sku);
       c.classList.toggle('sel'); c.querySelector('.chk').checked=sel.has(it.dw_sku); save(); };
     c.querySelector('.chk').addEventListener('click',e=>{e.stopPropagation();toggle();});

← f7de3f1 Mural Source onboarding: refresh staging (dead→live images),  ·  back to Muralsource Curator  ·  (newest)