[object Object]

← back to Designer Wallcoverings

chore: lint + refactor newwall remap/scrub scripts, token guard, version-up (session close)

68820077967ca88bdc71015eee7372fdd6038606 · 2026-06-26 10:51:45 -0700 · Steve

Files touched

Diff

commit 68820077967ca88bdc71015eee7372fdd6038606
Author: Steve <steve@designerwallcoverings.com>
Date:   Fri Jun 26 10:51:45 2026 -0700

    chore: lint + refactor newwall remap/scrub scripts, token guard, version-up (session close)
---
 .../designtex-uom-fix/220216-live-2026-06-26.png   | Bin 0 -> 3332038 bytes
 package-lock.json                                  |   4 +-
 package.json                                       |   2 +-
 shopify/_cwGRID/sections/collection-grouped.liquid | 140 +++++++++++++++++----
 shopify/scripts/scrub-newwall-metafields.js        |  19 +--
 5 files changed, 128 insertions(+), 37 deletions(-)

diff --git a/audits/designtex-uom-fix/220216-live-2026-06-26.png b/audits/designtex-uom-fix/220216-live-2026-06-26.png
new file mode 100644
index 00000000..d13cd951
Binary files /dev/null and b/audits/designtex-uom-fix/220216-live-2026-06-26.png differ
diff --git a/package-lock.json b/package-lock.json
index c9b87304..e4d53820 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "designer-wallcoverings",
-  "version": "1.0.1",
+  "version": "1.0.2",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "designer-wallcoverings",
-      "version": "1.0.1",
+      "version": "1.0.2",
       "dependencies": {
         "@anthropic-ai/sdk": "^0.71.2",
         "@google-cloud/vision": "^5.3.4",
diff --git a/package.json b/package.json
index bd745918..75681c27 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "designer-wallcoverings",
-  "version": "1.0.1",
+  "version": "1.0.2",
   "description": "Designer Wallcoverings Shopify Management",
   "scripts": {
     "build": "tsc --noEmit",
diff --git a/shopify/_cwGRID/sections/collection-grouped.liquid b/shopify/_cwGRID/sections/collection-grouped.liquid
index 21771efe..51754c02 100644
--- a/shopify/_cwGRID/sections/collection-grouped.liquid
+++ b/shopify/_cwGRID/sections/collection-grouped.liquid
@@ -75,27 +75,40 @@
     <h1 class="dw-grouped__title">{{ collection.title }}</h1>
   {%- endif -%}
 
-  <div class="dw-grouped__toolbar">
-    <div class="dw-grouped__count" id="dw-grouped-count"></div>
-    <div class="dw-grouped__controls">
-      <div class="dw-grouped__cats" id="dw-grouped-cats" role="tablist" aria-label="Filter and sort">
-        <button type="button" class="dw-grouped__chipbtn" data-cat="color">Color</button>
-        <button type="button" class="dw-grouped__chipbtn" data-cat="style">Style</button>
-        <button type="button" class="dw-grouped__chipbtn is-active" data-cat="sort">Sort</button>
+  <div class="dw-grouped__rail" id="dw-grouped-rail">
+    <div class="dw-grouped__toolbar">
+      <div class="dw-grouped__count" id="dw-grouped-count"></div>
+      <div class="dw-grouped__controls">
+        <div class="dw-grouped__cats" id="dw-grouped-cats" role="tablist" aria-label="Filter and sort">
+          <button type="button" class="dw-grouped__chipbtn" data-cat="color">Color</button>
+          <button type="button" class="dw-grouped__chipbtn" data-cat="style">Style</button>
+          <button type="button" class="dw-grouped__chipbtn is-active" data-cat="sort">Sort</button>
+        </div>
+        <label class="dw-grouped__ctl">
+          <span>Density</span>
+          <input id="dw-grouped-density" type="range" min="2" max="7" step="1" value="4">
+          <span class="dw-grouped__density-val" id="dw-grouped-density-val">4</span>
+        </label>
+        <label class="dw-grouped__ctl">
+          <span>Padding</span>
+          <input id="dw-grouped-pad" type="range" min="0" max="28" step="2" value="10">
+          <span class="dw-grouped__density-val" id="dw-grouped-pad-val">10</span>
+        </label>
+        <div class="dw-grouped__ctl dw-grouped__layout" role="group" aria-label="Filter layout">
+          <span>View</span>
+          <button type="button" class="dw-grouped__lbtn is-active" data-layout="top" title="Filters across the top" aria-label="Filters across the top">▤</button>
+          <button type="button" class="dw-grouped__lbtn" data-layout="left" title="Filters on the left" aria-label="Filters on the left">▥</button>
+        </div>
       </div>
-      <label class="dw-grouped__ctl">
-        <span>Density</span>
-        <input id="dw-grouped-density" type="range" min="2" max="7" step="1" value="4">
-        <span class="dw-grouped__density-val" id="dw-grouped-density-val">4</span>
-      </label>
     </div>
+    <div class="dw-grouped__subbar" id="dw-grouped-sub" aria-label="Options"></div>
   </div>
-  <div class="dw-grouped__subbar" id="dw-grouped-sub" aria-label="Options"></div>
 
-  <div id="dw-grouped-grid" class="dw-grouped__grid" role="list"></div>
-
-  <div id="dw-grouped-sentinel" class="dw-grouped__sentinel" hidden>
-    <span id="dw-grouped-status">Loading more…</span>
+  <div class="dw-grouped__main">
+    <div id="dw-grouped-grid" class="dw-grouped__grid" role="list"></div>
+    <div id="dw-grouped-sentinel" class="dw-grouped__sentinel" hidden>
+      <span id="dw-grouped-status">Loading more…</span>
+    </div>
   </div>
 </section>
 
@@ -106,7 +119,7 @@
   body.dw-grouped--active .collection-products,
   body.dw-grouped--active .boost-pfs-filter-products { display: none !important; }
 
-  #dw-grouped { --cols: 4; font-family: inherit; margin: 0 0 3rem; }
+  #dw-grouped { --cols: 4; --pad: 10px; font-family: inherit; margin: 0 0 3rem; }
 
   .dw-grouped__hero {
     position: relative; min-height: 320px; background-size: cover; background-position: center;
@@ -146,7 +159,13 @@
   @media (max-width: 1024px) { .dw-grouped__grid { grid-template-columns: repeat(min(3, var(--cols)), minmax(0,1fr)); } }
   @media (max-width: 700px)  { .dw-grouped__grid { grid-template-columns: repeat(2, minmax(0,1fr)); } }
 
-  .dw-grouped__card { display: block; text-decoration: none; color: inherit; }
+  /* Each tile ("chip") sits in a padded frame; the Padding slider drives --pad all around. */
+  .dw-grouped__card {
+    display: block; text-decoration: none; color: inherit;
+    padding: var(--pad); background: #fff; border: 1px solid #ececec; border-radius: 8px;
+    transition: box-shadow .16s ease, transform .16s ease;
+  }
+  .dw-grouped__card:hover { box-shadow: 0 6px 20px rgba(0,0,0,.07); transform: translateY(-2px); }
   .dw-grouped__img-wrap { position: relative; aspect-ratio: 1/1; background: #f4f4f4; border-radius: 4px; overflow: hidden; }
   .dw-grouped__img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform .4s ease; }
   /* truly-dead image (no live fallback) → clean neutral tile, never a broken-icon + alt text */
@@ -169,6 +188,30 @@
   .dw-grouped__skel { aspect-ratio: 1/1; border-radius: 4px; background: linear-gradient(90deg,#eee 0%,#f6f6f6 50%,#eee 100%); background-size: 200% 100%; animation: dwgShim 1.2s linear infinite; }
   @keyframes dwgShim { 0%{background-position:200% 0} 100%{background-position:-200% 0} }
   .dw-grouped__sentinel { text-align: center; padding: 26px; color: #999; font-size: 12px; }
+
+  /* Layout toggle (filters across the top vs. docked on the left) */
+  .dw-grouped__layout { gap: 5px; }
+  .dw-grouped__lbtn {
+    font-size: 14px; line-height: 1; padding: 5px 8px; border: 1px solid #ddd;
+    border-radius: 6px; background: #fff; color: #777; cursor: pointer;
+  }
+  .dw-grouped__lbtn:hover { border-color: #1a1a1a; color: #1a1a1a; }
+  .dw-grouped__lbtn.is-active { background: #1a1a1a; color: #fff; border-color: #1a1a1a; }
+
+  /* === Vertical layout: search info docked as a left rail, grid to its right === */
+  #dw-grouped.is-vrail { display: grid; grid-template-columns: 232px minmax(0, 1fr); column-gap: 30px; align-items: start; }
+  #dw-grouped.is-vrail .dw-grouped__hero,
+  #dw-grouped.is-vrail .dw-grouped__title { grid-column: 1 / -1; }
+  #dw-grouped.is-vrail .dw-grouped__rail { grid-column: 1; position: sticky; top: 16px; align-self: start; }
+  #dw-grouped.is-vrail .dw-grouped__main { grid-column: 2; min-width: 0; }
+  #dw-grouped.is-vrail .dw-grouped__toolbar { flex-direction: column; align-items: flex-start; gap: 16px; border-bottom: 0; padding: 0 0 12px; }
+  #dw-grouped.is-vrail .dw-grouped__controls { flex-direction: column; align-items: flex-start; gap: 16px; }
+  #dw-grouped.is-vrail .dw-grouped__cats { flex-wrap: wrap; }
+  #dw-grouped.is-vrail .dw-grouped__subbar { margin: 0 0 8px; }
+  @media (max-width: 760px) {
+    #dw-grouped.is-vrail { display: block; }
+    #dw-grouped.is-vrail .dw-grouped__rail { position: static; }
+  }
 </style>
 
 <script>
@@ -194,6 +237,9 @@
   var hueFilter = '', styleFilter = '';
   var densEl   = document.getElementById('dw-grouped-density');
   var densVal  = document.getElementById('dw-grouped-density-val');
+  var padEl    = document.getElementById('dw-grouped-pad');
+  var padVal   = document.getElementById('dw-grouped-pad-val');
+  var facets   = { color: [], style: [] };   // populated filter values, from the feed
 
   var PER = 48, page = 0, hasNext = true, loading = false, total = null;
 
@@ -213,12 +259,45 @@
     try { var s = parseInt(localStorage.getItem('dwg.cols'), 10); if (s >= 2 && s <= 7) densEl.value = s; } catch (_) {}
     densEl.addEventListener('input', apply); apply();
   }
+  // Padding slider — drives --pad (space all around each tile/chip), localStorage-persisted.
+  function wirePad() {
+    function apply() {
+      var n = Math.max(0, Math.min(28, parseInt(padEl.value, 10) || 0));
+      padVal.textContent = n; root.style.setProperty('--pad', n + 'px');
+      try { localStorage.setItem('dwg.pad', n); } catch (_) {}
+    }
+    try { var s = parseInt(localStorage.getItem('dwg.pad'), 10); if (s >= 0 && s <= 28) padEl.value = s; } catch (_) {}
+    padEl.addEventListener('input', apply); apply();
+  }
+  // Layout toggle — filters across the top (default) or docked as a left rail. Persisted.
+  function wireLayout() {
+    var btns = root.querySelectorAll('.dw-grouped__lbtn');
+    function apply(mode) {
+      root.classList.toggle('is-vrail', mode === 'left');
+      btns.forEach(function (b) { b.classList.toggle('is-active', b.getAttribute('data-layout') === mode); });
+      try { localStorage.setItem('dwg.layout', mode); } catch (_) {}
+    }
+    var saved = 'top';
+    try { var s = localStorage.getItem('dwg.layout'); if (s === 'left' || s === 'top') saved = s; } catch (_) {}
+    btns.forEach(function (b) { b.addEventListener('click', function () { apply(b.getAttribute('data-layout')); }); });
+    apply(saved);
+  }
   // Two-tier controls: a category chip (Color/Style/Sort) reveals its values on the next line.
-  var VALUES = {
-    color: [['','All'],['red','Red'],['orange','Orange'],['yellow','Yellow'],['green','Green'],['teal','Teal'],['blue','Blue'],['purple','Purple'],['pink','Pink'],['neutral','Neutral']],
-    style: [['','All'],['floral','Floral'],['geometric','Geometric'],['damask','Damask'],['stripe','Stripe'],['toile','Toile'],['trellis','Trellis'],['tropical','Tropical'],['textured','Textured'],['grasscloth','Grasscloth'],['solid','Solid']],
-    sort:  [['colors_desc','Most Colors'],['newest','Newest'],['pattern_az','Pattern A–Z'],['vendor_az','Vendor A–Z']]
-  };
+  // Color/Style values are driven by the feed's FACETS so only values that actually have
+  // products in THIS collection render — no dead tab that returns 0 results. Sort is fixed.
+  var COLOR_LABEL = { red:'Red', orange:'Orange', yellow:'Yellow', green:'Green', teal:'Teal', blue:'Blue', purple:'Purple', pink:'Pink', neutral:'Neutral' };
+  var SORT_VALUES = [['colors_desc','Most Colors'],['newest','Newest'],['pattern_az','Pattern A–Z'],['vendor_az','Vendor A–Z']];
+  function titleize(s) { return String(s).replace(/\b\w/g, function (c) { return c.toUpperCase(); }); }
+  function valuesFor(cat) {
+    if (cat === 'sort') return SORT_VALUES;
+    var keys = (facets[cat] || []).slice().sort(function (a, b) {
+      var la = cat === 'color' ? (COLOR_LABEL[a] || a) : a, lb = cat === 'color' ? (COLOR_LABEL[b] || b) : b;
+      return String(la).localeCompare(String(lb));
+    });
+    var opts = [['', 'All']];
+    keys.forEach(function (k) { opts.push([k, cat === 'color' ? (COLOR_LABEL[k] || titleize(k)) : titleize(k)]); });
+    return opts;
+  }
   var activeCat = 'sort';
   function currentValFor(cat) { return cat === 'color' ? hueFilter : cat === 'style' ? styleFilter : currentSort; }
   function renderSub(cat) {
@@ -227,7 +306,12 @@
       b.classList.toggle('is-active', b.getAttribute('data-cat') === cat);
     });
     var cur = currentValFor(cat);
-    subWrap.innerHTML = VALUES[cat].map(function (v) {
+    var vals = valuesFor(cat);
+    if ((cat === 'color' || cat === 'style') && vals.length <= 1) {
+      subWrap.innerHTML = '<span style="font-size:11px;color:#999;padding:6px 2px">No ' + cat + ' filters for this collection</span>';
+      return;
+    }
+    subWrap.innerHTML = vals.map(function (v) {
       return '<button type="button" class="dw-grouped__chipbtn dw-grouped__subbtn' + (v[0] === cur ? ' is-active' : '') + '" data-val="' + v[0] + '">' + v[1] + '</button>';
     }).join('');
   }
@@ -338,6 +422,10 @@
         if (next === 1 && !tiles.length) { root.hidden = true; document.body.classList.remove('dw-grouped--active'); return; }
         grid.insertAdjacentHTML('beforeend', tiles.map(card).join(''));
         healEmpty();
+        if (next === 1 && d.facets) {
+          facets = { color: d.facets.color || [], style: d.facets.style || [] };
+          if (activeCat === 'color' || activeCat === 'style') renderSub(activeCat);
+        }
         page = next;
         total = d.total_tiles;
         hasNext = !!d.has_next;
@@ -354,7 +442,7 @@
       });
   }
 
-  wireDensity(); wireControls();
+  wireDensity(); wirePad(); wireLayout(); wireControls();
   // Own the page immediately (we're the grid now) — toolbar + skeletons show on load,
   // not after the feed responds.
   document.body.classList.add('dw-grouped--active');
diff --git a/shopify/scripts/scrub-newwall-metafields.js b/shopify/scripts/scrub-newwall-metafields.js
index 3861cc64..3737b347 100644
--- a/shopify/scripts/scrub-newwall-metafields.js
+++ b/shopify/scripts/scrub-newwall-metafields.js
@@ -6,23 +6,26 @@
  * Targets the 74 by handle (vendor already fixed, so can't query vendor:Newwall).
  * APPLY=1 to write. Brand map: /tmp/newwall_brand_map.tsv ; handles: /tmp/newwall_live.tsv
  */
-const fs = require('path') && require('fs');
+const fs = require('fs');
 const path = require('path');
 const ROOT = path.resolve(__dirname, '..', '..');
-const env = Object.fromEntries(fs.readFileSync(path.join(ROOT, '.env'), 'utf8').split('\n')
-  .filter(l => l.includes('=') && !l.trim().startsWith('#'))
-  .map(l => { const i = l.indexOf('='); return [l.slice(0, i).trim(), l.slice(i + 1).trim()]; }));
-const S = (env.SHOPIFY_STORE_DOMAIN || 'designer-laboratory-sandbox.myshopify.com').replace(/^https?:\/\//, '');
-const T = env.SHOPIFY_ADMIN_TOKEN;
+const env = Object.fromEntries(
+  fs.readFileSync(path.join(ROOT, '.env'), 'utf8')
+    .split('\n').filter(l => l.includes('=') && !l.trim().startsWith('#'))
+    .map(l => { const i = l.indexOf('='); return [l.slice(0, i).trim(), l.slice(i + 1).trim()]; })
+);
+const STORE = (env.SHOPIFY_STORE_DOMAIN || 'designer-laboratory-sandbox.myshopify.com').replace(/^https?:\/\//, '');
+const TOKEN = env.SHOPIFY_ADMIN_TOKEN || env.SHOPIFY_ADMIN_ACCESS_TOKEN;
 const APPLY = process.env.APPLY === '1';
+if (!TOKEN) { console.error('No SHOPIFY_ADMIN_TOKEN in .env'); process.exit(1); }
 
 const brand = Object.fromEntries(fs.readFileSync('/tmp/newwall_brand_map.tsv', 'utf8').trim().split('\n')
   .map(l => l.split('\t')).map(([k, v]) => [k.toUpperCase(), v]));
 const handles = fs.readFileSync('/tmp/newwall_live.tsv', 'utf8').trim().split('\n')
   .map(l => l.split('\t')).map(([key, status, handle]) => ({ key: key.toUpperCase(), handle }));
 
-const gql = (q, v) => fetch(`https://${S}/admin/api/2024-01/graphql.json`,
-  { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Shopify-Access-Token': T }, body: JSON.stringify({ query: q, variables: v }) }).then(r => r.json());
+const gql = (q, v) => fetch(`https://${STORE}/admin/api/2024-01/graphql.json`,
+  { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Shopify-Access-Token': TOKEN }, body: JSON.stringify({ query: q, variables: v }) }).then(r => r.json());
 const sleep = ms => new Promise(r => setTimeout(r, ms));
 
 (async () => {

← 974c7dd0 auto-save: 2026-06-26T10:45:01 (8 files) — pending-approval/  ·  back to Designer Wallcoverings  ·  auto-save: 2026-06-26T11:15:12 (7 files) — pending-approval/ 9279cbe4 →