[object Object]

← back to Designer Wallcoverings

cadence: QH real-width classifier — 54in→Fabric, 27in→Wallcovering (Steve 2026-07-09)

f72482f63bbce4202ae85f87dc01208fdbcceff3 · 2026-07-09 14:02:29 -0700 · Steve

Extends the QH width guard to both anchors: real width 54"→Fabric (textile
bolt), 27"→Wallcovering (classic roll width). effType overrides cfg.productType
in buildSampleOnlyInput, scoped to quadrille_house_catalog, inch-guarded
(54/27 not 54cm, 27 not inside 127). Live audit: QH widths run 45-58" (0 at 27",
all 54" already Fabric) → 0 live flips; this is the go-forward guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit f72482f63bbce4202ae85f87dc01208fdbcceff3
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Jul 9 14:02:29 2026 -0700

    cadence: QH real-width classifier — 54in→Fabric, 27in→Wallcovering (Steve 2026-07-09)
    
    Extends the QH width guard to both anchors: real width 54"→Fabric (textile
    bolt), 27"→Wallcovering (classic roll width). effType overrides cfg.productType
    in buildSampleOnlyInput, scoped to quadrille_house_catalog, inch-guarded
    (54/27 not 54cm, 27 not inside 127). Live audit: QH widths run 45-58" (0 at 27",
    all 54" already Fabric) → 0 live flips; this is the go-forward guard.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 shopify/scripts/cadence/cadence-import.js          |  19 ++-
 .../snippets/color-palette.liquid                  | 179 ++++++++++++++++-----
 2 files changed, 153 insertions(+), 45 deletions(-)

