[object Object]

← back to New Engine

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

1e55589a6c84a6b78c4ce7cf266d53b008bb7c19 · 2026-08-12 08:53:19 -0700 · Steve

Files touched

Diff

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

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

diff --git a/public/index.html b/public/index.html
index 57962c4..d72e779 100644
--- a/public/index.html
+++ b/public/index.html
@@ -40,6 +40,26 @@ header{display:flex;align-items:center;justify-content:space-between;gap:14px;
 .btn{font-family:'Share Tech Mono',monospace;font-size:10px;letter-spacing:1px;color:var(--muted);
   background:#0e0d0c;border:1px solid #37352f;border-radius:9px;padding:8px 11px;cursor:pointer}
 .btn:hover{color:var(--gold-lit);border-color:var(--gold)}
+/* view toggle */
+.ne-vseg{display:flex;border:1px solid #37352f;border-radius:9px;overflow:hidden}
+.ne-vseg button{padding:7px 11px;background:#0e0d0c;border:none;font-family:'Share Tech Mono',monospace;font-size:10px;letter-spacing:1px;color:var(--muted);cursor:pointer}
+.ne-vseg button.on{background:var(--gold);color:#0e0d0c}
+/* fields menu */
+.ne-fieldmenu{position:relative}
+.ne-fbtn{font-family:'Share Tech Mono',monospace;font-size:10px;letter-spacing:1px;color:var(--muted);background:#0e0d0c;border:1px solid #37352f;border-radius:9px;padding:8px 11px;cursor:pointer}
+.ne-fbtn:hover{color:var(--gold-lit);border-color:var(--gold)}
+.ne-fpop{position:absolute;top:calc(100% + 6px);right:0;background:#191815;border:1px solid #2b2925;border-radius:10px;box-shadow:0 10px 28px rgba(0,0,0,.7);padding:8px 4px;z-index:30;min-width:170px}
+.ne-fpop[hidden]{display:none}
+.ne-fpop label{display:flex;align-items:center;gap:8px;padding:6px 12px;font-family:'Jost',sans-serif;font-size:11px;letter-spacing:.5px;color:var(--muted);cursor:pointer;white-space:nowrap}
+.ne-fpop label:hover{color:var(--ink)}
+.ne-fpop input{accent-color:var(--gold);width:13px;height:13px;margin:0;padding:0}
+/* field visibility */
+body.ne-hf-thumb [data-f="ne-thumb"]{display:none!important}
+body.ne-hf-title [data-f="ne-title"]{display:none!important}
+body.ne-hf-meta [data-f="ne-meta"]{display:none!important}
+body.ne-hf-chips [data-f="ne-chips"]{display:none!important}
+body.ne-hf-palette [data-f="ne-palette"]{display:none!important}
+body.ne-hf-gid [data-f="ne-gid"]{display:none!important}
 .subline{text-align:center;font-size:10px;letter-spacing:4px;color:#7d7a72;margin:6px 0 16px}
 /* list / grid */
 .list{display:grid;grid-template-columns:repeat(var(--cols),minmax(0,1fr));gap:9px}
@@ -115,6 +135,14 @@ a.open:hover{text-shadow:0 0 8px rgba(200,169,106,.6)}
       </select></div>
     <div class="dens"><label style="font-size:9px;letter-spacing:2px;color:var(--muted)">DENSITY</label>
       <input type="range" id="dens" min="1" max="6" step="1" value="1"></div>
+    <div class="ne-vseg">
+      <button id="neVList" class="on" title="List view">☰ LIST</button>
+      <button id="neVGrid" title="Grid view">⊞ GRID</button>
+    </div>
+    <div class="ne-fieldmenu">
+      <button class="ne-fbtn" id="neFieldsBtn" aria-haspopup="true" aria-expanded="false">FIELDS ▾</button>
+      <div class="ne-fpop" id="neFpop" hidden role="menu"></div>
+    </div>
     <button class="btn" id="reset">RESET</button>
   </div>
 
@@ -145,16 +173,17 @@ function qs(){ const p=new URLSearchParams({limit:'60', sort:state.sort, since:s
 
 function renderRow(p,i,isNew){
   const h=href(p), title=h?`<a href="${h}" target="_blank" rel="noopener noreferrer">${esc(p.title)}</a>`:esc(p.title);
-  const img=proxied(p.image), thumb=img?`<img class="thumb" src="${img}" loading="lazy" alt="">`:`<div class="thumb"></div>`;
+  const img=proxied(p.image);
+  const thumbEl=img?`<img class="thumb" data-f="ne-thumb" src="${img}" loading="lazy" alt="">`:`<div class="thumb" data-f="ne-thumb"></div>`;
   const chips=(p.tags||[]).map(t=>`<span class="chip">${esc(t)}</span>`).join('');
   const el=document.createElement('div'); el.className='row'+(isNew?' fresh':'');
   el.innerHTML=`<div class="idx ${i<3?'top':''}">${i+1}</div>
-    ${h?`<a href="${h}" target="_blank" rel="noopener noreferrer">${thumb}</a>`:thumb}
-    <div class="main"><div class="t">${title}</div>
-      <div class="meta">${esc(p.vendor)||'—'} · ${esc(p.sku)||'no sku'} · ${(p.palette||[]).length} colors</div>
-      <div class="chips">${chips}</div></div>
-    <div class="right"><div class="pal">${palHTML(p,true)}</div>
-      <div class="gidl">#${esc(p.gid)}</div>
+    ${h?`<a href="${h}" target="_blank" rel="noopener noreferrer">${thumbEl}</a>`:thumbEl}
+    <div class="main"><div class="t" data-f="ne-title">${title}</div>
+      <div class="meta" data-f="ne-meta">${esc(p.vendor)||'—'} · ${esc(p.sku)||'no sku'} · ${(p.palette||[]).length} colors</div>
+      <div class="chips" data-f="ne-chips">${chips}</div></div>
+    <div class="right"><div class="pal" data-f="ne-palette">${palHTML(p,true)}</div>
+      <div class="gidl" data-f="ne-gid">#${esc(p.gid)}</div>
       ${h?`<a class="open" href="${h}" target="_blank" rel="noopener noreferrer">OPEN PAGE ▸</a>`:''}</div>`;
   return el;
 }
@@ -162,17 +191,54 @@ function renderCard(p,isNew){
   const h=href(p), img=proxied(p.image);
   const ph=img?`<img src="${img}" loading="lazy" alt="">`:'';
   const el=document.createElement('div'); el.className='card'+(isNew?' fresh':'');
-  el.innerHTML=`${h?`<a class="ph" href="${h}" target="_blank" rel="noopener noreferrer">${ph}</a>`:`<div class="ph">${ph}</div>`}
+  el.innerHTML=`${h?`<a class="ph" data-f="ne-thumb" href="${h}" target="_blank" rel="noopener noreferrer">${ph}</a>`:`<div class="ph" data-f="ne-thumb">${ph}</div>`}
     <div class="cbody">
-      <div class="ct">${h?`<a href="${h}" target="_blank" rel="noopener noreferrer">${esc(p.title)}</a>`:esc(p.title)}</div>
-      <div class="cmeta">${esc(p.vendor)||'—'} · ${esc(p.sku)||''}</div>
-      <div class="pal">${palHTML(p,false)}</div>
+      <div class="ct" data-f="ne-title">${h?`<a href="${h}" target="_blank" rel="noopener noreferrer">${esc(p.title)}</a>`:esc(p.title)}</div>
+      <div class="cmeta" data-f="ne-meta">${esc(p.vendor)||'—'} · ${esc(p.sku)||''}</div>
+      <div class="pal" data-f="ne-palette">${palHTML(p,false)}</div>
     </div>`;
   return el;
 }
+// ── Grid / List explicit toggle (overrides density-driven compact) ──
+const NE_FIELDS=[['thumb','Thumbnail'],['title','Title'],['meta','Vendor / SKU'],['chips','Tags'],['palette','Palette'],['gid','GID / Link']];
+const NE_FIELDS_DEF=Object.fromEntries(NE_FIELDS.map(([k])=>[k,true]));
+let neFieldVis=Object.assign({},NE_FIELDS_DEF,JSON.parse(localStorage.getItem('ne_fields')||'{}'));
+let neViewOverride=localStorage.getItem('ne_view')||null; // 'list'|'grid'|null (null=density-driven)
+function isCompact(){return neViewOverride==='grid'||(neViewOverride===null&&state.cols>1);}
+function applyNeFields(){
+  NE_FIELDS.forEach(([k])=>document.body.classList.toggle('ne-hf-'+k,!neFieldVis[k]));
+  document.querySelectorAll('#neFpop input[data-field]').forEach(cb=>cb.checked=!!neFieldVis[cb.dataset.field]);
+  localStorage.setItem('ne_fields',JSON.stringify(neFieldVis));
+}
+function buildNeFieldMenu(){
+  const fp=document.getElementById('neFpop');
+  fp.innerHTML=NE_FIELDS.map(([k,lbl])=>`<label><input type="checkbox" data-field="${k}"${neFieldVis[k]?' checked':''}> ${lbl}</label>`).join('');
+  fp.querySelectorAll('input').forEach(cb=>cb.addEventListener('change',()=>{neFieldVis[cb.dataset.field]=cb.checked;localStorage.setItem('ne_fields',JSON.stringify(neFieldVis));applyNeFields();}));
+}
+function setNeView(v){
+  neViewOverride=v;
+  localStorage.setItem('ne_view',v);
+  document.getElementById('neVList').classList.toggle('on',v==='list');
+  document.getElementById('neVGrid').classList.toggle('on',v==='grid');
+  first=true; paint(lastRecords);
+}
+document.getElementById('neVList').onclick=()=>setNeView('list');
+document.getElementById('neVGrid').onclick=()=>setNeView('grid');
+if(neViewOverride){
+  document.getElementById('neVList').classList.toggle('on',neViewOverride==='list');
+  document.getElementById('neVGrid').classList.toggle('on',neViewOverride==='grid');
+}
+buildNeFieldMenu();
+applyNeFields();
+(function(){
+  const btn=document.getElementById('neFieldsBtn'), fp=document.getElementById('neFpop');
+  btn.addEventListener('click',e=>{e.stopPropagation();const h=fp.hidden;fp.hidden=!h;btn.setAttribute('aria-expanded',String(h));});
+  document.addEventListener('click',()=>{if(!fp.hidden){fp.hidden=true;btn.setAttribute('aria-expanded','false');}});
+  fp.addEventListener('click',e=>e.stopPropagation());
+})();
 function paint(records){
   const list=$('list'); list.innerHTML='';
-  const compact = state.cols>1;
+  const compact = isCompact();
   records.forEach((p,i)=>{
     const isNew=!seen.has(p.gid)&&!first;
     list.appendChild(compact?renderCard(p,isNew):renderRow(p,i,isNew));

← fe0b370 fix 'connecting' hang: add response cache (single-flight + s  ·  back to New Engine  ·  creds-safe fetch guard: resolve relative fetch vs credential 5d3e091 →