← back to Designer Wallcoverings
Add dw-boost-filter-topbar section: horizontal dropdown filter toolbar (DTD verdict B) driving Boost pf_* params, per-field ON/OFF toggle, ported new-import-viewer UX
a5a75bec3c6af667a55a341a7c727a1fbcd72ce7 · 2026-06-25 16:05:46 -0700 · Steve Abrams
Files touched
A shopify/sections/dw-boost-filter-topbar.liquid
Diff
commit a5a75bec3c6af667a55a341a7c727a1fbcd72ce7
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Jun 25 16:05:46 2026 -0700
Add dw-boost-filter-topbar section: horizontal dropdown filter toolbar (DTD verdict B) driving Boost pf_* params, per-field ON/OFF toggle, ported new-import-viewer UX
---
shopify/sections/dw-boost-filter-topbar.liquid | 459 +++++++++++++++++++++++++
1 file changed, 459 insertions(+)
diff --git a/shopify/sections/dw-boost-filter-topbar.liquid b/shopify/sections/dw-boost-filter-topbar.liquid
new file mode 100644
index 00000000..5240e534
--- /dev/null
+++ b/shopify/sections/dw-boost-filter-topbar.liquid
@@ -0,0 +1,459 @@
+{%- comment -%}
+ DW Boost Filter Topbar — DTD verdict B (officer-yolo 2026-06-25)
+
+ A CUSTOM horizontal DROPDOWN filter toolbar that sits ABOVE Boost's grid and
+ drives Boost's pf_* URL params + sort_by. Ported UX from the left-hand filters
+ of new.designerwallcoverings.com (new-import-viewer): toggleable controls,
+ sort + density + search, all persisted to localStorage.
+
+ ── HARD ARCHITECTURE RULES (do not violate) ──
+ 1. This bar READS Boost's filter-tree DOM (option labels/counts) + the current
+ URL's pf_* params, and WRITES pf_* / sort_by params then NAVIGATES.
+ It NEVER mutates Boost's result grid DOM, never removes a Boost node, never
+ restyles Boost's React internals. Zero grid-break surface (see memory
+ reference_dw_theme_grid_init_abort: dropping a Boost DOM hook silently breaks
+ every grid — this bar only ADDS a sibling node, queries are read-only).
+ 2. Boost is the filter ENGINE. The bar is UX-only. If Boost re-renders 1000x/day
+ the bar is independent — on each Boost mutation it re-reads option values
+ (debounced) but only to repopulate its OWN dropdowns.
+ 3. Each filter field is individually toggleable ON/OFF by the user; the choice
+ persists in localStorage (dw_topbar_fields), same convention as sort/density.
+ 4. NO private-label vendor names ever surface (Brewster/York/WallQuest/Command54
+ etc.) — the Brand/Vendor dropdown is built from Boost's emitted option list,
+ which already carries house-line names; a deny-list scrub is applied as a
+ belt-and-suspenders guard before any option is rendered.
+
+ ── Boost param contract (verified on the live page 2026-06-25) ──
+ pf_t_<tag> tag-backed facets → product_type, color, style, durability,
+ historical_era (extensible: material,
+ process, pattern, contents = pf_t_*)
+ pf_v_brand vendor/brand facet
+ pf_p_price price range (pf_p_price=<min>:<max>)
+ sort_by Boost/Shopify sort (created-descending, price-ascending, …)
+ q search term
+
+ Each FIELD below maps to one Boost param. Fields whose Boost facet is not yet
+ configured in the Boost SD dashboard render their dropdown EMPTY with a
+ "configure in Boost" hint (see the accompanying runbook) — they do not break.
+
+ Revision: 2026-06-25 (officer-yolo / vp-dw-commerce)
+{%- endcomment -%}
+
+{%- liquid
+ assign accent = settings.color_accent | default: '#111111'
+-%}
+
+<div class="dw-tbf" data-section-id="{{ section.id }}" data-dw-topbar>
+ <style>
+ .dw-tbf{width:100%;padding:14px 0;margin-bottom:18px;border-bottom:1px solid var(--border-color,rgba(0,0,0,.1));
+ font-family:inherit;--dw-acc:{{ accent }}}
+ .dw-tbf__row{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
+ .dw-tbf__label{font-size:10px;letter-spacing:.3em;text-transform:uppercase;font-weight:700;
+ color:var(--text-muted,rgba(0,0,0,.55));margin-right:2px;white-space:nowrap}
+ /* one dropdown unit */
+ .dw-dd{position:relative}
+ .dw-dd__btn{display:inline-flex;align-items:center;gap:7px;height:34px;padding:0 13px;
+ font-size:11px;letter-spacing:.12em;text-transform:uppercase;font-weight:600;
+ color:var(--text-color,inherit);background:transparent;border:1px solid var(--border-color,rgba(0,0,0,.14));
+ border-radius:20px;cursor:pointer;white-space:nowrap;transition:border-color .15s,background .15s;line-height:1}
+ .dw-dd__btn:hover{border-color:var(--dw-acc)}
+ .dw-dd__btn[aria-expanded="true"]{border-color:var(--dw-acc);background:var(--bg-hover,rgba(0,0,0,.03))}
+ .dw-dd__btn .dw-caret{font-size:8px;opacity:.6;transition:transform .15s}
+ .dw-dd__btn[aria-expanded="true"] .dw-caret{transform:rotate(180deg)}
+ .dw-dd__btn .dw-cnt{font-variant-numeric:tabular-nums;background:var(--dw-acc);color:#fff;
+ border-radius:999px;font-size:9px;min-width:15px;height:15px;display:inline-flex;align-items:center;
+ justify-content:center;padding:0 4px}
+ /* active (has a selection) */
+ .dw-dd.has-sel .dw-dd__btn{background:var(--dw-acc);color:#fff;border-color:var(--dw-acc)}
+ .dw-dd.has-sel .dw-dd__btn .dw-cnt{background:#fff;color:var(--dw-acc)}
+ /* panel */
+ .dw-dd__panel{position:absolute;top:calc(100% + 6px);left:0;z-index:40;min-width:220px;max-width:300px;
+ max-height:340px;overflow:auto;background:var(--bg-color,#fff);border:1px solid var(--border-color,rgba(0,0,0,.14));
+ border-radius:12px;box-shadow:0 12px 34px rgba(0,0,0,.16);padding:8px;display:none}
+ .dw-dd.open .dw-dd__panel{display:block}
+ .dw-opt{display:flex;align-items:center;gap:9px;padding:7px 9px;border-radius:8px;cursor:pointer;
+ font-size:12.5px;color:var(--text-color,inherit);user-select:none}
+ .dw-opt:hover{background:var(--bg-hover,rgba(0,0,0,.04))}
+ .dw-opt input{width:15px;height:15px;accent-color:var(--dw-acc);margin:0;cursor:pointer;flex:0 0 auto}
+ .dw-opt .dw-sw{width:15px;height:15px;border-radius:50%;border:1px solid rgba(0,0,0,.18);flex:0 0 auto}
+ .dw-opt .dw-on{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+ .dw-opt .dw-oc{font-size:10.5px;color:var(--text-muted,rgba(0,0,0,.45));font-variant-numeric:tabular-nums}
+ .dw-dd__empty{padding:14px 10px;font-size:11.5px;color:var(--text-muted,rgba(0,0,0,.5));line-height:1.4}
+ .dw-dd__price{display:flex;gap:8px;align-items:center;padding:10px 9px}
+ .dw-dd__price input{width:74px;height:30px;border:1px solid var(--border-color,rgba(0,0,0,.16));
+ border-radius:8px;padding:0 8px;font:inherit;font-size:12px}
+ .dw-dd__apply{margin-top:6px;width:100%;height:32px;border:0;border-radius:8px;background:var(--dw-acc);
+ color:#fff;font-size:11px;letter-spacing:.14em;text-transform:uppercase;font-weight:700;cursor:pointer}
+ /* sort + density + search reuse the same chip vocabulary */
+ .dw-tbf__sort{height:34px;padding:0 11px;border:1px solid var(--border-color,rgba(0,0,0,.14));border-radius:20px;
+ background:transparent;font:inherit;font-size:11px;letter-spacing:.1em;text-transform:uppercase;font-weight:600;cursor:pointer;color:inherit}
+ .dw-tbf__sort:hover{border-color:var(--dw-acc)}
+ .dw-density{display:inline-flex;align-items:center;gap:9px;height:34px}
+ .dw-density input[type=range]{width:96px;accent-color:var(--dw-acc);cursor:pointer}
+ .dw-density .dw-dlabel{font-size:10px;letter-spacing:.16em;text-transform:uppercase;font-weight:600;
+ color:var(--text-muted,rgba(0,0,0,.55));min-width:42px;text-align:right}
+ .dw-search{display:inline-flex;align-items:center;gap:7px;height:34px;border:1px solid var(--border-color,rgba(0,0,0,.14));
+ border-radius:20px;padding:0 12px;min-width:160px}
+ .dw-search input{flex:1;border:0;background:transparent;font:inherit;font-size:12px;outline:none;color:inherit}
+ .dw-search svg{width:13px;height:13px;stroke:var(--text-muted,rgba(0,0,0,.4));fill:none;stroke-width:1.6;flex:0 0 auto}
+ .dw-clear{margin-left:auto;font-size:10px;letter-spacing:.16em;text-transform:uppercase;font-weight:700;
+ color:var(--dw-acc);background:transparent;border:0;cursor:pointer;white-space:nowrap}
+ .dw-clear:disabled{opacity:.35;cursor:default}
+ /* the "which filters are shown" gear menu (per-field ON/OFF) */
+ .dw-gear{position:relative}
+ .dw-gear__btn{height:34px;width:34px;display:inline-flex;align-items:center;justify-content:center;
+ border:1px solid var(--border-color,rgba(0,0,0,.14));border-radius:50%;background:transparent;cursor:pointer;font-size:14px}
+ .dw-gear__btn:hover{border-color:var(--dw-acc)}
+ .dw-gear__panel{position:absolute;top:calc(100% + 6px);right:0;z-index:41;min-width:200px;background:var(--bg-color,#fff);
+ border:1px solid var(--border-color,rgba(0,0,0,.14));border-radius:12px;box-shadow:0 12px 34px rgba(0,0,0,.16);padding:8px;display:none}
+ .dw-gear.open .dw-gear__panel{display:block}
+ .dw-gear__panel .dw-gtitle{font-size:9.5px;letter-spacing:.22em;text-transform:uppercase;font-weight:700;
+ color:var(--text-muted,rgba(0,0,0,.5));padding:5px 9px 7px}
+ @media (max-width:720px){
+ .dw-tbf__label{display:none}
+ .dw-density{display:none}
+ .dw-search{min-width:0;flex:1 1 100%;order:5}
+ .dw-dd__panel{max-width:84vw}
+ }
+ </style>
+
+ <div class="dw-tbf__row" data-tbf-row>
+ <span class="dw-tbf__label">Filter</span>
+ <span data-tbf-fields></span>
+
+ {%- comment -%} sort {%- endcomment -%}
+ <span class="dw-tbf__label" style="margin-left:6px">Sort</span>
+ <select class="dw-tbf__sort" data-tbf-sort aria-label="Sort products">
+ <option value="">Newest</option>
+ <option value="created-descending">Created · Newest</option>
+ <option value="created-ascending">Created · Oldest</option>
+ <option value="title-ascending">Title A→Z</option>
+ <option value="title-descending">Title Z→A</option>
+ <option value="price-ascending">Price ↑</option>
+ <option value="price-descending">Price ↓</option>
+ <option value="best-selling">Best selling</option>
+ </select>
+
+ {%- comment -%} density {%- endcomment -%}
+ <span class="dw-density" data-tbf-density-wrap>
+ <span class="dw-tbf__label">Grid</span>
+ <input type="range" min="2" max="8" value="4" data-tbf-density aria-label="Grid density">
+ <span class="dw-dlabel"><span data-tbf-dlabel>4</span> col</span>
+ </span>
+
+ {%- comment -%} search {%- endcomment -%}
+ <span class="dw-search" data-tbf-search-wrap>
+ <svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"></circle><path d="m21 21-4.35-4.35"></path></svg>
+ <input type="text" data-tbf-search placeholder="Search pattern / sku / color…" aria-label="Search">
+ </span>
+
+ <button class="dw-clear" data-tbf-clear disabled>Clear all</button>
+
+ {%- comment -%} per-field ON/OFF gear {%- endcomment -%}
+ <span class="dw-gear" data-tbf-gear>
+ <button class="dw-gear__btn" data-tbf-gear-btn title="Choose which filters to show" aria-haspopup="true" aria-expanded="false">⚙</button>
+ <div class="dw-gear__panel" data-tbf-gear-panel>
+ <div class="dw-gtitle">Show filters</div>
+ <span data-tbf-gear-list></span>
+ </div>
+ </span>
+ </div>
+</div>
+
+<script>
+(function(){
+ 'use strict';
+ var root = document.currentScript.closest('[data-dw-topbar]') || document.querySelector('[data-dw-topbar]');
+ if(!root || root.__dwTbfInit) return; root.__dwTbfInit = true;
+
+ // ── FIELD MODEL ──────────────────────────────────────────────────────────
+ // key = stable id (localStorage)
+ // label= dropdown button text
+ // param= Boost URL param this field writes (the contract)
+ // kind = 'tag' (pf_t_*) | 'vendor' (pf_v_brand) | 'price' (pf_p_price)
+ // swatch=render a color dot per option (color/swatch field)
+ // The order here is the default left→right order in the bar.
+ var FIELDS = [
+ { key:'color', label:'Color', param:'pf_t_color', kind:'tag', swatch:true },
+ { key:'style', label:'Style', param:'pf_t_style', kind:'tag' },
+ { key:'material', label:'Material', param:'pf_t_material', kind:'tag' },
+ { key:'brand', label:'Brand', param:'pf_v_brand', kind:'vendor' },
+ { key:'process', label:'Process', param:'pf_t_process', kind:'tag' },
+ { key:'vendor', label:'Vendor', param:'pf_v_brand', kind:'vendor' },
+ { key:'swatch', label:'Swatch', param:'pf_t_color', kind:'tag', swatch:true },
+ { key:'contents', label:'Contents', param:'pf_t_contents', kind:'tag' },
+ { key:'pattern', label:'Pattern', param:'pf_t_pattern', kind:'tag' },
+ { key:'sku', label:'SKU', param:'pf_t_sku', kind:'tag' },
+ { key:'durability',label:'Durability',param:'pf_t_durability', kind:'tag' },
+ { key:'era', label:'Era', param:'pf_t_historical_era',kind:'tag' },
+ { key:'price', label:'Price', param:'pf_p_price', kind:'price' }
+ ];
+ // Which fields are ON by default (the common five + price/vendor/created via sort).
+ var DEFAULT_ON = ['color','style','material','brand','process','price'];
+
+ // Private-label deny-list — these upstream names must NEVER reach a shopper.
+ var DENY = [/brewster/i,/\byork\b/i,/wallquest/i,/chesapeake/i,/nextwall/i,/seabrook/i,/command\s*54/i,/desima/i,/carlsten/i,/nicolette\s*mayer/i];
+ function denied(s){ s=String(s||''); return DENY.some(function(r){return r.test(s);}); }
+
+ var LS = {
+ fields:'dw_topbar_fields', density:'dw_collection_density',
+ sort:'dw_collection_sort', search:'dw_collection_search'
+ };
+ function lsGet(k,d){ try{var v=localStorage.getItem(k); return v==null?d:v;}catch(e){return d;} }
+ function lsSet(k,v){ try{localStorage.setItem(k,v);}catch(e){} }
+
+ function curParams(){ return new URLSearchParams(window.location.search); }
+ function esc(s){ return String(s==null?'':s).replace(/[&<>"']/g,function(c){return({'&':'&','<':'<','>':'>','"':'"',"'":'''})[c];}); }
+
+ // ── read which fields are enabled ────────────────────────────────────────
+ function enabledSet(){
+ var saved = lsGet(LS.fields,null);
+ if(saved){ try{ var a=JSON.parse(saved); if(Array.isArray(a)) return new Set(a); }catch(e){} }
+ return new Set(DEFAULT_ON);
+ }
+
+ // ── read Boost's emitted option values for a tag/vendor param ────────────
+ // STRICTLY READ-ONLY: we query Boost's filter-tree DOM for labels+counts.
+ // We never remove/restyle those nodes. If Boost hasn't rendered them yet
+ // (client API still loading) we fall back to the value already in the URL so
+ // the active selection is always reflected. Never throws → never breaks grid.
+ function boostOptions(field){
+ var out=[], seen={};
+ var param = field.param;
+ // 1) values already active in the URL (always show these so state is honest)
+ curParams().getAll(param).forEach(function(v){
+ v=decodeURIComponent(v); if(!seen[v]&&!denied(v)){ seen[v]=1; out.push({value:v,label:v,count:null,checked:true}); }
+ });
+ // 2) options Boost has rendered in its (possibly-hidden) native tree.
+ // Boost emits filter options as anchors/inputs carrying the pf_* param in
+ // href or data attributes. We scan defensively across known shapes.
+ try{
+ var tree = document.querySelector('.boost-sd__filter-tree') || document.querySelector('.boost-sd__sidebar');
+ if(tree){
+ tree.querySelectorAll('a[href],input[type="checkbox"]').forEach(function(el){
+ var href = el.getAttribute('href')||'';
+ var match=null;
+ if(href.indexOf(param+'=')>-1){
+ var u; try{ u=new URL(href, window.location.origin); }catch(e){ return; }
+ (u.searchParams.getAll(param)||[]).forEach(function(v){ match=v; });
+ }
+ if(!match && el.getAttribute('data-filter-value') && (el.getAttribute('data-filter-type')||'').toLowerCase().indexOf(field.key)>-1){
+ match = el.getAttribute('data-filter-value');
+ }
+ if(!match) return;
+ var lbl = (el.textContent||match).replace(/\s+\(\d[\d,]*\)\s*$/,'').trim() || match;
+ var cm = (el.textContent||'').match(/\((\d[\d,]*)\)\s*$/);
+ if(seen[match]||denied(match)||denied(lbl)) return;
+ seen[match]=1; out.push({value:match,label:lbl,count:cm?cm[1]:null,checked:false});
+ });
+ }
+ }catch(e){ /* read-only scan; ignore */ }
+ return out;
+ }
+
+ // ── swatch hex guess from a color name (cheap, named-color map) ──────────
+ var NAMED={black:'#1a1a1a',white:'#f4f4f4',grey:'#9aa0a6',gray:'#9aa0a6',silver:'#c8ccd1',red:'#c0392b',
+ pink:'#e84393',orange:'#e67e22',yellow:'#f1c40f',gold:'#caa53d',green:'#27ae60',olive:'#7d8c3e',
+ teal:'#16a085',blue:'#2980b9',navy:'#27408b',cyan:'#36c5d6',purple:'#8e44ad',violet:'#7c4dbb',
+ brown:'#8d6748',beige:'#d8c7a8',cream:'#efe6cf',taupe:'#b3a394',ivory:'#f1ebda',charcoal:'#36393f',
+ bronze:'#9c6b3c',copper:'#b87333',natural:'#d6c8af',neutral:'#cfc6b8'};
+ function swatchHex(name){
+ var n=String(name||'').toLowerCase();
+ for(var k in NAMED){ if(n.indexOf(k)>-1) return NAMED[k]; }
+ return '#cfc6b8';
+ }
+
+ // ── build a single dropdown for a field ──────────────────────────────────
+ function buildDropdown(field){
+ var opts = (field.kind==='price') ? [] : boostOptions(field);
+ var active = curParams().getAll(field.param).map(decodeURIComponent).filter(function(v){return !denied(v);});
+ var dd=document.createElement('span');
+ dd.className='dw-dd'+(active.length?' has-sel':'');
+ dd.setAttribute('data-dd', field.key);
+
+ var btn=document.createElement('button');
+ btn.type='button'; btn.className='dw-dd__btn'; btn.setAttribute('aria-expanded','false'); btn.setAttribute('aria-haspopup','true');
+ btn.innerHTML = esc(field.label) + (active.length?(' <span class="dw-cnt">'+active.length+'</span>'):'') + ' <span class="dw-caret">▾</span>';
+ dd.appendChild(btn);
+
+ var panel=document.createElement('div'); panel.className='dw-dd__panel';
+
+ if(field.kind==='price'){
+ var cur=curParams().get('pf_p_price')||''; var mn='',mx='';
+ if(cur.indexOf(':')>-1){ var pr=cur.split(':'); mn=pr[0]; mx=pr[1]; }
+ panel.innerHTML =
+ '<div class="dw-dd__price">'+
+ '<input type="number" min="0" step="1" placeholder="Min" data-pmin value="'+esc(mn)+'">'+
+ '<span style="opacity:.5">–</span>'+
+ '<input type="number" min="0" step="1" placeholder="Max" data-pmax value="'+esc(mx)+'">'+
+ '</div>'+
+ '<button class="dw-dd__apply" data-papply>Apply price</button>';
+ panel.querySelector('[data-papply]').addEventListener('click',function(){
+ var lo=panel.querySelector('[data-pmin]').value.trim(), hi=panel.querySelector('[data-pmax]').value.trim();
+ var p=curParams();
+ if(lo||hi){ p.set('pf_p_price',(lo||'0')+':'+(hi||'999999')); } else { p.delete('pf_p_price'); }
+ navTo(p);
+ });
+ } else if(!opts.length){
+ panel.innerHTML='<div class="dw-dd__empty">No <b>'+esc(field.label)+'</b> facet is configured in Boost yet.<br>Add it in the Boost SD dashboard (param <code>'+esc(field.param)+'</code>) — see the runbook. Active selections still work.</div>';
+ } else {
+ var html=opts.map(function(o){
+ var sw = field.swatch ? '<span class="dw-sw" style="background:'+swatchHex(o.label)+'"></span>' : '';
+ return '<label class="dw-opt"><input type="checkbox" data-val="'+esc(o.value)+'" '+(o.checked?'checked':'')+'>'+
+ sw+'<span class="dw-on">'+esc(o.label)+'</span>'+(o.count?('<span class="dw-oc">'+esc(o.count)+'</span>'):'')+'</label>';
+ }).join('');
+ panel.innerHTML=html;
+ panel.querySelectorAll('input[type="checkbox"]').forEach(function(cb){
+ cb.addEventListener('change',function(){
+ var p=curParams(); var val=cb.getAttribute('data-val');
+ var have=p.getAll(field.param).map(decodeURIComponent);
+ p.delete(field.param);
+ if(cb.checked){ if(have.indexOf(val)<0) have.push(val); }
+ else { have=have.filter(function(v){return v!==val;}); }
+ have.forEach(function(v){ p.append(field.param,v); });
+ navTo(p);
+ });
+ });
+ }
+ dd.appendChild(panel);
+
+ btn.addEventListener('click',function(e){
+ e.stopPropagation();
+ var wasOpen=dd.classList.contains('open');
+ closeAll();
+ if(!wasOpen){ dd.classList.add('open'); btn.setAttribute('aria-expanded','true'); }
+ });
+ return dd;
+ }
+
+ // ── navigation: write params + go (resets paging so Boost re-queries) ─────
+ function navTo(params){
+ params.delete('page'); // jump back to page 1 on any filter change
+ var qs=params.toString();
+ window.location.href = window.location.pathname + (qs?('?'+qs):'');
+ }
+
+ function closeAll(){
+ root.querySelectorAll('.dw-dd.open').forEach(function(d){ d.classList.remove('open'); var b=d.querySelector('.dw-dd__btn'); if(b)b.setAttribute('aria-expanded','false'); });
+ var g=root.querySelector('[data-tbf-gear]'); if(g)g.classList.remove('open');
+ }
+ document.addEventListener('click',function(ev){ if(!root.contains(ev.target)) closeAll(); });
+
+ // ── render the field dropdowns according to enabled set ──────────────────
+ function renderFields(){
+ var host=root.querySelector('[data-tbf-fields]'); if(!host) return;
+ var en=enabledSet();
+ host.innerHTML='';
+ FIELDS.forEach(function(f){ if(en.has(f.key)) host.appendChild(buildDropdown(f)); });
+ // clear-all enabled when any pf_*/q is present
+ var anyActive=false; curParams().forEach(function(v,k){ if(k.indexOf('pf_')===0||k==='q') anyActive=true; });
+ var clr=root.querySelector('[data-tbf-clear]'); if(clr) clr.disabled=!anyActive;
+ }
+
+ // ── gear menu: per-field ON/OFF, persisted ───────────────────────────────
+ function renderGear(){
+ var list=root.querySelector('[data-tbf-gear-list]'); if(!list) return;
+ var en=enabledSet();
+ list.innerHTML=FIELDS.map(function(f){
+ return '<label class="dw-opt"><input type="checkbox" data-fkey="'+f.key+'" '+(en.has(f.key)?'checked':'')+'><span class="dw-on">'+esc(f.label)+'</span></label>';
+ }).join('');
+ list.querySelectorAll('input[data-fkey]').forEach(function(cb){
+ cb.addEventListener('change',function(){
+ var s=enabledSet();
+ if(cb.checked) s.add(cb.getAttribute('data-fkey')); else s.delete(cb.getAttribute('data-fkey'));
+ lsSet(LS.fields, JSON.stringify(Array.from(s)));
+ renderFields();
+ });
+ });
+ }
+ var gearBtn=root.querySelector('[data-tbf-gear-btn]'), gear=root.querySelector('[data-tbf-gear]');
+ if(gearBtn&&gear){ gearBtn.addEventListener('click',function(e){ e.stopPropagation(); var o=gear.classList.contains('open'); closeAll(); if(!o){gear.classList.add('open');gearBtn.setAttribute('aria-expanded','true');} else gearBtn.setAttribute('aria-expanded','false'); }); }
+
+ // ── sort ─────────────────────────────────────────────────────────────────
+ var sortSel=root.querySelector('[data-tbf-sort]');
+ if(sortSel){
+ var curSort=curParams().get('sort_by')||lsGet(LS.sort,'');
+ if(curSort){ var opt=sortSel.querySelector('option[value="'+curSort+'"]'); if(opt) sortSel.value=curSort; }
+ sortSel.addEventListener('change',function(){
+ lsSet(LS.sort,this.value); var p=curParams();
+ if(this.value) p.set('sort_by',this.value); else p.delete('sort_by');
+ navTo(p);
+ });
+ }
+
+ // ── density (drives Boost's grid var ONLY, never the grid nodes) ──────────
+ // We set the CSS custom property --dw-grid-cols that the live theme's Boost
+ // override already binds to .boost-sd__product-list. We do NOT touch list
+ // children, never set grid-template on the list directly here unless the var
+ // isn't wired — purely additive, desktop-only.
+ var dens=root.querySelector('[data-tbf-density]'), dlabel=root.querySelector('[data-tbf-dlabel]');
+ function applyDensity(n){
+ document.documentElement.style.setProperty('--dw-grid-cols', n);
+ var list=document.querySelector('.boost-sd__product-list');
+ if(list && !getComputedStyle(list).getPropertyValue('--dw-grid-cols')){
+ // fallback only if the theme var isn't bound — still never removes nodes
+ list.style.gridTemplateColumns='repeat('+n+',minmax(0,1fr))';
+ }
+ if(dlabel) dlabel.textContent=n;
+ }
+ if(dens){
+ var savedD=lsGet(LS.density,'4'); dens.value=savedD; applyDensity(parseInt(savedD,10)||4);
+ dens.addEventListener('input',function(){ var n=parseInt(this.value,10)||4; lsSet(LS.density,n); applyDensity(n); });
+ }
+
+ // ── search ────────────────────────────────────────────────────────────────
+ var srch=root.querySelector('[data-tbf-search]');
+ if(srch){
+ srch.value=curParams().get('q')||'';
+ var t; srch.addEventListener('input',function(){ clearTimeout(t); var self=this; t=setTimeout(function(){
+ lsSet(LS.search,self.value); var p=curParams();
+ if(self.value.trim()) p.set('q',self.value.trim()); else p.delete('q');
+ navTo(p);
+ },450); });
+ srch.addEventListener('keydown',function(e){ if(e.key==='Enter'){ clearTimeout(t); var p=curParams(); if(this.value.trim())p.set('q',this.value.trim());else p.delete('q'); navTo(p);} });
+ }
+
+ // ── clear all ──────────────────────────────────────────────────────────────
+ var clr=root.querySelector('[data-tbf-clear]');
+ if(clr) clr.addEventListener('click',function(){
+ var p=curParams(); var del=[];
+ p.forEach(function(v,k){ if(k.indexOf('pf_')===0||k==='q'||k==='page') del.push(k); });
+ del.forEach(function(k){ p.delete(k); });
+ navTo(p);
+ });
+
+ // ── re-read Boost options when Boost re-renders (debounced, read-only) ────
+ // Only repopulates OUR dropdowns; never writes to Boost's tree.
+ var ro; var target=document.querySelector('.boost-sd__filter-tree')||document.querySelector('.boost-sd__sidebar');
+ if(target){
+ var mo=new MutationObserver(function(){ clearTimeout(ro); ro=setTimeout(renderFields,300); });
+ mo.observe(target,{childList:true,subtree:true});
+ }
+
+ // ── boot ──────────────────────────────────────────────────────────────────
+ renderFields(); renderGear();
+})();
+</script>
+
+{% schema %}
+{
+ "name": "DW Boost Filter Topbar",
+ "tag": "section",
+ "class": "dw-boost-filter-topbar",
+ "settings": [
+ {
+ "type": "header",
+ "content": "Horizontal dropdown filter toolbar that drives Boost's pf_* params. Per-field show/hide is controlled by the shopper via the ⚙ menu (persisted in their browser); these defaults only seed first-visit."
+ },
+ {
+ "type": "color",
+ "id": "color_accent",
+ "label": "Accent color",
+ "default": "#111111"
+ }
+ ],
+ "presets": [
+ { "name": "DW Boost Filter Topbar" }
+ ]
+}
+{% endschema %}
← b360daed auto-save: 2026-06-25T16:05:06 (8 files) — DW-Agents/vendor-
·
back to Designer Wallcoverings
·
Add field->Boost mapping + data-layer spec README for dw-boo 3ebc8cd9 →