[object Object]

← back to All Designerwallcoverings

Astek-viewer alignment: accordion filter sections + CARD FIELDS toggles (image-only wall) + header count pill + --fs type scale + Basic Auth gate (admin/DW2024!, env-overridable, healthz open)

96f3c154411252634ea4bafeea548cd2d79e9d34 · 2026-07-02 17:09:30 -0700 · Steve Abrams

Files touched

Diff

commit 96f3c154411252634ea4bafeea548cd2d79e9d34
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Jul 2 17:09:30 2026 -0700

    Astek-viewer alignment: accordion filter sections + CARD FIELDS toggles (image-only wall) + header count pill + --fs type scale + Basic Auth gate (admin/DW2024!, env-overridable, healthz open)
---
 public/index.html | 107 +++++++++++++++++++++++++++++++++++++++---------------
 server.js         |  10 +++++
 2 files changed, 88 insertions(+), 29 deletions(-)

diff --git a/public/index.html b/public/index.html
index 5b37d37..2d35ae3 100644
--- a/public/index.html
+++ b/public/index.html
@@ -5,7 +5,7 @@
 <meta name="viewport" content="width=device-width, initial-scale=1">
 <title>All Products — Designer Wallcoverings</title>
 <style>
-  :root { --cols: 6; --bg:#0d0d0f; --card:#17171b; --line:#2a2a30; --txt:#e8e8ea; --mut:#9a9aa2; --accent:#c8a96a; --rail:260px; }
+  :root { --cols: 6; --fs: 1; --bg:#0d0d0f; --card:#17171b; --line:#2a2a30; --txt:#e8e8ea; --mut:#9a9aa2; --accent:#c8a96a; --rail:230px; }
   * { box-sizing: border-box; }
   body { margin:0; background:var(--bg); color:var(--txt); font:14px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; }
 
@@ -15,6 +15,7 @@
   h1 { margin:0; font-size:17px; font-weight:600; letter-spacing:.02em; white-space:nowrap; }
   h1 small { color:var(--mut); font-weight:400; font-size:12px; margin-left:8px; }
   h1 a { color:var(--mut); font-size:11px; font-weight:400; margin-left:10px; text-decoration:none; border-bottom:1px dotted var(--mut); }
+  .pill { background:var(--accent); color:#1a1407; border-radius:20px; padding:2px 10px; font-size:11.5px; font-weight:700; vertical-align:2px; }
   select, input[type=search], input[type=text] { background:var(--card); color:var(--txt); border:1px solid var(--line); border-radius:8px; padding:7px 10px; font-size:13px; }
   input[type=search] { flex:1 1 200px; min-width:180px; max-width:420px; }
   .bar { display:flex; align-items:center; gap:8px; color:var(--mut); font-size:12px; white-space:nowrap; }
@@ -28,10 +29,17 @@
   /* ── layout: Amazon-style left rail + content ── */
   .wrap { display:flex; align-items:flex-start; }
   aside { width:var(--rail); flex:0 0 var(--rail); position:sticky; top:59px; max-height:calc(100vh - 59px);
-    overflow-y:auto; padding:14px 14px 60px; border-right:1px solid var(--line); }
-  aside h4 { margin:16px 0 7px; font-size:11px; letter-spacing:.08em; text-transform:uppercase; color:var(--mut);
-    border-top:1px solid var(--line); padding-top:12px; }
-  aside h4:first-of-type { border-top:none; margin-top:2px; padding-top:0; }
+    overflow-y:auto; padding:10px 10px 60px; border-right:1px solid var(--line); }
+  /* Astek-style collapsed accordion sections per field */
+  details.sec { border:1px solid var(--line); border-radius:7px; margin-bottom:7px; background:var(--card); }
+  details.sec > summary { list-style:none; cursor:pointer; user-select:none; padding:8px 10px; font-size:10.5px;
+    letter-spacing:.09em; text-transform:uppercase; color:var(--mut); display:flex; align-items:center; gap:7px; }
+  details.sec > summary::-webkit-details-marker { display:none; }
+  details.sec > summary::before { content:'▸'; color:var(--accent); font-size:10px; }
+  details.sec[open] > summary::before { content:'▾'; }
+  details.sec > summary .n { margin-left:auto; background:var(--accent); color:#1a1407; border-radius:20px;
+    padding:0 7px; font-size:10px; font-weight:700; }
+  .fbody { padding:2px 8px 9px; }
   .fitem { display:flex; align-items:center; gap:7px; padding:2.5px 2px; cursor:pointer; font-size:12.5px; color:var(--txt); border-radius:5px; }
   .fitem:hover { background:var(--card); }
   .fitem .box { width:13px; height:13px; flex:0 0 13px; border:1px solid var(--mut); border-radius:3px; display:inline-flex; align-items:center; justify-content:center; font-size:10px; }
@@ -49,23 +57,23 @@
 
   main { flex:1; min-width:0; }
 
-  /* ── grid view ── */
-  .grid { display:grid; grid-template-columns:repeat(var(--cols),1fr); gap:10px; padding:14px 16px 30px; }
-  .card { background:var(--card); border:1px solid var(--line); border-radius:10px; overflow:hidden; display:flex; flex-direction:column; }
+  /* ── grid view (tight 1px cards, type scale rides --fs from the density slider) ── */
+  .grid { display:grid; grid-template-columns:repeat(var(--cols),1fr); gap:8px; padding:12px 14px 30px; }
+  .card { background:var(--card); border:1px solid var(--line); border-radius:4px; overflow:hidden; display:flex; flex-direction:column; }
   .thumb { aspect-ratio:1/1; background:#000 center/cover no-repeat; position:relative; }
   .thumb.empty { display:flex; align-items:center; justify-content:center; color:#444; font-size:11px; }
   .badge { position:absolute; top:6px; left:6px; font-size:10px; font-weight:700; letter-spacing:.04em; padding:2px 7px; border-radius:20px; text-transform:uppercase; }
   .badge.st-ARCHIVED { background:#5a3d3d; color:#e8c9c9; }
   .badge.st-DRAFT { background:#3d4a5a; color:#c9dae8; }
-  .meta { padding:8px 10px; }
-  .sku { font-weight:700; font-size:13px; color:var(--accent); word-break:break-all; }
-  .ttl { color:var(--txt); font-size:11.5px; margin-top:3px; max-height:2.8em; overflow:hidden; }
-  .vt { color:var(--mut); font-size:10.5px; margin-top:3px; }
-  .fac { margin-top:4px; font-size:10.5px; color:var(--accent); }
-  .price { color:#e8e8ea; font-size:12.5px; font-weight:600; margin-top:4px; display:block; }
-  /* dense grids: shed card text progressively so 20-across stays a clean texture wall */
-  .grid.d1 .ttl, .grid.d1 .fac { display:none; }
-  .grid.d2 .meta { display:none; }
+  .meta { padding:calc(7px*var(--fs)) calc(9px*var(--fs)); }
+  .sku { font-weight:700; font-size:calc(13px*var(--fs)); color:var(--accent); word-break:break-all; }
+  .ttl { color:var(--txt); font-size:calc(11.5px*var(--fs)); margin-top:3px; max-height:2.8em; overflow:hidden; }
+  .vt { color:var(--mut); font-size:calc(10.5px*var(--fs)); margin-top:3px; }
+  .fac { margin-top:4px; font-size:calc(10.5px*var(--fs)); color:var(--accent); }
+  .price { color:#e8e8ea; font-size:calc(12.5px*var(--fs)); font-weight:600; margin-top:4px; display:block; }
+  /* CARD FIELDS visibility toggles — all off = pure image-only texture wall */
+  .grid.h-sku .sku, .grid.h-ttl .ttl, .grid.h-vt .vt, .grid.h-fac .fac, .grid.h-price .price { display:none; }
+  .grid.h-sku.h-ttl.h-vt.h-fac.h-price .meta { display:none; }
 
   /* ── list view ── */
   .listwrap { padding:0 16px 30px; overflow-x:auto; }
@@ -95,7 +103,7 @@
 </head>
 <body>
 <header>
-  <h1>All Products <small id="sub">loading…</small><a href="/vendors.html">vendor directory →</a></h1>
+  <h1>All Products <span class="pill" id="pillN">…</span> <small id="sub">loading…</small><a href="/vendors.html">vendor directory →</a></h1>
   <button class="dirbtn" id="railToggle">☰ Filters</button>
   <input type="search" id="q" placeholder="Search anything — red, damask, DWKR, Thibaut…">
   <span class="bar">Sort
@@ -123,14 +131,15 @@
 <div class="wrap">
   <aside id="rail">
     <div class="active" id="activeRow"></div>
-    <h4>Vendor</h4><div id="vendorRow"></div>
-    <h4>Type</h4><div id="typeRow"></div>
-    <h4>Color</h4><div id="colorRow"></div>
-    <h4>Style</h4><div id="styleRow"></div>
-    <h4>Material</h4><div id="matRow"></div>
-    <h4>Price</h4><div id="priceRow"></div>
-    <h4>Status</h4><div id="statusRow"></div>
-    <h4>Image</h4><div id="imgRow"></div>
+    <details class="sec" data-k="fields"><summary>Card fields<span class="n" id="nFields"></span></summary><div class="fbody" id="fieldsRow"></div></details>
+    <details class="sec" data-k="vendor"><summary>Vendor<span class="n" id="nVendor"></span></summary><div class="fbody" id="vendorRow"></div></details>
+    <details class="sec" data-k="type"><summary>Type<span class="n" id="nType"></span></summary><div class="fbody" id="typeRow"></div></details>
+    <details class="sec" data-k="color"><summary>Color<span class="n" id="nColor"></span></summary><div class="fbody" id="colorRow"></div></details>
+    <details class="sec" data-k="style"><summary>Style<span class="n" id="nStyle"></span></summary><div class="fbody" id="styleRow"></div></details>
+    <details class="sec" data-k="material"><summary>Material<span class="n" id="nMaterial"></span></summary><div class="fbody" id="matRow"></div></details>
+    <details class="sec" data-k="price"><summary>Price<span class="n" id="nPrice"></span></summary><div class="fbody" id="priceRow"></div></details>
+    <details class="sec" data-k="status"><summary>Status<span class="n" id="nStatus"></span></summary><div class="fbody" id="statusRow"></div></details>
+    <details class="sec" data-k="image"><summary>Image<span class="n" id="nImage"></span></summary><div class="fbody" id="imgRow"></div></details>
   </aside>
 
   <main>
@@ -174,11 +183,45 @@ const rowUI = {
 /* ── top-bar wiring ── */
 const dens = document.getElementById('dens'), densN = document.getElementById('densN');
 dens.value = localStorage.getItem('all_cols') || 6;
-const setCols = () => { document.documentElement.style.setProperty('--cols', dens.value); densN.textContent = dens.value;
-  grid.classList.toggle('d1', dens.value >= 9); grid.classList.toggle('d2', dens.value >= 13); };
+const setCols = () => {
+  document.documentElement.style.setProperty('--cols', dens.value); densN.textContent = dens.value;
+  // type scale rides the density slider (Astek --fs pattern): 6-across = 1.0, shrinking toward .62 at 20
+  document.documentElement.style.setProperty('--fs', Math.max(.62, Math.min(1.15, 6 / dens.value)).toFixed(2));
+};
 setCols();
 dens.oninput = () => { setCols(); localStorage.setItem('all_cols', dens.value); };
 
+/* accordion sections: persist open/closed per section (collapsed by default, Astek-style) */
+document.querySelectorAll('details.sec').forEach((d) => {
+  d.open = localStorage.getItem('all_sec_' + d.dataset.k) === '1';
+  d.ontoggle = () => localStorage.setItem('all_sec_' + d.dataset.k, d.open ? '1' : '0');
+});
+
+/* CARD FIELDS: toggle which card elements render — all off = image-only texture wall */
+const FIELD_DEFS = [['sku', 'SKU'], ['ttl', 'Title'], ['vt', 'Vendor · Type'], ['fac', 'Color / Style / Material'], ['price', 'Price']];
+let hiddenFields = loadSet('all_hidden_fields');
+function drawFieldToggles() {
+  const row = document.getElementById('fieldsRow'); row.innerHTML = '';
+  FIELD_DEFS.forEach(([k, label]) => {
+    const on = !hiddenFields.has(k);
+    const el = document.createElement('div');
+    el.className = 'fitem' + (on ? ' on' : '');
+    el.innerHTML = `<span class="box">${on ? '✓' : ''}</span><span class="lbl">${label}</span>`;
+    el.onclick = () => {
+      hiddenFields.has(k) ? hiddenFields.delete(k) : hiddenFields.add(k);
+      localStorage.setItem('all_hidden_fields', JSON.stringify([...hiddenFields]));
+      applyFieldClasses(); drawFieldToggles();
+    };
+    row.appendChild(el);
+  });
+  document.getElementById('nFields').textContent = hiddenFields.size ? `${FIELD_DEFS.length - hiddenFields.size}/${FIELD_DEFS.length}` : '';
+  document.getElementById('nFields').style.display = hiddenFields.size ? '' : 'none';
+}
+function applyFieldClasses() {
+  FIELD_DEFS.forEach(([k]) => grid.classList.toggle('h-' + k, hiddenFields.has(k)));
+}
+applyFieldClasses(); drawFieldToggles();
+
 const sortSel = document.getElementById('sort'), dirBtn = document.getElementById('dir');
 sortSel.value = ['price_asc','price_desc'].includes(state.sort) ? 'price' : state.sort;
 state.sort = sortSel.value;
@@ -306,10 +349,15 @@ function renderFacets(f) {
   renderActive();
 }
 
+const SECN = { Vendor: 'nVendor', Type: 'nType', Color: 'nColor', Style: 'nStyle', Material: 'nMaterial', Price: 'nPrice', Status: 'nStatus', Image: 'nImage' };
 function renderActive() {
   const row = document.getElementById('activeRow'); row.innerHTML = '';
   const dims = [['Vendor', state.vendors], ['Type', state.types], ['Color', state.colors], ['Style', state.styles],
     ['Material', state.materials], ['Price', state.prices], ['Status', state.statuses], ['Image', state.images]];
+  for (const [label, set] of dims) { // selection-count badge on each collapsed section header
+    const b = document.getElementById(SECN[label]);
+    if (b) { b.textContent = set.size || ''; b.style.display = set.size ? '' : 'none'; }
+  }
   let n = 0;
   for (const [label, set] of dims) {
     set.forEach((v) => {
@@ -380,7 +428,8 @@ async function load() {
     (state.view === 'list' ? tbody : grid).appendChild(frag);
     state.offset += data.rows.length;
     state.done = state.offset >= data.total || data.rows.length === 0;
-    subEl.textContent = `${data.catalog.toLocaleString()} products · ${data.total.toLocaleString()} matching · showing ${state.offset.toLocaleString()}`;
+    document.getElementById('pillN').textContent = data.total.toLocaleString();
+    subEl.textContent = `of ${data.catalog.toLocaleString()} products · showing ${state.offset.toLocaleString()}`;
     statusEl.textContent = data.total === 0 ? '— no matching products —'
       : (state.done ? `— end · ${state.total.toLocaleString()} matching —` : 'scroll for more…');
   } catch (e) {
diff --git a/server.js b/server.js
index 8eda2af..3739b8f 100644
--- a/server.js
+++ b/server.js
@@ -190,9 +190,19 @@ function loadVendors() {
   return vendorsCache;
 }
 
+// Basic Auth gate (Steve 2026-07-02: all. is published but un/pw-gated).
+// Override with BASIC_AUTH=user:pass in .env; /healthz stays open for probes.
+const BASIC_AUTH = process.env.BASIC_AUTH || 'admin:DW2024!';
+const AUTH_OK = 'Basic ' + Buffer.from(BASIC_AUTH).toString('base64');
+
 const server = http.createServer((req, res) => {
   const u = new URL(req.url, `http://localhost:${PORT}`);
 
+  if (u.pathname !== '/healthz' && req.headers.authorization !== AUTH_OK) {
+    res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="Designer Wallcoverings - All Products"', 'Content-Type': 'text/plain' });
+    return res.end('Authentication required');
+  }
+
   if (u.pathname === '/api/products') {
     const f = parseFilters(u);
     const sort = u.searchParams.get('sort') || 'newest';

← bb41e31 amazon-style left filter rail + grid/list toggle + sortable  ·  back to All Designerwallcoverings  ·  all.: add live *.designerwallcoverings.com microsite crawl ( 260257f →