[object Object]

← back to Quadrille Internal

Retrofit index to internal-line-viewer spec (astek reference sync): mobile hamburger filter drawer, quadrille.* localStorage namespace (index + curate), facet tables trimmed to fields the data carries (drop all-null Length/Match; no Styles — no style field), list columns likewise, teal added to BUCKET_HEX, pattern type-ahead falls back to display_name for the 1,042 series-null rows; E2E-verified Chromium + WebKit creds-in-URL on :9947

0a49b7e086ea0733ae865a3f1256b0d61355816b · 2026-07-02 17:25:29 -0700 · Steve Abrams

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Files touched

Diff

commit 0a49b7e086ea0733ae865a3f1256b0d61355816b
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Jul 2 17:25:29 2026 -0700

    Retrofit index to internal-line-viewer spec (astek reference sync): mobile hamburger filter drawer, quadrille.* localStorage namespace (index + curate), facet tables trimmed to fields the data carries (drop all-null Length/Match; no Styles — no style field), list columns likewise, teal added to BUCKET_HEX, pattern type-ahead falls back to display_name for the 1,042 series-null rows; E2E-verified Chromium + WebKit creds-in-URL on :9947
    
    Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---
 public/curate.html |  4 ++--
 public/index.html  | 68 ++++++++++++++++++++++++++++++++++++------------------
 2 files changed, 48 insertions(+), 24 deletions(-)

diff --git a/public/curate.html b/public/curate.html
index 361bbee..0ff6ffb 100644
--- a/public/curate.html
+++ b/public/curate.html
@@ -69,11 +69,11 @@ const PAGE=400;
 const SEL=new Set();          // dw_sku set — the Phillipe Romano candidate list
 const BY_SKU={};              // dw_sku -> product
 function esc(s){return String(s==null?'':s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));}
