1import {widget,recipeFor} from './insight-model.js'; 2export function cascadeScope(scope,axis,value){ 3 const next=structuredClone(scope);next[axis]=value;next.products=[];next.productLabels=[];next.focus=[];next.focusLabels=[]; 4 if(axis==='locations')next.departments=[]; 5 return next; 6} 7export function facetRecipe(scope,group,search=''){ 8 const parent={...scope,products:[],productLabels:[],focus:[],focusLabels:[],search}; 9 if(group==='department')parent.departments=[]; 10 return recipeFor(widget({group}),parent,0,100); 11} 12export function productPicker(host,{h,scope,query,onChange}){ 13 let serial=0,timer;const selected=new Map((scope.products||[]).map((key,i)=>[key,scope.productLabels?.[i]||key])); 14 host.innerHTML='<details class="ix-multi" name="insights-filter-menu"><summary></summary><div class="ix-multi-menu"><input type="search" aria-label="Search available products" placeholder="Name or item code"><div data-picked></div><div data-results></div><small data-status></small><button class="btn" data-clear-products>Clear selection</button><button class="btn primary" data-apply-products>Apply products</button></div></details>'; 15 const summary=host.querySelector('summary'),status=host.querySelector('[data-status]'),results=host.querySelector('[data-results]'); 16 const label=()=>{summary.textContent=selected.size?`Products · ${selected.size} selected`:'Products · All';const picked=host.querySelector('[data-picked]');picked.innerHTML=[...selected].map(([key,name],i)=>`<button class="ix-chip ix-filter-chip" data-unpick="${i}" aria-label="Remove ${h(name)}">${h(name)} Ã</button>`).join('');picked.querySelectorAll('[data-unpick]').forEach(b=>b.onclick=()=>{selected.delete([...selected.keys()][Number(b.dataset.unpick)]);label();load();});};label(); 17 async function load(){const turn=++serial;status.textContent='Finding matching productsâ¦';results.innerHTML='';try{const data=await query(facetRecipe(scope,'item',host.querySelector('input').value.trim()));if(turn!==serial||!host.isConnected)return;results.innerHTML=data.rows.map(r=>{const key=JSON.parse(r.bucket)[0];return `<label><input type="checkbox" value="${h(key)}" data-name="${h(r.item)}" ${selected.has(key)?'checked':''}>${h(r.item)}</label>`;}).join('');if(!data.rows.length)results.innerHTML='<p>No products match. Try another name or broaden the department selection.</p>';status.textContent=`${data.rows.length} of ${data.groups.toLocaleString()} matching products. Search to narrow.`;results.querySelectorAll('input').forEach(input=>input.onchange=()=>{if(input.checked&&selected.size>=100){input.checked=false;status.textContent='Select up to 100 products, or use a department filter.';return;}input.checked?selected.set(input.value,input.dataset.name):selected.delete(input.value);label();});}catch(e){if(turn===serial)status.textContent=e.message;}} 18 host.querySelector('details').ontoggle=e=>{if(e.target.open)load();}; 19 host.querySelector('input').oninput=()=>{serial++;clearTimeout(timer);timer=setTimeout(load,300);}; 20 host.querySelector('[data-clear-products]').onclick=()=>{selected.clear();label();load();}; 21 host.querySelector('[data-apply-products]').onclick=()=>{serial++;clearTimeout(timer);onChange([...selected.keys()],[...selected.values()]);}; 22}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.