← 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 →