← back to Dw Theme Boost Fix

sections/dw-boost-filter-topbar.liquid

515 lines

{%- 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}
    /* product-type segmented control (ports Boost's Wallcoverings/Fabrics tabs) */
    .dw-seg{display:inline-flex;border:1px solid var(--border-color,rgba(0,0,0,.14));border-radius:20px;overflow:hidden;height:34px;margin-right:4px}
    .dw-seg button{border:0;background:transparent;padding:0 14px;font:inherit;font-size:11px;letter-spacing:.12em;text-transform:uppercase;font-weight:600;cursor:pointer;color:inherit;white-space:nowrap;line-height:1}
    .dw-seg button+button{border-left:1px solid var(--border-color,rgba(0,0,0,.12))}
    .dw-seg button:hover{background:var(--bg-hover,rgba(0,0,0,.04))}
    .dw-seg button[aria-pressed="true"]{background:var(--dw-acc);color:#fff}
    /* live product count (ports Boost's "N products") */
    .dw-count{font-size:11px;letter-spacing:.06em;text-transform:uppercase;font-weight:600;color:var(--text-muted,rgba(0,0,0,.55));white-space:nowrap;margin-left:2px}
    /* REPLACE Boost's native toolbar — this topbar is the single control surface.
       Cosmetic hide ONLY: the node stays in the DOM so Boost's own JS and our
       read-only count-reader keep working (never removes a Boost hook). */
    .boost-sd__toolbar{display:none!important}
    /* 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>
    {%- comment -%} product-type segmented control (replaces Boost's native Wallcoverings/Fabrics tabs) {%- endcomment -%}
    <span class="dw-seg" data-tbf-types role="group" aria-label="Product type">
      <button type="button" data-ptype="" aria-pressed="false">All</button>
      <button type="button" data-ptype="Wallcovering" aria-pressed="false">Wallcoverings</button>
      <button type="button" data-ptype="Fabric" aria-pressed="false">Fabrics</button>
    </span>

    <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>
    <span class="dw-count" data-tbf-count></span>

    {%- 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. Data-backed only (verified 2026-06-25): color
  // (color: tags), style/brand (global.Style/Brand facets), price (pf_p_price), and
  // MATERIAL — backed by global.Contents (vinyl/grasscloth/raffia/paper, 78% fill);
  // a Boost facet labeled "Material" from global.Contents emits pf_t_material to match.
  // Process OFF (Construction/Finish empty — no data); Pattern OFF (near-unique → poor
  // facet). All remain user-enableable via the ⚙ gear when their data lands.
  var DEFAULT_ON = ['color','style','brand','material','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({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'})[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> options are available for this collection yet.</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(function(){ renderFields(); renderCount(); },300); });
    mo.observe(target,{childList:true,subtree:true});
  }

  // ── product-type segmented control (ports Boost's Wallcoverings/Fabrics tabs) ─
  var typeWrap=root.querySelector('[data-tbf-types]');
  if(typeWrap){
    var curType=curParams().get('pf_t_product_type')||'';
    typeWrap.querySelectorAll('button[data-ptype]').forEach(function(b){
      if(b.getAttribute('data-ptype')===curType) b.setAttribute('aria-pressed','true');
      b.addEventListener('click',function(){
        var p=curParams(); var t=b.getAttribute('data-ptype');
        if(t) p.set('pf_t_product_type',t); else p.delete('pf_t_product_type');
        navTo(p);
      });
    });
  }

  // ── live product count (ports Boost's "N products"); STRICTLY read-only ──────
  function readCount(){
    var sels=['.boost-sd__total-product','.boost-sd__filter-total-product','.boost-sd__total-product-count','.boost-sd__refine-total'];
    for(var i=0;i<sels.length;i++){ var el=document.querySelector(sels[i]); if(el){ var m=(el.textContent||'').match(/[\d,]+/); if(m) return m[0]; } }
    var tb=document.querySelector('.boost-sd__toolbar'); // hidden via CSS but present in DOM
    if(tb){ var mm=(tb.textContent||'').match(/([\d,]+)\s*(products|results|items)/i); if(mm) return mm[1]; }
    return null;
  }
  function renderCount(){ var c=root.querySelector('[data-tbf-count]'); if(!c) return; var n=readCount(); c.textContent = n ? (n+' products') : ''; }

  // ── boot ──────────────────────────────────────────────────────────────────
  renderFields(); renderGear(); renderCount();
  // Boost loads async — retry the count a few times until it appears.
  var _ct=0, _ci=setInterval(function(){ renderCount(); if(++_ct>8 || root.querySelector('[data-tbf-count]').textContent) clearInterval(_ci); },500);
})();
</script>

{% schema %}
{
  "name": "DW Boost Filter Topbar",
  "tag": "section",
  "class": "dw-boost-filter-topbar",
  "settings": [
    {
      "type": "header",
      "content": "Filter Topbar"
    },
    {
      "type": "paragraph",
      "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 %}