← 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
M shopify/scripts/cadence/cadence-import.jsM shopify/theme-LIVE-591-freshpull-20260709/snippets/color-palette.liquid
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">−</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 →