[object Object]

← 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

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 { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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 →