← 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 ↓</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…</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 →