diff --git a/shopify/scripts/cadence/cadence-import.js b/shopify/scripts/cadence/cadence-import.js
index 2a35c28b..d818f50d 100644
--- a/shopify/scripts/cadence/cadence-import.js
+++ b/shopify/scripts/cadence/cadence-import.js
@@ -466,15 +466,18 @@ function buildSampleOnlyInput(vendor, cfg, row, activate) {
   // Home Couture, Plains…) — only Grasscloth is genuinely wallcovering. Respect cfg.productType
   // so a Fabric line is never titled "Wallcoverings" (mirrors the roll path @ ~L612). Fixing this
   // at birth stops the recurring Alan-Campbell wallcovering→fabric re-sweep (4th cleanup, 2026-07-09).
-  // Steve's width rule (2026-07-09): ANY Quadrille-house product 54" wide is FABRIC (54" = textile
-  // bolt width; wallcovering rolls are narrower). Overrides cfg.productType so a 54" Grasscloth row
-  // flips to Fabric at birth. Scoped to the QH staging table (never PJ / Pierre Frey, which are
-  // legitimately wallcovering) and guarded to INCHES (54" not 54 cm ≈ 21").
+  // Steve's QH real-width classifier (2026-07-09): real width 54" → FABRIC (textile bolt width),
+  // real width 27" → WALLCOVERING (classic wallcovering roll width). Overrides cfg.productType so a
+  // 54" Grasscloth row is born Fabric, and a 27" fabric-line row is born Wallcovering. Scoped to the
+  // QH staging table (never PJ / Pierre Frey, which are legitimately wallcovering) and guarded to
+  // INCHES (54"/27" not 54 cm ≈ 21", and 27 must not match inside 127). Anything else → cfg default.
   const _w = String(row.width || '');
-  const _is54inch = cfg.table === 'quadrille_house_catalog'
-    && /(?:^|[^0-9])54(?:[^0-9]|$)/.test(_w)
-    && !/\b(?:cm|mm|centim|millim|met(?:re|er))\b/i.test(_w);
-  const effType = _is54inch ? 'Fabric' : (cfg.productType || 'Wallcovering');
+  const _isQH = cfg.table === 'quadrille_house_catalog';
+  const _cm   = /\b(?:cm|mm|centim|millim|met(?:re|er))\b/i.test(_w);
+  const _inch = n => _isQH && new RegExp(`(?:^|[^0-9])${n}(?:[^0-9]|$)`).test(_w) && !_cm;
+  const effType = _inch('54') ? 'Fabric'
+                : _inch('27') ? 'Wallcovering'
+                : (cfg.productType || 'Wallcovering');
   const isFabric = /fabric/i.test(effType);
   const typeWord = isFabric ? 'Fabric' : 'Wallcoverings';
   const title = `${core} ${typeWord} | ${vendor}`.replace(/wallpaper/gi,'Wallcovering');
diff --git a/shopify/theme-LIVE-591-freshpull-20260709/snippets/color-palette.liquid b/shopify/theme-LIVE-591-freshpull-20260709/snippets/color-palette.liquid
index da3c2cb7..4a13456c 100644
--- a/shopify/theme-LIVE-591-freshpull-20260709/snippets/color-palette.liquid
+++ b/shopify/theme-LIVE-591-freshpull-20260709/snippets/color-palette.liquid
@@ -1,10 +1,15 @@
 {% comment %}
   color-palette — "Colors In This Pattern" swatch spectrum.
   Shows the pattern's real colors AND interpolated in-between colors, laid out as
-  uniform swatches centered in even rows. The TOTAL rendered dots are HARD-capped
-  at MAX_SWATCHES = 20 (Steve 2026-07-09c: "only show a total of 20 dots"): every
-  real base color is kept, and the remaining budget of interpolated dots is spread
-  EVENLY across the gaps (never truncating one end of the spectrum).
+  uniform swatches centered in even rows. The TOTAL rendered dots are driven by a
+  user DENSITY SLIDER (Steve 2026-07-09d: "allow the user to grid slide the amount
+  of dots that they can see.. expand.. contract"): every real base color is kept,
+  and the remaining budget of interpolated dots is spread EVENLY across the gaps
+  (never truncating one end of the spectrum). The slider runs MIN_DOTS(6) →
+  MAX_DOTS(64) with DEFAULT 20 (the former hard cap became the default value),
+  re-renders live on drag with NO page reload, and persists to localStorage under
+  'dw_palette_dot_count' (a palette-specific key, distinct from the product-grid
+  density slider). (was Steve 2026-07-09c: a fixed 20-dot cap, MAX_SWATCHES=20.)
 
   DOT CLICK TARGET (Steve 2026-07-09b): "bring up an index of many images, not
   just that exact hex. tolerance 10% for color to pull more." A swatch click now
@@ -52,6 +57,23 @@
   data-product-style="{{ product.metafields.custom.style.value | default: product.type | escape }}"
 >
   <p class="dw-color-palette__title">Colors In This Pattern</p>
+
+  {%- comment -%} DENSITY SLIDER (Steve 2026-07-09d): expand/contract the number of
+    dots shown. Range MIN_DOTS(6) → MAX_DOTS(64), default 20; live re-render on drag,
+    value persisted to localStorage 'dw_palette_dot_count'. Hidden until the palette
+    actually has base colors to interpolate (JS unhides it). {%- endcomment -%}
+  <div class="dw-color-palette__density" data-dw-palette-density hidden>
+    <button type="button" class="dw-cp-dense__step" data-dw-cp-minus aria-label="Show fewer colors">&minus;</button>
+    <label class="dw-cp-dense__wrap">
+      <span class="dw-cp-dense__lbl">Colors shown</span>
+      <input type="range" class="dw-cp-dense__range" data-dw-cp-range
+             min="6" max="64" step="1" value="20"
+             aria-label="Number of colors shown in the palette">
+      <output class="dw-cp-dense__out" data-dw-cp-out>20</output>
+    </label>
+    <button type="button" class="dw-cp-dense__step" data-dw-cp-plus aria-label="Show more colors">+</button>
+  </div>
+
   <div class="dw-color-palette__cards" data-dw-color-palette-cards aria-live="polite"></div>
 
   {%- comment -%} In-page color-index grid the dot brings up (Steve 2026-07-09b).
@@ -71,6 +93,21 @@
 <style>
   .dw-color-palette{padding:16px 0 10px;text-align:center;min-height:10px;}
   .dw-color-palette__title{font-family:Lora,serif;font-size:15px;font-weight:500;color:#3D4246;margin:0 0 12px;letter-spacing:-0.01em;}
+  /* ── DENSITY SLIDER (expand/contract dot count) ── */
+  .dw-color-palette__density{display:flex;align-items:center;justify-content:center;gap:8px;max-width:660px;margin:0 auto 14px;}
+  .dw-color-palette__density[hidden]{display:none;}
+  .dw-cp-dense__wrap{display:inline-flex;align-items:center;gap:8px;}
+  .dw-cp-dense__lbl{font-family:Lora,serif;font-size:11px;color:#8a8577;letter-spacing:.02em;text-transform:uppercase;white-space:nowrap;}
+  .dw-cp-dense__range{-webkit-appearance:none;appearance:none;width:150px;max-width:38vw;height:3px;border-radius:3px;background:#e0ded9;outline:none;cursor:pointer;margin:0;}
+  .dw-cp-dense__range::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:14px;height:14px;border-radius:50%;background:#3D4246;border:2px solid #fff;box-shadow:0 0 0 1px rgba(0,0,0,.18);cursor:pointer;}
+  .dw-cp-dense__range::-moz-range-thumb{width:14px;height:14px;border-radius:50%;background:#3D4246;border:2px solid #fff;box-shadow:0 0 0 1px rgba(0,0,0,.18);cursor:pointer;}
+  .dw-cp-dense__out{font-family:Lora,serif;font-size:12px;font-weight:600;color:#3D4246;min-width:18px;text-align:right;font-variant-numeric:tabular-nums;}
+  .dw-cp-dense__step{font-family:Lora,serif;font-size:16px;line-height:1;color:#6b6b6b;background:none;border:1px solid #e0ded9;border-radius:50%;width:24px;height:24px;padding:0;cursor:pointer;flex:0 0 auto;}
+  .dw-cp-dense__step:hover{color:#000;border-color:#b8b3a8;}
+  @media (max-width:600px){
+    .dw-cp-dense__lbl{display:none;}
+    .dw-cp-dense__range{width:120px;}
+  }
   /* Centered EVEN ROWS of uniform swatches: flex-wrap + justify-content:center
      centers every row, including a partial last row. (Steve 2026-07-08) */
   .dw-color-palette__cards{display:flex;flex-wrap:wrap;justify-content:center;align-content:center;gap:2px;max-width:660px;margin:0 auto;}
@@ -293,20 +330,32 @@
       var r=Math.round(ca[0]+(cb[0]-ca[0])*t),g=Math.round(ca[1]+(cb[1]-ca[1])*t),bl=Math.round(ca[2]+(cb[2]-ca[2])*t);
       return '#'+[r,g,bl].map(function(x){return x.toString(16).padStart(2,'0');}).join('');
     }
-    /* TOTAL rendered dots are HARD-capped at MAX_SWATCHES (Steve 2026-07-09c:
-       "only show a total of 20 dots"). We keep EVERY real base color, then spend
-       whatever budget is left on interpolated in-between dots, distributed EVENLY
-       across the gaps (never truncating one end of the spectrum). See buildSteps. */
-    var MAX_SWATCHES = 20; /* HARD total-dot cap (base + interpolated) */
+    /* TOTAL rendered dots are driven by a user DENSITY SLIDER (Steve 2026-07-09d:
+       "allow the user to grid slide the amount of dots that they can see..
+       expand.. contract"). The slider value is the target TOTAL dot count. We keep
+       EVERY real base color, then spend whatever budget is left on interpolated
+       in-between dots, distributed EVENLY across the gaps (never truncating one end
+       of the spectrum). See buildSteps. The former hard cap of 20 is now the slider
+       DEFAULT; the range is MIN_DOTS..MAX_DOTS, persisted to localStorage. */
+    var MIN_DOTS = 6, MAX_DOTS = 64, DEFAULT_DOTS = 20; /* slider bounds + default */
+    var LS_KEY = 'dw_palette_dot_count';               /* palette-specific persistence key */
+    function readDotCount(){
+      var v = DEFAULT_DOTS;
+      try { var s = localStorage.getItem(LS_KEY); if(s!=null){ var n=parseInt(s,10); if(!isNaN(n)) v=n; } } catch(e){}
+      if(v<MIN_DOTS) v=MIN_DOTS; if(v>MAX_DOTS) v=MAX_DOTS;
+      return v;
+    }
+    function writeDotCount(v){ try { localStorage.setItem(LS_KEY, String(v)); } catch(e){} }
+    var dotTarget = readDotCount();   /* current TOTAL-dot target (base + interpolated) */
 
     /* Given the base-color count, return the number of interpolated dots to place
-       in EACH of the (n-1) gaps, so that base + sum(steps) <= MAX_SWATCHES and the
+       in EACH of the (n-1) gaps, so that base + sum(steps) <= `total` and the
        leftover is spread across the earliest gaps (keeps the spectrum balanced,
-       left-to-right, rather than starving the tail). */
-    function buildSteps(n){
+       left-to-right, rather than starving the tail). `total` = current slider value. */
+    function buildSteps(n, total){
       var gaps = n - 1;
       if(gaps <= 0) return [];
-      var budget = MAX_SWATCHES - n;            // interpolated dots we can afford
+      var budget = total - n;                   // interpolated dots we can afford
       if(budget < 0) budget = 0;
       var per = Math.floor(budget / gaps);
       var extra = budget - per * gaps;          // spread +1 across the first `extra` gaps
@@ -343,6 +392,72 @@
       return el;
     }
 
+    /* Resolved base-color state, cached once so the density slider can re-render
+       the spectrum WITHOUT re-fetching / re-extracting the product image. */
+    var _baseList = null;    // [{hex,name,pct}] real base colors (deduped, sorted)
+    var _hasPct = false;
+    var _legEl = null, _barEl = null;  // legend + proportion-bar elements (built once)
+
+    /* Rebuild ONLY the dot spectrum (cards) from the cached base list at the current
+       `dotTarget`. Called on first render and on every slider change. Every real base
+       color is kept; interpolated dots fill the remaining budget, evenly across gaps.
+       Legend + proportion bar reflect the real base colors and are built once (they
+       don't change with the dot count). */
+    function paintSpectrum(){
+      if(!_baseList || !_baseList.length) return;
+      var list = _baseList;
+      var stepsPerGap = buildSteps(list.length, dotTarget);
+      var frag=document.createDocumentFragment();
+      for(var i=0;i<list.length;i++){
+        frag.appendChild(makeSwatch(list[i].hex,{base:true,name:list[i].name,pct:list[i].pct}));
+        if(i<list.length-1){
+          var steps = stepsPerGap[i] || 0;
+          for(var k=1;k<=steps;k++){
+            var t=k/(steps+1);
+            frag.appendChild(makeSwatch(mix(list[i].hex,list[i+1].hex,t),{base:false}));
+          }
+        }
+      }
+      cards.innerHTML='';           // clear prior dots (re-render), keep legend/bar intact
+      cards.appendChild(frag);
+    }
+
+    /* Wire the density slider once. Clamps the slider max to the number of dots that
+       are actually meaningful for this palette's base count (a 2-base palette can't
+       usefully show 64 distinct dots — but we keep at least the base count visible).
+       Persists to localStorage; live re-renders on input. */
+    function wireDensity(){
+      var wrap = root.querySelector('[data-dw-palette-density]');
+      if(!wrap) return;
+      var range = wrap.querySelector('[data-dw-cp-range]');
+      var out   = wrap.querySelector('[data-dw-cp-out]');
+      var minus = wrap.querySelector('[data-dw-cp-minus]');
+      var plus  = wrap.querySelector('[data-dw-cp-plus]');
+      if(!range) return;
+      // A single base color has nothing to interpolate — hide the slider entirely.
+      if(_baseList.length < 2){ wrap.hidden = true; return; }
+      wrap.hidden = false;
+      // clamp current target into [MIN_DOTS, MAX_DOTS]
+      if(dotTarget < MIN_DOTS) dotTarget = MIN_DOTS;
+      if(dotTarget > MAX_DOTS) dotTarget = MAX_DOTS;
+      range.min = String(MIN_DOTS); range.max = String(MAX_DOTS);
+      range.value = String(dotTarget);
+      if(out) out.textContent = String(dotTarget);
+      function apply(v, persist){
+        v = parseInt(v,10); if(isNaN(v)) v = DEFAULT_DOTS;
+        if(v < MIN_DOTS) v = MIN_DOTS; if(v > MAX_DOTS) v = MAX_DOTS;
+        dotTarget = v;
+        range.value = String(v);
+        if(out) out.textContent = String(v);
+        if(persist) writeDotCount(v);
+        paintSpectrum();
+      }
+      range.addEventListener('input',  function(){ apply(range.value, false); });
+      range.addEventListener('change', function(){ apply(range.value, true); });  // persist on release
+      if(minus) minus.addEventListener('click', function(){ apply(dotTarget-1, true); });
+      if(plus)  plus.addEventListener('click',  function(){ apply(dotTarget+1, true); });
+    }
+
     function render(colors){
       if(!colors || !colors.length) return false;
       /* de-dupe by hex/name, keep order; resolve each to a real hex */
@@ -357,29 +472,19 @@
       if(!list.length) return false;
       var hasPct = list.some(function(c){return c.pct!=null;});
       if(hasPct) list.sort(function(a,b){return (b.pct||0)-(a.pct||0);});
-      /* Keep the real base colors. Legacy cap was 8; the total-dot cap
-         (MAX_SWATCHES) is now the real limiter, but if bases alone ever exceed it
-         (they're sorted pct-desc) keep only the MAX_SWATCHES most-dominant. */
-      list = list.slice(0, Math.min(8, MAX_SWATCHES));
+      /* Keep the real base colors (still capped at ≤8 dominant hues — bases are the
+         pattern's actual colors; interpolation between them fills the slider budget). */
+      list = list.slice(0, 8);
 
-      /* Build the spectrum: each real base color, then an EVENLY-DISTRIBUTED number
-         of interpolated dots to the next base, so the TOTAL rendered dots
-         (base + interpolated) never exceeds MAX_SWATCHES. (Steve 2026-07-09c) */
-      var stepsPerGap = buildSteps(list.length);
-      var frag=document.createDocumentFragment();
-      for(var i=0;i<list.length;i++){
-        frag.appendChild(makeSwatch(list[i].hex,{base:true,name:list[i].name,pct:list[i].pct}));
-        if(i<list.length-1){
-          var steps = stepsPerGap[i] || 0;
-          for(var k=1;k<=steps;k++){
-            var t=k/(steps+1);
-            frag.appendChild(makeSwatch(mix(list[i].hex,list[i+1].hex,t),{base:false}));
-          }
-        }
-      }
-      cards.appendChild(frag);
+      /* Cache the resolved base list + wire the slider, then paint the spectrum at
+         the current (persisted) dot target. Slider re-renders call paintSpectrum
+         directly — no image re-extraction. (Steve 2026-07-09d) */
+      _baseList = list; _hasPct = hasPct;
+      wireDensity();
+      paintSpectrum();
 
-      /* named legend for the real colors (so names/% stay available, rows stay even) */
+      /* named legend for the real colors (so names/% stay available, rows stay even).
+         Built ONCE — independent of the dot count. */
       var named=list.filter(function(c){return c.name;});
       if(named.length){
         var leg=document.createElement('div'); leg.className='dw-color-palette__legend';
@@ -396,15 +501,15 @@
           it.innerHTML='<i style="background:'+c.hex+'"></i>'+c.name+(c.pct!=null?' <b style="font-weight:600;color:#8a8577">'+c.pct+'%</b>':'');
           leg.appendChild(it);
         });
-        root.appendChild(leg);
+        root.appendChild(leg); _legEl=leg;
       }
 
-      /* proportion bar when we have percentages */
+      /* proportion bar when we have percentages (reflects the real base colors). */
       if(hasPct){
         var bar=document.createElement('div'); bar.className='dw-color-palette__bar';
         var tot=list.reduce(function(s,c){return s+(c.pct||0);},0)||1;
         list.forEach(function(c){ var sp=document.createElement('span'); sp.style.background=c.hex; sp.style.flex=(c.pct||0)/tot; bar.appendChild(sp); });
-        root.appendChild(bar);
+        root.appendChild(bar); _barEl=bar;
       }
       return true;
     }

← 83701d75 stage: 20-dot color-palette cap — LIVE push script + pending  ·  back to Designer Wallcoverings  ·  color-palette: density slider anchor — dots become user-adju be3bcbd1 →