[object Object]

← back to Fentucci

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

097d3441413f9a5dbf6922efcc581191669104a0 · 2026-08-12 08:33:44 -0700 · Steve

Files touched

Diff

commit 097d3441413f9a5dbf6922efcc581191669104a0
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/css/site.css | 110 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 public/index.html   |   8 ++++
 public/js/app.js    |  77 ++++++++++++++++++++++++++++++++----
 3 files changed, 188 insertions(+), 7 deletions(-)

diff --git a/public/css/site.css b/public/css/site.css
index ad3e06b..8b744f2 100644
--- a/public/css/site.css
+++ b/public/css/site.css
@@ -640,3 +640,113 @@ html[data-theme="dark"]       .icon-moon { display: none; }
   position: absolute; width: 1px; height: 1px; padding: 0;
   margin: -1px; overflow: hidden; clip: rect(0,0,0,0); border: 0;
 }
+
+/* ── Grid / List view toggle ──────────────────────────────────── */
+.view-seg {
+  display: flex;
+  border: 1px solid var(--line);
+  border-radius: var(--radius-sm);
+  overflow: hidden;
+  flex-shrink: 0;
+}
+.view-seg button {
+  padding: 0.42rem 0.85rem;
+  background: var(--card-bg);
+  border: none;
+  font: inherit;
+  font-size: 0.78rem;
+  font-weight: 500;
+  letter-spacing: 0.08em;
+  text-transform: uppercase;
+  cursor: pointer;
+  color: var(--ink-faint);
+  transition: background 0.15s, color 0.15s;
+}
+.view-seg button.on { background: var(--accent); color: #fff; }
+
+/* List mode — product-card becomes a horizontal row */
+.product-grid.list-view {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+}
+.product-grid.list-view .product-card {
+  flex-direction: row;
+  align-items: center;
+  border-radius: var(--radius-sm);
+  transform: none !important;
+}
+.product-grid.list-view .product-card:hover { box-shadow: var(--shadow-md); }
+.product-grid.list-view .card-image-wrap {
+  flex: 0 0 72px;
+  width: 72px;
+  height: 72px;
+  aspect-ratio: 1/1;
+  border-radius: var(--radius-sm) 0 0 var(--radius-sm);
+}
+.product-grid.list-view .card-body {
+  flex: 1;
+  padding: 0.55rem 0.8rem;
+  gap: 0.15rem;
+}
+.product-grid.list-view .card-actions {
+  flex-direction: row;
+  padding: 0 0.8rem 0 0;
+  gap: 0.4rem;
+  flex-shrink: 0;
+}
+.product-grid.list-view .card-btn { font-size: 0.72rem; padding: 0.35rem 0.65rem; }
+
+/* ── Fields on/off popover ────────────────────────────────────── */
+.fieldmenu { position: relative; flex-shrink: 0; }
+.fbtn {
+  padding: 0.42rem 0.85rem;
+  border: 1px solid var(--line);
+  border-radius: var(--radius-sm);
+  background: var(--card-bg);
+  font: inherit;
+  font-size: 0.78rem;
+  font-weight: 500;
+  letter-spacing: 0.08em;
+  text-transform: uppercase;
+  color: var(--ink-soft);
+  cursor: pointer;
+  transition: border-color 0.15s, color 0.15s;
+  white-space: nowrap;
+}
+.fbtn:hover { border-color: var(--accent); color: var(--ink); }
+.fpop {
+  position: absolute;
+  top: calc(100% + 8px);
+  right: 0;
+  background: var(--card-bg);
+  border: 1px solid var(--line);
+  border-radius: var(--radius-md);
+  box-shadow: 0 14px 34px rgba(0,0,0,.16);
+  padding: 6px 4px;
+  z-index: 200;
+  min-width: 176px;
+}
+.fpop[hidden] { display: none; }
+.fpop label {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  padding: 7px 14px;
+  font-size: 0.78rem;
+  letter-spacing: 0.08em;
+  text-transform: uppercase;
+  color: var(--ink-soft);
+  cursor: pointer;
+  white-space: nowrap;
+}
+.fpop label:hover { background: var(--hover-bg); }
+.fpop input[type=checkbox] { accent-color: var(--accent); width: 13px; height: 13px; }
+
+/* Per-field hide rules keyed on html.hf-<field> (survives re-renders) */
+html.hf-image  [data-f="image"]  { display: none !important; }
+html.hf-title  [data-f="title"]  { display: none !important; }
+html.hf-sku    [data-f="sku"]    { display: none !important; }
+html.hf-price  [data-f="price"]  { display: none !important; }
+html.hf-sample [data-f="sample"] { display: none !important; }
+html.hf-shop   [data-f="shop"]   { display: none !important; }
diff --git a/public/index.html b/public/index.html
index 6f6bf25..fbd33f2 100644
--- a/public/index.html
+++ b/public/index.html
@@ -162,6 +162,14 @@
         </select>
         <label class="ctrl-label" for="density-slider">Density</label>
         <input type="range" id="density-slider" class="density-slider" min="2" max="6" value="4" step="1" aria-label="Grid column count">
+        <div class="view-seg" role="group" aria-label="View">
+          <button id="vGrid" class="on" type="button" title="Grid view">Grid</button>
+          <button id="vList" type="button" title="List view">List</button>
+        </div>
+        <div class="fieldmenu">
+          <button class="fbtn" id="fieldsBtn" type="button" aria-haspopup="true" aria-expanded="false">Fields ▾</button>
+          <div class="fpop" id="fpop" hidden role="menu"></div>
+        </div>
       </div>
     </div>
 
diff --git a/public/js/app.js b/public/js/app.js
index 29a70dc..e709ad2 100644
--- a/public/js/app.js
+++ b/public/js/app.js
@@ -271,8 +271,8 @@ function cardHTML(p) {
     : '';
 
   const img = p.image_url
-    ? `<img class="card-img" src="${esc(p.image_url)}" alt="${esc(p.title || '')}" loading="lazy" width="400" height="533">`
-    : `<div class="card-img-placeholder">No image</div>`;
+    ? `<img data-f="image" class="card-img" src="${esc(p.image_url)}" alt="${esc(p.title || '')}" loading="lazy" width="400" height="533">`
+    : `<div data-f="image" class="card-img-placeholder">No image</div>`;
 
   const sku = p.dw_sku || p.mfr_sku || '';
 
@@ -281,13 +281,13 @@ function cardHTML(p) {
     ${img}
   </a>
   <div class="card-body">
-    <a href="${esc(shopUrl)}" target="_blank" rel="noopener noreferrer" class="card-title">${esc(p.title || '')}</a>
-    ${sku ? `<span class="card-sku">${esc(sku)}</span>` : ''}
-    ${priceStr ? `<div class="card-price">${priceStr}</div>` : ''}
+    <a href="${esc(shopUrl)}" target="_blank" rel="noopener noreferrer" class="card-title" data-f="title">${esc(p.title || '')}</a>
+    ${sku ? `<span class="card-sku" data-f="sku">${esc(sku)}</span>` : ''}
+    ${priceStr ? `<div class="card-price" data-f="price">${priceStr}</div>` : ''}
   </div>
   <div class="card-actions">
-    <a href="${esc(sampleUrl)}" target="_blank" rel="noopener noreferrer" class="card-btn card-btn-sample" title="Order a free memo sample of ${esc(p.title || 'this product')}">Order Memo Sample (free)</a>
-    <a href="${esc(shopUrl)}" target="_blank" rel="noopener noreferrer" class="card-btn card-btn-shop">Shop on DW</a>
+    <a href="${esc(sampleUrl)}" target="_blank" rel="noopener noreferrer" class="card-btn card-btn-sample" data-f="sample" title="Order a free memo sample of ${esc(p.title || 'this product')}">Order Memo Sample (free)</a>
+    <a href="${esc(shopUrl)}" target="_blank" rel="noopener noreferrer" class="card-btn card-btn-shop" data-f="shop">Shop on DW</a>
   </div>
 </article>`;
 }
@@ -411,5 +411,68 @@ function esc(str) {
     .catch(() => {/* keep the empty placeholder */});
 })();
 
+// ── Grid / List view toggle + Fields menu ───────────────────────
+const FT_FIELDS = [
+  ['image',  'Image',  true],
+  ['title',  'Title',  true],
+  ['sku',    'SKU',    true],
+  ['price',  'Price',  true],
+  ['sample', 'Sample CTA', true],
+  ['shop',   'Shop CTA',   true],
+];
+function ftLoadFields() {
+  const def = {}; FT_FIELDS.forEach(([k,,d]) => def[k] = d);
+  try { return Object.assign(def, JSON.parse(localStorage.getItem('fentucci-fields') || '{}')); }
+  catch (e) { return def; }
+}
+const ftViewState = {
+  view: localStorage.getItem('fentucci-view') || 'grid',
+  fields: ftLoadFields(),
+};
+
+function ftSetView(v) {
+  ftViewState.view = v;
+  localStorage.setItem('fentucci-view', v);
+  if (grid) grid.classList.toggle('list-view', v === 'list');
+  const vg = document.getElementById('vGrid'), vl = document.getElementById('vList');
+  if (vg) vg.classList.toggle('on', v === 'grid');
+  if (vl) vl.classList.toggle('on', v === 'list');
+}
+function ftApplyFields() {
+  FT_FIELDS.forEach(([k]) => document.documentElement.classList.toggle('hf-' + k, !ftViewState.fields[k]));
+}
+function ftBuildFieldMenu() {
+  const pop = document.getElementById('fpop'); if (!pop) return;
+  pop.innerHTML = FT_FIELDS.map(([k, label]) =>
+    `<label><input type="checkbox" data-field="${k}"${ftViewState.fields[k] ? ' checked' : ''}> ${label}</label>`).join('');
+  pop.querySelectorAll('input').forEach(cb => cb.addEventListener('change', () => {
+    ftViewState.fields[cb.dataset.field] = cb.checked;
+    localStorage.setItem('fentucci-fields', JSON.stringify(ftViewState.fields));
+    ftApplyFields();
+  }));
+}
+
+(function ftInit() {
+  ftSetView(ftViewState.view);
+  ftApplyFields();
+  ftBuildFieldMenu();
+  const vg = document.getElementById('vGrid'), vl = document.getElementById('vList');
+  const fb = document.getElementById('fieldsBtn'), fp = document.getElementById('fpop');
+  if (vg) vg.addEventListener('click', () => ftSetView('grid'));
+  if (vl) vl.addEventListener('click', () => ftSetView('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('.fieldmenu')) {
+        fp.hidden = true; fb.setAttribute('aria-expanded', 'false');
+      }
+    });
+  }
+})();
+
 // ── Boot ────────────────────────────────────────────────────────
 resetAndLoad();

← 7dd2d3e SEO: rel=nofollow sponsored on designerwallcoverings.com lin  ·  back to Fentucci  ·  TK-11341: add AdSense Auto-Ads loader + ads.txt (revert to r 3629547 →