← 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
M public/css/site.cssM public/index.htmlM public/js/app.js
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 →