[object Object]

← back to Astek Landing

Density slider to 20/row + all data fields as sortable list columns (sticky header, asc/desc, empties-last); E2E-verified

8b7247b35b4c3bf681a37f550ab3f7773039e3da · 2026-07-02 08:37:02 -0700 · Steve

Files touched

Diff

commit 8b7247b35b4c3bf681a37f550ab3f7773039e3da
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Jul 2 08:37:02 2026 -0700

    Density slider to 20/row + all data fields as sortable list columns (sticky header, asc/desc, empties-last); E2E-verified
---
 public/index.html | 64 +++++++++++++++++++++++++++++++++++++++++++++----------
 1 file changed, 53 insertions(+), 11 deletions(-)

diff --git a/public/index.html b/public/index.html
index 2480a4f..f8db5a8 100644
--- a/public/index.html
+++ b/public/index.html
@@ -62,25 +62,36 @@ input[type=range]{accent-color:var(--acc)}
 #empty{padding:60px;text-align:center;color:var(--mut)}
 /* ── List view ── aligned CSS-grid rows (same trick as new.dw: .b uses
    display:contents so its children become columns of the card grid).
-   Columns: 42px thumb · book · pattern+color · color chip · SKU · DW SKU */
-.grid.list{grid-template-columns:1fr;gap:4px;padding:12px 16px 60px}
+   Every data field is a column; the shared template lives in --listcols so
+   the sortable header row (#listhead) and every card row stay aligned. */
+:root{--listcols:42px 105px minmax(130px,1.4fr) minmax(90px,1fr) 92px minmax(105px,1fr) 112px minmax(90px,1fr) 64px 64px 84px 92px}
+.grid.list{grid-template-columns:1fr;gap:4px;padding:4px 16px 60px}
 .grid.list .card{
   display:grid;align-items:center;border-radius:8px;
-  grid-template-columns:42px 130px minmax(150px,1.7fr) 120px minmax(110px,0.9fr) 130px;
-  column-gap:14px;min-height:52px;padding:0 12px 0 8px;
+  grid-template-columns:var(--listcols);
+  column-gap:12px;min-height:52px;padding:0 12px 0 8px;
 }
 .grid.list .card>.b{display:contents}
 .grid.list .imgwrap{width:42px;min-width:42px;height:42px;aspect-ratio:auto;border-radius:6px;overflow:hidden;margin:5px 0}
 .grid.list .imgwrap.noimg::after{font-size:8px}
 .grid.list .roombadge{display:none}
 .grid.list .vend{font-size:10.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
-.grid.list .ttl{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+.grid.list .ttl{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:12.5px}
 .grid.list .chips{flex-wrap:nowrap;gap:6px;overflow:hidden}
 .grid.list .chips .chip.bk{display:none}
-.grid.list .chips .chip.toggle{font-size:9.5px;padding:2px 7px}
+.grid.list .chips .chip.toggle{display:none}
+.grid.list .lc{display:block;font-size:11px;color:var(--mut);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
 .grid.list .lsku,.grid.list .ldw{display:block;font-family:ui-monospace,Menlo,monospace;font-size:11px;color:var(--mut);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
 .grid.list .info{grid-column:1 / -1;margin:2px 0 6px;padding:8px 4px 2px}
-.lsku,.ldw{display:none}
+.lsku,.ldw,.lc{display:none}
+/* sortable column header — same template as rows, sticky under the main header */
+#listhead{display:none;position:sticky;z-index:8;background:#0e0f15;border-bottom:1px solid var(--line);
+ grid-template-columns:var(--listcols);column-gap:12px;padding:6px 28px 6px 24px;align-items:center}
+body.listmode #listhead{display:grid}
+#listhead .lh{font-size:10.5px;font-weight:700;color:var(--mut);text-transform:uppercase;letter-spacing:.4px;
+ cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;user-select:none}
+#listhead .lh:hover{color:var(--ink)}
+#listhead .lh.on{color:var(--acc)}
 /* ── corner refresh countdown pill ── */
 .astek-refresh-pill{position:fixed;right:14px;bottom:14px;z-index:25;display:flex;align-items:center;gap:7px;
  background:#11131bF2;border:1px solid var(--line);border-radius:999px;padding:6px 13px;font-size:11.5px;
@@ -112,10 +123,11 @@ input[type=range]{accent-color:var(--acc)}
       <option value="book">Book</option>
     </select></div>
   <div class="ctl"><label class="muted">Density</label>
-    <input type="range" id="density" min="3" max="10" step="1"></div>
+    <input type="range" id="density" min="3" max="20" step="1"></div>
   <button class="btn" id="view">▤ List view</button>
 </header>
 <div id="facets" aria-label="catalog facets"></div>
+<div id="listhead" aria-label="sortable columns"></div>
 <div class="grid" id="grid"></div>
 <button id="more" style="display:none">Load more</button>
 <div id="empty" style="display:none">No products match.</div>
@@ -144,6 +156,7 @@ function card(p){
     ${img}<div class="b">
     <span class="vend">${esc(p.book||'Astek')}${p.series&&p.series!==p.display_name?(' · '+esc(p.series)):''}</span>
     <div class="ttl">${title}</div>
+    <span class="lc" title="Color">${esc(p.color||'')}</span>
     <div class="chips">
       <span class="chip bk">${esc(p.book||'ASTEK').toUpperCase()}</span>
       ${p.color_bucket?`<span class="chip cl"><span class="dot" style="background:${dot}"></span>${esc(p.color_bucket)}</span>`:''}
@@ -151,6 +164,11 @@ function card(p){
     </div>
     <span class="lsku" title="Vendor SKU">${esc(p.sku||'')}</span>
     <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 class="info" onclick="event.preventDefault();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)}
@@ -185,7 +203,14 @@ function applyFilters(){
     title:(a,b)=>String(a.display_name||'').localeCompare(String(b.display_name||'')),
     book:(a,b)=>String(a.book||'').localeCompare(String(b.book||''))||String(a.series||'').localeCompare(String(b.series||'')),
   }[s];
-  if(cmp)VIEW=[...VIEW].sort(cmp);
+  if(COLSORT.key){
+    const k=COLSORT.key, dir=COLSORT.dir;
+    VIEW=[...VIEW].sort((a,b)=>{
+      const av=a[k], bv=b[k];
+      if(av==null||av==='')return 1; if(bv==null||bv==='')return -1;   // empties last either way
+      return dir*String(av).localeCompare(String(bv),undefined,{numeric:true});
+    });
+  }else if(cmp)VIEW=[...VIEW].sort(cmp);
   shown=0; grid.innerHTML=''; renderMore();
   $('#total').textContent=VIEW.length.toLocaleString()+' designs';
   $('#empty').style.display=VIEW.length?'none':'block';
@@ -225,12 +250,29 @@ function pickColor(c){const s=$('#color');s.value=(s.value===c?'':c);applyFilter
 window.pickBook=pickBook; window.pickColor=pickColor;
 $('#book').onchange=applyFilters;
 $('#color').onchange=applyFilters;
-$('#sort').onchange=()=>{localStorage.setItem('astek.sort',$('#sort').value);applyFilters();};
+$('#sort').onchange=()=>{localStorage.setItem('astek.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);};
 $('#density').oninput=()=>{document.documentElement.style.setProperty('--cols',$('#density').value);localStorage.setItem('astek.density',$('#density').value);};
 $('#more').onclick=renderMore;
-function applyView(){const list=localStorage.getItem('astek.view')==='list';grid.classList.toggle('list',list);$('#view').textContent=list?'▦ Grid view':'▤ List view';}
+// ── sortable column header (list view) ── every data field is a column;
+// click = sort asc, click again = desc; overrides the Sort select until cleared.
+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']];
+const COLSORT={key:null,dir:1};
+function renderListHead(){
+  $('#listhead').innerHTML=LIST_COLS.map(([k,lbl])=>k
+    ?`<span class="lh${COLSORT.key===k?' on':''}" onclick="colSort('${k}')">${lbl}${COLSORT.key===k?(COLSORT.dir>0?' ▲':' ▼'):''}</span>`
+    :'<span></span>').join('');
+  $('#listhead').style.top=document.querySelector('header').offsetHeight+'px';
+}
+function colSort(k){
+  if(COLSORT.key===k)COLSORT.dir=-COLSORT.dir; else{COLSORT.key=k;COLSORT.dir=1;}
+  renderListHead(); applyFilters();
+}
+window.colSort=colSort;
+addEventListener('resize',()=>{$('#listhead').style.top=document.querySelector('header').offsetHeight+'px';});
+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();};
 applyView();
 addEventListener('scroll',()=>{if(shown<VIEW.length&&innerHeight+scrollY>=document.body.offsetHeight-600)renderMore();});

← f845679 Fix Safari no-items bug: resolve API fetches via location.or  ·  back to Astek Landing  ·  auto-save: 2026-07-02T08:46:35 (1 files) — data/products.jso e2a9192 →