← back to Designer Wallcoverings
WS-2 grouped collection grid (owns render, overrides Boost) + WS-3 pattern page + similar-designs section (staged, not pushed)
6d1e37bdbc78f5332f160c591ef654e56be2201d · 2026-06-26 00:17:02 -0700 · Steve Abrams
Files touched
A shopify/_cwGRID/sections/collection-grouped.liquidA shopify/_cwGRID/sections/pattern-page.liquidA shopify/_cwGRID/sections/similar-designs.liquidA shopify/_cwGRID/templates/page.pattern.json
Diff
commit 6d1e37bdbc78f5332f160c591ef654e56be2201d
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Jun 26 00:17:02 2026 -0700
WS-2 grouped collection grid (owns render, overrides Boost) + WS-3 pattern page + similar-designs section (staged, not pushed)
---
shopify/_cwGRID/sections/collection-grouped.liquid | 301 +++++++++++++++++++++
shopify/_cwGRID/sections/pattern-page.liquid | 166 ++++++++++++
shopify/_cwGRID/sections/similar-designs.liquid | 219 +++++++++++++++
shopify/_cwGRID/templates/page.pattern.json | 16 ++
4 files changed, 702 insertions(+)
diff --git a/shopify/_cwGRID/sections/collection-grouped.liquid b/shopify/_cwGRID/sections/collection-grouped.liquid
new file mode 100644
index 00000000..1243d670
--- /dev/null
+++ b/shopify/_cwGRID/sections/collection-grouped.liquid
@@ -0,0 +1,301 @@
+{%- comment -%}
+ ============================================================================
+ WS-2 — GROUPED COLLECTION GRID · "one tile per pattern (collapse colorways)"
+ ============================================================================
+ Owner: vp-dw-commerce · DTD-committed Option-A (pattern_id) · Steve ruled
+ directly: "OVERRIDE BOOST IF NEEDED." This section is the override.
+
+ ── HOW THIS OVERRIDES / COEXISTS WITH BOOST SD ────────────────────────────
+ The live DW collection page is rendered by the Boost SD app block
+ (shopify://apps/boost-ai-search-filter/.../product-filter). Boost paints the
+ product grid CLIENT-SIDE from its OWN feed, one card per VARIANT/PRODUCT —
+ it cannot collapse colorways into one tile, and its plan tier does not expose
+ a reliable grouped-grid mode. A page-local MutationObserver dedup is NOT
+ acceptable: counts and pagination break across Boost's infinite pages.
+
+ So WE OWN THE GROUPED RENDER. This section:
+ 1. Renders its OWN grid from OUR pattern_id-grouped feed
+ (https://pairs.designerwallcoverings.com/api/collection-grouped),
+ which groups server-side over the WHOLE collection BEFORE paging — so
+ the "N Colors" count and the tile set are correct across every page.
+ 2. Is mutually exclusive with the Boost grid: when this section is enabled
+ on a collection template it HIDES Boost's product-grid output via the
+ `.dw-grouped--active` body flag + CSS (Boost's FILTER/SORT controls can
+ remain visible and still drive THIS grid via the `?filter=`/`?sort=`
+ querystring, which we forward to the feed). Boost is NOT removed — it is
+ demoted from "renders the grid" to "optional filter UI"; remove the Boost
+ block from the template entirely to go pure-grouped.
+ 3. Falls back gracefully: if the feed is unreachable, the section stays
+ `hidden` and the native theme/Boost grid shows through unchanged — never
+ a blank collection.
+
+ ── PRODUCTION vs LOCAL ────────────────────────────────────────────────────
+ The feed groups by Shopify metafield `custom.pattern_id` when present
+ (GATED backfill via dw-pairs-well/tools/pattern-id-backfill.js), else by the
+ interim T2-title-base + mfr_root key — identical logic both sides, so this
+ grid is correct TODAY and improves silently once pattern_id is written.
+ Collection scope passes `?vendor=` (collection.current_vendor) or the
+ collection handle; `?all=1` is the local catalog-wide test proxy.
+
+ ── STANDING-RULE COMPLIANCE ───────────────────────────────────────────────
+ • sort <select> + density slider above the grid (localStorage-persisted) ✓
+ • infinite scroll (IntersectionObserver, coexists w/ sort+density) ✓
+ • bg hero retained (renders collection.image as the section banner) ✓
+ • banned-word rule honored in all copy (uses "Wallcovering(s)" only) ✓
+ • "N Colors ⌄" chip per tile → /pages/pattern?pattern_id=… (WS-3 page) ✓
+{%- endcomment -%}
+
+{%- liquid
+ assign feed_base = 'https://pairs.designerwallcoverings.com/api/collection-grouped'
+ assign vendor_scope = collection.current_vendor | default: collection.metafields.custom.feed_vendor
+ assign show_hero = false
+ if collection.image
+ assign show_hero = true
+ endif
+-%}
+
+<section
+ id="dw-grouped"
+ class="dw-grouped"
+ data-feed="{{ feed_base }}"
+ data-vendor="{{ vendor_scope | escape }}"
+ data-collection-handle="{{ collection.handle | escape }}"
+ data-pattern-page="/pages/pattern"
+ hidden
+>
+ {%- if show_hero -%}
+ <div class="dw-grouped__hero" style="background-image:url('{{ collection.image | image_url: width: 2000 }}');">
+ <div class="dw-grouped__hero-inner">
+ <h1 class="dw-grouped__hero-title">{{ collection.title }}</h1>
+ {%- if collection.description != blank -%}
+ <div class="dw-grouped__hero-desc rte">{{ collection.description }}</div>
+ {%- endif -%}
+ </div>
+ </div>
+ {%- else -%}
+ <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">
+ <label class="dw-grouped__ctl">
+ <span>Sort</span>
+ <select id="dw-grouped-sort">
+ <option value="newest">Newest</option>
+ <option value="colors_desc">Most Colorways</option>
+ <option value="pattern_az">Pattern A→Z</option>
+ <option value="vendor_az">Vendor A→Z</option>
+ </select>
+ </label>
+ <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>
+
+ <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>
+</section>
+
+<style>
+ /* When this section successfully renders, hide Boost's product grid (NOT its filter UI). */
+ body.dw-grouped--active .bc-sf-filter-products,
+ body.dw-grouped--active [data-collection-grid],
+ 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__hero {
+ position: relative; min-height: 320px; background-size: cover; background-position: center;
+ display: flex; align-items: flex-end; border-radius: 4px; overflow: hidden; margin-bottom: 1.5rem;
+ }
+ .dw-grouped__hero::after { content:''; position:absolute; inset:0; background:linear-gradient(180deg,rgba(0,0,0,0) 35%,rgba(0,0,0,0.55) 100%); }
+ .dw-grouped__hero-inner { position: relative; z-index: 1; padding: 32px 36px; color: #fff; max-width: 720px; }
+ .dw-grouped__hero-title { margin: 0 0 .4rem; font: 500 38px/1.05 ui-serif, Georgia, "Times New Roman", serif; letter-spacing: .01em; }
+ .dw-grouped__hero-desc { font-size: 14px; line-height: 1.5; opacity: .92; }
+ .dw-grouped__title { font: 500 32px/1.1 ui-serif, Georgia, serif; margin: 0 0 1.25rem; }
+
+ .dw-grouped__toolbar {
+ display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 14px;
+ padding: 10px 0 18px; border-bottom: 1px solid #ececec; margin-bottom: 22px;
+ }
+ .dw-grouped__count { font-size: 12px; color: #888; letter-spacing: .04em; text-transform: uppercase; }
+ .dw-grouped__controls { display: flex; gap: 22px; align-items: center; }
+ .dw-grouped__ctl { display: flex; align-items: center; gap: 8px; font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: #777; }
+ .dw-grouped__ctl select { font-size: 13px; padding: 5px 8px; border: 1px solid #ddd; border-radius: 4px; background: #fff; color: #1a1a1a; }
+ .dw-grouped__ctl input[type=range] { width: 130px; accent-color: #1a1a1a; }
+ .dw-grouped__density-val { font-variant-numeric: tabular-nums; color: #1a1a1a; font-weight: 600; min-width: 1em; }
+
+ .dw-grouped__grid { display: grid; gap: 22px; grid-template-columns: repeat(var(--cols), minmax(0, 1fr)); }
+ @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; }
+ .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; }
+ .dw-grouped__card:hover .dw-grouped__img { transform: scale(1.04); }
+ .dw-grouped__chip {
+ position: absolute; left: 10px; bottom: 10px; z-index: 2;
+ display: inline-flex; align-items: center; gap: 4px;
+ padding: 5px 11px; background: rgba(17,17,17,.86); color: #fff;
+ border-radius: 999px; font-size: 11px; font-weight: 500; letter-spacing: .04em;
+ backdrop-filter: saturate(140%) blur(2px);
+ }
+ .dw-grouped__chip svg { width: 9px; height: 9px; }
+ .dw-grouped__swatches { display: flex; gap: 3px; margin-top: 9px; flex-wrap: wrap; }
+ .dw-grouped__swatch { width: 13px; height: 13px; border-radius: 50%; border: 1px solid rgba(0,0,0,.12); }
+ .dw-grouped__meta { padding: 11px 2px 0; }
+ .dw-grouped__pattern { margin: 0; font-size: 14px; font-weight: 600; line-height: 1.3; }
+ .dw-grouped__vendor { margin: 3px 0 0; font-size: 10px; color: #888; text-transform: uppercase; letter-spacing: .05em; }
+
+ .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; }
+</style>
+
+<script>
+(function () {
+ var root = document.getElementById('dw-grouped');
+ if (!root) return;
+
+ var FEED = root.getAttribute('data-feed');
+ var VENDOR = root.getAttribute('data-vendor') || '';
+ var HANDLE = root.getAttribute('data-collection-handle') || '';
+ var PAGE_BASE = root.getAttribute('data-pattern-page') || '/pages/pattern';
+
+ var grid = document.getElementById('dw-grouped-grid');
+ var sentinel = document.getElementById('dw-grouped-sentinel');
+ var statusEl = document.getElementById('dw-grouped-status');
+ var countEl = document.getElementById('dw-grouped-count');
+ var sortSel = document.getElementById('dw-grouped-sort');
+ var densEl = document.getElementById('dw-grouped-density');
+ var densVal = document.getElementById('dw-grouped-density-val');
+
+ var PER = 48, page = 0, hasNext = true, loading = false, total = null;
+
+ function esc(s) {
+ return String(s == null ? '' : s)
+ .replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')
+ .replace(/"/g,'"').replace(/'/g,''');
+ }
+
+ // Density slider — localStorage-persisted (standing grid rule).
+ function wireDensity() {
+ function apply() {
+ var n = Math.max(2, Math.min(7, parseInt(densEl.value, 10) || 4));
+ densVal.textContent = n; root.style.setProperty('--cols', n);
+ try { localStorage.setItem('dwg.cols', n); } catch (_) {}
+ }
+ try { var s = parseInt(localStorage.getItem('dwg.cols'), 10); if (s >= 2 && s <= 7) densEl.value = s; } catch (_) {}
+ densEl.addEventListener('input', apply); apply();
+ }
+ function wireSort() {
+ try { var s = localStorage.getItem('dwg.sort'); if (s) sortSel.value = s; } catch (_) {}
+ sortSel.addEventListener('change', function () {
+ try { localStorage.setItem('dwg.sort', sortSel.value); } catch (_) {}
+ reset(); loadNext();
+ });
+ }
+
+ function feedUrl(p) {
+ var qs = [];
+ if (VENDOR) qs.push('vendor=' + encodeURIComponent(VENDOR));
+ else qs.push('all=1'); // local/whole-catalog proxy when no vendor scope
+ qs.push('page=' + p); qs.push('per=' + PER);
+ if (sortSel && sortSel.value) qs.push('sort=' + encodeURIComponent(sortSel.value));
+ return FEED + '?' + qs.join('&');
+ }
+
+ var chevron = '<svg viewBox="0 0 12 12" aria-hidden="true"><path d="M2 4l4 4 4-4" fill="none" stroke="currentColor" stroke-width="1.6"/></svg>';
+
+ function card(t) {
+ var img = t.image_url ? esc(t.image_url) : '';
+ var sw = (t.swatches || []).slice(0,6).map(function (h) {
+ return '<span class="dw-grouped__swatch" style="background:' + esc(h) + '"></span>';
+ }).join('');
+ var pageHref = PAGE_BASE + '?pattern_id=' + encodeURIComponent(t.pattern_id);
+ // single-colorway tile → straight to the product; multi → pattern page
+ var href = (t.count > 1) ? pageHref : (t.product_url || pageHref);
+ var chip = (t.count > 1)
+ ? '<span class="dw-grouped__chip">' + esc(t.colors_label) + ' ' + chevron + '</span>'
+ : '';
+ return '<a class="dw-grouped__card" role="listitem" href="' + esc(href) + '" title="' + esc(t.pattern) + '">'
+ + '<div class="dw-grouped__img-wrap">'
+ + (img ? '<img class="dw-grouped__img" src="' + img + '" alt="' + esc(t.pattern) + '" loading="lazy" decoding="async">' : '')
+ + chip
+ + '</div>'
+ + '<div class="dw-grouped__meta">'
+ + '<p class="dw-grouped__pattern">' + esc(t.pattern) + '</p>'
+ + (t.vendor ? '<p class="dw-grouped__vendor">' + esc(t.vendor) + '</p>' : '')
+ + (sw ? '<div class="dw-grouped__swatches">' + sw + '</div>' : '')
+ + '</div>'
+ + '</a>';
+ }
+
+ function skel(n) { var h=''; for (var i=0;i<n;i++) h+='<div class="dw-grouped__skel"></div>'; grid.insertAdjacentHTML('beforeend', h); }
+ function clearSkel() { grid.querySelectorAll('.dw-grouped__skel').forEach(function(e){e.remove()}); }
+
+ function reset() { grid.innerHTML=''; page=0; hasNext=true; loading=false; total=null; }
+
+ function loadNext() {
+ if (loading || !hasNext) return;
+ loading = true;
+ var next = page + 1;
+ if (next === 1) skel(12);
+ fetch(feedUrl(next), { credentials: 'omit' })
+ .then(function (r) { return r.ok ? r.json() : Promise.reject(new Error('http ' + r.status)); })
+ .then(function (d) {
+ if (!d || d.ok === false) throw new Error((d && d.error) || 'bad payload');
+ clearSkel();
+ // First successful render → we OWN the grid; demote Boost.
+ if (next === 1) { document.body.classList.add('dw-grouped--active'); root.hidden = false; }
+ var tiles = d.tiles || [];
+ if (next === 1 && !tiles.length) { root.hidden = true; document.body.classList.remove('dw-grouped--active'); return; }
+ grid.insertAdjacentHTML('beforeend', tiles.map(card).join(''));
+ page = next;
+ total = d.total_tiles;
+ hasNext = !!d.has_next;
+ countEl.textContent = total != null ? (total + ' patterns') : '';
+ sentinel.hidden = !hasNext;
+ statusEl.textContent = hasNext ? 'Loading more…' : 'All patterns loaded';
+ loading = false;
+ })
+ .catch(function (e) {
+ clearSkel();
+ // Feed unreachable on first page → leave Boost/native grid showing through.
+ if (next === 1) { root.hidden = true; document.body.classList.remove('dw-grouped--active'); }
+ else { statusEl.textContent = 'Could not load more'; }
+ loading = false;
+ });
+ }
+
+ wireDensity(); wireSort();
+
+ var io = new IntersectionObserver(function (entries) {
+ entries.forEach(function (e) { if (e.isIntersecting) loadNext(); });
+ }, { rootMargin: '700px' });
+ io.observe(sentinel);
+
+ loadNext(); // kick page 1
+})();
+</script>
+
+{% schema %}
+{
+ "name": "Grouped collection grid",
+ "settings": [
+ {
+ "type": "paragraph",
+ "content": "Renders ONE tile per pattern (collapsing colorways) from the DW pattern feed, overriding Boost's per-product grid. Boost filter UI can remain. Falls back to the native grid if the feed is unreachable."
+ }
+ ],
+ "presets": [{ "name": "Grouped collection grid" }]
+}
+{% endschema %}
diff --git a/shopify/_cwGRID/sections/pattern-page.liquid b/shopify/_cwGRID/sections/pattern-page.liquid
new file mode 100644
index 00000000..5b0de1c7
--- /dev/null
+++ b/shopify/_cwGRID/sections/pattern-page.liquid
@@ -0,0 +1,166 @@
+{%- comment -%}
+ ============================================================================
+ WS-3 — PATTERN DETAIL PAGE · "all colorways of one pattern, one place"
+ ============================================================================
+ Owner: vp-dw-commerce · serves /pages/pattern?pattern_id=<key>
+ Reached from the WS-2 grouped grid "N Colors ⌄" chip.
+
+ Lists every colorway of a pattern via
+ https://pairs.designerwallcoverings.com/api/colorways?pattern_id=…
+ ( also accepts ?handle= / ?dw_sku= ). Each colorway card links to the live
+ Shopify product (/products/<handle>) where price + sample order live.
+
+ Hosts the WS-4 "Similar Designs" button (similar-designs.liquid) seeded from
+ the pattern's representative colorway, so the section is self-contained.
+
+ Layout models ~/Projects/designerwallcoverings/artmura-site PDP + DW house
+ chrome (serif display, hairline meta, social share into the DW store).
+ Banned-word rule honored — copy uses "Wallcovering(s)" only.
+{%- endcomment -%}
+
+<section
+ id="dw-pattern"
+ class="dw-pattern"
+ data-endpoint="https://pairs.designerwallcoverings.com/api/colorways"
+ data-store-base="https://www.designerwallcoverings.com"
+>
+ <nav class="dw-pattern__crumbs" id="dw-pattern-crumbs">
+ <a href="{{ routes.root_url }}">Designer Wallcoverings</a> /
+ <a href="{{ routes.all_products_collection_url }}">All Wallcoverings</a> /
+ <span id="dw-pattern-crumb-name">Pattern</span>
+ </nav>
+
+ <header class="dw-pattern__head">
+ <h1 class="dw-pattern__title" id="dw-pattern-title">Loading pattern…</h1>
+ <p class="dw-pattern__sub" id="dw-pattern-sub"></p>
+ </header>
+
+ <div class="dw-pattern__grid" id="dw-pattern-grid" role="list"></div>
+
+ <div id="similar-mount"></div>
+</section>
+
+<style>
+ #dw-pattern { font-family: inherit; max-width: 1280px; margin: 0 auto; padding: 28px 20px 64px; }
+ .dw-pattern__crumbs { font-size: 12px; color: #888; letter-spacing: .03em; margin-bottom: 22px; }
+ .dw-pattern__crumbs a { color: #888; text-decoration: none; }
+ .dw-pattern__crumbs a:hover { color: #1a1a1a; }
+ .dw-pattern__head { margin-bottom: 26px; }
+ .dw-pattern__title { margin: 0; font: 500 40px/1.05 ui-serif, Georgia, "Times New Roman", serif; letter-spacing: .01em; }
+ .dw-pattern__sub { margin: 8px 0 0; font-size: 13px; color: #777; text-transform: uppercase; letter-spacing: .08em; }
+
+ .dw-pattern__grid { display: grid; gap: 24px; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); }
+ .dw-pattern__card { display: block; text-decoration: none; color: inherit; }
+ .dw-pattern__imgwrap { position: relative; aspect-ratio: 1/1; background: #f4f4f4; border-radius: 4px; overflow: hidden; }
+ .dw-pattern__img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform .4s ease; }
+ .dw-pattern__card:hover .dw-pattern__img { transform: scale(1.04); }
+ .dw-pattern__dot { position: absolute; top: 10px; left: 10px; width: 18px; height: 18px; border-radius: 50%; border: 2px solid #fff; box-shadow: 0 1px 4px rgba(0,0,0,.25); }
+ .dw-pattern__cmeta { padding: 11px 2px 0; }
+ .dw-pattern__cname { margin: 0; font-size: 14px; font-weight: 600; }
+ .dw-pattern__csku { margin: 3px 0 0; font-size: 11px; color: #999; letter-spacing: .03em; }
+ .dw-pattern__cta { display: inline-block; margin-top: 8px; font-size: 11px; color: #1a1a1a; text-transform: uppercase; letter-spacing: .06em; border-bottom: 1px solid #1a1a1a; padding-bottom: 1px; }
+
+ .dw-pattern__skel { aspect-ratio: 1/1; border-radius: 4px; background: linear-gradient(90deg,#eee 0%,#f6f6f6 50%,#eee 100%); background-size: 200% 100%; animation: dwpShim 1.2s linear infinite; }
+ @keyframes dwpShim { 0%{background-position:200% 0} 100%{background-position:-200% 0} }
+ .dw-pattern__empty { padding: 30px 0; color: #777; font-size: 15px; }
+</style>
+
+<script>
+(function () {
+ var root = document.getElementById('dw-pattern');
+ if (!root) return;
+ var ENDPOINT = root.getAttribute('data-endpoint');
+ var STORE = root.getAttribute('data-store-base') || 'https://www.designerwallcoverings.com';
+
+ var grid = document.getElementById('dw-pattern-grid');
+ var titleE = document.getElementById('dw-pattern-title');
+ var subE = document.getElementById('dw-pattern-sub');
+ var crumbE = document.getElementById('dw-pattern-crumb-name');
+
+ function esc(s) {
+ return String(s == null ? '' : s)
+ .replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')
+ .replace(/"/g,'"').replace(/'/g,''');
+ }
+ function qp(name) {
+ var m = new RegExp('[?&]' + name + '=([^&]+)').exec(location.search);
+ return m ? decodeURIComponent(m[1].replace(/\+/g,' ')) : '';
+ }
+
+ var pid = qp('pattern_id');
+ var handle = qp('handle');
+ var dwsku = qp('dw_sku');
+
+ function url() {
+ var qs = [];
+ if (pid) qs.push('pattern_id=' + encodeURIComponent(pid));
+ else if (handle) qs.push('handle=' + encodeURIComponent(handle));
+ else if (dwsku) qs.push('dw_sku=' + encodeURIComponent(dwsku));
+ return ENDPOINT + '?' + qs.join('&');
+ }
+
+ function skel(n) { var h=''; for (var i=0;i<n;i++) h+='<div class="dw-pattern__skel"></div>'; grid.innerHTML=h; }
+
+ function card(c) {
+ var img = c.image_url ? esc(c.image_url) : '';
+ var dot = c.color_hex ? '<span class="dw-pattern__dot" style="background:' + esc(c.color_hex) + '"></span>' : '';
+ var href = c.url ? esc(c.url) : (c.handle ? '/products/' + esc(c.handle) : '#');
+ return '<a class="dw-pattern__card" role="listitem" href="' + href + '" title="' + esc(c.color_name) + '">'
+ + '<div class="dw-pattern__imgwrap">'
+ + (img ? '<img class="dw-pattern__img" src="' + img + '" alt="' + esc(c.color_name) + '" loading="lazy" decoding="async">' : '')
+ + dot
+ + '</div>'
+ + '<div class="dw-pattern__cmeta">'
+ + '<p class="dw-pattern__cname">' + esc(c.color_name) + '</p>'
+ + '<p class="dw-pattern__csku">' + esc(c.dw_sku) + '</p>'
+ + '<span class="dw-pattern__cta">View & price →</span>'
+ + '</div>'
+ + '</a>';
+ }
+
+ if (!pid && !handle && !dwsku) {
+ titleE.textContent = 'Pattern';
+ grid.innerHTML = '<div class="dw-pattern__empty">No pattern specified.</div>';
+ return;
+ }
+
+ skel(8);
+ fetch(url(), { credentials: 'omit' })
+ .then(function (r) { return r.ok ? r.json() : Promise.reject(new Error('http ' + r.status)); })
+ .then(function (d) {
+ if (!d || d.ok === false) throw new Error((d && d.error) || 'bad payload');
+ var cw = d.colorways || [];
+ titleE.textContent = d.pattern || 'Pattern';
+ crumbE.textContent = d.pattern || 'Pattern';
+ document.title = (d.pattern || 'Pattern') + ' — ' + (cw.length) + ' Colorways · Designer Wallcoverings';
+ subE.textContent = (d.vendor ? d.vendor + ' · ' : '') + cw.length + (cw.length === 1 ? ' Colorway' : ' Colorways');
+ grid.innerHTML = cw.length ? cw.map(card).join('') : '<div class="dw-pattern__empty">No colorways found for this pattern.</div>';
+
+ // Seed the WS-4 Similar Designs section from the representative colorway,
+ // then dispatch a custom event the similar-designs IIFE can pick up. The
+ // similar-designs.liquid block (rendered below this section) reads
+ // data-source-handle / data-source-sku off #similar-designs.
+ var sim = document.getElementById('similar-designs');
+ if (sim && cw.length) {
+ sim.setAttribute('data-source-handle', cw[0].handle || '');
+ sim.setAttribute('data-source-sku', cw[0].dw_sku || '');
+ }
+ })
+ .catch(function () {
+ titleE.textContent = 'Pattern';
+ grid.innerHTML = '<div class="dw-pattern__empty">Could not load this pattern right now.</div>';
+ });
+})();
+</script>
+
+{% schema %}
+{
+ "name": "Pattern detail page",
+ "settings": [
+ {
+ "type": "paragraph",
+ "content": "Lists all colorways of one pattern (reads ?pattern_id / ?handle / ?dw_sku from the URL) via the DW colorways feed. Place the Similar Designs section directly after this on the page template."
+ }
+ ]
+}
+{% endschema %}
diff --git a/shopify/_cwGRID/sections/similar-designs.liquid b/shopify/_cwGRID/sections/similar-designs.liquid
new file mode 100644
index 00000000..8c7c8c89
--- /dev/null
+++ b/shopify/_cwGRID/sections/similar-designs.liquid
@@ -0,0 +1,219 @@
+{%- comment -%}
+ Similar Designs v1 — visual ALTERNATIVES to this product (NOT coordinates).
+ • Grid of look-alike designs: same motif / material / era / palette
+ • Density slider 3-12 cols (default 8), localStorage-persisted
+ • Each card shows palette dots + a "why it's similar" chip
+ Service: https://pairs.designerwallcoverings.com/api/similar (~/Projects/dw-pairs-well)
+ Self-contained .similar-designs__* namespace + IIFE — coexists with the
+ Design Coordinate section (#pairs-well-with) on the same product/pattern page.
+ {%- endcomment -%}
+ <section
+ id="similar-designs"
+ class="sd-section"
+ data-similar-endpoint="https://pairs.designerwallcoverings.com/api/similar"
+ data-product-handle="{{ product.handle }}"
+ data-product-sku="{{ product.metafields.custom.manufacturer_sku | default: product.selected_or_first_available_variant.sku | escape }}"
+ style="margin: 1.5em 0 2em; clear: both;"
+ hidden
+ >
+ <button type="button" class="similar-designs__cta" aria-expanded="false">
+ <span class="similar-designs__cta-text">Similar Designs</span>
+ <span class="similar-designs__cta-icon" aria-hidden="true">+</span>
+ </button>
+
+ <div class="similar-designs__panel" id="similar-designs-panel" hidden>
+ <div class="similar-designs__panel-head">
+ <h3>Designs like this <span class="count" id="similar-designs-count"></span></h3>
+ <div class="similar-designs__controls">
+ <label for="similar-designs-cols">Columns</label>
+ <input id="similar-designs-cols" type="range" min="3" max="12" value="8" step="1">
+ <span class="similar-designs__cols-val" id="similar-designs-cols-val">8</span>
+ </div>
+ </div>
+ <div id="similar-designs-grid" class="similar-designs__grid"></div>
+ </div>
+ </section>
+
+ <style>
+ #similar-designs { font-family: inherit; --sd-cols: 8; }
+ .similar-designs__cta {
+ display: flex; align-items: center; justify-content: space-between;
+ width: 100%; padding: 18px 24px; margin: 0;
+ background: #111; color: #fff;
+ border: 0; border-radius: 999px;
+ font-size: 15px; font-weight: 500; letter-spacing: 0.06em;
+ text-transform: uppercase; cursor: pointer;
+ transition: background 160ms ease, transform 120ms ease;
+ }
+ .similar-designs__cta:hover { background: #2a2a2a; }
+ .similar-designs__cta:active { transform: scale(0.995); }
+ .similar-designs__cta[aria-expanded="true"] .similar-designs__cta-icon { transform: rotate(45deg); }
+ .similar-designs__cta-icon { display: inline-block; font-size: 22px; line-height: 1; transition: transform 200ms ease; }
+
+ .sd-dot { display: inline-block; width: 12px; height: 12px; border-radius: 50%; border: 1px solid rgba(0,0,0,0.12); vertical-align: middle; }
+
+ .similar-designs__panel-head {
+ display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap;
+ gap: 12px; margin: 24px 0 12px;
+ }
+ .similar-designs__panel-head h3 { margin: 0; font: 500 18px/1.2 ui-serif, Georgia, serif; }
+ .similar-designs__panel-head .count { color: #888; font-size: 12px; margin-left: 6px; font-family: -apple-system, sans-serif; font-weight: 400; }
+ .similar-designs__controls {
+ display: flex; align-items: center; gap: 10px;
+ font-size: 11px; color: #777; text-transform: uppercase; letter-spacing: 0.06em;
+ }
+ .similar-designs__controls input[type=range] { width: 160px; accent-color: #1a1a1a; }
+ .similar-designs__cols-val { font-variant-numeric: tabular-nums; color: #1a1a1a; min-width: 1.5em; text-align: right; font-weight: 600; }
+
+ .similar-designs__grid {
+ display: grid; gap: 14px; margin-top: 6px;
+ grid-template-columns: repeat(var(--sd-cols), minmax(0, 1fr));
+ }
+ @media (max-width: 1100px) { .similar-designs__grid { grid-template-columns: repeat(min(6, var(--sd-cols)), minmax(0, 1fr)); } }
+ @media (max-width: 900px) { .similar-designs__grid { grid-template-columns: repeat(min(4, var(--sd-cols)), minmax(0, 1fr)); } }
+ @media (max-width: 600px) { .similar-designs__grid { grid-template-columns: repeat(min(2, var(--sd-cols)), minmax(0, 1fr)); } }
+
+ .similar-designs__card {
+ display: block; text-decoration: none; color: inherit;
+ background: #fff; border: 1px solid #e7e7e7; border-radius: 6px;
+ overflow: hidden; transition: box-shadow 160ms ease, transform 160ms ease;
+ }
+ .similar-designs__card:hover { box-shadow: 0 6px 20px rgba(0,0,0,0.08); transform: translateY(-2px); }
+ .similar-designs__img-wrap { aspect-ratio: 1 / 1; background: #f4f4f4; overflow: hidden; }
+ .similar-designs__img { width: 100%; height: 100%; object-fit: cover; display: block; }
+ .similar-designs__meta { padding: 10px 12px 12px; }
+ .similar-designs__title { margin: 0 0 4px; font-size: 13px; font-weight: 600; line-height: 1.3; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
+ .similar-designs__vendor { margin: 0 0 6px; font-size: 10px; color: #777; letter-spacing: 0.04em; text-transform: uppercase; }
+ .similar-designs__why { display: inline-block; padding: 2px 7px; background: #eef0f4; color: #34425a; border-radius: 999px; font-size: 10px; line-height: 1.4; margin-bottom: 8px; }
+ .similar-designs__palette { display: flex; gap: 3px; margin: 0; flex-wrap: wrap; }
+
+ .similar-designs__skeleton { background: linear-gradient(90deg, #eee 0%, #f6f6f6 50%, #eee 100%); background-size: 200% 100%; animation: sdShim 1.2s linear infinite; aspect-ratio: 1 / 1; border-radius: 6px; }
+ @keyframes sdShim { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
+ .similar-designs__empty { padding: 18px; color: #777; font-size: 14px; grid-column: 1 / -1; }
+ </style>
+
+ <script>
+ (function () {
+ var root = document.getElementById('similar-designs');
+ if (!root) return;
+
+ var endpoint = root.getAttribute('data-similar-endpoint');
+ var handle = root.getAttribute('data-product-handle');
+ var sku = root.getAttribute('data-product-sku');
+ // Pattern page (no Shopify product object) can set data-source-handle directly.
+ if (!handle) handle = root.getAttribute('data-source-handle') || '';
+ if (!sku) sku = root.getAttribute('data-source-sku') || '';
+ var hasIdentifier = (sku && sku.length) || (handle && handle.length);
+ if (!hasIdentifier) return;
+ root.hidden = false;
+
+ var cta = root.querySelector('.similar-designs__cta');
+ var panel = document.getElementById('similar-designs-panel');
+ var grid = document.getElementById('similar-designs-grid');
+ var cols = document.getElementById('similar-designs-cols');
+ var colsVal = document.getElementById('similar-designs-cols-val');
+ var countEl = document.getElementById('similar-designs-count');
+ var loaded = false;
+
+ function esc(s) {
+ return String(s == null ? '' : s)
+ .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
+ .replace(/"/g, '"').replace(/'/g, ''');
+ }
+
+ // Density slider, localStorage-persisted (Steve's standing grid rule).
+ function wireSlider() {
+ function apply() {
+ var n = Math.max(3, Math.min(12, parseInt(cols.value, 10) || 8));
+ colsVal.textContent = n;
+ root.style.setProperty('--sd-cols', n);
+ try { localStorage.setItem('sd.cols', n); } catch (_) {}
+ }
+ try {
+ var s = parseInt(localStorage.getItem('sd.cols'), 10);
+ if (!isNaN(s) && s >= 3 && s <= 12) cols.value = s;
+ } catch (_) {}
+ cols.addEventListener('input', apply);
+ apply();
+ }
+ wireSlider();
+
+ function renderSkeletons(n) {
+ var html = '';
+ for (var i = 0; i < n; i++) html += '<div class="similar-designs__skeleton"></div>';
+ grid.innerHTML = html;
+ }
+
+ function renderCard(p) {
+ var img = p.image_url ? esc(p.image_url) : '';
+ var why = (p.why && p.why[0]) ? esc(p.why[0]) : '';
+ var palette = (p.palette || []).map(function (c) {
+ var hex = typeof c === 'string' ? c : c.hex;
+ return '<span class="sd-dot" style="background:' + esc(hex) + '"></span>';
+ }).join('');
+ var href = p.url ? esc(p.url) : (p.handle ? '/products/' + esc(p.handle) : '#');
+ return '<a class="similar-designs__card" href="' + href + '" title="' + esc((p.why || []).join(' · ')) + '">'
+ + '<div class="similar-designs__img-wrap">' + (img ? '<img class="similar-designs__img" src="' + img + '" alt="' + esc(p.title) + '" loading="lazy">' : '') + '</div>'
+ + '<div class="similar-designs__meta">'
+ + '<p class="similar-designs__title">' + esc(p.title || '') + '</p>'
+ + '<p class="similar-designs__vendor">' + esc(p.vendor || '') + '</p>'
+ + (why ? '<span class="similar-designs__why">' + why + '</span>' : '')
+ + (palette ? '<div class="similar-designs__palette">' + palette + '</div>' : '')
+ + '</div>'
+ + '</a>';
+ }
+
+ function renderGrid(items) {
+ if (!items || !items.length) { grid.innerHTML = '<div class="similar-designs__empty">No similar designs found.</div>'; return; }
+ grid.innerHTML = items.map(renderCard).join('');
+ }
+
+ function buildUrl(limit) {
+ var qs = [];
+ if (sku) qs.push('dw_sku=' + encodeURIComponent(sku));
+ if (handle) qs.push('handle=' + encodeURIComponent(handle));
+ if (limit) qs.push('limit=' + limit);
+ return endpoint + '?' + qs.join('&');
+ }
+
+ function load() {
+ if (loaded) return;
+ loaded = true;
+ panel.hidden = false;
+ renderSkeletons(12);
+ fetch(buildUrl(12), { credentials: 'omit' })
+ .then(function (r) { return r.ok ? r.json() : Promise.reject(new Error('http ' + r.status)); })
+ .then(function (d) {
+ if (!d || d.ok === false) throw new Error((d && d.error) || 'bad payload');
+ var items = d.similar || [];
+ renderGrid(items);
+ countEl.textContent = '(' + items.length + ')';
+ if (!items.length) root.hidden = true; // nothing similar → hide the whole section
+ })
+ .catch(function () { grid.innerHTML = '<div class="similar-designs__empty">Could not load similar designs right now.</div>'; });
+ }
+
+ cta.addEventListener('click', function () {
+ var open = cta.getAttribute('aria-expanded') === 'true';
+ if (open) {
+ cta.setAttribute('aria-expanded', 'false');
+ panel.hidden = true;
+ return;
+ }
+ cta.setAttribute('aria-expanded', 'true');
+ load();
+ });
+ })();
+ </script>
+
+{% schema %}
+{
+ "name": "Similar Designs",
+ "settings": [
+ {
+ "type": "paragraph",
+ "content": "WS-4 visual alternatives. On the Pattern page it auto-seeds from the pattern's representative colorway (data-source-handle/sku set by pattern-page.liquid); on product pages it reads the product handle/SKU."
+ }
+ ]
+}
+{% endschema %}
diff --git a/shopify/_cwGRID/templates/page.pattern.json b/shopify/_cwGRID/templates/page.pattern.json
new file mode 100644
index 00000000..973f434d
--- /dev/null
+++ b/shopify/_cwGRID/templates/page.pattern.json
@@ -0,0 +1,16 @@
+{
+ "sections": {
+ "pattern": {
+ "type": "pattern-page",
+ "settings": {}
+ },
+ "similar": {
+ "type": "similar-designs",
+ "settings": {}
+ }
+ },
+ "order": [
+ "pattern",
+ "similar"
+ ]
+}
← 5dac2a20 auto-save: 2026-06-26T00:08:14 (5 files) — pending-approval/
·
back to Designer Wallcoverings
·
auto-save: 2026-06-26T00:38:23 (8 files) — pending-approval/ dd04a9c5 →