← back to Designer Wallcoverings
Fix grid slider: target Boost Commerce grid + hide hover overlays
178a125e816af15aa4f8dba975112cec4c5cd108 · 2026-06-22 18:30:09 -0700 · Steve Abrams
Files touched
M shopify/_cwGRID/assets/collection-grid-density.jsM shopify/_cwGRID/assets/custom.cssM shopify/theme-5.0-duplicate/assets/dw-filter-horizontal.cssM shopify/theme-5.0-duplicate/assets/dw-filter-horizontal.jsA shopify/theme-5.0-duplicate/assets/dw-hfilter.cssA shopify/theme-5.0-duplicate/assets/dw-hfilter.jsM shopify/theme-5.0-duplicate/layout/theme.liquidM shopify/theme-5.0-duplicate/snippets/dw-boost-overrides.liquidM shopify/theme-infinite-scroll-142250278963/assets/dw-card-hover.jsA shopify/theme-infinite-scroll-142250278963/assets/dw-card-hover.js.v1-bak
Diff
commit 178a125e816af15aa4f8dba975112cec4c5cd108
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Jun 22 18:30:09 2026 -0700
Fix grid slider: target Boost Commerce grid + hide hover overlays
---
shopify/_cwGRID/assets/collection-grid-density.js | 11 +-
shopify/_cwGRID/assets/custom.css | 9 +
.../assets/dw-filter-horizontal.css | 1 +
.../assets/dw-filter-horizontal.js | 1 +
shopify/theme-5.0-duplicate/assets/dw-hfilter.css | 138 ++++++++
shopify/theme-5.0-duplicate/assets/dw-hfilter.js | 384 +++++++++++++++++++++
shopify/theme-5.0-duplicate/layout/theme.liquid | 5 +
.../snippets/dw-boost-overrides.liquid | 12 +-
.../assets/dw-card-hover.js | 130 +++++--
.../assets/dw-card-hover.js.v1-bak | 71 ++++
10 files changed, 729 insertions(+), 33 deletions(-)
diff --git a/shopify/_cwGRID/assets/collection-grid-density.js b/shopify/_cwGRID/assets/collection-grid-density.js
index 88650f02..7642be78 100644
--- a/shopify/_cwGRID/assets/collection-grid-density.js
+++ b/shopify/_cwGRID/assets/collection-grid-density.js
@@ -13,7 +13,10 @@
function initGridDensity() {
const slider = document.querySelector('[data-collection-grid-slider]');
- const gridContainer = document.querySelector('[data-collection-grid]');
+ // Target Boost Commerce grid first, fall back to native _cwGRID grid
+ const gridContainer =
+ document.querySelector('.boost-sd__product-list') ||
+ document.querySelector('[data-collection-grid]');
const valueDisplay = document.querySelector('[data-grid-density-value]');
if (!slider || !gridContainer) return;
@@ -34,13 +37,15 @@
});
function updateGridDensity(cols) {
- // Remove old class
+ // Remove old classes (both native and Boost Commerce)
for (let i = MIN_COLS; i <= MAX_COLS; i++) {
gridContainer.classList.remove(`grid-cols-${i}`);
+ gridContainer.classList.remove(`boost-sd__product-list-grid--${i}-col`);
}
- // Add new class
+ // Add new classes (both for compatibility)
gridContainer.classList.add(`grid-cols-${cols}`);
+ gridContainer.classList.add(`boost-sd__product-list-grid--${cols}-col`);
// Update display
if (valueDisplay) {
diff --git a/shopify/_cwGRID/assets/custom.css b/shopify/_cwGRID/assets/custom.css
index 503aa585..5251df51 100644
--- a/shopify/_cwGRID/assets/custom.css
+++ b/shopify/_cwGRID/assets/custom.css
@@ -1361,3 +1361,12 @@ input.add-to-cart:hover,
opacity: 0.9;
transform: scale(1.02);
}
+
+/* Hide Boost Commerce hover overlays on collection cards */
+.dw-hover-label {
+ display: none !important;
+}
+
+.boost-sd__product-item-image-overlay {
+ display: none !important;
+}
diff --git a/shopify/theme-5.0-duplicate/assets/dw-filter-horizontal.css b/shopify/theme-5.0-duplicate/assets/dw-filter-horizontal.css
index 10c97b6f..6bcf0fa7 100644
--- a/shopify/theme-5.0-duplicate/assets/dw-filter-horizontal.css
+++ b/shopify/theme-5.0-duplicate/assets/dw-filter-horizontal.css
@@ -1,3 +1,4 @@
+/* DEPRECATED 2026-06-22 — Approach A reverted; replaced by dw-hfilter.* (Approach B); not loaded by any template. */
/* ============================================================================
DW Horizontal Filter Bar — restyles Boost AI Search & Discovery filter groups
from a vertical LEFT SIDEBAR into a HORIZONTAL, collapsible bar across the
diff --git a/shopify/theme-5.0-duplicate/assets/dw-filter-horizontal.js b/shopify/theme-5.0-duplicate/assets/dw-filter-horizontal.js
index 5a22dbe8..b972d87a 100644
--- a/shopify/theme-5.0-duplicate/assets/dw-filter-horizontal.js
+++ b/shopify/theme-5.0-duplicate/assets/dw-filter-horizontal.js
@@ -1,3 +1,4 @@
+/* DEPRECATED 2026-06-22 — Approach A reverted; replaced by dw-hfilter.* (Approach B); not loaded by any template. */
/* ============================================================================
DW Horizontal Filter Bar — JS
Turns the Boost AI Search & Discovery vertical filter sidebar into a
diff --git a/shopify/theme-5.0-duplicate/assets/dw-hfilter.css b/shopify/theme-5.0-duplicate/assets/dw-hfilter.css
new file mode 100644
index 00000000..542e9ea4
--- /dev/null
+++ b/shopify/theme-5.0-duplicate/assets/dw-hfilter.css
@@ -0,0 +1,138 @@
+/* ============================================================================
+ DW Horizontal Filter Bar — CSS (Approach B)
+ Custom chip bar that proxies into Boost's hidden native filter tree.
+
+ STRICT SCOPING (this is what prevents re-breaking the global page layout):
+ Only TWO selector families are allowed in this file —
+ 1. body.dw-filters-horizontal .boost-sd__<specific-class>
+ (hide native tree + free the grid to full width; mirrors the proven
+ dw-filters-collapsed width rules in dw-boost-overrides.liquid 17-22)
+ 2. .dw-hfilter-* (our own unique namespace — these strings exist nowhere
+ else in the theme, so they're safe globally)
+ NO bare element/body/html/* selectors, NO .shopify-section, NO generic
+ [class*=...] substring selectors, NO unprefixed .boost-sd__* rules.
+ ============================================================================ */
+
+/* --- 1. Hide Boost's native vertical filter tree + free the grid full-width --- */
+body.dw-filters-horizontal .boost-sd__filter-tree-wrapper{display:none !important}
+body.dw-filters-horizontal .boost-sd__product-list{width:100% !important;max-width:100% !important}
+body.dw-filters-horizontal .boost-sd__filter-products-wrap{display:block !important;max-width:100% !important;width:100% !important}
+body.dw-filters-horizontal .boost-sd__filter-products-wrap > div{display:block !important;max-width:100% !important;width:100% !important}
+body.dw-filters-horizontal .boost-sd__product-list-wrapper{max-width:100% !important;width:100% !important;flex:1 !important}
+
+/* --- 2. Bar collapse: the toggle pill (in dw-boost-overrides.liquid) hides our bar --- */
+body.dw-filters-horizontal.dw-hbar-collapsed .dw-hfilter-bar{display:none !important}
+
+/* --- 3. Our own chip bar --- */
+.dw-hfilter-bar{
+ display:flex;flex-wrap:wrap;align-items:center;gap:8px;
+ position:relative;z-index:20;
+ width:100%;box-sizing:border-box;
+ margin:4px 0 14px;padding:0;
+ font-family:-apple-system,BlinkMacSystemFont,sans-serif;
+}
+
+.dw-hfilter-chip{
+ position:relative;
+ display:inline-flex;align-items:center;gap:6px;
+ padding:6px 14px;
+ font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:1.2px;
+ color:#000;background:#fff;border:1.5px solid #000;border-radius:999px;
+ cursor:pointer;user-select:none;white-space:nowrap;line-height:1;
+}
+.dw-hfilter-chip:hover{background:#f5f5f5}
+.dw-hfilter-chip.dw-hfilter-active{background:#000;color:#fff}
+.dw-hfilter-chip.dw-hfilter-open{background:#000;color:#fff}
+
+.dw-hfilter-chip-label{display:inline-block}
+
+.dw-hfilter-badge{
+ display:inline-flex;align-items:center;justify-content:center;
+ min-width:16px;height:16px;padding:0 4px;box-sizing:border-box;
+ font-size:10px;font-weight:700;letter-spacing:0;
+ background:#9B3B3B;color:#fff;border-radius:999px;
+}
+.dw-hfilter-chip.dw-hfilter-active .dw-hfilter-badge{background:#fff;color:#000}
+
+.dw-hfilter-caret{
+ width:0;height:0;
+ border-left:4px solid transparent;border-right:4px solid transparent;
+ border-top:5px solid currentColor;
+ transition:transform .15s ease;
+}
+.dw-hfilter-chip.dw-hfilter-open .dw-hfilter-caret{transform:rotate(180deg)}
+
+/* --- 4. Accordion dropdown panel --- */
+.dw-hfilter-panel{
+ display:none;
+ position:absolute;top:calc(100% + 6px);left:0;z-index:30;
+ min-width:200px;max-width:320px;max-height:340px;overflow-y:auto;
+ padding:8px;box-sizing:border-box;
+ background:#fff;border:1.5px solid #000;border-radius:10px;
+ box-shadow:0 8px 24px rgba(0,0,0,.14);
+}
+.dw-hfilter-chip.dw-hfilter-open .dw-hfilter-panel{display:block}
+
+.dw-hfilter-opt{
+ display:flex;align-items:center;gap:8px;
+ padding:7px 9px;margin:1px 0;
+ font-size:12px;font-weight:500;text-transform:none;letter-spacing:0;color:#333;
+ border-radius:7px;cursor:pointer;white-space:nowrap;
+}
+.dw-hfilter-opt:hover{background:#f3f3f3}
+.dw-hfilter-opt.dw-hfilter-opt-selected{font-weight:700;color:#000}
+
+.dw-hfilter-check{
+ display:inline-flex;align-items:center;justify-content:center;
+ width:15px;height:15px;flex:0 0 15px;
+ border:1.5px solid #999;border-radius:4px;
+ font-size:11px;line-height:1;color:#fff;background:#fff;
+}
+.dw-hfilter-opt.dw-hfilter-opt-selected .dw-hfilter-check{
+ background:#000;border-color:#000;
+}
+
+.dw-hfilter-swatch{
+ display:inline-block;width:15px;height:15px;flex:0 0 15px;
+ border-radius:50%;border:1px solid rgba(0,0,0,.2);
+}
+
+.dw-hfilter-opt-text{flex:1 1 auto;overflow:hidden;text-overflow:ellipsis}
+
+.dw-hfilter-opt-empty{
+ padding:8px 9px;font-size:12px;color:#999;font-style:italic;
+}
+
+/* Price passthrough panel: open native control hint */
+.dw-hfilter-price-note{
+ padding:8px 9px;font-size:11px;color:#666;line-height:1.4;
+}
+
+/* --- 5. Fallback toggle (only injected if the liquid pill is absent) --- */
+.dw-hfilter-toggle{
+ display:inline-flex;align-items:center;gap:6px;padding:6px 14px;
+ font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:1.2px;
+ background:#fff;border:1.5px solid #000;border-radius:999px;cursor:pointer;
+}
+.dw-hfilter-toggle:hover{background:#f5f5f5}
+
+/* --- 6. Responsiveness --- */
+@media(max-width:768px){
+ .dw-hfilter-bar{
+ flex-wrap:nowrap;overflow-x:auto;-webkit-overflow-scrolling:touch;
+ gap:6px;padding-bottom:4px;margin-bottom:10px;
+ scrollbar-width:none;
+ }
+ .dw-hfilter-bar::-webkit-scrollbar{display:none}
+ .dw-hfilter-chip{flex:0 0 auto}
+
+ /* Panels become a fixed bottom-sheet */
+ .dw-hfilter-panel{
+ position:fixed;top:auto;left:0;right:0;bottom:0;
+ width:100%;max-width:100%;min-width:0;max-height:60vh;
+ border-radius:16px 16px 0 0;border-width:1.5px 0 0 0;
+ box-shadow:0 -8px 28px rgba(0,0,0,.22);
+ padding:14px 14px calc(14px + env(safe-area-inset-bottom));
+ }
+ .dw-hfilter-opt{padding:11px 10px;font-size:14px}
+}
diff --git a/shopify/theme-5.0-duplicate/assets/dw-hfilter.js b/shopify/theme-5.0-duplicate/assets/dw-hfilter.js
new file mode 100644
index 00000000..232cf6f9
--- /dev/null
+++ b/shopify/theme-5.0-duplicate/assets/dw-hfilter.js
@@ -0,0 +1,384 @@
+/* ============================================================================
+ DW Horizontal Filter Bar — JS (Approach B: custom chip bar that proxies
+ into Boost's hidden native filter tree)
+
+ Boost AI Search & Discovery renders its filter groups as a vertical sidebar
+ (.boost-sd__filter-tree). We keep that tree in the DOM as the authoritative
+ "state engine" (hidden via dw-hfilter.css, gated by body.dw-filters-horizontal)
+ and render our OWN horizontal chip bar above the grid. On click we PROXY by
+ programmatically clicking the matching hidden native option — Boost then does
+ the real filtering (sets pf_* URL params + re-renders the grid). A
+ MutationObserver re-syncs our bar to Boost's authoritative state after every
+ re-render. We never touch the URL or own any filter state — Boost does.
+
+ Strict rules:
+ - Never cache DOM refs of native options (Boost re-renders constantly);
+ store groups by INDEX + normalized option TEXT and re-query on click.
+ - Selected/active state is ALWAYS read from Boost's native classes/aria,
+ never from our own source of truth.
+ - The collapse toggle lives in dw-boost-overrides.liquid (#dw-filter-toggle);
+ we only inject a fallback toggle if that pill does not exist.
+ ============================================================================ */
+(function () {
+ 'use strict';
+
+ if (!/\/(collections|search)/.test(location.pathname)) return;
+
+ var TREE_WRAP = '.boost-sd__filter-tree-wrapper';
+ var TREE = '.boost-sd__filter-tree';
+ var TOOLBAR = '.boost-sd__toolbar-item--product-count, .boost-sd__product-count';
+ var GRID = '.boost-sd__product-list';
+ var BAR_ID = 'dw-hfilter-bar';
+
+ /* ---- helpers -------------------------------------------------------- */
+ function norm(s) {
+ return (s || '')
+ .replace(/\s*\(\d[\d,]*\)\s*$/, '') // strip trailing count "(123)"
+ .replace(/\s+/g, ' ')
+ .trim()
+ .toLowerCase();
+ }
+
+ /* Read the option's visible label, stripping a trailing "(123)" count. */
+ function optLabel(optEl) {
+ var labelEl = optEl.querySelector('.boost-sd__filter-option-label, .boost-sd__filter-option-item-name, label');
+ var txt = (labelEl ? labelEl.textContent : optEl.textContent) || '';
+ return txt.replace(/\s*\(\d[\d,]*\)\s*$/, '').replace(/\s+/g, ' ').trim();
+ }
+
+ /* Read a swatch background-color from a native option, if any. */
+ function optSwatch(optEl) {
+ var sw = optEl.querySelector('.boost-sd__filter-option-swatch-item-img, [style*="background-color"]');
+ if (!sw) return null;
+ var bg = sw.style && sw.style.backgroundColor;
+ if (bg) return bg;
+ var m = (sw.getAttribute('style') || '').match(/background-color:\s*([^;]+)/i);
+ return m ? m[1].trim() : null;
+ }
+
+ /* Is the native option selected? Read Boost's own active class / aria. */
+ function optSelected(optEl) {
+ if (!optEl) return false;
+ var cls = optEl.className || '';
+ if (/--(selected|active|checked)/.test(cls) || /\bselected\b|\bactive\b/.test(cls)) return true;
+ if (optEl.getAttribute('aria-checked') === 'true' || optEl.getAttribute('aria-selected') === 'true') return true;
+ var inp = optEl.querySelector('input[type=checkbox],input[type=radio]');
+ if (inp && inp.checked) return true;
+ var innerSel = optEl.querySelector('.boost-sd__filter-option--selected, [aria-checked="true"], .is-selected');
+ return !!innerSel;
+ }
+
+ function groupTitle(filterEl) {
+ var t = filterEl.querySelector('.boost-sd__filter-title');
+ return t ? t.textContent.replace(/\s+/g, ' ').trim() : '';
+ }
+
+ /* Is this group a price slider (no discrete options)? */
+ function isPriceGroup(filterEl) {
+ var opts = filterEl.querySelectorAll('.boost-sd__filter-option');
+ if (opts.length > 0) return false;
+ var hasSlider = filterEl.querySelector('input[type=range], .boost-sd__filter-slider, .boost-sd__price-slider, .rc-slider, input[type=number]');
+ return !!hasSlider;
+ }
+
+ /* ---- read the hidden tree into a plain model -------------------------
+ Returns: [{ index, title, isPrice, options:[{text, swatch, selected}] }] */
+ function readGroups() {
+ var wrap = document.querySelector(TREE_WRAP);
+ if (!wrap) return [];
+ var filters = wrap.querySelectorAll('.boost-sd__filter');
+ var groups = [];
+ for (var i = 0; i < filters.length; i++) {
+ var f = filters[i];
+ var title = groupTitle(f);
+ if (!title) continue;
+ var price = isPriceGroup(f);
+ var options = [];
+ if (!price) {
+ var opts = f.querySelectorAll('.boost-sd__filter-option');
+ for (var j = 0; j < opts.length; j++) {
+ var label = optLabel(opts[j]);
+ if (!label) continue;
+ options.push({ text: label, swatch: optSwatch(opts[j]), selected: optSelected(opts[j]) });
+ }
+ if (!options.length) continue; // empty discrete group — skip
+ }
+ groups.push({ index: i, title: title, isPrice: price, options: options });
+ }
+ return groups;
+ }
+
+ /* A stable signature of the group set to detect structural changes. */
+ function groupsSig(groups) {
+ return groups.map(function (g) {
+ return g.index + ':' + g.title + ':' + (g.isPrice ? 'P' : g.options.length);
+ }).join('|');
+ }
+
+ /* ---- proxy a click into the live hidden native tree ------------------ */
+ function proxyClick(groupIndex, optText) {
+ var wrap = document.querySelector(TREE_WRAP);
+ if (!wrap) return false;
+ var filters = wrap.querySelectorAll('.boost-sd__filter');
+ var f = filters[groupIndex];
+ if (!f) return false;
+ var want = norm(optText);
+ var opts = f.querySelectorAll('.boost-sd__filter-option');
+ for (var i = 0; i < opts.length; i++) {
+ if (norm(optLabel(opts[i])) === want) {
+ var target =
+ opts[i].querySelector('input[type=checkbox],input[type=radio]') ||
+ opts[i].querySelector('label') ||
+ opts[i].querySelector('a') ||
+ opts[i].querySelector('.boost-sd__filter-option-label, .boost-sd__filter-option-item-name') ||
+ opts[i];
+ try { target.click(); } catch (e) { return false; }
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /* Try to apply Boost's native price control (best-effort, may be unreliable). */
+ function proxyPriceApply(filterEl, minVal, maxVal) {
+ if (!filterEl) return false;
+ var nums = filterEl.querySelectorAll('input[type=number], input[type=range]');
+ if (nums.length >= 2) {
+ try {
+ if (minVal != null) { nums[0].value = minVal; nums[0].dispatchEvent(new Event('input', { bubbles: true })); nums[0].dispatchEvent(new Event('change', { bubbles: true })); }
+ if (maxVal != null) { nums[1].value = maxVal; nums[1].dispatchEvent(new Event('input', { bubbles: true })); nums[1].dispatchEvent(new Event('change', { bubbles: true })); }
+ } catch (e) {}
+ }
+ var applyBtn = filterEl.querySelector('.boost-sd__filter-button-apply, button[class*="apply"], .boost-sd__button-apply, button[type=submit]');
+ if (applyBtn) { try { applyBtn.click(); return true; } catch (e) {} }
+ return false;
+ }
+
+ /* ---- render ---------------------------------------------------------- */
+ var openChip = null;
+
+ function closePanels() {
+ if (openChip) { openChip.classList.remove('dw-hfilter-open'); openChip = null; }
+ }
+
+ function buildOpt(group, opt) {
+ var row = document.createElement('div');
+ row.className = 'dw-hfilter-opt' + (opt.selected ? ' dw-hfilter-opt-selected' : '');
+
+ var check = document.createElement('span');
+ check.className = 'dw-hfilter-check';
+ check.textContent = opt.selected ? '✓' : '';
+ row.appendChild(check);
+
+ if (opt.swatch) {
+ var sw = document.createElement('span');
+ sw.className = 'dw-hfilter-swatch';
+ sw.style.backgroundColor = opt.swatch;
+ row.appendChild(sw);
+ }
+
+ var txt = document.createElement('span');
+ txt.className = 'dw-hfilter-opt-text';
+ txt.textContent = opt.text;
+ row.appendChild(txt);
+
+ row.addEventListener('click', function (e) {
+ e.stopPropagation();
+ proxyClick(group.index, opt.text);
+ // Boost re-renders; our observer re-syncs. Reflect optimistic state.
+ closePanels();
+ });
+ return row;
+ }
+
+ function buildChip(group) {
+ var activeCount = group.options.filter(function (o) { return o.selected; }).length;
+
+ var chip = document.createElement('div');
+ chip.className = 'dw-hfilter-chip' + (activeCount ? ' dw-hfilter-active' : '');
+ chip.setAttribute('data-dw-group', group.index);
+
+ var label = document.createElement('span');
+ label.className = 'dw-hfilter-chip-label';
+ label.textContent = group.title;
+ chip.appendChild(label);
+
+ if (activeCount) {
+ var badge = document.createElement('span');
+ badge.className = 'dw-hfilter-badge';
+ badge.textContent = activeCount;
+ chip.appendChild(badge);
+ }
+
+ var caret = document.createElement('span');
+ caret.className = 'dw-hfilter-caret';
+ chip.appendChild(caret);
+
+ var panel = document.createElement('div');
+ panel.className = 'dw-hfilter-panel';
+
+ if (group.isPrice) {
+ var note = document.createElement('div');
+ note.className = 'dw-hfilter-price-note';
+ note.textContent = 'Use the price range control to filter by price.';
+ panel.appendChild(note);
+ // Best-effort: surface the native price control inputs would go here; v1
+ // leaves Price as a passthrough chip (see proxyPriceApply, validated on 5.1).
+ } else if (!group.options.length) {
+ var empty = document.createElement('div');
+ empty.className = 'dw-hfilter-opt-empty';
+ empty.textContent = 'No options';
+ panel.appendChild(empty);
+ } else {
+ for (var i = 0; i < group.options.length; i++) {
+ panel.appendChild(buildOpt(group, group.options[i]));
+ }
+ }
+ chip.appendChild(panel);
+
+ chip.addEventListener('click', function (e) {
+ // Clicks on option rows handle themselves (stopPropagation); this is the chip body.
+ if (e.target.closest('.dw-hfilter-panel')) return;
+ var isOpen = chip.classList.contains('dw-hfilter-open');
+ closePanels();
+ if (!isOpen) { chip.classList.add('dw-hfilter-open'); openChip = chip; }
+ });
+
+ return chip;
+ }
+
+ function renderBar(groups) {
+ var existing = document.getElementById(BAR_ID);
+ if (existing) existing.remove();
+
+ var bar = document.createElement('div');
+ bar.id = BAR_ID;
+ bar.className = 'dw-hfilter-bar';
+
+ for (var i = 0; i < groups.length; i++) {
+ bar.appendChild(buildChip(groups[i]));
+ }
+
+ // Insert directly below the Boost toolbar row, above the grid.
+ var toolbar = document.querySelector(TOOLBAR);
+ var grid = document.querySelector(GRID);
+ if (toolbar && toolbar.parentElement && toolbar.parentElement.parentElement) {
+ var toolbarRow = toolbar.parentElement;
+ toolbarRow.parentElement.insertBefore(bar, toolbarRow.nextSibling);
+ } else if (grid && grid.parentElement) {
+ grid.parentElement.insertBefore(bar, grid);
+ } else {
+ return false;
+ }
+ openChip = null;
+ return true;
+ }
+
+ /* ---- live re-sync of chip active-counts / checkmarks ----------------- */
+ function syncBar(groups) {
+ var bar = document.getElementById(BAR_ID);
+ if (!bar) return;
+ for (var i = 0; i < groups.length; i++) {
+ var g = groups[i];
+ var chip = bar.querySelector('.dw-hfilter-chip[data-dw-group="' + g.index + '"]');
+ if (!chip) continue;
+ var activeCount = g.options.filter(function (o) { return o.selected; }).length;
+
+ chip.classList.toggle('dw-hfilter-active', !!activeCount);
+ var badge = chip.querySelector('.dw-hfilter-badge');
+ if (activeCount) {
+ if (!badge) {
+ badge = document.createElement('span');
+ badge.className = 'dw-hfilter-badge';
+ chip.insertBefore(badge, chip.querySelector('.dw-hfilter-caret'));
+ }
+ badge.textContent = activeCount;
+ } else if (badge) {
+ badge.remove();
+ }
+
+ // Update option checkmarks/selected by normalized text (panel order matches model).
+ var rows = chip.querySelectorAll('.dw-hfilter-opt');
+ for (var r = 0; r < rows.length && r < g.options.length; r++) {
+ var sel = g.options[r].selected;
+ rows[r].classList.toggle('dw-hfilter-opt-selected', sel);
+ var ck = rows[r].querySelector('.dw-hfilter-check');
+ if (ck) ck.textContent = sel ? '✓' : '';
+ }
+ }
+ }
+
+ /* ---- outside-click closes panels ------------------------------------- */
+ document.addEventListener('click', function (e) {
+ if (e.target.closest('#' + BAR_ID)) return;
+ closePanels();
+ });
+
+ /* ---- fallback toggle (only if the liquid pill is absent) ------------- */
+ function ensureFallbackToggle() {
+ if (document.getElementById('dw-filter-toggle')) return; // liquid pill owns collapse
+ if (document.getElementById('dw-hfilter-toggle')) return;
+ var toolbar = document.querySelector(TOOLBAR);
+ if (!toolbar || !toolbar.parentElement) return;
+ var btn = document.createElement('button');
+ btn.id = 'dw-hfilter-toggle';
+ btn.className = 'dw-hfilter-toggle';
+ btn.type = 'button';
+ function sync() {
+ btn.textContent = document.body.classList.contains('dw-hbar-collapsed') ? 'Filters' : 'Hide Filters';
+ }
+ btn.addEventListener('click', function () {
+ var collapsed = document.body.classList.toggle('dw-hbar-collapsed');
+ try { localStorage.setItem('dw-hfilter-collapsed', collapsed ? '1' : '0'); } catch (e) {}
+ sync();
+ });
+ toolbar.parentElement.insertBefore(btn, toolbar);
+ sync();
+ }
+
+ /* ---- main wire-up ---------------------------------------------------- */
+ var lastSig = '';
+
+ function build() {
+ var groups = readGroups();
+ if (!groups.length) return false;
+ lastSig = groupsSig(groups);
+ renderBar(groups);
+ ensureFallbackToggle();
+ return true;
+ }
+
+ function observe() {
+ var wrap = document.querySelector(TREE_WRAP);
+ if (!wrap) return;
+ var t = null;
+ var obs = new MutationObserver(function () {
+ clearTimeout(t);
+ t = setTimeout(function () {
+ var groups = readGroups();
+ if (!groups.length) return;
+ var sig = groupsSig(groups);
+ if (sig !== lastSig || !document.getElementById(BAR_ID)) {
+ lastSig = sig;
+ renderBar(groups); // group set changed OR bar was wiped -> re-render wholesale
+ ensureFallbackToggle();
+ } else {
+ syncBar(groups); // same structure -> just re-sync active state
+ }
+ }, 150);
+ });
+ obs.observe(wrap, { childList: true, subtree: true });
+ }
+
+ var tries = 0;
+ var poll = setInterval(function () {
+ var haveTree = document.querySelector(TREE);
+ var haveToolbar = document.querySelector(TOOLBAR);
+ if (haveTree && haveToolbar) {
+ clearInterval(poll);
+ if (build()) observe();
+ } else if (tries++ > 60) {
+ clearInterval(poll);
+ }
+ }, 250);
+})();
diff --git a/shopify/theme-5.0-duplicate/layout/theme.liquid b/shopify/theme-5.0-duplicate/layout/theme.liquid
index 7a0347ad..035a1645 100644
--- a/shopify/theme-5.0-duplicate/layout/theme.liquid
+++ b/shopify/theme-5.0-duplicate/layout/theme.liquid
@@ -1020,6 +1020,11 @@ alt="web statistics"></a></div></noscript>
<script src="{{ 'dw-pj-hide.js' | asset_url }}" defer></script>
{%- comment -%} DW card hover label: Pattern, Color + Vendor on hover (2026-06-22) {%- endcomment -%}
<script src="{{ 'dw-card-hover.js' | asset_url }}" defer></script>
+{%- comment -%} DW horizontal collapsible Boost filter bar (Approach B) — collection/search only (2026-06-22) {%- endcomment -%}
+{%- if template contains 'collection' or template contains 'search' -%}
+ {{ 'dw-hfilter.css' | asset_url | stylesheet_tag }}
+ <script src="{{ 'dw-hfilter.js' | asset_url }}" defer></script>
+{%- endif -%}
{%- comment -%} DW PDP restyle: size <select> -> pills + uniform CTA pill layout (2026-06-22) {%- endcomment -%}
{{ 'dw-pdp-restyle.css' | asset_url | stylesheet_tag }}
<script src="{{ 'dw-pdp-size-pills.js' | asset_url }}" defer></script>
diff --git a/shopify/theme-5.0-duplicate/snippets/dw-boost-overrides.liquid b/shopify/theme-5.0-duplicate/snippets/dw-boost-overrides.liquid
index 322bbf9c..263effa0 100644
--- a/shopify/theme-5.0-duplicate/snippets/dw-boost-overrides.liquid
+++ b/shopify/theme-5.0-duplicate/snippets/dw-boost-overrides.liquid
@@ -84,6 +84,10 @@ body.dw-filters-collapsed .boost-sd__product-list[class*="grid--"]{grid-template
(function(){
/* Collapse filters immediately */
document.body.classList.add('dw-filters-collapsed');
+ /* Horizontal filter bar scope hook (Approach B — dw-hfilter.*); dw-filters-collapsed stays the always-on state engine that keeps Boost's native tree hidden */
+ document.body.classList.add('dw-filters-horizontal');
+ /* Restore the horizontal-bar collapsed state (our own bar, NOT the native tree) */
+ try { if (localStorage.getItem('dw-hfilter-collapsed') === '1') document.body.classList.add('dw-hbar-collapsed'); } catch(e){}
/* Set CSS var for default 4 cols */
document.documentElement.style.setProperty('--dw-grid-cols', 4);
@@ -161,11 +165,13 @@ body.dw-filters-collapsed .boost-sd__product-list[class*="grid--"]{grid-template
btn.appendChild(svg);
var filterLabel = document.createElement('span');
filterLabel.id = 'dw-filter-label';
- filterLabel.textContent = 'Filters';
+ /* Initial label reflects our horizontal-bar state (default = bar visible -> 'Hide Filters') */
+ filterLabel.textContent = document.body.classList.contains('dw-hbar-collapsed') ? 'Filters' : 'Hide Filters';
btn.appendChild(filterLabel);
btn.addEventListener('click', function(){
- document.body.classList.toggle('dw-filters-collapsed');
- filterLabel.textContent = document.body.classList.contains('dw-filters-collapsed') ? 'Filters' : 'Hide Filters';
+ var collapsed = document.body.classList.toggle('dw-hbar-collapsed');
+ filterLabel.textContent = collapsed ? 'Filters' : 'Hide Filters';
+ try { localStorage.setItem('dw-hfilter-collapsed', collapsed ? '1' : '0'); } catch(e){}
});
/* Build per-page selector INSIDE the grid slider pill */
diff --git a/shopify/theme-infinite-scroll-142250278963/assets/dw-card-hover.js b/shopify/theme-infinite-scroll-142250278963/assets/dw-card-hover.js
index 8f7a43c1..adabe1db 100644
--- a/shopify/theme-infinite-scroll-142250278963/assets/dw-card-hover.js
+++ b/shopify/theme-infinite-scroll-142250278963/assets/dw-card-hover.js
@@ -1,52 +1,128 @@
+/* ============================================================================
+ DW card hover overlay v2 (2026-06-22) — collection / search product grids.
+ Layout (Steve, 2026-06-22): clean image-ONLY grid — the entire native text
+ band below each card image is removed (nothing below). On HOVER of the image
+ an overlay fades in over the image bottom showing:
+ Pattern name (full SEO title minus "wallcovering(s)" and "| vendor")
+ VENDOR (smaller, uppercase, beneath the pattern)
-/* ============================================================================
- DW card hover label (2026-06-22) — Boost product grid cards.
- Default: clean image-only grid (full SEO title hidden).
- On hover: reveal "Pattern, Color" (full title minus "wallcovering(s) | vendor")
- + the vendor on a smaller line. Full title stays in DOM for SEO.
- Re-applies to infinite-scroll-appended / Boost-rerendered cards via observer.
+ The full SEO title + vendor links stay in the DOM (visually hidden via the
+ clip technique, not removed) so search engines / screen readers still see
+ them, and the image itself stays a link to the product page.
+
+ This store's grids are rendered through the THEME product template
+ (.product-list-item*), even on Boost AI Search & Discovery surfaces.
+ We also support Boost's native markup (.boost-sd__product-item) so the
+ behavior is correct on every collection / search / vendor / tag grid.
+ Re-applies to lazy/infinite-scroll-appended cards via a MutationObserver.
+
+ Source asset: assets/dw-card-hover.js (loaded by layout/theme.liquid).
+ v1 backup: assets/dw-card-hover.js.v1-bak
========================================================================== */
(function () {
if (window.__dwCardHover) return;
window.__dwCardHover = true;
+ // selector config — first match wins per card, covers theme + Boost markup
+ var CARD = '.product-list-item, .boost-sd__product-item';
+ var TITLE = '.product-list-item-title, .boost-sd__product-title';
+ var VENDOR = '.product-list-item-vendor, .boost-sd__product-vendor';
+ var THUMB = '.product-list-item-thumbnail, [class*="boost-sd__product-image"]:not(img)';
+ var BAND = '.product-list-item-details, .boost-sd__product-info';
+
var STYLE_ID = 'dw-card-hover-style';
if (!document.getElementById(STYLE_ID)) {
var st = document.createElement('style');
st.id = STYLE_ID;
- st.textContent =
- '.boost-sd__product-item .boost-sd__product-title,' +
- '.boost-sd__product-item .boost-sd__product-vendor{opacity:0;transition:opacity .15s;}' +
- '.boost-sd__product-item:hover .dw-hover-label{opacity:1;}' +
- '.dw-hover-label{opacity:0;transition:opacity .15s;padding:6px 2px 0;pointer-events:none;}' +
- '.dw-hover-label .pat{font-size:13px;font-weight:600;color:#1a1a1a;line-height:1.2;}' +
- '.dw-hover-label .ven{font-size:11px;letter-spacing:.04em;text-transform:uppercase;color:#888;margin-top:2px;}';
+ st.textContent = [
+ /* image wrapper = positioning context for the overlay */
+ '.product-list-item .product-list-item-thumbnail,',
+ '.boost-sd__product-item [class*="boost-sd__product-image"]:not(img){position:relative;}',
+
+ /* collapse the native text band below the image to zero height. Boost keeps
+ the vendor/title/price as siblings (not all inside product-info), so each
+ is clipped directly. Kept in DOM for SEO + a11y via the clip technique. */
+ '.product-list-item .product-list-item-details,',
+ '.boost-sd__product-item .boost-sd__product-info,',
+ '.boost-sd__product-item .boost-sd__product-title,',
+ '.boost-sd__product-item .boost-sd__product-vendor,',
+ '.boost-sd__product-item [class*="boost-sd__product-price"],',
+ '.boost-sd__product-item [class*="boost-sd__format-price"]{',
+ 'position:absolute!important;width:1px;height:1px;padding:0;margin:-1px;',
+ 'overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0;}',
+
+ /* the hover overlay — sits on the bottom of the image */
+ '.dw-hover-overlay{position:absolute;left:0;right:0;bottom:0;z-index:2;',
+ 'padding:16px 12px 11px;pointer-events:none;opacity:0;transition:opacity .18s ease;',
+ 'background:linear-gradient(to top,rgba(20,20,20,.66) 0%,rgba(20,20,20,.30) 55%,rgba(20,20,20,0) 100%);}',
+ '.product-list-item:hover .dw-hover-overlay,',
+ '.product-list-item:focus-within .dw-hover-overlay,',
+ '.boost-sd__product-item:hover .dw-hover-overlay,',
+ '.boost-sd__product-item:focus-within .dw-hover-overlay{opacity:1;}',
+ '.dw-hover-overlay .pat{font-size:13px;font-weight:600;line-height:1.25;color:#fff;',
+ 'white-space:normal;text-shadow:0 1px 2px rgba(0,0,0,.4);}',
+ '.dw-hover-overlay .ven{font-size:10.5px;letter-spacing:.07em;text-transform:uppercase;',
+ 'color:rgba(255,255,255,.85);margin-top:3px;white-space:normal;text-shadow:0 1px 2px rgba(0,0,0,.4);}'
+ ].join('');
document.head.appendChild(st);
}
+ function esc(s) {
+ return s.replace(/[&<>"']/g, function (c) {
+ return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
+ });
+ }
+
+ // Boost renders titles lowercase; Title-Case for a luxe look. Small stop-words
+ // (and/or/of/on/in/the/a/an) stay lowercase unless they lead the line, per DW's
+ // title-case conventions. Only the first letter of other words is forced up so
+ // already-proper text is left intact.
+ var STOP = { and:1, or:1, of:1, on:1, in:1, the:1, a:1, an:1, to:1, with:1 };
function titleCase(s) {
- return s.replace(/\w\S*/g, function (w) { return w.charAt(0).toUpperCase() + w.slice(1); });
+ var i = 0;
+ return s.replace(/\w\S*/g, function (w) {
+ var lower = w.toLowerCase();
+ i++;
+ if (i > 1 && STOP[lower]) return lower;
+ return w.charAt(0).toUpperCase() + w.slice(1);
+ });
+ }
+
+ function patternName(title) {
+ var name = title.split('|')[0].trim() // drop "| vendor"
+ .replace(/\s*wallcoverings?\b/ig, '') // drop "wallcovering(s)"
+ .replace(/[,\s]+$/, '').trim(); // tidy trailing comma/space
+ return titleCase(name);
}
function labelCard(card) {
- if (card.querySelector('.dw-hover-label')) return;
- var tEl = card.querySelector('.boost-sd__product-title');
- var vEl = card.querySelector('.boost-sd__product-vendor');
+ if (card.querySelector('.dw-hover-overlay')) return; // already done
+ var tEl = card.querySelector(TITLE);
if (!tEl) return;
+ var name = patternName(tEl.textContent || '');
+ if (!name) return;
+ var vEl = card.querySelector(VENDOR);
var vendor = (vEl ? vEl.textContent : '').trim();
- var name = tEl.textContent.split('|')[0].trim() // drop "| vendor"
- .replace(/\s*wallcoverings?\b/ig, '') // drop "wallcovering(s)"
- .replace(/[,\s]+$/, '').trim();
- var lab = document.createElement('div');
- lab.className = 'dw-hover-label';
- lab.innerHTML = '<div class="pat">' + titleCase(name) + '</div>' +
- (vendor ? '<div class="ven">' + titleCase(vendor) + '</div>' : '');
- tEl.parentElement.appendChild(lab);
+
+ // anchor the overlay to the image wrapper (never the <img> itself)
+ var imgWrap = card.querySelector(THUMB);
+ if (!imgWrap) {
+ var img = card.querySelector('img');
+ imgWrap = img ? img.parentElement : null;
+ }
+ if (!imgWrap) return;
+ if (getComputedStyle(imgWrap).position === 'static') imgWrap.style.position = 'relative';
+
+ var ov = document.createElement('div');
+ ov.className = 'dw-hover-overlay';
+ ov.innerHTML = '<div class="pat">' + esc(name) + '</div>' +
+ (vendor ? '<div class="ven">' + esc(vendor) + '</div>' : '');
+ imgWrap.appendChild(ov);
}
function run() {
- var cards = document.querySelectorAll('.boost-sd__product-item');
+ var cards = document.querySelectorAll(CARD);
for (var i = 0; i < cards.length; i++) labelCard(cards[i]);
}
@@ -56,7 +132,7 @@
run();
}
- // Re-apply when Boost renders a grid or infinite scroll appends more cards.
+ // Re-apply when the grid re-renders or lazy/infinite scroll appends cards.
var t;
var obs = new MutationObserver(function (muts) {
for (var i = 0; i < muts.length; i++) {
diff --git a/shopify/theme-infinite-scroll-142250278963/assets/dw-card-hover.js.v1-bak b/shopify/theme-infinite-scroll-142250278963/assets/dw-card-hover.js.v1-bak
new file mode 100644
index 00000000..8f7a43c1
--- /dev/null
+++ b/shopify/theme-infinite-scroll-142250278963/assets/dw-card-hover.js.v1-bak
@@ -0,0 +1,71 @@
+
+
+/* ============================================================================
+ DW card hover label (2026-06-22) — Boost product grid cards.
+ Default: clean image-only grid (full SEO title hidden).
+ On hover: reveal "Pattern, Color" (full title minus "wallcovering(s) | vendor")
+ + the vendor on a smaller line. Full title stays in DOM for SEO.
+ Re-applies to infinite-scroll-appended / Boost-rerendered cards via observer.
+ ========================================================================== */
+(function () {
+ if (window.__dwCardHover) return;
+ window.__dwCardHover = true;
+
+ var STYLE_ID = 'dw-card-hover-style';
+ if (!document.getElementById(STYLE_ID)) {
+ var st = document.createElement('style');
+ st.id = STYLE_ID;
+ st.textContent =
+ '.boost-sd__product-item .boost-sd__product-title,' +
+ '.boost-sd__product-item .boost-sd__product-vendor{opacity:0;transition:opacity .15s;}' +
+ '.boost-sd__product-item:hover .dw-hover-label{opacity:1;}' +
+ '.dw-hover-label{opacity:0;transition:opacity .15s;padding:6px 2px 0;pointer-events:none;}' +
+ '.dw-hover-label .pat{font-size:13px;font-weight:600;color:#1a1a1a;line-height:1.2;}' +
+ '.dw-hover-label .ven{font-size:11px;letter-spacing:.04em;text-transform:uppercase;color:#888;margin-top:2px;}';
+ document.head.appendChild(st);
+ }
+
+ function titleCase(s) {
+ return s.replace(/\w\S*/g, function (w) { return w.charAt(0).toUpperCase() + w.slice(1); });
+ }
+
+ function labelCard(card) {
+ if (card.querySelector('.dw-hover-label')) return;
+ var tEl = card.querySelector('.boost-sd__product-title');
+ var vEl = card.querySelector('.boost-sd__product-vendor');
+ if (!tEl) return;
+ var vendor = (vEl ? vEl.textContent : '').trim();
+ var name = tEl.textContent.split('|')[0].trim() // drop "| vendor"
+ .replace(/\s*wallcoverings?\b/ig, '') // drop "wallcovering(s)"
+ .replace(/[,\s]+$/, '').trim();
+ var lab = document.createElement('div');
+ lab.className = 'dw-hover-label';
+ lab.innerHTML = '<div class="pat">' + titleCase(name) + '</div>' +
+ (vendor ? '<div class="ven">' + titleCase(vendor) + '</div>' : '');
+ tEl.parentElement.appendChild(lab);
+ }
+
+ function run() {
+ var cards = document.querySelectorAll('.boost-sd__product-item');
+ for (var i = 0; i < cards.length; i++) labelCard(cards[i]);
+ }
+
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', run);
+ } else {
+ run();
+ }
+
+ // Re-apply when Boost renders a grid or infinite scroll appends more cards.
+ var t;
+ var obs = new MutationObserver(function (muts) {
+ for (var i = 0; i < muts.length; i++) {
+ if (muts[i].addedNodes && muts[i].addedNodes.length) {
+ clearTimeout(t);
+ t = setTimeout(run, 80);
+ return;
+ }
+ }
+ });
+ obs.observe(document.body, { childList: true, subtree: true });
+})();
← 24cda327 Collection cards: image-only, no text (hover: subtle opacity
·
back to Designer Wallcoverings
·
Restore hover overlay: vendor + pattern name only (no text b e2b84e81 →