← back to Newmor Onboard
data/live-2026-09-09/theme/snippets/dw-boost-overrides.liquid
1216 lines
<!-- DW Grid Slider — CSS + Control + JS only (products rendered by Boost AI) -->
<style>
/* Override Boost color filter dots to match shop-by-color page palette */
.boost-sd__filter-option-swatch-item-img[style*="background-color: pink"]{background-color:#C9899A !important}
.boost-sd__filter-option-swatch-item-img[style*="background-color: red"]{background-color:#9B3B3B !important}
.boost-sd__filter-option-swatch-item-img[style*="background-color: orange"]{background-color:#C8784A !important}
.boost-sd__filter-option-swatch-item-img[style*="background-color: yellow"]{background-color:#D4B96A !important}
.boost-sd__filter-option-swatch-item-img[style*="background-color: green"]{background-color:#6B8E6B !important}
.boost-sd__filter-option-swatch-item-img[style*="background-color: blue"]{background-color:#4A6FA5 !important}
.boost-sd__filter-option-swatch-item-img[style*="background-color: cyan"]{background-color:#5F9EA0 !important}
.boost-sd__filter-option-swatch-item-img[style*="background-color: aqua"]{background-color:#5F9EA0 !important}
.boost-sd__filter-option-swatch-item-img[style*="background-color: purple"]{background-color:#7A6B8A !important}
.boost-sd__filter-option-swatch-item-img[style*="background-color: brown"]{background-color:#8B6F4E !important}
.boost-sd__filter-option-swatch-item-img{border-radius:50%}
/* Collapsed filter panel on load — full-width product grid */
body.dw-filters-collapsed .boost-sd__filter-tree-wrapper{display:none !important}
body.dw-filters-collapsed .boost-sd__filter-tree-vertical{display:none !important}
body.dw-filters-collapsed .boost-sd__product-list{width:100% !important;max-width:100% !important}
body.dw-filters-collapsed .boost-sd__filter-products-wrap{display:block !important;max-width:100% !important;width:100% !important}
body.dw-filters-collapsed .boost-sd__filter-products-wrap > div{display:block !important;max-width:100% !important;width:100% !important}
body.dw-filters-collapsed .boost-sd__product-list-wrapper{max-width:100% !important;width:100% !important;flex:1 !important}
body.dw-filters-collapsed .boost-sd__product-list{padding-right:0 !important;padding-left:0 !important}
body.dw-filters-collapsed .boost-sd__product-list[class*="grid--"]{grid-template-columns:repeat(auto-fill, minmax(min(47%, var(--dw-card-min, 215px)), 1fr)) !important}
.boost-sd__product-list[class*="grid--"]{grid-template-columns:repeat(auto-fill, minmax(min(47%, var(--dw-card-min, 215px)), 1fr)) !important}
.boost-sd__product-list{grid-template-columns:repeat(auto-fill, minmax(min(47%, var(--dw-card-min, 215px)), 1fr)) !important}
.boost-sd__product-list-grid--3-col{grid-template-columns:repeat(auto-fill, minmax(min(47%, var(--dw-card-min, 215px)), 1fr)) !important}
.boost-sd__product-list-grid--4-col{grid-template-columns:repeat(auto-fill, minmax(min(47%, var(--dw-card-min, 215px)), 1fr)) !important}
/* Force product items to fit grid columns */
.boost-sd__product-item,.boost-sd__product-item *{min-width:0 !important;max-width:100% !important;box-sizing:border-box !important}
.boost-sd__product-item{width:auto !important;overflow:hidden !important}
.boost-sd__product-item img{max-width:100% !important;height:auto !important;width:100% !important}
.boost-sd__product-list{overflow:hidden !important;display:grid !important;gap:16px !important;grid-gap:16px !important;column-gap:16px !important;row-gap:16px !important}
/* DW grid breathing room between products (Steve 2026-07-06); card internals stay tight */
.boost-sd__product-item{padding:0 !important;margin:0 !important}
.boost-sd__product-item-image,.boost-sd__product-item-inner,.boost-sd__product-item > *{margin:0 !important}
/* === MOBILE: 2 products per row + full-width index images (DW 2026-06-23) === */
@media (max-width:768px){
/* full-bleed: kill Boost's reserved side padding + width cap so the 2 columns
span the entire viewport (mobile never gets .dw-filters-collapsed, which is
what strips this padding on desktop) */
.boost-sd-right,
.boost-sd__product-list-wrapper,
.boost-sd__product-list{
width:100% !important;max-width:100% !important;
padding-left:0 !important;padding-right:0 !important;
}
.boost-sd__product-list,
.boost-sd__product-list[class*="grid--"],
body.dw-filters-collapsed .boost-sd__product-list[class*="grid--"]{
grid-template-columns:repeat(2,1fr) !important;
}
/* === Unique mobile card template (officer-refined 2026-06-23) ===
Fix "images cut off": show the WHOLE pattern, never crop.
- object-fit:contain (was cover) on a paper-white ground, so square
swatches fill the cell and only odd-ratio images get a subtle margin.
- title + vendor move BELOW the image (was an absolute overlay covering
the pattern). Card becomes a clean vertical stack: image -> name -> vendor. */
.boost-sd__product-item{
display:flex !important;flex-direction:column !important;
}
.boost-sd__product-image-wrapper,
[class*="product-image-wrap"]{
aspect-ratio:1/1 !important;overflow:hidden !important;
background:#FAFAF8 !important; /* paper-white ground, never black bars */
width:100% !important;height:auto !important;
}
.boost-sd__product-item img,
.boost-sd__product-image-wrapper img,
[class*="product-image-wrap"] img,
.product-list-item img,
.product-list-item-thumbnail img{
width:100% !important;height:100% !important;display:block !important;
object-fit:contain !important; /* contain = full image, nothing cut off */
object-position:center !important;
}
/* Title + vendor: out of the overlay, in normal flow below the image */
.boost-sd__product-title,
.boost-sd__product-vendor,
.boost-sd__product-info{
position:static !important;width:100% !important;
background:none !important;text-align:left !important;opacity:1 !important;
inset:auto !important;
}
.boost-sd__product-title,.boost-sd__product-title a{
font-size:13px !important;font-weight:500 !important;
color:#1A1A1A !important;line-height:1.3 !important;padding:6px 4px 0 !important;
}
.boost-sd__product-vendor{
font-size:11px !important;letter-spacing:.06em !important;
text-transform:uppercase !important;color:#8A8A85 !important;padding:2px 4px 2px !important;
}
/* === Appended infinite-scroll cards (neutral .dw-if-* classes so no Boost/theme
script strips the <img>). Styled to MATCH the page-1 cards: contained image
on a paper ground with the name/vendor overlaid at the bottom. === */
.dw-if-card{position:relative !important;display:block !important;}
.dw-if-card > a{display:block !important;text-decoration:none !important;color:#fff !important;}
.dw-if-img{aspect-ratio:1/1 !important;width:100% !important;background:#FAFAF8 !important;overflow:hidden !important;}
.dw-if-img img{width:100% !important;height:100% !important;object-fit:contain !important;display:block !important;}
.dw-if-card .dw-if-name,
.dw-if-card .dw-if-ven{position:absolute !important;left:0;right:0;padding:0 8px !important;
text-align:center !important;color:#fff !important;text-shadow:0 1px 5px rgba(0,0,0,.65) !important;
pointer-events:none !important;}
.dw-if-card .dw-if-name{bottom:26px !important;font-size:13px !important;font-weight:600 !important;line-height:1.2 !important;}
.dw-if-card .dw-if-ven{bottom:8px !important;font-size:10px !important;letter-spacing:.08em !important;text-transform:uppercase !important;opacity:.92 !important;}
/* === Uniform toolbar pills (DW 2026-06-23): every control the same size, simple ===
Sort, Refine By, Filters, Show, and Wallcoverings/Fabrics/More -> one pill shape. */
.dw-filter-toggle,
.dw-grid-slider,
.dw-ptype-bar,
.boost-sd__toolbar-item--sorting,
.boost-sd__filter-tree-toggle-button{
box-sizing:border-box !important;
height:34px !important;min-height:34px !important;
display:inline-flex !important;align-items:center !important;justify-content:center !important;
padding:0 14px !important;margin:5px 6px 0 0 !important;
border:1.5px solid #1A1A1A !important;border-radius:999px !important;
background:#fff !important;color:#1A1A1A !important;
font-size:11px !important;font-weight:600 !important;letter-spacing:.06em !important;
line-height:1 !important;white-space:nowrap !important;
}
/* normalize inner text/controls of the native Boost sort + refine so they match */
.boost-sd__toolbar-item--sorting *,
.boost-sd__filter-tree-toggle-button,
.boost-sd__filter-tree-toggle-button *{
font-size:11px !important;padding-bottom:0 !important;line-height:1 !important;
text-transform:uppercase !important;letter-spacing:.06em !important;
}
.dw-grid-slider select,.dw-grid-slider label,
.dw-ptype-bar label{ font-size:11px !important;font-weight:600 !important; }
}
/* Filter toggle button */
.dw-filter-toggle{display:inline-flex;align-items:center;gap:6px;padding:4px 12px;margin:4px 0 4px 8px;font-family:-apple-system,BlinkMacSystemFont,sans-serif;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;user-select:none;background:#fff;border:1.5px solid #000;border-radius:999px;cursor:pointer;vertical-align:middle}
.dw-filter-toggle:hover{background:#f5f5f5}
.dw-filter-toggle svg{width:14px;height:14px}
/* Hide static grid slider from main section — dynamic one is injected into Boost toolbar */
.shopify-section > .dw-grid-slider{display:none !important}
/* Hide old dw-gc grid control — replaced by toolbar grid slider */
.dw-gc{display:none !important}
/* Per-page selector — inside grid pill */
/* === Product type — INDIVIDUAL pills (DW 2026-06-25 consolidation) ===
The container is now a transparent flex wrapper (NO border / NO pill).
Each Wallcoverings / Fabrics / More <label> is its OWN bordered rounded
chip, uniform height with the FILTERS / SORT pills (see .dw-pill-uniform
group rule below). Active (checked) chip fills black. */
.dw-ptype-bar{display:inline-flex;align-items:center;gap:6px;margin:5px 0 0 0;padding:0;background:none;border:0;font-family:-apple-system,BlinkMacSystemFont,sans-serif;user-select:none}
.dw-ptype-bar label{
box-sizing:border-box;display:inline-flex;align-items:center;gap:6px;cursor:pointer;white-space:nowrap;
height:34px;min-height:34px;padding:0 14px;margin:0;
border:1.5px solid #1A1A1A;border-radius:999px;background:#fff;color:#1A1A1A;
font-size:11px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;line-height:1;
}
.dw-ptype-bar label:hover{background:#f5f5f5}
/* checked chip = filled (set via .dw-ptype-on class by JS so it works without :has) */
.dw-ptype-bar label.dw-ptype-on{background:#1A1A1A;color:#fff}
.dw-ptype-bar input[type=checkbox]{width:13px;height:13px;margin:0;cursor:pointer;accent-color:#3D4246}
.dw-ptype-bar .dw-ptype-sep{display:none !important} /* separators removed — pills stand alone */
/* === SINGLE uniform-height rule for ALL toolbar pills (DW 2026-06-25) ===
Applies on DESKTOP too (the old rule was inside @media max-width:768 only,
so live desktop pills were inconsistent). Every control — the FILTERS toggle,
the grid/Show pill, Boost's native SORT + Refine-By, and each product-type
chip — shares one 34px height + radius. This is the "same height" guarantee. */
.dw-filter-toggle,
.dw-grid-slider,
.dw-ptype-bar label,
.boost-sd__toolbar-item--sorting,
.boost-sd__filter-tree-toggle-button{
box-sizing:border-box !important;
height:34px !important;min-height:34px !important;
display:inline-flex !important;align-items:center !important;
padding:0 14px !important;margin:5px 6px 0 0 !important;
border:1.5px solid #1A1A1A !important;border-radius:999px !important;
background:#fff;color:#1A1A1A;
font-size:11px;font-weight:600;letter-spacing:.06em;line-height:1;white-space:nowrap;
}
.dw-ptype-bar label.dw-ptype-on{background:#1A1A1A !important;color:#fff !important}
.dw-perpage-inline{display:inline-flex;align-items:baseline;gap:4px;margin-left:10px;padding-left:10px;border-left:1px solid #ccc}
.dw-perpage-inline select{border:none;background:transparent;font-size:14px;font-weight:700;cursor:pointer;padding:0;margin:0;-webkit-appearance:none;appearance:none}
.dw-perpage-inline label{color:#000;font-size:14px;font-weight:700;text-transform:uppercase;letter-spacing:1px}
/* Fix doubled product titles in Boost cards */
.boost-sd__product-title a{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
</style>
<style>
.dw-grid-slider{display:inline-flex;align-items:center;gap:6px;padding:4px 12px;margin:4px 0 4px 10px;font-family:-apple-system,BlinkMacSystemFont,sans-serif;font-size:11px;user-select:none;background:#fff;border:1.5px solid #000;border-radius:999px}
.dw-grid-slider label{color:#000;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:1.5px}
.dw-grid-slider input[type=range]{-webkit-appearance:none;width:120px;height:3px;background:#ccc;border-radius:2px;outline:none;cursor:pointer}
.dw-grid-slider input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:16px;height:16px;background:#000;border-radius:50%;cursor:pointer}
.dw-grid-slider input[type=range]::-moz-range-thumb{width:16px;height:16px;background:#000;border-radius:50%;cursor:pointer;border:none}
.dw-grid-slider .gc-num{color:#000;font-weight:700;font-size:14px;min-width:14px;text-align:center}
/* Toolbar chips REFLOW cleanly as the window narrows (DW 2026-07-28): make the
visible toolbar container + the product-type chip bar wrap onto tidy rows with
even spacing, so nothing clips or scatters when shrinking from the right. */
.boost-sd__toolbar,
.boost-sd__toolbar-inner,
.boost-sd__toolbar-content,
.boost-sd__toolbar-inner-right,
.boost-sd__toolbar-inner-left{
display:flex !important;
flex-wrap:wrap !important;
align-items:center !important;
gap:8px !important;
row-gap:8px !important;
min-width:0 !important;
}
.dw-ptype-bar{flex-wrap:wrap !important;gap:8px !important}
.dw-perpage-inline{white-space:nowrap}
/* === BEST-PRACTICE MOBILE TOOLBAR (DW 2026-07-28) ===
Below 768px every control wraps to tidy full-width / 2-up rows, centered,
with even gaps — nothing overlaps or clips as the window shrinks. The density
slider STAYS (goes full-width). */
@media (max-width:768px){
.boost-sd__toolbar,.boost-sd__toolbar-inner,.boost-sd__toolbar-content,
.boost-sd__toolbar-inner-left,.boost-sd__toolbar-inner-right{
flex-wrap:wrap !important;justify-content:center !important;
gap:8px !important;row-gap:8px !important;width:100% !important;
padding-left:0 !important;padding-right:0 !important;
}
.dw-filter-toggle,.dw-colorsort,.boost-sd__toolbar-item--sorting{
flex:1 1 44% !important;min-width:0 !important;max-width:100% !important;
justify-content:center !important;margin:0 !important;
}
.dw-grid-slider{flex:1 1 100% !important;margin:0 !important;justify-content:center !important}
.dw-grid-slider input[type=range]{flex:1 1 auto;width:auto !important;min-width:120px}
.dw-perpage-inline{flex:1 1 44% !important;justify-content:center !important;
margin:0 !important;padding:0 !important;border-left:none !important}
.boost-sd__toolbar-item--product-count,.boost-sd__product-count{
flex:1 1 100% !important;text-align:center !important;margin:0 !important}
.dw-ptype-bar{justify-content:center !important;width:100% !important}
}
/* The Endless-scroll / Pages-of-100 toggle floats position:fixed bottom-left and
overlaps the toolbar's Hide Filters button on short mobile pages. Hide it on
small screens — infinite scroll is the default + best mobile experience; the
paged-mode toggle stays a desktop control. (DW 2026-07-28) */
@media (max-width:600px){ #dw-page-mode{display:none !important} }
.collection-products.rows-of-3 .product-list-item,.collection-products.rows-of-3 .product-grid-masonry-sizer{width:33.333%}
.collection-products.rows-of-4 .product-list-item,.collection-products.rows-of-4 .product-grid-masonry-sizer{width:25%}
.collection-products.rows-of-5 .product-list-item,.collection-products.rows-of-5 .product-grid-masonry-sizer{width:20%}
.collection-products.rows-of-6 .product-list-item,.collection-products.rows-of-6 .product-grid-masonry-sizer{width:16.666%}
.collection-products.rows-of-7 .product-list-item,.collection-products.rows-of-7 .product-grid-masonry-sizer{width:14.285%}
.collection-products.rows-of-8 .product-list-item,.collection-products.rows-of-8 .product-grid-masonry-sizer{width:12.5%}
.collection-products.rows-of-5 .product-list-item .product-vendor,.collection-products.rows-of-5 .product-list-item .product-title,
.collection-products.rows-of-6 .product-list-item .product-vendor,.collection-products.rows-of-6 .product-list-item .product-title,
.collection-products.rows-of-7 .product-list-item .product-vendor,.collection-products.rows-of-7 .product-list-item .product-title,
.collection-products.rows-of-8 .product-list-item .product-vendor,.collection-products.rows-of-8 .product-list-item .product-title{display:none}
.collection-products.rows-of-5 .product-list-item .product-price,
.collection-products.rows-of-6 .product-list-item .product-price,
.collection-products.rows-of-7 .product-list-item .product-price,
.collection-products.rows-of-8 .product-list-item .product-price{font-size:11px}
@media(max-width:768px){
.collection-products.rows-of-5 .product-list-item,.collection-products.rows-of-6 .product-list-item,.collection-products.rows-of-7 .product-list-item,.collection-products.rows-of-8 .product-list-item{width:50%}
}
/* ===== Amazon-style LEFT filter rail (desktop) — DW 2026-07-14 =====
Un-suppress Boost's native vertical tree that .dw-filters-collapsed hides
(rules ~17-24 above) and lay the collection out as a 2-column rail + grid.
Desktop only (>=769px); mobile keeps Boost's native filter drawer untouched.
Placed AFTER the collapse rules so it wins at equal specificity — no arms race. */
@media (min-width:769px){
body.dw-filters-collapsed .boost-sd__filter-tree-wrapper,
body.dw-filters-collapsed .boost-sd__filter-tree-vertical,
body.dw-filters-collapsed .boost-sd__filter-tree-vertical-content,
body.dw-filters-collapsed .boost-sd__filter-tree-vertical-refine-by{display:block !important;width:auto !important;visibility:visible !important}
.boost-sd-layout--has-filter-vertical{display:flex !important;flex-direction:row !important;align-items:flex-start !important;gap:30px !important}
.boost-sd-left{display:block !important;width:264px !important;flex:0 0 264px !important;max-width:264px !important;min-width:264px !important;position:sticky !important;top:14px !important;padding-right:22px !important;border-right:1px solid rgba(0,0,0,.09) !important;overflow:visible !important}
body.dw-filters-collapsed .boost-sd-right,
.boost-sd-right{flex:1 1 auto !important;width:auto !important;min-width:0 !important;max-width:none !important}
body.dw-filters-collapsed .boost-sd__product-list{width:100% !important;max-width:100% !important}
.boost-sd-left .dw-rail-dept{margin:2px 0 18px}
.boost-sd-left .dw-ptype-bar{display:flex !important;flex-wrap:wrap;gap:8px}
.dw-hfilter-bar{display:none !important}
}
</style>
<script>
(function(){
/* DESKTOP ONLY: hide Boost's native vertical tree + run the horizontal bar.
On mobile (<=768) Boost renders its own Filter-button drawer and lazy-loads
the filter options only when opened — so we must NOT hide it or try to read
it; leave the native mobile filter completely alone. */
if (window.innerWidth > 768) {
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 6 cols */
document.documentElement.style.setProperty('--dw-grid-cols', 4);
/* === Amazon-style LEFT rail (desktop): move the product-type pill into Boost's
native left column and drop the now-redundant Hide-Filters toggle. The CSS
above un-hides the native tree + lays out the 2 columns; this only relocates
our custom controls. Self-healing poller so it works regardless of Boost's
async render timing. Mobile (<=768) is left entirely alone. */
if (window.innerWidth > 768) {
/* Boost injects its own CSS at RUNTIME (after any static inline <style>), so a
static override loses the source-order tie. Instead: drop the collapse class
(so Boost's native left/vertical layout is no longer suppressed) and append
our rail CSS at runtime so it lands LAST and wins. Mirrors the verified path. */
document.body.classList.remove('dw-filters-collapsed', 'dw-filters-horizontal', 'dw-hbar-collapsed');
var __railCss = document.createElement('style');
__railCss.textContent =
'@media (min-width:769px){' +
'.boost-sd-layout--has-filter-vertical{display:flex !important;flex-direction:row !important;align-items:flex-start !important;gap:30px !important}' +
'.boost-sd-left{display:block !important;width:264px !important;flex:0 0 264px !important;max-width:264px !important;min-width:264px !important;position:sticky !important;top:14px !important;padding-right:22px !important;border-right:1px solid rgba(0,0,0,.09) !important;overflow:visible !important}' +
'.boost-sd__filter-tree-wrapper,.boost-sd__filter-tree-vertical,.boost-sd__filter-tree-vertical-content,.boost-sd__filter-tree-vertical-refine-by{display:block !important;width:auto !important;visibility:visible !important}' +
'.boost-sd-right{flex:1 1 auto !important;width:auto !important;min-width:0 !important;max-width:none !important}' +
'.boost-sd__product-list{width:100% !important;max-width:100% !important}' +
'.boost-sd-left .dw-rail-dept{margin:2px 0 18px}' +
'.boost-sd-left .dw-ptype-bar{display:flex !important;flex-wrap:wrap;gap:8px}' +
'.dw-hfilter-bar{display:none !important}' +
'.dw-filter-toggle{display:none !important}' + /* redundant Hide/Show-Filters toggle: rail is always visible on desktop */
'}';
document.head.appendChild(__railCss);
var __railTries = 0;
var __railTimer = setInterval(function(){
__railTries++;
/* Boost may re-add the collapse class on re-render — keep it off on desktop */
document.body.classList.remove('dw-filters-collapsed');
/* Boost injects CSS at runtime; re-append ours so it stays LAST and wins */
document.head.appendChild(__railCss);
var left = document.querySelector('.boost-sd-left');
var ptype = document.querySelector('.dw-ptype-bar');
if (left && ptype && !left.contains(ptype)) {
var dept = left.querySelector('.dw-rail-dept');
if (!dept) { dept = document.createElement('div'); dept.className = 'dw-rail-dept'; left.insertBefore(dept, left.firstChild); }
dept.appendChild(ptype);
}
var btns = document.querySelectorAll('button, a, label');
for (var i = 0; i < btns.length; i++) {
var t = (btns[i].textContent || '').trim().toLowerCase();
if (t === 'hide filters' || t === 'show filters') btns[i].style.display = 'none';
}
if ((left && ptype && left.contains(ptype)) || __railTries > 40) clearInterval(__railTimer);
}, 250);
}
var initialized = false;
/* NO product-type persistence: every load starts with nothing selected so ALL
products show, until the user toggles a pill. Product-type state is driven
purely by the URL's pf_t_product_type params (set on click), never re-applied
from a saved choice on load. (Deliberately no auto-apply block here.) */
function initAll(){
if (initialized) return true;
var boostGrid = document.querySelector('.boost-sd__product-list');
var toolbar = document.querySelector('.boost-sd__toolbar-item--product-count, .boost-sd__product-count');
if (!boostGrid || !toolbar) return false;
initialized = true;
/* === GRID SLIDER === */
var saved = localStorage.getItem('dw-grid-cols');
var defaultCols = 4; /* DW 2026-07-28: ALWAYS start 4-across on desktop load (ignore saved pref) */
/* Responsive density (DW 2026-07-28): the slider picks a card MIN-WIDTH so
that, AT THE CURRENT grid width, `val` columns fit exactly — then CSS
auto-fill keeps cards at that min size and DROPS columns as the window
narrows (shrinking from the right reflows the grid live). We derive the
min from the REAL grid width (sidebar eats ~380px), so slider=6 shows 6
columns instead of 5. card-min is fixed once set, so ONLY narrowing drops
columns; widening can add them (more room = more cards). */
var GAP = 16;
var currentCardMin = 150;
function computeCardMin(val){
val = Math.max(3, Math.min(8, parseInt(val) || 6));
var W = boostGrid.getBoundingClientRect().width;
if (!W || W < 320) W = 920; // pre-layout fallback
return Math.max(96, Math.floor((W - GAP * (val - 1)) / val));
}
function gridTemplate(){
return 'repeat(auto-fill, minmax(min(47%, ' + currentCardMin + 'px), 1fr))';
}
function applyGrid(val){
val = Math.max(3, Math.min(8, parseInt(val) || 6));
currentCardMin = computeCardMin(val);
document.documentElement.style.setProperty('--dw-grid-cols', val);
document.documentElement.style.setProperty('--dw-card-min', currentCardMin + 'px');
boostGrid.style.setProperty('grid-template-columns', gridTemplate(), 'important');
var num = document.getElementById('dw-grid-num');
var slider = document.getElementById('dw-grid-range');
if (num) num.textContent = val;
if (slider) slider.value = val;
}
/* === GRID DENSITY SLIDER (DW 2026-06-25 consolidation: RE-ADDED) ===
Drives Boost's REAL grid via applyGrid() -> --dw-grid-cols +
boostGrid.grid-template-columns; persists to localStorage('dw-grid-cols').
Desktop only (mobile is locked to 2-col + infinite scroll). The pill also
hosts the per-page "Show" selector below. */
var sliderWrap = document.createElement('div');
sliderWrap.className = 'dw-grid-slider';
/* DW 2026-07-28: render the density slider at ALL widths (mobile too) so it
reorganizes with the other chips instead of vanishing below 768px. */
if (true) {
var gLbl = document.createElement('label');
gLbl.textContent = 'Grid';
var gRange = document.createElement('input');
gRange.type = 'range'; gRange.id = 'dw-grid-range';
gRange.min = '3'; gRange.max = '8'; gRange.step = '1';
gRange.value = String(defaultCols);
var gNum = document.createElement('span');
gNum.className = 'gc-num'; gNum.id = 'dw-grid-num';
gNum.textContent = String(defaultCols);
gRange.addEventListener('input', function(){
applyGrid(this.value); /* live re-flow */
try { localStorage.setItem('dw-grid-cols', String(this.value)); } catch(e){}
});
sliderWrap.appendChild(gLbl);
sliderWrap.appendChild(gRange);
sliderWrap.appendChild(gNum);
}
/* === FILTER TOGGLE === */
var btn = document.createElement('button');
btn.id = 'dw-filter-toggle';
btn.className = 'dw-filter-toggle';
btn.setAttribute('aria-label', 'Toggle filters');
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('fill', 'none');
svg.setAttribute('stroke', 'currentColor');
svg.setAttribute('stroke-width', '2');
svg.style.cssText = 'width:14px;height:14px';
var ys = ['6','12','18'];
for (var i = 0; i < ys.length; i++) {
var l = document.createElementNS('http://www.w3.org/2000/svg', 'line');
l.setAttribute('x1','4'); l.setAttribute('y1',ys[i]);
l.setAttribute('x2','20'); l.setAttribute('y2',ys[i]);
svg.appendChild(l);
}
btn.appendChild(svg);
var filterLabel = document.createElement('span');
filterLabel.id = 'dw-filter-label';
/* 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(){
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 */
var perpageInline = document.createElement('span');
perpageInline.className = 'dw-perpage-inline';
var perpageLbl = document.createElement('label');
perpageLbl.textContent = 'Show';
var perpageSelect = document.createElement('select');
perpageSelect.id = 'dw-perpage';
var options = [24, 25, 50, 100];
var currentLimit = parseInt(new URLSearchParams(window.location.search).get('limit')) || 24;
options.forEach(function(val) {
var opt = document.createElement('option');
opt.value = val;
opt.textContent = val;
if (val === currentLimit) opt.selected = true;
perpageSelect.appendChild(opt);
});
perpageSelect.addEventListener('change', function() {
var url = new URL(window.location.href);
url.searchParams.set('limit', this.value);
url.searchParams.delete('page');
window.location.href = url.toString();
});
perpageInline.appendChild(perpageLbl);
perpageInline.appendChild(perpageSelect);
sliderWrap.appendChild(perpageInline);
/* === PRODUCT TYPE CHECKBOXES === */
var ptypeBar = document.createElement('div');
ptypeBar.className = 'dw-ptype-bar';
var types = [
{label: 'Wallcoverings', value: 'Wallcovering', defaultOn: false},
{label: 'Fabrics', value: 'Fabric', defaultOn: false},
{label: 'More', value: '', defaultOn: false}
];
// Pills reflect the URL ONLY (no persistence): a fresh load with no
// pf_t_product_type shows everything ("More"/All active); a filtered URL
// checks the matching pills. So state never sticks across loads.
var activeTypes = new URL(window.location.href).searchParams.getAll('pf_t_product_type');
function applyTypeFilter() {
var checked = [];
ptypeBar.querySelectorAll('input[type=checkbox]').forEach(function(cb) {
if (cb.checked && cb.value) checked.push(cb.value);
});
// (no sessionStorage — selection lives only in the URL, so it never persists across loads)
// Apply via URL parameter for Boost
var url = new URL(window.location.href);
url.searchParams.delete('pf_t_product_type');
if (checked.length > 0 && checked.length < types.filter(function(t){return t.value;}).length) {
checked.forEach(function(v) { url.searchParams.append('pf_t_product_type', v); });
}
// Remove page param to reset
url.searchParams.delete('page');
window.location.href = url.toString();
}
types.forEach(function(t, i) {
if (i > 0) {
var sep = document.createElement('span');
sep.className = 'dw-ptype-sep';
ptypeBar.appendChild(sep);
}
var lbl = document.createElement('label');
var cb = document.createElement('input');
cb.type = 'checkbox';
cb.value = t.value;
// "More" = everything else (no value, acts as "all others")
if (t.value) {
cb.checked = activeTypes.indexOf(t.value) >= 0;
} else {
// "More" is NEVER auto-lit on load — it's a show-all/reset action, not a
// persistent state. Empty selection = nothing highlighted (all products show).
cb.checked = false;
}
if (cb.checked) lbl.classList.add('dw-ptype-on'); /* fill active chip */
cb.addEventListener('change', function(){
/* Segmented semantics: "More"/All is mutually exclusive with the
specific types. Selecting All clears the specific types; selecting a
specific type clears All — so clicking All actually resets to all
products instead of leaving a stale type filter applied. */
if (this.checked) {
var selectingAll = !this.value;
ptypeBar.querySelectorAll('input[type=checkbox]').forEach(function(o){
if (o === cb) return;
if (selectingAll ? !!o.value : !o.value) {
o.checked = false;
var ol = o.closest('label'); if (ol) ol.classList.remove('dw-ptype-on');
}
});
}
/* reflect filled/unfilled immediately, then navigate */
var l = this.closest('label'); if (l) l.classList.toggle('dw-ptype-on', this.checked);
applyTypeFilter();
});
lbl.appendChild(cb);
var txt = document.createTextNode(' ' + t.label);
lbl.appendChild(txt);
ptypeBar.appendChild(lbl);
});
/* Insert controls into Boost toolbar */
var toolbarRow = toolbar.parentElement;
toolbarRow.insertBefore(btn, toolbar);
toolbarRow.insertBefore(sliderWrap, toolbar);
toolbarRow.insertBefore(ptypeBar, toolbar);
/* === VISIBLE-TOOLBAR RE-MOUNT (DW 2026-06-27, vp-dw-commerce) ===
Boost renders SEVERAL toolbar variants and hides all but one via
display:none (live = .boost-sd__toolbar--3_1 is hidden). The product-count
item we anchor to (toolbar.parentElement) can therefore be inside a HIDDEN
variant, so the density slider + filter toggle + product-type bar never
reach real users — only the color-sort pill survives because IT already
has a fallback into the visible .boost-sd__toolbar-inner.
Fix (mirrors the proven color-sort mount EXACTLY): only when our anchored
controls land in a HIDDEN toolbar, MOVE them into the same visible
.boost-sd__toolbar-inner / .boost-sd__toolbar-content container the color
pill uses. When --3_1 (or whatever variant) IS visible, this no-ops, so the
existing multi-variant robustness is preserved — we never regress the
already-working case, we only rescue the hidden one. Deferred so Boost has
finished sizing the variants (offsetParent is reliable post-layout). */
function isShownEl(el){ return !!(el && el.offsetParent !== null); }
(function remountIfHidden(tries){
tries = tries || 0;
/* If our slider is already visible to the user, nothing to do. */
if (isShownEl(sliderWrap)) return;
/* The slider/toggle/ptype were anchored next to the product-count item,
which lives inside the HIDDEN .boost-sd__toolbar--3_1 row. The VISIBLE
container Boost actually paints is .boost-sd__toolbar-inner (the same one
the color-sort pill resolves to and mounts as a DIRECT child of — proven
live). Note: that inner is an ANCESTOR of the hidden --3_1, so a naive
.contains(slider) check is true even though the slider isn't visible — do
NOT reject the target on contains(); instead append our controls as DIRECT
children of the visible inner, which LIFTS them out of the hidden --3_1. */
var inners = [].slice.call(document.querySelectorAll(
'.boost-sd__toolbar-inner, .boost-sd__toolbar-content'));
var target = null;
for (var i = 0; i < inners.length; i++){
/* skip an inner whose DIRECT child is already our slider (== it's visible,
we'd have returned above) — but accept a visible inner that only
transitively contains the slider via the hidden --3_1 row. */
if (isShownEl(inners[i]) && sliderWrap.parentElement !== inners[i]) { target = inners[i]; break; }
}
if (target){
/* Move (not clone) so all wired event listeners + state come along.
appendChild relocates them out of the hidden --3_1 into the visible inner. */
target.appendChild(btn);
target.appendChild(sliderWrap);
target.appendChild(ptypeBar);
return;
}
/* Boost may still be laying out / swapping the visible variant — retry a
few times, then give up (worst case = current pre-fix behavior, no regression). */
if (tries < 40) setTimeout(function(){ remountIfHidden(tries + 1); }, 250);
})(0);
/* Apply grid and watch for Boost overrides */
applyGrid(defaultCols);
/* On resize, re-assert the template but DO NOT recompute card-min — that
keeps the min size fixed so narrowing genuinely DROPS columns (auto-fill),
instead of re-pinning the count and squishing cards again. */
window.addEventListener('resize', function(){
boostGrid.style.setProperty('grid-template-columns', gridTemplate(), 'important');
});
/* Observe Boost grid for style changes and re-apply our columns + kill padding */
var observer = new MutationObserver(function(){
/* Re-assert our RESPONSIVE template whenever Boost rewrites the grid style,
so auto-fill reflow survives Boost's re-renders. */
boostGrid.style.setProperty('display', 'grid', 'important');
boostGrid.style.setProperty('grid-template-columns', gridTemplate(), 'important');
if (document.body.classList.contains('dw-filters-collapsed') || window.innerWidth <= 768) {
boostGrid.style.setProperty('padding-right', '0', 'important');
boostGrid.style.setProperty('padding-left', '0', 'important');
}
});
observer.observe(boostGrid, {attributes: true, attributeFilter: ['style', 'class']});
/* Force display:grid + kill padding immediately */
boostGrid.style.setProperty('display', 'grid', 'important');
if (document.body.classList.contains('dw-filters-collapsed') || window.innerWidth <= 768) {
boostGrid.style.setProperty('padding-right', '0', 'important');
boostGrid.style.setProperty('padding-left', '0', 'important');
}
/* Hide any leftover static grid slider */
var old = document.querySelectorAll('div.dw-grid-slider');
for (var j = 1; j < old.length; j++) old[j].style.display = 'none';
return true;
}
var retries = 0;
var poll = setInterval(function(){
if (initAll() || retries++ > 60) clearInterval(poll);
}, 250);
})();
</script>
<script>
/* === MOBILE INFINITE SCROLL (DW 2026-06-23) ===
Boost runs numbered ("default") pagination + renders client-side. On mobile we
replace the pager with TRUE infinite scroll by replaying Boost's own product
API (services.mybcapps.com/bc-sf-filter/filter) one page at a time and
appending rendered cards. Mobile only; top window only; desktop untouched. */
(function(){
if (window.top !== window.self) return;
function isMobile(){ return window.innerWidth <= 768; }
if (window.__dwInfScroll) return; window.__dwInfScroll = true;
var LIMIT = 24;
var capturedUrl = null;
var loadedPage = parseInt(new URLSearchParams(location.search).get('page') || '1', 10) || 1;
var total = Infinity, loading = false, done = false, started = false;
/* Capture Boost's exact filter request so sort + filters + session stay correct */
var _fetch = window.fetch.bind(window);
try {
window.fetch = function(input){
try {
var u = (typeof input === 'string') ? input : (input && input.url);
if (u && u.indexOf('bc-sf-filter/filter') !== -1) capturedUrl = u;
} catch(e){}
return _fetch.apply(this, arguments);
};
} catch(e){}
function buildUrl(page){
var u;
if (capturedUrl) { u = new URL(capturedUrl); }
else {
u = new URL('https://services.mybcapps.com/bc-sf-filter/filter');
u.searchParams.set('shop','designer-laboratory-sandbox.myshopify.com');
u.searchParams.set('sort', new URLSearchParams(location.search).get('sort_by') || 'title-ascending');
u.searchParams.set('event_type','filter');
u.searchParams.set('pg','collection_page');
u.searchParams.set('build_filter_tree','true');
u.searchParams.set('collection_scope','0');
u.searchParams.set('product_available','false');
u.searchParams.set('variant_available','false');
u.searchParams.set('urlScheme','2');
new URLSearchParams(location.search).forEach(function(v,k){ if(/^pf_/.test(k)) u.searchParams.append(k,v); });
}
u.searchParams.set('page', page);
u.searchParams.set('limit', LIMIT);
u.searchParams.set('t', Date.now());
return u.toString();
}
function nameOf(t){ return (t||'').split('|')[0].replace(/\s*wallcoverings?\b/ig,'').replace(/[,\s]+$/,'').trim(); }
function imgOf(p){
if (p.images_info && p.images_info[0] && p.images_info[0].src) return p.images_info[0].src;
if (p.images && typeof p.images === 'object'){
var k = Object.keys(p.images)[0];
if (k){ var v = p.images[k]; return (v && v.src) || (typeof v === 'string' ? v : '') || ''; }
}
return p.image || '';
}
function cardHtml(p){
var img = imgOf(p);
if (img) img += (img.indexOf('?') > -1 ? '&' : '?') + 'width=600';
var nm = nameOf(p.title) || (p.title||'');
/* NEUTRAL classes (dw-if-*) so Boost/theme card scripts never re-process and
strip our <img>. Styled by our own .dw-if-card CSS in the mobile block. */
return '<div class="dw-if-card">' +
'<a href="/products/'+ p.handle +'">' +
'<div class="dw-if-img">' +
(img ? '<img loading="lazy" src="'+ img +'" alt="'+ nm.replace(/"/g,'"') +'">' : '') +
'</div>' +
'<div class="dw-if-name">'+ nm +'</div>' +
'<div class="dw-if-ven">'+ (p.vendor || '') +'</div>' +
'</a></div>';
}
function grid(){ return document.querySelector('.boost-sd__product-list'); }
function spinner(g, show){
var sp = document.getElementById('dw-if-spin');
if (show && !sp){
sp = document.createElement('div'); sp.id = 'dw-if-spin';
sp.textContent = 'Loading more…';
sp.style.cssText = 'grid-column:1/-1;text-align:center;padding:22px 0;font:600 11px/1 -apple-system,sans-serif;letter-spacing:.14em;text-transform:uppercase;color:#8A8A85';
g.parentNode.insertBefore(sp, g.nextSibling);
} else if (!show && sp) { sp.remove(); }
}
function loadMore(){
if (loading || done || !isMobile()) return;
var g = grid(); if (!g) return;
if (loadedPage * LIMIT >= total) { done = true; return; }
loading = true; spinner(g, true);
_fetch(buildUrl(loadedPage + 1), { headers: { 'Accept':'application/json' } })
.then(function(r){ return r.json(); })
.then(function(j){
var t = j.total_product != null ? j.total_product : j.total;
if (typeof t === 'number') total = t;
var prods = j.products || [];
if (!prods.length) { done = true; spinner(g, false); loading = false; return; }
var frag = document.createElement('div');
frag.innerHTML = prods.map(cardHtml).join('');
while (frag.firstChild) g.appendChild(frag.firstChild);
loadedPage++; loading = false; spinner(g, false);
if (loadedPage * LIMIT >= total) done = true;
setTimeout(onScroll, 60);
})
.catch(function(){ loading = false; spinner(g, false); });
}
function onScroll(){
if (loading || done || !isMobile()) return;
if (window.innerHeight + window.scrollY > document.documentElement.scrollHeight - 1200) loadMore();
}
function hidePager(){
if (document.getElementById('dw-if-css')) return;
var s = document.createElement('style'); s.id = 'dw-if-css';
s.textContent = '@media(max-width:768px){.boost-sd__pagination{display:none !important}}';
document.head.appendChild(s);
}
function start(){
if (started || !isMobile()) return;
var tries = 0;
var c = setInterval(function(){
tries++;
if (grid()) { clearInterval(c); started = true; hidePager(); window.addEventListener('scroll', onScroll, { passive:true }); window.addEventListener('resize', onScroll, { passive:true }); onScroll(); }
else if (tries > 50) clearInterval(c);
}, 400);
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start);
else start();
})();
</script>
<!-- DW REC GRID FIX — You-May-Also-Like: 3-up+text on mobile, 4-up image-only on desktop (2026-06-23) -->
<style id="dw-rec-grid-fix">
/* MOBILE: 3 fixed columns, keep the title/vendor overlay text */
@media (max-width:768px){
.product-recommendations-wrapper--section .product-recommendations{
display:grid !important;
grid-template-columns:repeat(3,1fr) !important;
gap:10px 8px !important;
position:static !important;
height:auto !important;
}
.product-recommendations-wrapper--section .product-recommendations .product-list-item{
width:auto !important;float:none !important;position:static !important;
left:auto !important;top:auto !important;right:auto !important;margin:0 !important;
}
.product-recommendations-wrapper--section .product-recommendations .product-grid-masonry-sizer{display:none !important}
}
/* DESKTOP (4-up rows-of-4): image-only — hide all title/vendor/hover text */
@media (min-width:769px){
.product-recommendations-wrapper--section .product-recommendations .dw-hover-overlay,
.product-recommendations-wrapper--section .product-recommendations .product-list-item-details{display:none !important}
}
</style>
<!-- DW SHOP-BY-COLOR ONE-ROW — all color dots on a single row (2026-06-23) -->
<style id="dw-sbc-onerow">
#sbc-circles.sbc-circles, .sbc-circles{
display:flex !important; flex-wrap:nowrap !important;
justify-content:safe center !important; align-items:flex-start !important;
gap:clamp(4px,0.9vw,14px) !important; width:100% !important; max-width:100% !important;
margin:0 auto !important; padding:20px 10px 40px !important;
overflow-x:auto !important; -webkit-overflow-scrolling:touch !important; scrollbar-width:thin !important;
grid-template-columns:none !important;
}
.sbc-circles .sbc-circle{ flex:1 1 0 !important; min-width:42px !important; max-width:96px !important; }
.sbc-circles .sbc-circle-dot{ width:100% !important; height:auto !important; aspect-ratio:1/1 !important; }
.sbc-circles .sbc-circle-name{ white-space:nowrap !important; font-size:clamp(9px,0.85vw,13px) !important; }
</style>
{%- comment -%}
DW SHOP-BY-COLOR PRODUCT-TYPE CHECKBOXES (Steve 2026-07-17).
The color-wheel on /pages/shop-by-color lives in the Page BODY (write_content
scope still blocked, same as the 2026-07-09 handle fix), so this adds the
Wallcoverings / Fabrics / More pills from the theme side:
· wraps window.fetch (this snippet renders BEFORE the page body, so the wrap
is installed before the wheel's first /apps/color-widen call) to harvest
each result's product_type by handle — the dwphoto API returns product_type
as of 2026-07-17;
· inserts the pill bar above #sbc-grid and hides/shows the wheel's own
.sbc-card nodes via a MutationObserver on every re-render;
· NO guessed default — nothing checked on first visit (all types show);
once the shopper picks, the choice persists for the SESSION
(sessionStorage 'dw-sbc-ptype') and re-applies on return. "More" = show
everything (reset), never lit — same semantics as the collection-page
dw-ptype-bar. Inert on every other page.
{%- endcomment -%}
{%- if request.path contains '/pages/shop-by-color' -%}
<style>
#dwSbcPtype{display:flex;justify-content:center;align-items:center;gap:8px;flex-wrap:wrap;padding:14px 16px 0;user-select:none;font-family:-apple-system,BlinkMacSystemFont,sans-serif}
#dwSbcPtype label{box-sizing:border-box;display:inline-flex;align-items:center;gap:6px;cursor:pointer;white-space:nowrap;height:34px;min-height:34px;padding:0 14px;margin:0;border:1.5px solid #1A1A1A;border-radius:999px;background:#fff;color:#1A1A1A;font-size:11px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;line-height:1}
#dwSbcPtype label:hover{background:#f5f5f5}
#dwSbcPtype label.on{background:#1A1A1A;color:#fff}
#dwSbcPtype input[type=checkbox]{width:13px;height:13px;margin:0;cursor:pointer;accent-color:#3D4246}
#dwSbcPtypeEmpty{grid-column:1/-1;text-align:center;color:#9a9276;padding:40px;font-size:14px}
</style>
<script>
(function(){
var TYPES={}; /* handle -> product_type, harvested from API responses */
var _fetch=window.fetch;
window.fetch=function(input){
var url=(typeof input==='string')?input:(input&&input.url)||'';
var p=_fetch.apply(this,arguments);
if(url.indexOf('/apps/color-widen')!==-1||url.indexOf('/apps/color-index')!==-1){
p.then(function(r){try{r.clone().json().then(function(j){
((j&&j.results)||[]).forEach(function(x){if(x&&x.handle)TYPES[x.handle]=x.product_type||'';});
setTimeout(applyFilter,60);
}).catch(function(){});}catch(e){}});
}
return p;
};
var KEY='dw-sbc-ptype';
var DEFS=[
{label:'Wallcoverings',value:'Wallcovering',re:/wallcover|wallpaper/i},
{label:'Fabrics', value:'Fabric', re:/fabric/i},
{label:'More', value:'', re:null}
];
function sel(){try{var v=JSON.parse(sessionStorage.getItem(KEY)||'[]');return Array.isArray(v)?v:[];}catch(e){return[];}}
function save(a){try{sessionStorage.setItem(KEY,JSON.stringify(a));}catch(e){}}
function pass(pt){var s=sel();if(!s.length)return true;pt=pt||'';
for(var i=0;i<DEFS.length;i++){var d=DEFS[i];
if(d.value&&s.indexOf(d.value)>=0&&d.re&&d.re.test(pt))return true;}
return false;}
var bar=null,emptyMsg=null,applying=false;
function buildBar(grid){
if(bar&&bar.parentNode)bar.parentNode.removeChild(bar);
bar=document.createElement('div');bar.id='dwSbcPtype';
bar.setAttribute('aria-label','Filter results by product type');
var s=sel();
DEFS.forEach(function(t){
var lbl=document.createElement('label');
var cb=document.createElement('input');cb.type='checkbox';cb.value=t.value;
cb.checked=!!t.value&&s.indexOf(t.value)>=0; /* "More" never lit; no guessed default */
if(cb.checked)lbl.className='on';
cb.addEventListener('change',function(){
var cur=sel();
if(!this.value){cur=[];this.checked=false;} /* More = reset/show-all */
else if(this.checked){if(cur.indexOf(this.value)<0)cur.push(this.value);}
else{var v=this.value;cur=cur.filter(function(x){return x!==v;});}
save(cur);buildBar(grid);applyFilter();
});
lbl.appendChild(cb);lbl.appendChild(document.createTextNode(' '+t.label));
bar.appendChild(lbl);
});
grid.parentNode.insertBefore(bar,grid);
}
function applyFilter(){
var grid=document.getElementById('sbc-grid');if(!grid)return;
applying=true;
var cards=grid.querySelectorAll('.sbc-card'),shown=0;
Array.prototype.forEach.call(cards,function(card){
var a=card.querySelector('a[href*="/products/"]');
var h=a?decodeURIComponent(((a.getAttribute('href')||'').split('/products/')[1]||'')).split('?')[0]:'';
var ok=pass(TYPES[h]);
card.style.display=ok?'':'none';
if(ok)shown++;
});
if(emptyMsg&&emptyMsg.parentNode)emptyMsg.parentNode.removeChild(emptyMsg);
emptyMsg=null;
if(cards.length&&!shown&&sel().length){
emptyMsg=document.createElement('div');emptyMsg.id='dwSbcPtypeEmpty';
emptyMsg.textContent='No matches of that product type in this shade — try another hue, or tap More to see everything.';
grid.appendChild(emptyMsg);
}
setTimeout(function(){applying=false;},0); /* re-entrance guard for the observer */
}
function init(){
var grid=document.getElementById('sbc-grid');
if(!grid){setTimeout(init,300);return;}
buildBar(grid);
new MutationObserver(function(){if(!applying)applyFilter();}).observe(grid,{childList:true});
applyFilter();
}
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',init);else init();
})();
</script>
{%- endif -%}
<style>/* DW 2026-06-25 consolidation: RE-ENABLE the density slider on the Boost grid.
The 06-24 block that hid .dw-grid-slider is superseded — the slider now drives
--dw-grid-cols + boostGrid.grid-template-columns directly (see initAll JS),
persists to localStorage('dw-grid-cols'), and shares the uniform pill shape.
Reversible: restore `.dw-grid-slider{display:none !important}` to roll back. */
.dw-grid-slider{display:inline-flex !important}
@media(max-width:768px){.dw-grid-slider{display:none !important}} /* mobile = fixed 2-col + infinite scroll, no slider */
</style>
<!-- ============================================================================
DW HUE / COLOR SORT (2026-06-27, vp-dw-commerce)
Fulfills the hard rule "every product grid MUST have sort + density" — the
density slider + Boost-native sort already exist; this adds the MISSING
color-sort modes (Light→Dark, Dark→Light, Color Wheel, By Color buckets).
Per the DTD HYBRID verdict (approach C): this is the CLIENT-SIDE ORDER of
the currently-loaded Boost cards. Boost SD's native color FILTER scopes the
full collection (Steve-gated dashboard config, drafted separately); this
re-orders whatever cards are on screen by each card's dominant hue.
Per-card hex source: computed live from the card's own thumbnail via a tiny
downscaled <canvas> average ($0, local, no data-file key dependency — the
dw-color-wheel-data.json `h` keys only match ~30% of Boost handles, so we
read the pixels instead). Cached per image src. Re-applies on Boost
re-render / infinite scroll via MutationObserver.
============================================================================ -->
<style>
.dw-colorsort{display:inline-flex;align-items:center;gap:6px;box-sizing:border-box;
height:34px;min-height:34px;padding:0 12px;margin:5px 6px 0 0;
border:1.5px solid #1A1A1A;border-radius:999px;background:#fff;color:#1A1A1A;
font-family:-apple-system,BlinkMacSystemFont,sans-serif;font-size:11px;font-weight:600;
letter-spacing:.06em;line-height:1;white-space:nowrap;user-select:none}
.dw-colorsort label{display:inline-flex;align-items:center;gap:5px;text-transform:uppercase;cursor:pointer}
.dw-colorsort .sw{width:13px;height:13px;border-radius:50%;
background:conic-gradient(#c0392b,#e67e22,#f1c40f,#2ecc71,#1ab6c4,#2e6fd4,#7d4fd4,#c0392b);
border:1px solid rgba(0,0,0,.15)}
.dw-colorsort select{border:none;background:transparent;font:inherit;font-weight:700;
text-transform:uppercase;letter-spacing:.06em;cursor:pointer;padding:0 2px 0 0;margin:0;
min-width:84px;-webkit-appearance:none;appearance:none;color:#1A1A1A}
.dw-colorsort.is-busy{opacity:.6}
@media(max-width:768px){.dw-colorsort{height:34px;margin:5px 6px 0 0;overflow:hidden}
.dw-colorsort label{min-width:0}
.dw-colorsort select{min-width:0;max-width:100%;flex:1 1 auto;text-overflow:ellipsis}}
</style>
<script>
(function(){
if (window.__dwColorSort) return; window.__dwColorSort = true;
if (!/\/(collections|search)/.test(location.pathname)) return;
var KEY = 'dw-colorsort-mode';
var GRID_SEL = '.boost-sd__product-list';
var CARD_SEL = '.boost-sd__product-item';
/* ---- color math (mirrors dw-color-wheel.js) ---- */
function rgb2lab(c){var R=c.map(function(v){v/=255;return v>0.04045?Math.pow((v+0.055)/1.055,2.4):v/12.92;});
var X=(R[0]*0.4124+R[1]*0.3576+R[2]*0.1805)/0.95047,Y=R[0]*0.2126+R[1]*0.7152+R[2]*0.0722,Z=(R[0]*0.0193+R[1]*0.1192+R[2]*0.9505)/1.08883;
var f=function(t){return t>0.008856?Math.cbrt(t):7.787*t+16/116;};X=f(X);Y=f(Y);Z=f(Z);return[116*Y-16,500*(X-Y),200*(Y-Z)];}
function rgb2hsl(r,g,b){r/=255;g/=255;b/=255;var mx=Math.max(r,g,b),mn=Math.min(r,g,b),h,s,l=(mx+mn)/2;
if(mx===mn){h=s=0;}else{var d=mx-mn;s=l>0.5?d/(2-mx-mn):d/(mx+mn);
switch(mx){case r:h=(g-b)/d+(g<b?6:0);break;case g:h=(b-r)/d+2;break;default:h=(r-g)/d+4;}h/=6;}
return[h*360,s,l];}
/* dominant color of an image: downscale to 16x16, drop near-white/near-black
paper margins, average the rest. Returns [r,g,b] or null. */
var hexCache = {};
function colorOfImg(img){
var src = img.currentSrc || img.src; if(!src) return null;
if (hexCache[src]) return hexCache[src];
try{
var n=16, cv=document.createElement('canvas'); cv.width=n; cv.height=n;
var cx=cv.getContext('2d',{willReadFrequently:true});
cx.drawImage(img,0,0,n,n);
var data=cx.getImageData(0,0,n,n).data, r=0,g=0,b=0,k=0;
for(var i=0;i<data.length;i+=4){
var R=data[i],G=data[i+1],B=data[i+2],A=data[i+3];
if(A<128) continue;
var mx=Math.max(R,G,B),mn=Math.min(R,G,B),l=(mx+mn)/510;
if(l>0.94||l<0.06) continue; /* skip paper-white & pure-black ground */
r+=R;g+=G;b+=B;k++;
}
if(!k){ /* fallback: plain average */
for(var j=0;j<data.length;j+=4){if(data[j+3]<128)continue;r+=data[j];g+=data[j+1];b+=data[j+2];k++;}
}
if(!k) return null;
var out=[Math.round(r/k),Math.round(g/k),Math.round(b/k)];
hexCache[src]=out; return out;
}catch(e){ return null; } /* tainted canvas etc. */
}
/* hue bucket order for "By Color" — reds→oranges→…→neutrals last */
function hueBucket(rgb){
var hsl=rgb2hsl(rgb[0],rgb[1],rgb[2]), h=hsl[0], s=hsl[1], l=hsl[2];
if(s<0.12) return 100 + (1-l); /* greys/neutrals → end, light→dark */
return Math.floor(h/30); /* 12 hue buckets 0..11 */
}
function cardKey(card, mode){
var img = card.querySelector('img.boost-sd__product-image-img, img');
var rgb = img ? colorOfImg(img) : null;
/* cards we can't read sort to the END for every mode (sort key always large) */
if (!rgb) return mode==='wheel' ? 999 : 999999;
if (mode==='light' || mode==='dark'){
var L = rgb2lab(rgb)[0]; /* 0 (black) .. 100 (white) */
/* "Light → Dark" = lightest FIRST => sort ascending on (100 - L)
"Dark → Light" = darkest FIRST => sort ascending on L */
return mode==='light' ? (100 - L) : L;
}
if (mode==='wheel'){ return rgb2hsl(rgb[0],rgb[1],rgb[2])[0]; } /* 0..360 hue */
return hueBucket(rgb); /* bucket */
}
function reorder(mode){
var grid=document.querySelector(GRID_SEL); if(!grid) return false;
var cards=[].slice.call(grid.querySelectorAll(CARD_SEL));
if(cards.length<2) return false;
/* only reorder once images are decodable; if many are still loading, retry */
var ready=0; cards.forEach(function(c){var im=c.querySelector('img'); if(im&&im.complete&&im.naturalWidth)ready++;});
if(ready < Math.min(cards.length, 6)) return false;
var decorated=cards.map(function(c,i){return {c:c,i:i,k:cardKey(c,mode)};});
decorated.sort(function(a,b){ return a.k===b.k ? a.i-b.i : (a.k<b.k?-1:1); });
var frag=document.createDocumentFragment();
decorated.forEach(function(d){frag.appendChild(d.c);});
grid.appendChild(frag);
return true;
}
var sel; /* the <select> */
function currentMode(){ return (sel && sel.value) || localStorage.getItem(KEY) || 'default'; }
function apply(){
var mode=currentMode();
if(mode==='default') return; /* leave Boost's native order untouched */
var box=sel?sel.closest('.dw-colorsort'):null; if(box)box.classList.add('is-busy');
/* enable CORS on card imgs so canvas isn't tainted (Shopify CDN allows it) */
document.querySelectorAll(CARD_SEL+' img').forEach(function(im){
if(!im.crossOrigin){ im.crossOrigin='anonymous'; if(im.complete&&im.src){var s=im.src;im.src='';im.src=s;} }
});
var tries=0;
(function go(){
var ok=reorder(mode);
if(!ok && tries++<20){ setTimeout(go,250); return; }
if(box)box.classList.remove('is-busy');
})();
}
function buildControl(toolbarRow, beforeNode){
if(document.querySelector('.dw-colorsort')) return;
var wrap=document.createElement('div'); wrap.className='dw-colorsort';
var lab=document.createElement('label');
var sw=document.createElement('span'); sw.className='sw';
lab.appendChild(sw);
sel=document.createElement('select'); sel.id='dw-colorsort-sel';
[['default','Color Sort'],['light','Light → Dark'],['dark','Dark → Light'],
['wheel','Color Wheel'],['bucket','By Color']].forEach(function(o){
var op=document.createElement('option'); op.value=o[0]; op.textContent=o[1]; sel.appendChild(op);
});
var saved=localStorage.getItem(KEY); if(saved) sel.value=saved;
sel.addEventListener('change',function(){
localStorage.setItem(KEY,this.value);
if(this.value==='default'){ location.reload(); return; } /* restore Boost order cleanly */
apply();
});
lab.appendChild(sel); wrap.appendChild(lab);
if(beforeNode && beforeNode.parentNode===toolbarRow) toolbarRow.insertBefore(wrap,beforeNode);
else toolbarRow.appendChild(wrap);
}
/* Mount the color-sort pill RIGHT NEXT TO the grid-density slider. The
density slider's own init (in the block above) already resolved the correct
(visible) Boost toolbar variant and inserted itself there — so anchoring to
it inherits that correctness exactly and the two pills always sit together.
This avoids the Boost "several toolbar variants, all-but-one display:none"
trap (a guess at the visible toolbar mis-fires headless). Fallback: the
product-count toolbar item if the slider never appears (e.g. mobile). */
function isShown(el){ return !!(el && el.offsetParent !== null); }
var mountTries=0;
var mt=setInterval(function(){
if (document.querySelector('.dw-colorsort')) { clearInterval(mt); return; }
var slider = document.querySelector('.dw-grid-slider');
/* Primary: sit right after the density slider IF its toolbar is actually
shown (keeps the two density/color pills together). */
if (slider && slider.parentElement && isShown(slider)){
clearInterval(mt);
buildControl(slider.parentElement, slider.nextSibling);
if(currentMode()!=='default') apply();
return;
}
/* Fallback: Boost renders several toolbar variants and hides all but one.
If the slider's variant is hidden, drop the pill into the VISIBLE
toolbar content/inner container so real users always get it. */
var content = document.querySelector('.boost-sd__toolbar-content, .boost-sd__toolbar-inner');
if (content && isShown(content) && mountTries > 6){
clearInterval(mt);
buildControl(content, content.firstChild);
if(currentMode()!=='default') apply();
return;
}
/* Last resort after a generous wait: mount next to slider regardless of
computed visibility (real users see it even when automation reads 0px —
the live shipped density slider behaves identically). */
if (slider && slider.parentElement && mountTries > 40){
clearInterval(mt);
buildControl(slider.parentElement, slider.nextSibling);
if(currentMode()!=='default') apply();
return;
}
if(mountTries++>80){ clearInterval(mt); }
},300);
/* re-apply after Boost re-renders / appends cards */
var rt;
new MutationObserver(function(muts){
if(currentMode()==='default') return;
for(var i=0;i<muts.length;i++){ if(muts[i].addedNodes&&muts[i].addedNodes.length){
clearTimeout(rt); rt=setTimeout(apply,200); return; } }
}).observe(document.body,{childList:true,subtree:true});
})();
</script>
<!-- ============================================================================
DW COMPACT TOOLBAR (2026-07-15, vp-dw-commerce)
Collapses the tall, spread-out Boost search/collection toolbar into a single
tight row — matching the approved compact mockup (dw-search-toolbar-mockup):
• every control 32px tall (was 34) with its stacked top-margin zeroed,
• the Boost toolbar row's own vertical padding/gap tightened,
• the LOUD bold product count muted to a quiet 12px grey label,
• a slimmer density-slider track that sits cleanly inside the pill.
CSS-ONLY — the controls themselves are still the existing JS-injected .dw-*
pills + Boost-native sort; this only tightens their box metrics, so none of
the hard-won mount/remount/arms-race JS above is touched. Appended LAST so
source order wins the ties. Desktop only (>=769px): mobile keeps its own
34px stacked pills + infinite scroll untouched. Reversible: delete this
whole <style id="dw-compact-toolbar"> block to restore the prior spacing.
============================================================================ -->
<style id="dw-compact-toolbar">
@media (min-width:769px){
/* 1 — tighten the Boost toolbar row itself: kill the tall vertical padding */
.boost-sd__toolbar,
.boost-sd__toolbar-inner,
.boost-sd__toolbar-content{
min-height:0 !important;
padding-top:6px !important;padding-bottom:6px !important;
row-gap:6px !important;column-gap:8px !important;
align-items:center !important;
}
/* 2 — every control on ONE height (32px), no stacked top-margin, tighter gap */
.dw-filter-toggle,
.dw-grid-slider,
.dw-colorsort,
.dw-ptype-bar label,
.boost-sd__toolbar-item--sorting,
.boost-sd__filter-tree-toggle-button{
height:32px !important;min-height:32px !important;
margin:0 8px 0 0 !important;padding:0 12px !important;
}
/* 3 — product count: from loud/bold to a quiet muted label */
.boost-sd__product-count,
.boost-sd__toolbar-item--product-count,
.boost-sd__page-total-text{
font-size:12px !important;font-weight:500 !important;color:#8a8a8a !important;
text-transform:none !important;letter-spacing:.02em !important;
margin:0 4px 0 0 !important;
}
/* 4 — slimmer density track + smaller labels so it fits the 32px pill */
.dw-grid-slider input[type=range]{width:84px !important;height:3px !important}
.dw-grid-slider label{font-size:10px !important}
.dw-grid-slider .gc-num{font-size:12px !important}
}
</style>