[object Object]

← back to Trade Portal

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

6f71c4f6c3cd421a40e7f0b90e86f4cd37b57bf2 · 2026-08-12 08:53:18 -0700 · Steve

Files touched

Diff

commit 6f71c4f6c3cd421a40e7f0b90e86f4cd37b57bf2
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
---
 public/index.html | 76 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 76 insertions(+)

diff --git a/public/index.html b/public/index.html
index 99cf92c..ff0afeb 100644
--- a/public/index.html
+++ b/public/index.html
@@ -65,6 +65,37 @@
   .btn-outline{background:transparent;color:#fff;border:1px solid rgba(255,255,255,.4)}
   .btn-outline:hover{background:rgba(255,255,255,.1)}
   .btn-sm{padding:6px 14px;font-size:12px}
+  /* ── List view ── */
+  .grid.list{display:flex;flex-direction:column;gap:6px;padding:12px 24px 60px}
+  .grid.list .card{flex-direction:row;align-items:stretch;border-radius:6px}
+  .grid.list .card img{width:80px;height:80px;aspect-ratio:1/1;flex:0 0 80px;object-fit:cover}
+  .grid.list .card-body{flex:1;flex-direction:row;flex-wrap:wrap;align-items:center;gap:12px;padding:8px 12px;display:flex}
+  .grid.list .card-body .card-title{flex:1;min-width:160px}
+  .grid.list .card-body .card-vendor{min-width:100px}
+  .grid.list .card-body .price-row{margin-top:0}
+  .grid.list .card-body .card-actions{margin-top:0;padding-top:0}
+  .grid.list .card-body .card-sku{margin-top:0}
+  /* ── View toggle ── */
+  .tp-view-btn{background:#fff;border:1px solid #e2ddd7;border-radius:5px;padding:6px 11px;cursor:pointer;font-size:12px;font-family:inherit;color:#666}
+  .tp-view-btn:hover{border-color:var(--accent)}
+  .tp-view-btn.active{border-color:var(--accent);color:var(--accent)}
+  /* ── Fields ▾ popover ── */
+  .tp-fieldmenu{position:relative}
+  .tp-fbtn{background:#fff;border:1px solid #e2ddd7;border-radius:5px;padding:6px 11px;cursor:pointer;font-size:12px;font-family:inherit;color:#666;white-space:nowrap}
+  .tp-fbtn:hover{border-color:var(--accent);color:var(--accent)}
+  .tp-fpop{position:absolute;top:calc(100% + 6px);right:0;background:#fff;border:1px solid #e2ddd7;border-radius:6px;box-shadow:0 8px 24px rgba(0,0,0,.12);padding:8px 4px;z-index:30;min-width:200px}
+  .tp-fpop[hidden]{display:none}
+  .tp-fpop label{display:flex;align-items:center;gap:9px;padding:7px 14px;font-size:12px;color:#666;cursor:pointer;white-space:nowrap}
+  .tp-fpop label:hover{background:#f5f5f5;color:#333}
+  .tp-fpop input{accent-color:var(--accent);margin:0}
+  /* field-hide classes (grid only) */
+  body.tphf-img    .grid:not(.list) .card img{display:none}
+  body.tphf-title  .grid:not(.list) .card-title{display:none}
+  body.tphf-vendor .grid:not(.list) .card-vendor{display:none}
+  body.tphf-type   .grid:not(.list) .card-type{display:none}
+  body.tphf-sku    .grid:not(.list) .card-sku{display:none}
+  body.tphf-price  .grid:not(.list) .price-row{display:none}
+  body.tphf-actions .grid:not(.list) .card-actions{display:none}
   .modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:100;display:none;align-items:center;justify-content:center}
   .modal-overlay.open{display:flex}
   .modal{background:#fff;border-radius:10px;width:100%;max-width:400px;padding:28px;position:relative}
@@ -134,9 +165,24 @@
         <option value="price-desc">Price &#x2193;</option>
         <option value="vendor">Vendor</option>
         <option value="color">Color</option>
+        <option value="type">Product Type</option>
       </select>
     </label>
     <label>Density <input id="density" type="range" min="2" max="6" value="4"></label>
+    <button class="tp-view-btn" id="tp-viewgrid" title="Grid view">⊞ Grid</button>
+    <button class="tp-view-btn" id="tp-viewlist" title="List view">☰ List</button>
+    <div class="tp-fieldmenu">
+      <button class="tp-fbtn" id="tp-fieldbtn">Fields ▾</button>
+      <div class="tp-fpop" id="tp-fpop" hidden>
+        <label><input type="checkbox" data-tpf="img" checked> Image</label>
+        <label><input type="checkbox" data-tpf="title" checked> Title</label>
+        <label><input type="checkbox" data-tpf="vendor" checked> Vendor</label>
+        <label><input type="checkbox" data-tpf="type" checked> Product type</label>
+        <label><input type="checkbox" data-tpf="sku" checked> SKU</label>
+        <label><input type="checkbox" data-tpf="price" checked> Price</label>
+        <label><input type="checkbox" data-tpf="actions" checked> Buy / actions</label>
+      </div>
+    </div>
     <span class="count" id="count"></span>
   </div>
   <div class="grid" id="grid"><div class="empty">Loading&hellip;</div></div>
@@ -416,6 +462,36 @@ $id('apply-form').onsubmit = async e => {
   } catch { err.textContent = 'Network error'; err.style.display = 'block'; }
 };
 
+// ── VIEW TOGGLE ──────────────────────────────────────────────────
+const tpGrid = $id('grid');
+function setTpView(v) {
+  tpGrid.classList.toggle('list', v === 'list');
+  $id('tp-viewgrid').classList.toggle('active', v === 'grid');
+  $id('tp-viewlist').classList.toggle('active', v === 'list');
+  $id('density').disabled = v === 'list';
+  $id('density').parentElement.style.opacity = v === 'list' ? '.35' : '1';
+  try { localStorage.setItem('tp-view', v); } catch(_) {}
+}
+$id('tp-viewgrid').addEventListener('click', () => setTpView('grid'));
+$id('tp-viewlist').addEventListener('click', () => setTpView('list'));
+setTpView(localStorage.getItem('tp-view') || 'grid');
+
+// ── FIELDS MENU ──────────────────────────────────────────────────
+const TPF_DEF = { img:true, title:true, vendor:true, type:true, sku:true, price:true, actions:true };
+let TPF = Object.assign({}, TPF_DEF);
+try { const _s = localStorage.getItem('tp-fields'); if (_s) TPF = Object.assign({}, TPF_DEF, JSON.parse(_s)); } catch(_) {}
+function applyTpFields() {
+  for (const k of Object.keys(TPF_DEF)) document.body.classList.toggle('tphf-'+k, !TPF[k]);
+  document.querySelectorAll('#tp-fpop input[data-tpf]').forEach(cb => { cb.checked = !!TPF[cb.dataset.tpf]; });
+  try { localStorage.setItem('tp-fields', JSON.stringify(TPF)); } catch(_) {}
+}
+document.querySelectorAll('#tp-fpop input[data-tpf]').forEach(cb => {
+  cb.addEventListener('change', () => { TPF[cb.dataset.tpf] = cb.checked; applyTpFields(); });
+});
+applyTpFields();
+$id('tp-fieldbtn').addEventListener('click', e => { e.stopPropagation(); $id('tp-fpop').hidden = !$id('tp-fpop').hidden; });
+document.addEventListener('click', e => { if (!e.target.closest('.tp-fieldmenu')) { const p = $id('tp-fpop'); if(p) p.hidden = true; } });
+
 // Init
 (async () => { await refreshSession(); await loadProducts(); })();
 </script>

← ed2c486 Handle sample_only + buyable catalog fields; eliminate \$nul  ·  back to Trade Portal  ·  nav-agent: universal grid-controls drop-in on internal viewe 7383f73 →