← back to Majilite Onboard

public/index.html

389 lines

<!doctype html>
<html lang="en">
<head>
<script>
// Credential-safe fetch guard (fleet drop-in) — if this page is opened with
// credentials in the URL (a saved bookmark / Chrome-remembered basic-auth:
// https://user:pass@host/…), the browser poisons document.baseURI, so any
// relative or root-relative fetch('/api/…') throws "Request cannot be constructed
// from a URL that includes credentials" and callers silently fall to an empty
// state. Resolve every non-absolute request URL against the credential-free
// location instead of baseURI. Placed first so it wraps window.fetch before any
// app script runs. Ref: creds-in-url-fetch-guard-fleet-pattern.
(function () {
  var _fetch = window.fetch.bind(window);
  var cleanBase = function () { return location.origin + location.pathname; };
  window.fetch = function (input, init) {
    try {
      if (typeof input === 'string' && !/^[a-z]+:\/\//i.test(input) && input.indexOf('//') !== 0) {
        input = new URL(input, cleanBase()).href;
      } else if (input instanceof Request && !/^[a-z]+:\/\//i.test(input.url)) {
        input = new Request(new URL(input.url, cleanBase()).href, input);
      }
    } catch (_) { /* fall through to native */ }
    return _fetch(input, init);
  };
})();
</script>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Majilite · Metallic Specialties I — Onboarding Viewer</title>
<style>
  :root{ --cols:5; --bg:#0d0f12; --panel:#14171c; --line:#232830; --ink:#e9ecf1; --dim:#8a929e; --teal:#39c2c9; --accent:#5b6cff; }
  *{box-sizing:border-box}
  body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,Arial,sans-serif}
  a{color:inherit;text-decoration:none}
  header{padding:16px 20px;border-bottom:1px solid var(--line);display:flex;align-items:center;gap:16px;flex-wrap:wrap;background:linear-gradient(180deg,#111318,#0d0f12)}
  header h1{font-size:16px;margin:0;font-weight:600;letter-spacing:.02em}
  header h1 small{color:var(--dim);font-weight:400}
  .pill{font-size:11px;color:var(--dim);border:1px solid var(--line);border-radius:20px;padding:3px 10px}
  .pill b{color:var(--teal)}
  /* import bar */
  #import{padding:12px 20px;border-bottom:1px solid var(--line);background:var(--panel)}
  .stages{display:flex;gap:10px;flex-wrap:wrap;margin-bottom:8px}
  .stage{flex:1;min-width:130px;border:1px solid var(--line);border-radius:8px;padding:8px 10px;background:#0f1216}
  .stage .n{font-size:20px;font-weight:700}
  .stage .l{font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.05em}
  .stage.active{border-color:var(--teal);box-shadow:0 0 0 1px var(--teal) inset}
  .bar{height:8px;background:#0f1216;border:1px solid var(--line);border-radius:6px;overflow:hidden}
  .bar>i{display:block;height:100%;width:0;background:linear-gradient(90deg,var(--accent),var(--teal));transition:width .12s}
  .barrow{display:flex;align-items:center;gap:12px}
  .barrow .txt{font-size:12px;color:var(--dim);white-space:nowrap;min-width:220px}
  .btn{border:1px solid var(--line);background:#0f1216;color:var(--ink);border-radius:7px;padding:6px 12px;cursor:pointer;font-size:12px}
  .btn:hover{border-color:var(--teal)}
  /* layout */
  main{display:flex;align-items:flex-start}
  aside{width:250px;flex:0 0 250px;border-right:1px solid var(--line);padding:14px;position:sticky;top:0;max-height:100vh;overflow:auto}
  aside h3{font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--dim);margin:16px 0 6px}
  .facet{display:flex;justify-content:space-between;align-items:center;padding:4px 8px;border-radius:6px;cursor:pointer;font-size:13px}
  .facet:hover{background:#0f1216}
  .facet.on{background:#0f1216;outline:1px solid var(--teal)}
  .facet .cnt{font-size:11px;color:#0b0d10;background:var(--teal);border-radius:10px;padding:0 7px;font-weight:600}
  .dot{width:11px;height:11px;border-radius:50%;display:inline-block;border:1px solid #0006;margin-right:7px;vertical-align:-1px}
  section{flex:1;padding:14px 18px;min-width:0}
  .toolbar{display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-bottom:12px}
  .toolbar input[type=search]{flex:1;min-width:180px;background:#0f1216;border:1px solid var(--line);color:var(--ink);border-radius:7px;padding:7px 10px}
  select,.toolbar label{font-size:12px;color:var(--dim)}
  select{background:#0f1216;border:1px solid var(--line);color:var(--ink);border-radius:7px;padding:6px 8px}
  input[type=range]{vertical-align:middle}
  .grid{display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:12px}
  .card{border:1px solid var(--line);border-radius:10px;overflow:hidden;background:var(--panel);cursor:pointer;transition:transform .1s,border-color .1s;position:relative}
  .card:hover{transform:translateY(-2px);border-color:var(--teal)}
  .card.new{animation:pop .5s ease}
  @keyframes pop{0%{transform:scale(.9);opacity:0}100%{transform:scale(1);opacity:1}}
  .thumb{aspect-ratio:1/1;background:#0f1216;display:flex;align-items:center;justify-content:center;position:relative}
  .thumb img{width:100%;height:100%;object-fit:cover;display:block}
  .thumb .ph{font-size:10px;color:#0007;font-weight:700;letter-spacing:.1em}
  .meta{padding:8px 10px}
  .meta .t{font-size:12.5px;font-weight:600;line-height:1.25}
  .meta .sku{font:11px ui-monospace,Menlo,monospace;color:var(--teal);margin-top:2px}
  .meta .row{display:flex;align-items:center;gap:6px;margin-top:6px;flex-wrap:wrap}
  .chip{font-size:10.5px;color:var(--dim);border:1px solid var(--line);border-radius:20px;padding:1px 8px;cursor:pointer}
  .chip:hover{border-color:var(--teal);color:var(--ink)}
  .when{font-size:10px;color:var(--dim);margin-top:6px}
  .spec-badge{position:absolute;top:6px;right:6px;font-size:9px;background:#0b0d10cc;border:1px solid var(--line);color:var(--teal);border-radius:6px;padding:1px 6px}
  .imgonly .meta{display:none}
  /* ── List view ── */
  .grid.list{display:flex;flex-direction:column;gap:6px}
  .grid.list .card{display:grid;grid-template-columns:52px 1fr auto;align-items:center;border-radius:8px;gap:10px;padding:6px 12px 6px 6px}
  .grid.list .thumb{width:52px;height:52px;min-width:52px;border-radius:6px;overflow:hidden}
  .grid.list .thumb img{width:100%;height:100%;object-fit:cover}
  .grid.list .meta{display:flex;flex-direction:column;gap:2px;padding:0}
  .grid.list .meta .t{font-size:13px}
  .grid.list .meta .sku{font-size:11px}
  .grid.list .meta .row{margin-top:4px}
  .grid.list .meta .when{margin-top:0}
  .grid.list .spec-badge{position:static;font-size:9px}
  /* ── View toggle buttons ── */
  .view-btn{background:var(--panel);border:1px solid var(--line);color:var(--dim);border-radius:7px;padding:6px 11px;cursor:pointer;font-size:12px;transition:border-color .12s,color .12s}
  .view-btn:hover{border-color:var(--teal);color:var(--ink)}
  .view-btn.active{border-color:var(--teal);color:var(--teal)}
  /* ── Fields ▾ popover ── */
  .fieldmenu{position:relative}
  .fbtn{background:var(--panel);border:1px solid var(--line);color:var(--dim);border-radius:7px;padding:6px 11px;cursor:pointer;font-size:12px;white-space:nowrap}
  .fbtn:hover{border-color:var(--teal);color:var(--ink)}
  .fpop{position:absolute;top:calc(100% + 6px);left:0;background:#0f1216;border:1px solid var(--line);border-radius:8px;box-shadow:0 10px 30px #0008;padding:8px 4px;z-index:30;min-width:180px}
  .fpop[hidden]{display:none}
  .fpop label{display:flex;align-items:center;gap:9px;padding:6px 12px;font-size:12.5px;color:var(--dim);cursor:pointer;border-radius:6px;white-space:nowrap}
  .fpop label:hover{background:var(--panel);color:var(--ink)}
  .fpop input{accent-color:var(--teal);margin:0}
  /* ── field hide classes (grid only) ── */
  body.hf-thumb   .grid:not(.list) .thumb{display:none}
  body.hf-title   .grid:not(.list) .meta .t{display:none}
  body.hf-sku     .grid:not(.list) .meta .sku{display:none}
  body.hf-color   .grid:not(.list) .meta .row .dot,
  body.hf-color   .grid:not(.list) .meta .row .chip[data-f="family"]{display:none}
  body.hf-pattern .grid:not(.list) .meta .row .chip[data-f="pattern"]{display:none}
  body.hf-when    .grid:not(.list) .meta .when{display:none}
  /* drawer */
  #drawer{position:fixed;top:0;right:0;height:100vh;width:420px;max-width:92vw;background:var(--panel);border-left:1px solid var(--line);transform:translateX(100%);transition:transform .18s;overflow:auto;z-index:20}
  #drawer.open{transform:none}
  #drawer .dh{padding:16px;border-bottom:1px solid var(--line)}
  #drawer .db{padding:16px}
  #drawer img{width:100%;border-radius:8px;border:1px solid var(--line)}
  table.spec{width:100%;border-collapse:collapse;font-size:12.5px;margin-top:10px}
  table.spec td{padding:6px 4px;border-bottom:1px solid var(--line);vertical-align:top}
  table.spec td:first-child{color:var(--dim);width:42%}
  .x{position:absolute;top:12px;right:14px;cursor:pointer;color:var(--dim);font-size:20px}
  .quote{display:inline-block;font-size:10px;color:#f4c04e;border:1px solid #f4c04e55;border-radius:5px;padding:1px 6px;margin-top:6px}
</style>
  <link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
</head>
<body>
<header>
  <h1>Majilite · <em>Metallic Specialties&nbsp;I</em> &nbsp;<small>The Nytek® Collection — onboarding</small></h1>
  <span class="pill">DW series <b id="pfx">DWCC-</b></span>
  <span class="pill"><b id="cnt">0</b> SKUs</span>
  <span class="pill">names <b>kept original</b></span>
  <span class="pill">pricing <b id="pmode">quote-only</b></span>
</header>

<div id="import">
  <div class="stages" id="stages"></div>
  <div class="barrow">
    <div class="bar"><i id="fill"></i></div>
    <div class="txt" id="ticker">Ready to import…</div>
    <button class="btn" id="run">▶ Run import</button>
    <button class="btn" id="replay" style="display:none">↻ Replay</button>
  </div>
</div>

<main>
  <aside>
    <h3>Onboard status</h3>
    <div id="statusFacets"></div>
    <h3>Color family</h3>
    <div id="colorFacets"></div>
    <h3>Pattern</h3>
    <div id="patternFacets" style="max-height:260px;overflow:auto"></div>
  </aside>
  <section>
    <div class="toolbar">
      <input type="search" id="q" placeholder="Search pattern, colorway, SKU, tag… (space = AND)"/>
      <label>Sort
        <select id="sort">
          <option value="seq">Newest (import order)</option>
          <option value="family">Color family</option>
          <option value="light">Light → Dark</option>
          <option value="dark">Dark → Light</option>
          <option value="pattern">Pattern A→Z</option>
          <option value="title">Title A→Z</option>
          <option value="sku">SKU A→Z</option>
          <option value="color">Color A→Z</option>
          <option value="vendor">Vendor A→Z</option>
        </select>
      </label>
      <label>Density <input type="range" id="dens" min="3" max="9" value="5"/></label>
      <label><input type="checkbox" id="imgonly"/> image-only</label>
      <button class="view-btn" id="viewgrid" title="Grid view">⊞ Grid</button>
      <button class="view-btn" id="viewlist" title="List view">☰ List</button>
      <div class="fieldmenu">
        <button class="fbtn" id="fieldbtn">Fields ▾</button>
        <div class="fpop" id="fpop" hidden>
          <label><input type="checkbox" data-mf="thumb" checked> Image</label>
          <label><input type="checkbox" data-mf="title" checked> Title / Color</label>
          <label><input type="checkbox" data-mf="sku" checked> SKU</label>
          <label><input type="checkbox" data-mf="color" checked> Color swatch</label>
          <label><input type="checkbox" data-mf="pattern" checked> Pattern chip</label>
          <label><input type="checkbox" data-mf="when" checked> Created date</label>
        </div>
      </div>
      <button class="btn" id="reset">Reset</button>
    </div>
    <div class="grid" id="grid"></div>
  </section>
</main>

<div id="drawer"><span class="x" onclick="closeDrawer()">×</span><div class="dh" id="dh"></div><div class="db" id="db"></div></div>

<script>
const $=s=>document.querySelector(s), $$=s=>[...document.querySelectorAll(s)];
const STAGE_LABELS={parsed:'Parsed',sku_assigned:'DW SKU',spec_attached:'Spec',image_linked:'Image',staged:'Staged'};
let ALL=[], VIEW=[], FILTER={family:null,pattern:null,status:null}, COLL=null;
const hexLum=h=>{const c=h.replace('#','');const r=parseInt(c.slice(0,2),16),g=parseInt(c.slice(2,4),16),b=parseInt(c.slice(4,6),16);return .299*r+.587*g+.114*b;};

// ---- persisted UI prefs ----
const LS=k=>'majilite_'+k;
$('#dens').value=localStorage.getItem(LS('dens'))||5;
$('#sort').value=localStorage.getItem(LS('sort'))||'seq';
$('#imgonly').checked=localStorage.getItem(LS('imgonly'))==='1';
document.documentElement.style.setProperty('--cols',$('#dens').value);
$('#grid').classList.toggle('imgonly',$('#imgonly').checked);

// ---- import stream ----
function buildStages(stages){
  $('#stages').innerHTML=stages.map(s=>`<div class="stage" data-s="${s}"><div class="n" id="n-${s}">0</div><div class="l">${STAGE_LABELS[s]||s}</div></div>`).join('');
}
function runImport(speed=45){
  $('#grid').innerHTML=''; ALL=[]; $('#run').disabled=true; $('#replay').style.display='none';
  const es=new EventSource('/api/import/stream?speed='+speed);
  let total=0, stages=[];
  es.addEventListener('start',e=>{const d=JSON.parse(e.data);total=d.total;stages=d.stages;buildStages(stages);});
  es.addEventListener('item',e=>{
    const it=JSON.parse(e.data); ALL.push(it);
    // bump every stage counter (each item passes all stages)
    stages.forEach(s=>{const el=$('#n-'+s); if(el) el.textContent=ALL.length;});
    const st=$('#stages .stage:last-child'); $$('.stage').forEach(x=>x.classList.remove('active')); if(st)st.classList.add('active');
    $('#fill').style.width=(ALL.length/total*100)+'%';
    $('#cnt').textContent=ALL.length;
    $('#ticker').textContent=`Importing ${it.dw_sku} — ${it.title}  (${ALL.length}/${total})`;
    addCard(it,true);
  });
  es.addEventListener('done',e=>{
    es.close(); $('#run').disabled=false; $('#replay').style.display='';
    $('#ticker').textContent=`✓ Imported ${ALL.length} SKUs → all staged with spec (quote-only until Majilite cost list).`;
    $$('.stage').forEach(x=>x.classList.remove('active'));
    afterLoad();
  });
}
function seedStaged(d){ ALL=d.map(p=>({...p})); $('#cnt').textContent=ALL.length; buildStages(['parsed','sku_assigned','spec_attached','image_linked','staged']); ['parsed','sku_assigned','spec_attached','image_linked','staged'].forEach(s=>{const el=$('#n-'+s); if(el)el.textContent=ALL.length;}); $('#fill').style.width='100%'; $('#ticker').textContent=`${ALL.length} SKUs staged — spec on every SKU. Hit ▶ Run import to watch the pipeline.`; afterLoad(); }
function loadStatic(){ // prefer server-injected bootstrap (no second authed fetch); fall back to API
  if(window.__BOOT__&&window.__BOOT__.products){ seedStaged(window.__BOOT__.products); return; }
  fetch('/api/products',{credentials:'same-origin'}).then(r=>r.json()).then(seedStaged);
}

// ---- rendering ----
function cardHTML(p){
  const img=p.image?`<img loading="lazy" src="${p.image}" alt="${p.title}"/>`:`<span class="ph">PENDING</span>`;
  const bg=p.image?'':`background:linear-gradient(135deg,${p.color_hex},#0f1216)`;
  const when=p.created_at?new Date(p.created_at).toLocaleString(undefined,{month:'short',day:'numeric',hour:'numeric',minute:'2-digit'}):'';
  return `<div class="card${p._new?' new':''}" data-sku="${p.dw_sku}">
    <div class="thumb" style="${bg}">${img}<span class="spec-badge">SPEC</span></div>
    <div class="meta">
      <div class="t">${p.pattern} <span style="color:var(--dim)">${p.color}</span></div>
      <div class="sku">${p.dw_sku}</div>
      <div class="row">
        <span class="dot" style="background:${p.color_hex}"></span>
        <a class="chip drill" href="${drillHref('family',p.color_family)}" data-filter="family" data-f="family" data-v="${p.color_family}">${p.color_family}</a>
        <a class="chip drill" href="${drillHref('pattern',p.pattern)}" data-filter="pattern" data-f="pattern" data-v="${p.pattern}">${p.pattern}</a>
        ${p.pricing?`<span class="chip" style="border-color:#5fd08a55;color:#5fd08a">$${p.pricing.sample_price} sample</span>`:''}
      </div>
      <div class="when" title="${p.created_at||''}">🕓 ${when}</div>
    </div></div>`;
}
function addCard(p,isNew){ p._new=isNew; const el=document.createElement('div'); el.innerHTML=cardHTML(p); $('#grid').appendChild(el.firstElementChild); }

function afterLoad(){ try{readURL();}catch(_){} $('#cnt').textContent=ALL.length; buildFacets(); apply(); }

function buildFacets(){
  const byFam={}, byPat={}, bySt={};
  for(const p of ALL){ byFam[p.color_family]=(byFam[p.color_family]||0)+1; byPat[p.pattern]=(byPat[p.pattern]||0)+1; bySt[p.onboard_status||'staged']=(bySt[p.onboard_status||'staged']||0)+1; }
  const famHex={}; for(const p of ALL) famHex[p.color_family]=p.color_hex;
  $('#colorFacets').innerHTML=Object.entries(byFam).sort((a,b)=>b[1]-a[1]).map(([k,v])=>
    `<div class="facet${FILTER.family===k?' on':''}" data-f="family" data-v="${k}"><span><span class="dot" style="background:${famHex[k]}"></span>${k}</span><span class="cnt">${v}</span></div>`).join('');
  $('#patternFacets').innerHTML=Object.entries(byPat).sort((a,b)=>a[0].localeCompare(b[0])).map(([k,v])=>
    `<div class="facet${FILTER.pattern===k?' on':''}" data-f="pattern" data-v="${k}"><span>${k}</span><span class="cnt">${v}</span></div>`).join('');
  $('#statusFacets').innerHTML=Object.entries(bySt).map(([k,v])=>
    `<div class="facet${FILTER.status===k?' on':''}" data-f="status" data-v="${k}"><span>${k}</span><span class="cnt">${v}</span></div>`).join('');
}

function apply(){
  const q=$('#q').value.toLowerCase().trim().split(/\s+/).filter(Boolean);
  VIEW=ALL.filter(p=>{
    if(FILTER.family&&p.color_family!==FILTER.family)return false;
    if(FILTER.pattern&&p.pattern!==FILTER.pattern)return false;
    if(FILTER.status&&(p.onboard_status||'staged')!==FILTER.status)return false;
    if(q.length){ const hay=(p.title+' '+p.dw_sku+' '+p.mfr_sku+' '+(p.tags||[]).join(' ')).toLowerCase(); return q.every(t=>hay.includes(t)); }
    return true;
  });
  const s=$('#sort').value;
  const cmp={ seq:(a,b)=>a.seq-b.seq, sku:(a,b)=>a.dw_sku.localeCompare(b.dw_sku),
    pattern:(a,b)=>a.pattern.localeCompare(b.pattern)||a.color.localeCompare(b.color),
    title:(a,b)=>a.title.localeCompare(b.title),
    family:(a,b)=>a.color_family.localeCompare(b.color_family)||a.title.localeCompare(b.title),
    light:(a,b)=>hexLum(b.color_hex)-hexLum(a.color_hex), dark:(a,b)=>hexLum(a.color_hex)-hexLum(b.color_hex),
    color:(a,b)=>String(a.color||'').localeCompare(String(b.color||'')),
    vendor:(a,b)=>String(a.vendor||'').localeCompare(String(b.vendor||'')) }[s];
  VIEW.sort(cmp);
  $('#grid').innerHTML=VIEW.map(p=>cardHTML({...p,_new:false})).join('');
}

// ---- drawer ----
function openDrawer(sku){
  const p=ALL.find(x=>x.dw_sku===sku); if(!p)return;
  const sp=p.spec||(COLL&&COLL.spec)||{};
  $('#dh').innerHTML=`<div style="font-size:18px;font-weight:600">${p.pattern} <span style="color:var(--dim)">${p.color}</span></div>
    <div class="sku" style="font:12px ui-monospace,monospace;color:var(--teal)">${p.dw_sku} &nbsp;·&nbsp; mfr: ${p.mfr_sku}</div>
    ${p.pricing?'<span class="quote" style="color:#5fd08a;border-color:#5fd08a55">SAMPLE $'+(p.pricing.sample_price)+' · by-the-yard retail pending Majilite cost list</span>':''}`;
  const rows=[['Vendor',p.vendor+' — '+p.brand],['Collection',p.collection],['Type',p.product_type],['Material',p.material],
    ['Color family',p.color_family],['Width',sp.width],['Weight',sp.weight],['Thickness',sp.thickness],
    ['Durability',sp.durability],['Cleanability',sp.cleanability_code],['Crocking',sp.crocking],
    ['Colorfastness',sp.colorfastness],['Flammability',sp.flammability_inherent],['Optional FR',sp.flammability_optional],
    ['Uses',sp.uses],['Care',sp.care],['Environmental',sp.environmental],['Custom',sp.custom_options],['Manufacturer',sp.manufacturer]];
  $('#db').innerHTML=(p.image?`<img src="${p.image}"/>`:`<div style="height:160px;border-radius:8px;background:linear-gradient(135deg,${p.color_hex},#0f1216)"></div>`)+
    `<table class="spec">${rows.filter(r=>r[1]).map(r=>`<tr><td>${r[0]}</td><td>${r[1]}</td></tr>`).join('')}</table>
     <div style="margin-top:10px"><b style="font-size:11px;color:var(--dim)">TAGS</b><div class="row" style="margin-top:6px">${(p.tags||[]).map(t=>`<span class="chip">${t}</span>`).join('')}</div></div>`;
  $('#drawer').classList.add('open');
}
function closeDrawer(){ $('#drawer').classList.remove('open'); }

// ---- events ----
$('#grid').addEventListener('click',e=>{
  const chip=e.target.closest('.chip[data-f]');
  if(chip){ if(e.metaKey||e.ctrlKey)return; e.preventDefault(); FILTER[chip.dataset.f]= FILTER[chip.dataset.f]===chip.dataset.v?null:chip.dataset.v; buildFacets(); apply(); e.stopPropagation(); return; }
  const card=e.target.closest('.card'); if(card) openDrawer(card.dataset.sku);
});
document.addEventListener('click',e=>{const f=e.target.closest('aside .facet'); if(!f)return; const k=f.dataset.f; FILTER[k]=FILTER[k]===f.dataset.v?null:f.dataset.v; buildFacets(); apply();});
$('#q').addEventListener('input',apply);
$('#sort').addEventListener('change',()=>{localStorage.setItem(LS('sort'),$('#sort').value);apply();});
$('#dens').addEventListener('input',()=>{document.documentElement.style.setProperty('--cols',$('#dens').value);localStorage.setItem(LS('dens'),$('#dens').value);});
$('#imgonly').addEventListener('change',()=>{$('#grid').classList.toggle('imgonly',$('#imgonly').checked);localStorage.setItem(LS('imgonly'),$('#imgonly').checked?'1':'0');});
$('#reset').addEventListener('click',()=>{FILTER={family:null,pattern:null,status:null};$('#q').value='';buildFacets();apply();});
$('#run').addEventListener('click',()=>runImport(45));
$('#replay').addEventListener('click',()=>runImport(20));

// ── VIEW TOGGLE ────────────────────────────────────────────────────
const GRID_EL=$('#grid');
function setView(v){
  GRID_EL.classList.toggle('list',v==='list');
  $('#viewgrid').classList.toggle('active',v==='grid');
  $('#viewlist').classList.toggle('active',v==='list');
  // hide density slider in list mode (columns irrelevant)
  $('#dens').parentElement.style.opacity=v==='list'?'.35':'1';
  $('#dens').disabled=v==='list';
  try{localStorage.setItem(LS('view'),v);}catch(_){}
}
$('#viewgrid').addEventListener('click',()=>setView('grid'));
$('#viewlist').addEventListener('click',()=>setView('list'));
setView(localStorage.getItem(LS('view'))||'grid');

// ── FIELDS MENU ───────────────────────────────────────────────────
const MFIELDS_DEFAULT={thumb:true,title:true,sku:true,color:true,pattern:true,when:true};
let MFIELDS=Object.assign({},MFIELDS_DEFAULT);
try{const s=localStorage.getItem(LS('fields'));if(s)MFIELDS=Object.assign({},MFIELDS_DEFAULT,JSON.parse(s));}catch(_){}
function applyMFields(){
  for(const k of Object.keys(MFIELDS_DEFAULT)){
    document.body.classList.toggle('hf-'+k,!MFIELDS[k]);
  }
  document.querySelectorAll('#fpop input[data-mf]').forEach(cb=>{cb.checked=!!MFIELDS[cb.dataset.mf];});
  try{localStorage.setItem(LS('fields'),JSON.stringify(MFIELDS));}catch(_){}
}
document.querySelectorAll('#fpop input[data-mf]').forEach(cb=>{
  cb.addEventListener('change',()=>{MFIELDS[cb.dataset.mf]=cb.checked;applyMFields();});
});
applyMFields();
// popover open/close
$('#fieldbtn').addEventListener('click',e=>{e.stopPropagation();$('#fpop').hidden=!$('#fpop').hidden;});
document.addEventListener('click',e=>{if(!e.target.closest('.fieldmenu'))$('#fpop').hidden=true;});

// ── href-to-deeper-data primitives (page-native over FILTER) — HARD rule 2026-07-31 ──
// Every displayed data point (chip/facet) is a real, shareable href to its filtered view.
const FIELDS=[['family'],['pattern'],['status']];
function qstr(f,q){ const u=new URLSearchParams(); for(const [k] of FIELDS){ if(f[k]) u.set(k,f[k]); } const s=(q!=null?q:$('#q').value.trim()); if(s) u.set('q',s); const o=u.toString(); return o?('?'+o):location.pathname; }
function drillHref(k,v){ return qstr({...FILTER,[k]:String(v)}); }
function readURL(){ const u=new URLSearchParams(location.search); for(const [k] of FIELDS){ FILTER[k]=u.get(k)||null; } const q=u.get('q'); if(q!=null&&$('#q')) $('#q').value=q; }
function writeURL(push){ history[push?'pushState':'replaceState']({},'',qstr(FILTER)); }
window.qstr=qstr; window.readURL=readURL; window.writeURL=writeURL; window.drillHref=drillHref;
// wrap apply() so every filter change reflects into the URL (deep-link + back-button)
const _applyBase=apply; apply=function(){ _applyBase(); try{writeURL(false);}catch(_){} };
window.addEventListener('popstate',()=>{ readURL(); buildFacets(); apply(); });

// ---- boot ----
function seedColl(c){COLL=c;$('#pfx').textContent=c.sku_prefix;$('#pmode').textContent=c.onboard_as==='sample'?('$'+c.sample_price+' sample'):(c.display_prices?'priced':'quote-only');}
if(window.__BOOT__&&window.__BOOT__.collection){seedColl(window.__BOOT__.collection);} else {fetch('/api/collection',{credentials:'same-origin'}).then(r=>r.json()).then(seedColl);}
loadStatic(); // show staged catalog immediately; user can hit ▶ Run import to watch the stream
</script>
  <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
</body>
</html>