-const dens=localStorage.getItem('astek.cur.density')||'8';
+const dens=localStorage.getItem('quadrille.cur.density')||'8';
 $('#density').value=dens; $('#denv').textContent=dens;
 document.documentElement.style.setProperty('--cols',dens);
 $('#density').oninput=()=>{const v=$('#density').value;document.documentElement.style.setProperty('--cols',v);
- $('#denv').textContent=v;localStorage.setItem('astek.cur.density',v);};
+ $('#denv').textContent=v;localStorage.setItem('quadrille.cur.density',v);};
 
 function tile(p,idx){
   const on=SEL.has(p.dw_sku);
diff --git a/public/index.html b/public/index.html
index 2f74d07..c4e1ade 100644
--- a/public/index.html
+++ b/public/index.html
@@ -42,9 +42,20 @@ aside summary .cur{margin-left:auto;color:var(--acc);text-transform:none;letter-
 .fmore{padding:3px 8px;font-size:11px;color:var(--mut);font-style:italic}
 #clearf{display:none;margin-top:10px;width:100%}
 main{flex:1;min-width:0}
+/* mobile: hamburger + full-screen filter drawer (sidebar no longer stacks above the grid) */
+#burger{display:none;background:var(--panel);color:var(--ink);border:1px solid var(--line);border-radius:6px;padding:6px 12px;cursor:pointer;font-size:13px;font-weight:600;white-space:nowrap}
+#fclose{display:none}
 @media(max-width:880px){
  #layout{display:block}
- aside{position:static;width:auto;max-height:none;border-right:0;border-bottom:1px solid var(--line)}
+ header{padding:8px 10px;gap:8px}
+ #burger{display:inline-flex;align-items:center;gap:6px}
+ .ctl:has(#density){display:none} /* grid is fixed 2-up on phones — slider is noise */
+ /* top:0!important beats the inline style.top the desktop sticky-layout JS sets */
+ aside{display:none;position:fixed;inset:0;top:0!important;z-index:40;width:auto;min-width:0;max-height:none!important;
+  overflow:auto;border-right:0;padding:12px 12px 40px;background:var(--bg)}
+ aside.open-mobile{display:block}
+ #fclose{display:block;position:sticky;top:0;z-index:2;width:100%;background:var(--acc);color:#062a24;border:none;border-radius:8px;padding:11px;font-size:14px;font-weight:700;cursor:pointer;margin-bottom:10px}
+ .grid:not(.list){grid-template-columns:repeat(2,1fr)}
 }
 /* ── grid — tight, 1pt stroke edges; text scales with density via --fs ── */
 .grid{display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:8px;padding:10px 12px 60px}
@@ -88,8 +99,9 @@ body.imgonly .grid:not(.list) .card .b{display:none}
 #empty{padding:60px;text-align:center;color:var(--mut)}
 /* ── List view ── aligned CSS-grid rows (.b uses display:contents so its
    children become columns of the card grid). Fixed type sizes (1 column —
-   density/font scaling is a grid-view concern). */
-:root{--listcols:42px 105px minmax(130px,1.4fr) minmax(90px,1fr) 92px minmax(105px,1fr) 112px minmax(90px,1fr) 64px 64px 84px 92px}
+   density/font scaling is a grid-view concern). Columns = the fields this
+   line's data actually carries (no length/match — all-null in quadrille). */
+:root{--listcols:42px 105px minmax(130px,1.4fr) minmax(90px,1fr) 92px minmax(105px,1fr) 112px minmax(90px,1fr) 64px 84px}
 .grid.list{grid-template-columns:1fr;gap:4px;padding:4px 12px 60px}
 .grid.list .card{
   display:grid;align-items:center;border-radius:2px;
@@ -128,12 +140,14 @@ body.listmode #listhead{display:grid}
 <body>
 <header>
   <h1>Quadrille House · Catalog</h1>
+  <button id="burger" type="button" aria-label="Open filters">☰ Filters</button>
   <span class="pill" id="total">…</span>
   <span class="muted" id="meta"></span>
   <div class="ctl"><label class="muted">Sort</label>
     <select id="sort">
       <option value="newest">Newest</option>
       <option value="color">Color</option>
+      <!-- no Style option: quadrille data carries no style field -->
       <option value="pattern">Pattern A→Z</option>
       <option value="sku">SKU A→Z</option>
       <option value="title">Title A→Z</option>
@@ -146,6 +160,7 @@ body.listmode #listhead{display:grid}
 </header>
 <div id="layout">
 <aside aria-label="filters">
+  <button id="fclose" type="button">✕ Done — show products</button>
   <input type="text" id="q" placeholder="Search pattern / sku / color…">
   <input type="text" id="pattern" list="patternlist" placeholder="Pattern type-ahead…" title="type-ahead over every pattern in the catalog">
   <datalist id="patternlist"></datalist>
@@ -172,19 +187,21 @@ const grid=$('#grid');
 let ALL=[], FACETS=null, VIEW=[], shown=0;
 const PAGE=120;
 const BUCKET_HEX={white:'#f4f4f2',grey:'#9aa0aa',black:'#1c1e24',pink:'#f3a8c0',red:'#d0453e',orange:'#e08a3c',
- brown:'#8a6242',gold:'#c9a75a',green:'#5f8f5c',blue:'#4f7fb5',purple:'#8a6fb5'};
+ brown:'#8a6242',gold:'#c9a75a',green:'#5f8f5c',teal:'#4fa8a0',blue:'#4f7fb5',purple:'#8a6fb5'};
 function esc(s){return String(s==null?'':s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));}
-// ── every data field is a left-panel table: collapsed on load, click value = filter ──
+// ── every data field the quadrille catalog ACTUALLY carries is a left-panel
+// table: collapsed on load, click value = filter. No Styles (no style field in
+// the data), no Length / Match / price tables (all-null across 1,942 rows).
 const FIELDS=[
  ['book','Books'],['color_bucket','Colors'],['series','Patterns'],['color','Colorways'],
- ['material','Material'],['width','Width'],['length','Length'],['repeat','Repeat'],['match','Match'],
+ ['material','Material'],['width','Width'],['repeat','Repeat'],
 ];
 const FIL={};            // field -> active value ('' = off)
 FIELDS.forEach(([k])=>FIL[k]='');
 let FCOUNTS={};          // field -> [[value,count]…] computed once from ALL
 const FCAP=40;           // rows shown per table (top-by-count)
-// persisted prefs (CLAUDE.md: sort + density persist in localStorage)
-$('#sort').value=localStorage.getItem('astek.sort')||'newest';
+// persisted prefs (CLAUDE.md: sort + density persist in localStorage) — quadrille.* namespace
+$('#sort').value=localStorage.getItem('quadrille.sort')||'newest';
 // ── density also drives type scale: fewer/larger cards → bigger type, 20/row → smallest ──
 function setDensity(v){
   v=Number(v);
@@ -192,16 +209,23 @@ function setDensity(v){
   document.documentElement.style.setProperty('--cols',v);
   document.documentElement.style.setProperty('--fs',fs.toFixed(3));
 }
-const dens=localStorage.getItem('astek.density')||'6';
+const dens=localStorage.getItem('quadrille.density')||'6';
 $('#density').value=dens; setDensity(dens);
-$('#density').oninput=()=>{setDensity($('#density').value);localStorage.setItem('astek.density',$('#density').value);};
+$('#density').oninput=()=>{setDensity($('#density').value);localStorage.setItem('quadrille.density',$('#density').value);};
+// ── mobile: hamburger opens the filter sidebar as a full-screen drawer ──
+const asideEl=document.querySelector('aside');
+$('#burger').onclick=()=>{asideEl.classList.add('open-mobile');document.body.style.overflow='hidden';};
+$('#fclose').onclick=()=>{asideEl.classList.remove('open-mobile');document.body.style.overflow='';};
+window.matchMedia('(max-width:880px)').addEventListener('change',e=>{
+  if(!e.matches){asideEl.classList.remove('open-mobile');document.body.style.overflow='';}
+});
 // ── card-field visibility toggles (image-only when all off) ──
-const FVIS=JSON.parse(localStorage.getItem('astek.fieldvis')||'{"vend":true,"ttl":true,"chips":true}');
+const FVIS=JSON.parse(localStorage.getItem('quadrille.fieldvis')||'{"vend":true,"ttl":true,"chips":true}');
 function applyFieldVis(){
   for(const k of ['vend','ttl','chips'])document.body.classList.toggle('f-off-'+k,!FVIS[k]);
   document.body.classList.toggle('imgonly',!FVIS.vend&&!FVIS.ttl&&!FVIS.chips);
   document.querySelectorAll('#fieldsbox input[data-fv]').forEach(cb=>cb.checked=!!FVIS[cb.dataset.fv]);
-  localStorage.setItem('astek.fieldvis',JSON.stringify(FVIS));
+  localStorage.setItem('quadrille.fieldvis',JSON.stringify(FVIS));
 }
 document.querySelectorAll('#fieldsbox input[data-fv]').forEach(cb=>{
   cb.onchange=()=>{FVIS[cb.dataset.fv]=cb.checked;applyFieldVis();};
@@ -231,13 +255,11 @@ function card(p){
     <span class="ldw" title="DW SKU">${esc(p.dw_sku||'')}</span>
     <span class="lc" title="Material">${esc(p.material||'')}</span>
     <span class="lc" title="Width">${esc(p.width||'')}</span>
-    <span class="lc" title="Length">${esc(p.length||'')}</span>
     <span class="lc" title="Repeat">${esc(p.repeat||'')}</span>
-    <span class="lc" title="Match">${esc(p.match||'')}</span>
     </div>
     <div class="info" onclick="event.stopPropagation()">
       ${kv('DW SKU',p.dw_sku)}${kv('Vendor SKU',p.sku)}${kv('Series',p.series)}${kv('Book',p.book)}
-      ${kv('Material',p.material)}${kv('Width',p.width)}${kv('Repeat',p.repeat)}${kv('Match',p.match)}
+      ${kv('Material',p.material)}${kv('Width',p.width)}${kv('Repeat',p.repeat)}
       <div class="acts">
         ${p.sku?`<button class="act" data-copy="${esc(p.sku)}" onclick="copyTxt(this)" title="copy vendor SKU">📋 SKU</button>`:''}
         ${p.dw_sku?`<button class="act" data-copy="${esc(p.dw_sku)}" onclick="copyTxt(this)" title="copy DW SKU">📋 DW</button>`:''}
@@ -303,8 +325,10 @@ function buildFacetCache(){
     for(const p of ALL){const v=p[k];if(v==null||v==='')continue;m[v]=(m[v]||0)+1;}
     FCOUNTS[k]=Object.entries(m).sort((a,b)=>b[1]-a[1]);
   }
-  // Pattern type-ahead over EVERY distinct pattern
-  const series=(FCOUNTS.series||[]).map(([v])=>v).sort((a,b)=>String(a).localeCompare(String(b),undefined,{numeric:true}));
+  // Pattern type-ahead over EVERY distinct pattern (series when present, else display_name)
+  const seen=new Set();
+  for(const p of ALL){const v=p.series||p.display_name;if(v)seen.add(v);}
+  const series=[...seen].sort((a,b)=>String(a).localeCompare(String(b),undefined,{numeric:true}));
   $('#patternlist').innerHTML=series.map(s=>`<option value="${esc(s)}">`).join('');
 }
 // left panel — one collapsed <details> table per field; open state survives re-render
@@ -325,13 +349,13 @@ function renderSide(){
 function pickF(k,v){FIL[k]=(FIL[k]===v?'':v);applyFilters();}
 window.pickF=pickF;
 $('#clearf').onclick=()=>{FIELDS.forEach(([k])=>FIL[k]='');$('#q').value='';$('#pattern').value='';applyFilters();};
-$('#sort').onchange=()=>{localStorage.setItem('astek.sort',$('#sort').value);COLSORT.key=null;renderListHead();applyFilters();};
+$('#sort').onchange=()=>{localStorage.setItem('quadrille.sort',$('#sort').value);COLSORT.key=null;renderListHead();applyFilters();};
 let t; $('#q').oninput=()=>{clearTimeout(t);t=setTimeout(applyFilters,300);};
 let tp; $('#pattern').oninput=()=>{clearTimeout(tp);tp=setTimeout(applyFilters,300);};
 $('#more').onclick=renderMore;
-// ── sortable column header (list view) ──
+// ── sortable column header (list view) — columns = fields the data carries ──
 const LIST_COLS=[[null,''],['book','Book'],['display_name','Pattern'],['color','Color'],['color_bucket','Bucket'],
- ['sku','SKU'],['dw_sku','DW SKU'],['material','Material'],['width','Width'],['length','Length'],['repeat','Repeat'],['match','Match']];
+ ['sku','SKU'],['dw_sku','DW SKU'],['material','Material'],['width','Width'],['repeat','Repeat']];
 const COLSORT={key:null,dir:1};
 function renderListHead(){
   $('#listhead').innerHTML=LIST_COLS.map(([k,lbl])=>k
@@ -351,8 +375,8 @@ function syncSticky(){
   document.querySelector('aside').style.maxHeight=`calc(100vh - ${h}px)`;
 }
 addEventListener('resize',syncSticky);
-function applyView(){const list=localStorage.getItem('astek.view')==='list';grid.classList.toggle('list',list);document.body.classList.toggle('listmode',list);$('#view').textContent=list?'▦ Grid view':'▤ List view';renderListHead();}
-$('#view').onclick=()=>{localStorage.setItem('astek.view',localStorage.getItem('astek.view')==='list'?'grid':'list');applyView();};
+function applyView(){const list=localStorage.getItem('quadrille.view')==='list';grid.classList.toggle('list',list);document.body.classList.toggle('listmode',list);$('#view').textContent=list?'▦ Grid view':'▤ List view';renderListHead();}
+$('#view').onclick=()=>{localStorage.setItem('quadrille.view',localStorage.getItem('quadrille.view')==='list'?'grid':'list');applyView();};
 applyView();
 addEventListener('scroll',()=>{if(shown<VIEW.length&&innerHeight+scrollY>=document.body.offsetHeight-600)renderMore();});
 async function loadAll(){

← d937938 chore: addendum gate clean, v0.2.1 (session close)  ·  back to Quadrille Internal  ·  vendor-requests: self-bootstrap vendor_requests table (fix ' 18bd667 →