[object Object]

← back to Astek Landing

Restyle landing to new.designerwallcoverings.com catalog UI — dark viewer, book+color facet chips, sort+density (persisted), grid/list toggle, room hover-swap, details drawer, countdown pill kept; E2E-verified 6164 cards, 0 console errors

72830d07590c199643383b49524d721baab4cc8f · 2026-07-02 08:23:58 -0700 · Steve

Files touched

Diff

commit 72830d07590c199643383b49524d721baab4cc8f
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Jul 2 08:23:58 2026 -0700

    Restyle landing to new.designerwallcoverings.com catalog UI — dark viewer, book+color facet chips, sort+density (persisted), grid/list toggle, room hover-swap, details drawer, countdown pill kept; E2E-verified 6164 cards, 0 console errors
---
 public/index.html | 525 +++++++++++++++++++++++-------------------------------
 1 file changed, 224 insertions(+), 301 deletions(-)

diff --git a/public/index.html b/public/index.html
index 4747620..af49c83 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1,303 +1,238 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8">
-<meta name="viewport" content="width=device-width,initial-scale=1">
-<title>Designer Wallcoverings</title>
-<meta name="description" content="A curated collection at Designer Wallcoverings — to the trade.">
-<meta property="og:type" content="website">
-<meta property="og:title" content="Designer Wallcoverings">
-<meta property="og:description" content="A curated collection at Designer Wallcoverings.">
-<meta name="theme-color" content="#f6f2ec">
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
-<link href="https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400;0,500;0,600;1,400;1,500&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
-<link rel="stylesheet" href="/styles.css">
-</head>
+<!doctype html><html lang="en"><head><meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Astek · Catalog · dw_unified</title>
+<style>
+:root{--cols:6;--bg:#0c0d12;--panel:#15171f;--card:#181b24;--ink:#eef0f5;--mut:#9aa0b0;--acc:#5eead4;--line:#262a36;
+ --new:#f59e0b;--pub:#34d399;--unpub:#94a3b8;--arch:#f87171;--ship:#60a5fa}
+*{box-sizing:border-box}
+body{margin:0;background:var(--bg);color:var(--ink);font:13px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
+header{position:sticky;top:0;z-index:9;background:linear-gradient(180deg,#0c0d12,#0c0d12e8);backdrop-filter:blur(8px);
+ border-bottom:1px solid var(--line);padding:12px 16px;display:flex;gap:12px;align-items:center;flex-wrap:wrap}
+h1{font-size:15px;margin:0;font-weight:700;letter-spacing:.2px}
+.pill{background:var(--new);color:#1a1205;font-weight:700;border-radius:999px;padding:2px 10px;font-size:12px}
+.muted{color:var(--mut)}
+select,input[type=text]{background:var(--panel);color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:6px 9px;font-size:13px}
+input[type=text]{min-width:180px}
+.ctl{display:flex;align-items:center;gap:7px}
+input[type=range]{accent-color:var(--acc)}
+.btn{background:var(--panel);color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:6px 12px;cursor:pointer;font-size:13px}
+.btn:hover{border-color:var(--acc)}
+/* facet strip — one colored count chip per book / color bucket; click to filter */
+#facets{display:flex;flex-wrap:wrap;gap:8px;align-items:center;padding:8px 16px;border-bottom:1px solid var(--line);background:#0e0f15}
+#facets .hc{display:inline-flex;align-items:center;gap:6px;font-size:11.5px;font-weight:700;border-radius:999px;padding:3px 11px;cursor:pointer;border:1px solid var(--line);background:var(--panel);color:var(--mut)}
+#facets .hc .n{font-variant-numeric:tabular-nums}
+#facets .hc:hover{border-color:var(--acc)}
+#facets .hc.active{outline:2px solid var(--acc);outline-offset:1px;color:var(--ink)}
+#facets .hc.zero{opacity:.4}
+#facets .hlabel{font-size:10.5px;color:var(--mut);text-transform:uppercase;letter-spacing:.5px;margin-right:2px}
+#facets .dot{width:10px;height:10px;border-radius:50%;border:1px solid #0008;display:inline-block}
+.grid{display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:12px;padding:16px}
+.card{background:var(--card);border:1px solid var(--line);border-radius:12px;overflow:hidden;display:flex;flex-direction:column;position:relative}
+.imgwrap{position:relative;width:100%;aspect-ratio:1/1;background:#0a0b0f}
+.imgwrap img{width:100%;height:100%;object-fit:cover;object-position:center;display:block}
+.imgwrap img.roomimg{position:absolute;inset:0;opacity:0;transition:opacity .18s ease}
+.imgwrap:hover img.roomimg{opacity:1}
+.roombadge{position:absolute;top:7px;right:7px;z-index:2;background:#000a;color:#cbd5e1;font-size:9.5px;font-weight:700;border-radius:6px;padding:2px 7px;letter-spacing:.3px}
+.imgwrap.noimg{background:repeating-linear-gradient(45deg,#0a0b0f,#0a0b0f 8px,#101218 8px,#101218 16px)}
+.imgwrap.noimg::after{content:'🚫 no image';position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:#5b6373;font-size:11px;font-weight:600;letter-spacing:.3px}
+.card .b{padding:9px 10px;display:flex;flex-direction:column;gap:6px}
+.vend{font-size:10.5px;color:var(--mut);font-weight:600;letter-spacing:.2px}
+.ttl{font-weight:600;font-size:14px;line-height:1.25;letter-spacing:-.1px}
+.sku{font-family:ui-monospace,Menlo,monospace;font-size:11px;color:var(--mut)}
+/* ── chips ── */
+.chips{display:flex;flex-wrap:wrap;gap:5px;align-items:center}
+.chip{font-size:10.5px;font-weight:700;border-radius:999px;padding:2px 9px;letter-spacing:.3px;white-space:nowrap;border:1px solid transparent}
+.chip.bk{background:var(--ship);color:#04122a}
+.chip.cl{border-color:var(--line);color:var(--ink);background:var(--panel);display:inline-flex;align-items:center;gap:5px}
+.chip.cl .dot{width:9px;height:9px;border-radius:50%;border:1px solid #0008;display:inline-block}
+.chip.toggle{background:transparent;color:var(--mut);border-color:var(--line);cursor:pointer;user-select:none}
+.chip.toggle:hover{border-color:var(--acc);color:var(--ink)}
+/* ── collapsed detail drawer ── */
+.info{display:none;flex-direction:column;gap:7px;border-top:1px dashed var(--line);margin-top:2px;padding-top:8px}
+.card.info-open .info{display:flex}
+.kv{display:grid;grid-template-columns:88px 1fr;gap:4px 10px;font-size:11.5px;align-items:baseline}
+.kv .k{color:var(--mut)}
+.kv .v{text-align:left;word-break:break-word;font-weight:500}
+.lnk{color:var(--acc);text-decoration:none;border:1px solid var(--line);border-radius:6px;padding:2px 7px;font-size:11px}
+.lnk:hover{background:var(--panel)}
+.acts{display:flex;flex-wrap:wrap;gap:6px;margin-top:3px}
+.act{font-size:11px;font-weight:600;border:1px solid var(--line);background:var(--panel);color:var(--ink);border-radius:7px;padding:4px 10px;cursor:pointer}
+.act:hover{border-color:var(--acc)}
+#more{display:block;margin:8px auto 60px;background:var(--panel);color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:9px 18px;cursor:pointer}
+#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}
+.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.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 .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 .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}
+/* ── 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;
+ color:var(--mut);backdrop-filter:blur(8px);cursor:default;user-select:none}
+.astek-refresh-pill .rp-dot{width:8px;height:8px;border-radius:50%;background:var(--pub)}
+.astek-refresh-pill.rp-live .rp-dot{background:var(--new);animation:rppulse 1s ease-in-out infinite}
+@keyframes rppulse{0%,100%{opacity:1}50%{opacity:.25}}
+</style></head>
 <body>
-
-<a class="corner ul on-hero wordmark" id="cUL" href="https://www.designerwallcoverings.com">Designer Wallcoverings</a>
-<nav class="corner ur on-hero" id="cUR"></nav>
-
-<!-- HERO -->
-<header class="hero" id="hero">
-  <div class="hero-inner">
-    <div class="hero-eyebrow" id="heroEyebrow">A Designer Wallcoverings Collection</div>
-    <h1 id="heroH1">&nbsp;</h1>
-    <div class="sub" id="heroSub">Architectural surfaces, made to order — curated by Designer Wallcoverings.</div>
-    <div class="hero-kicker" id="heroKicker">&nbsp;</div>
-  </div>
-  <a class="enter" href="#catalog">Explore the Collection</a>
+<header>
+  <h1>Astek · Catalog</h1>
+  <span class="pill" id="total">…</span>
+  <span class="muted" id="meta"></span>
+  <div class="ctl"><label class="muted">Book</label>
+    <select id="book"><option value="">All books</option></select></div>
+  <div class="ctl"><label class="muted">Color</label>
+    <select id="color"><option value="">All colors</option></select></div>
+  <div class="ctl"><input type="text" id="q" placeholder="search pattern / sku / color…"></div>
+  <div class="ctl"><label class="muted">Sort</label>
+    <select id="sort">
+      <option value="newest">Newest</option>
+      <option value="color">Color</option>
+      <option value="pattern">Pattern A→Z</option>
+      <option value="sku">SKU A→Z</option>
+      <option value="title">Title A→Z</option>
+      <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>
+  <button class="btn" id="view">▤ List view</button>
 </header>
-
-<!-- COLLECTIONS -->
-<section class="books" id="collections">
-  <div class="eyebrow" id="booksEyebrow">The Collection</div>
-  <h2 id="booksHeading">Designer Wallcoverings</h2>
-  <div class="chips" id="chips"></div>
-</section>
-
-<!-- CONTROLS -->
-<div class="controls" id="catalog">
-  <div class="controls-in">
-    <a href="https://www.designerwallcoverings.com" class="bar-wm">Designer Wallcoverings</a>
-    <div class="count"><b id="cnt">—</b> wallcoverings</div>
-    <div class="ctl">
-      <label for="pattern">Pattern</label>
-      <select id="pattern"><option value="">All patterns</option></select>
-    </div>
-    <div class="ctl">
-      <label for="sort">Sort</label>
-      <select id="sort">
-        <option value="newest">Newest</option>
-        <option value="color">Color</option>
-        <option value="series">Pattern</option>
-        <option value="sku">SKU A→Z</option>
-        <option value="title">Title A→Z</option>
-      </select>
-    </div>
-    <div class="ctl">
-      <label for="density">Density</label>
-      <input type="range" id="density" min="2" max="6" step="1" value="4">
-    </div>
-  </div>
-</div>
-
-<!-- COLOR FILTER -->
-<div class="colorbar" id="colorbar"></div>
-
-<!-- GRID -->
-<main class="grid-wrap">
-  <div class="grid" id="grid"></div>
-</main>
-
-<!-- ABOUT -->
-<section class="about" id="aboutSection" hidden>
-  <div class="about-in">
-    <div class="eyebrow" id="aboutEyebrow">About the House</div>
-    <h2 id="aboutHeading">About</h2>
-    <div class="about-body" id="aboutBody"></div>
-    <p class="dw-collab" id="aboutCollab"></p>
-  </div>
-</section>
-
-<footer>
-  <div class="foot-in">
-    <div>
-      <a class="wm" href="https://www.designerwallcoverings.com">Designer Wallcoverings</a>
-      <div style="margin-top:8px">Designer Wallcoverings &amp; Fabrics · To the Trade</div>
-      <div id="footLine" style="margin-top:4px;opacity:.8">One of 200+ lines we carry.</div>
-      <div class="share" id="shareCollection" style="margin-top:18px"></div>
-    </div>
-    <div class="gate" id="footGate">Made to order and sold by the roll at Designer Wallcoverings. Request a memo sample before specifying.</div>
-  </div>
-</footer>
-
-<a class="shopbar" id="shopbar" href="#catalog">Explore the Astek collection at <b>Designer Wallcoverings</b> →</a>
-
-<!-- MEMO-SAMPLE / INQUIRY MODAL -->
-<div class="modal-back" id="inqBack">
-  <div class="modal" role="dialog" aria-modal="true" aria-labelledby="inqTitle">
-    <button class="mclose" id="inqClose" aria-label="Close">×</button>
-    <div class="eyebrow">To the Trade</div>
-    <h3 id="inqTitle">Request a Memo Sample</h3>
-    <div class="msub" id="inqSub">A Designer Wallcoverings specialist will follow up with pricing and availability.</div>
-    <input type="hidden" id="inqSku">
-    <label for="inqName">Name</label>
-    <input id="inqName" autocomplete="name">
-    <label for="inqEmail">Email</label>
-    <input id="inqEmail" type="email" autocomplete="email" required>
-    <label for="inqNote">Note (optional)</label>
-    <textarea id="inqNote" placeholder="Project, quantity, timeline…"></textarea>
-    <div class="msg" id="inqMsg"></div>
-    <div class="mrow">
-      <button class="cta" id="inqSend" style="margin-top:0">Request Memo Sample</button>
-    </div>
-  </div>
-</div>
-
+<div id="facets" aria-label="catalog facets"></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>
 <script>
-const $=s=>document.querySelector(s), grid=$('#grid');
-let ALL=[], view=[], activeBook=null, activeColor=null, activeSeries=null, FACETS=null, LINE='';
-const LS={sort:'astek.sort', dens:'astek.density'};
-const BUCKET_HEX={white:'#efeae0',grey:'#9e978c',black:'#2a2722',red:'#a23b2e',orange:'#c8763a',brown:'#7b5a3c',gold:'#b8923f',green:'#5d7150',teal:'#3f8a86',blue:'#5a7494',purple:'#7a5f86',pink:'#bb8aa0'};
-
-// hero rotation from room shots
-function startHero(items){
-  const pool=items.filter(p=>p.room).slice(0,7);
-  const h=$('#hero');
-  pool.forEach((p,i)=>{
-    const d=document.createElement('div');
-    d.className='slide'+(i===0?' on':'');
-    d.style.backgroundImage=`url("${p.room}")`;
-    h.insertBefore(d,h.firstChild);
-  });
-  const slides=[...document.querySelectorAll('.hero .slide')];
-  let i=0; if(slides.length>1) setInterval(()=>{if(document.hidden)return;slides[i].classList.remove('on');i=(i+1)%slides.length;slides[i].classList.add('on');},5200);
-}
-
-function colorKey(p){
-  const b=p.color_bucket, hue=p.hue??999;
-  if(b==='white')return [0, p.val??1];
-  if(b==='grey') return [1, p.val??.5];
-  if(b==='black')return [2, -(p.val??0)];
-  return [3, hue];
-}
-function sortItems(items,mode){
-  const a=[...items];
-  switch(mode){
-    case 'color': a.sort((x,y)=>{const kx=colorKey(x),ky=colorKey(y);return kx[0]-ky[0]||kx[1]-ky[1];});break;
-    case 'series': a.sort((x,y)=>(x.series||'').localeCompare(y.series)|| (x.color||'').localeCompare(y.color));break;
-    case 'sku': a.sort((x,y)=>(x.sku||'').localeCompare(y.sku));break;
-    case 'title': a.sort((x,y)=>(x.title||'').localeCompare(y.title));break;
-    default: a.sort((x,y)=> new Date(y.published_at||0)-new Date(x.published_at||0));
-  }
-  return a;
-}
-
-function render(){
-  let items = ALL;
-  if(activeBook) items = items.filter(p=>p.book===activeBook);
-  if(activeColor) items = items.filter(p=>p.color_bucket===activeColor);
-  if(activeSeries) items = items.filter(p=>p.series===activeSeries);
-  items = sortItems(items, $('#sort').value);
-  view=items;
-  $('#cnt').textContent=items.length;
-  // cap DOM to first 600 for perf; controls still filter the full set
-  grid.innerHTML = items.slice(0,600).map(p=>`
-    <div class="card" data-h="${p.handle}">
-      <img class="ph" loading="lazy" decoding="async" width="400" height="400" alt="${(p.title||'').replace(/"/g,'&quot;')}" src="${p.swatch||p.room||''}">
-      <div class="body">
-        <div class="ser">${p.display_eyebrow||''}</div>
-        <div class="nm">${p.display_name||p.title}</div>
-        <div class="meta">
-          <span class="col">${p.color_bucket?`<span class="dot" style="background:${BUCKET_HEX[p.color_bucket]||'#ccc'}"></span>`:''}${p.book||LINE}</span>
-          <span class="pr view">View →</span>
-        </div>
+const $=s=>document.querySelector(s);
+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'};
+function esc(s){return String(s==null?'':s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));}
+// persisted prefs (CLAUDE.md: sort + density persist in localStorage)
+$('#sort').value=localStorage.getItem('astek.sort')||'newest';
+const dens=localStorage.getItem('astek.density')||'6'; $('#density').value=dens;
+document.documentElement.style.setProperty('--cols',dens);
+function card(p){
+  const title=esc(p.display_name||p.series||p.sku||'(untitled)')+(p.color&&p.color!==p.display_name?(', '+esc(p.color)):'');
+  const hasRoom=p.room&&p.room!==p.swatch;
+  const img=p.swatch
+    ?`<div class="imgwrap"><img loading="lazy" src="${esc(p.swatch)}" alt="${title}" onerror="this.style.display='none';this.parentNode.classList.add('noimg')">`+
+      (hasRoom?`<img loading="lazy" class="roomimg" src="${esc(p.room)}" alt=""><span class="roombadge">ROOM</span>`:'')+`</div>`
+    :`<div class="imgwrap noimg"></div>`;
+  const kv=(k,v)=>v?`<div class="kv"><span class="k">${k}</span><span class="v">${esc(v)}</span></div>`:'';
+  const dot=BUCKET_HEX[p.color_bucket]||'#666';
+  return `<a style="all:unset;cursor:pointer" href="/product/${encodeURIComponent(p.handle)}"><div class="card" data-handle="${esc(p.handle)}">
+    ${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>
+    <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>`:''}
+      <span class="chip toggle" onclick="event.preventDefault();event.stopPropagation();this.closest('.card').classList.toggle('info-open')">ⓘ details</span>
+    </div>
+    <span class="lsku" title="Vendor SKU">${esc(p.sku||'')}</span>
+    <span class="ldw" title="DW SKU">${esc(p.dw_sku||'')}</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)}
+      <div class="acts">
+        ${p.sku?`<button class="act" data-copy="${esc(p.sku)}" onclick="event.preventDefault();copyTxt(this)" title="copy vendor SKU">📋 SKU</button>`:''}
+        ${p.dw_sku?`<button class="act" data-copy="${esc(p.dw_sku)}" onclick="event.preventDefault();copyTxt(this)" title="copy DW SKU">📋 DW</button>`:''}
+        <a class="lnk" href="/product/${encodeURIComponent(p.handle)}">open ↗</a>
       </div>
-    </div>`).join('');
+    </div>
+  </div></div></a>`;
 }
-grid.addEventListener('click',e=>{const c=e.target.closest('.card');if(c)location.href='/product/'+c.dataset.h;});
-
-function chips(facets){
-  const wrap=$('#chips');
-  const all=`<div class="chip ${!activeBook?'active':''}" data-b="">All<span class="n">${facets.total}</span></div>`;
-  wrap.innerHTML=all+facets.books.map(([b,n])=>`<div class="chip ${activeBook===b?'active':''}" data-b="${b}">${b}<span class="n">${n}</span></div>`).join('');
-  wrap.querySelectorAll('.chip').forEach(ch=>ch.onclick=()=>{activeBook=ch.dataset.b||null;chips(facets);render();$('#catalog').scrollIntoView({behavior:'smooth'});});
+function copyTxt(btn){
+  const t=btn.dataset.copy||'';
+  navigator.clipboard.writeText(t).then(()=>{
+    const o=btn.textContent; btn.textContent='✓ copied';
+    setTimeout(()=>{btn.textContent=o;},1200);
+  });
 }
-function colorbar(facets){
-  const wrap=$('#colorbar');
-  if(!facets.colors||!facets.colors.length){wrap.style.display='none';return;}
-  const all=`<button class="cdot all ${!activeColor?'active':''}" data-c="" title="All colors">All</button>`;
-  wrap.innerHTML=all+facets.colors.map(([c,n])=>`<button class="cdot ${activeColor===c?'active':''}" data-c="${c}" title="${c} (${n})"><span style="background:${BUCKET_HEX[c]||'#ccc'}"></span>${c}</button>`).join('');
-  wrap.querySelectorAll('.cdot').forEach(b=>b.onclick=()=>{activeColor=b.dataset.c||null;colorbar(facets);render();});
+window.copyTxt=copyTxt;
+function applyFilters(){
+  const bk=$('#book').value, cl=$('#color').value, q=$('#q').value.trim().toLowerCase();
+  VIEW=ALL.filter(p=>
+    (!bk||p.book===bk)&&(!cl||p.color_bucket===cl)&&
+    (!q||[p.display_name,p.series,p.sku,p.dw_sku,p.color,p.book].some(v=>v&&String(v).toLowerCase().includes(q))));
+  const s=$('#sort').value, cmp={
+    newest:null,
+    color:(a,b)=>(a.hue??999)-(b.hue??999)||(a.val??0)-(b.val??0),
+    pattern:(a,b)=>String(a.series||a.display_name||'').localeCompare(String(b.series||b.display_name||'')),
+    sku:(a,b)=>String(a.sku||a.handle).localeCompare(String(b.sku||b.handle)),
+    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);
+  shown=0; grid.innerHTML=''; renderMore();
+  $('#total').textContent=VIEW.length.toLocaleString()+' designs';
+  $('#empty').style.display=VIEW.length?'none':'block';
+  renderFacetStrip();
 }
-
-function applyDensity(v){document.documentElement.style.setProperty('--cols',v);}
-$('#density').addEventListener('input',e=>{applyDensity(e.target.value);localStorage.setItem(LS.dens,e.target.value);});
-$('#sort').addEventListener('change',()=>{localStorage.setItem(LS.sort,$('#sort').value);render();});
-$('#pattern').addEventListener('change',()=>{activeSeries=$('#pattern').value||null;render();$('#catalog').scrollIntoView({behavior:'smooth'});});
-function fillPatterns(facets){
-  const sel=$('#pattern');
-  sel.innerHTML='<option value="">All patterns</option>'+facets.series.map(([s,n])=>`<option value="${s}">${s} (${n})</option>`).join('');
+function renderMore(){
+  const slice=VIEW.slice(shown,shown+PAGE);
+  grid.insertAdjacentHTML('beforeend',slice.map(card).join(''));
+  shown+=slice.length;
+  $('#meta').textContent=`${shown.toLocaleString()} of ${VIEW.length.toLocaleString()} shown`;
+  $('#more').style.display=shown<VIEW.length?'block':'none';
 }
-
-addEventListener('scroll',()=>{document.body.classList.toggle('scrolled',scrollY>innerHeight*0.82);});
-
-function applyConfig(cfg){
-  if(cfg.house){ $('#cUL').textContent=cfg.house; if(cfg.houseUrl)$('#cUL').href=cfg.houseUrl; }
-  if(cfg.nav&&cfg.nav.length){
-    $('#cUR').innerHTML=cfg.nav.map(n=>{
-      const ext=/^https?:/.test(n.href);
-      return `<a href="${n.href}"${ext?' target="_blank" rel="noopener noreferrer"':''}>${n.label}</a>`;
-    }).join('');
-  }
-  if(cfg.eyebrow) $('#heroEyebrow').textContent=cfg.eyebrow;
-  if(cfg.line)    $('#heroH1').textContent=cfg.line;
-  if(cfg.tagline) $('#heroSub').textContent=cfg.tagline;
-  if(cfg.kicker)  $('#heroKicker').textContent=cfg.kicker;
-  if(cfg.title)   document.title=cfg.title;
-  if(cfg.metaDescription){ const m=document.querySelector('meta[name=description]'); if(m)m.content=cfg.metaDescription; }
-  LINE = cfg.line || LINE;
-  if(cfg.slug){ document.body.dataset.line = cfg.slug; }
-  if(cfg.booksHeading){ const bh=$('#booksHeading'); if(bh)bh.textContent=cfg.booksHeading; }
-  const fl=$('#footLine'); if(fl) fl.textContent=`The ${LINE} Collection — one of 200+ lines we carry.`;
-  if(cfg.about && (cfg.about.paragraphs||[]).length){
-    const sec=$('#aboutSection');
-    $('#aboutHeading').textContent = `About ${cfg.line||cfg.vendor||''}`.trim();
-    $('#aboutBody').innerHTML = cfg.about.paragraphs.map(p=>`<p>${p}</p>`).join('');
-    if(cfg.about.collab) $('#aboutCollab').textContent = cfg.about.collab;
-    if(sec) sec.hidden = false;
-  }
-  renderShare(cfg.houseUrl, (cfg.title||`${LINE} at Designer Wallcoverings`));
+function fillSelects(){
+  const bkSel=$('#book'), clSel=$('#color');
+  const keepB=bkSel.value, keepC=clSel.value;
+  bkSel.innerHTML='<option value="">All books</option>'+(FACETS.books||[]).map(([b,n])=>`<option value="${esc(b)}">${esc(b)} (${n.toLocaleString()})</option>`).join('');
+  const colors={}; ALL.forEach(p=>{if(p.color_bucket)colors[p.color_bucket]=(colors[p.color_bucket]||0)+1;});
+  clSel.innerHTML='<option value="">All colors</option>'+Object.entries(colors).sort((a,b)=>b[1]-a[1]).map(([c,n])=>`<option value="${esc(c)}">${esc(c)} (${n.toLocaleString()})</option>`).join('');
+  bkSel.value=keepB; clSel.value=keepC;
+  window.__colors=colors;
 }
-
-// social share — shares this landing (internal collection), points people to DW
-function renderShare(url, title){
-  const el=$('#shareCollection'); if(!el)return;
-  const share=location.origin+'/';
-  const U=encodeURIComponent(share), T=encodeURIComponent(title);
-  const ic={
-    pin:'<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M12 2C6.5 2 4 5.8 4 8.9c0 1.9.7 3.6 2.3 4.2.3.1.5 0 .6-.3l.2-.9c.1-.3 0-.4-.2-.6-.4-.5-.7-1.2-.7-2.1 0-2.7 2-5.1 5.3-5.1 2.9 0 4.5 1.8 4.5 4.1 0 3.1-1.4 5.7-3.4 5.7-1.1 0-1.9-.9-1.7-2 .3-1.3.9-2.7.9-3.6 0-.8-.4-1.5-1.4-1.5-1.1 0-2 1.1-2 2.7 0 1 .3 1.6.3 1.6l-1.4 5.7c-.4 1.6 0 3.7 0 3.9 0 .1.2.2.3.1.1-.1 1.6-2 2.1-3.8l.8-3c.4.8 1.5 1.4 2.7 1.4 3.6 0 6-3.3 6-7.6C20.4 5 17 2 12 2z"/></svg>',
-    fb:'<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M22 12a10 10 0 10-11.6 9.9v-7H7.9V12h2.5V9.8c0-2.5 1.5-3.9 3.8-3.9 1.1 0 2.2.2 2.2.2v2.5h-1.2c-1.2 0-1.6.8-1.6 1.6V12h2.7l-.4 2.9h-2.3v7A10 10 0 0022 12z"/></svg>',
-    x:'<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M18.9 2H22l-7.3 8.3L23.3 22h-6.8l-5.3-7-6.1 7H2l7.8-8.9L1.2 2H8l4.8 6.3L18.9 2zm-1.2 18h1.9L7.1 4H5.1l12.6 16z"/></svg>',
-    mail:'<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M4 4h16a2 2 0 012 2v12a2 2 0 01-2 2H4a2 2 0 01-2-2V6a2 2 0 012-2zm8 7L4 6.5V18h16V6.5L12 11z"/></svg>',
-    link:'<svg viewBox="0 0 24 24" width="17" height="17" fill="currentColor"><path d="M10.6 13.4a1 1 0 001.4 0l4-4a3 3 0 00-4.2-4.2l-1.3 1.3a1 1 0 101.4 1.4l1.3-1.3a1 1 0 011.4 1.4l-4 4a1 1 0 000 1.4zm2.8-2.8a1 1 0 00-1.4 0l-4 4a3 3 0 004.2 4.2l1.3-1.3a1 1 0 10-1.4-1.4l-1.3 1.3a1 1 0 01-1.4-1.4l4-4a1 1 0 000-1.4z"/></svg>',
-  };
-  el.innerHTML=`<span class="share-lbl">Share</span>
-    <a class="sbtn pin" title="Pin on Pinterest" target="_blank" rel="noopener noreferrer" href="https://pinterest.com/pin/create/button/?url=${U}&description=${T}">${ic.pin}</a>
-    <a class="sbtn fb" title="Share on Facebook" target="_blank" rel="noopener noreferrer" href="https://www.facebook.com/sharer/sharer.php?u=${U}">${ic.fb}</a>
-    <a class="sbtn x" title="Share on X" target="_blank" rel="noopener noreferrer" href="https://twitter.com/intent/tweet?url=${U}&text=${T}">${ic.x}</a>
-    <a class="sbtn mail" title="Email" href="mailto:?subject=${T}&body=${T}%20${U}">${ic.mail}</a>
-    <button class="sbtn copy" title="Copy link" type="button">${ic.link}</button>`;
-  el.querySelector('.copy').addEventListener('click',async()=>{
-    const b=el.querySelector('.copy');
-    try{ await navigator.clipboard.writeText(share); b.classList.add('done'); b.title='Copied!';
-      setTimeout(()=>{b.classList.remove('done');b.title='Copy link';},1600);
-    }catch(e){ console.warn('[astek] clipboard write failed', e); }
-  });
-}
-
-// memo-sample modal wiring (homepage uses generic collection inquiry; PDP passes a sku)
-function openInquiry(sku, label){
-  $('#inqSku').value=sku||'';
-  $('#inqSub').textContent = sku
-    ? `Requesting a memo sample of ${label||sku}. A specialist will follow up with pricing and availability.`
-    : 'A Designer Wallcoverings specialist will follow up with pricing and availability.';
-  $('#inqMsg').textContent=''; $('#inqMsg').className='msg';
-  $('#inqBack').classList.add('on');
+// facet strip — clickable book + color-dot chips, new.dw health-strip style
+function renderFacetStrip(){
+  const el=$('#facets'); if(!el||!FACETS)return;
+  const curB=$('#book').value, curC=$('#color').value;
+  const books=(FACETS.books||[]).map(([b,n])=>
+    `<span class="hc${b===curB?' active':''}${n?'':' zero'}" onclick="pickBook('${esc(b)}')">${esc(b)} <span class="n">${n.toLocaleString()}</span></span>`).join('');
+  const colors=Object.entries(window.__colors||{}).sort((a,b)=>b[1]-a[1]).map(([c,n])=>
+    `<span class="hc${c===curC?' active':''}" onclick="pickColor('${esc(c)}')"><span class="dot" style="background:${BUCKET_HEX[c]||'#666'}"></span>${esc(c)} <span class="n">${n.toLocaleString()}</span></span>`).join('');
+  el.innerHTML=`<span class="hlabel">books</span>${books}<span class="hlabel" style="margin-left:10px">colors</span>${colors}`;
 }
-$('#inqClose').onclick=()=>$('#inqBack').classList.remove('on');
-$('#inqBack').addEventListener('click',e=>{if(e.target.id==='inqBack')$('#inqBack').classList.remove('on');});
-$('#inqSend').onclick=async()=>{
-  const email=$('#inqEmail').value.trim();
-  const m=$('#inqMsg');
-  if(!/.+@.+\..+/.test(email)){m.textContent='Please enter a valid email.';m.className='msg err';return;}
-  $('#inqSend').disabled=true;
-  try{
-    const r=await fetch('/api/inquiry',{method:'POST',headers:{'Content-Type':'application/json'},
-      body:JSON.stringify({sku:$('#inqSku').value||'astek-collection',name:$('#inqName').value.trim(),email,note:$('#inqNote').value.trim()})});
-    const j=await r.json();
-    if(j.ok){m.textContent=j.message;m.className='msg ok';setTimeout(()=>$('#inqBack').classList.remove('on'),1800);}
-    else{m.textContent=j.error||'Something went wrong.';m.className='msg err';}
-  }catch(e){m.textContent='Network error — please email info@designerwallcoverings.com.';m.className='msg err';}
-  $('#inqSend').disabled=false;
-};
-window.__openInquiry=openInquiry;
-$('#footGate')&&$('#footGate').addEventListener('click',()=>openInquiry(null));
-
-(async()=>{
-  const savedSort=localStorage.getItem(LS.sort); if(savedSort)$('#sort').value=savedSort;
-  const savedDens=localStorage.getItem(LS.dens)||'4'; $('#density').value=savedDens; applyDensity(savedDens);
-  const [cfg,pd,fc]=await Promise.all([
-    fetch('/api/config').then(r=>r.json()),
+function pickBook(b){const s=$('#book');s.value=(s.value===b?'':b);applyFilters();}
+function pickColor(c){const s=$('#color');s.value=(s.value===c?'':c);applyFilters();}
+window.pickBook=pickBook; window.pickColor=pickColor;
+$('#book').onchange=applyFilters;
+$('#color').onchange=applyFilters;
+$('#sort').onchange=()=>{localStorage.setItem('astek.sort',$('#sort').value);applyFilters();};
+let t; $('#q').oninput=()=>{clearTimeout(t);t=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';}
+$('#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();});
+async function loadAll(){
+  const [pd,fc]=await Promise.all([
     fetch('/api/products').then(r=>r.json()),
     fetch('/api/facets').then(r=>r.json())]);
-  applyConfig(cfg);
   ALL=pd.products; FACETS=fc;
-  const sb=$('#shopbar'); if(sb){ sb.innerHTML=`Explore all ${ALL.length} ${cfg.line||''} designs at <b>Designer Wallcoverings</b> →`; }
-  startHero(ALL); chips(fc); colorbar(fc); fillPatterns(fc); render();
-})();
+  fillSelects(); applyFilters();
+}
+loadAll();
 
 /* ── Corner refresh countdown ──────────────────────────────────────────────
    Data is rebuilt from dw_unified every 15 min by a cron. This pill counts down
@@ -312,17 +247,6 @@ $('#footGate')&&$('#footGate').addEventListener('click',()=>openInquiry(null));
   document.body.appendChild(pill);
   var txt=pill.querySelector('.rp-txt');
   function fmt(s){s=Math.max(0,Math.round(s));var m=Math.floor(s/60);return m+':'+String(s%60).padStart(2,'0');}
-  async function refreshData(){
-    try{
-      var res=await Promise.all([
-        fetch('/api/config').then(r=>r.json()),
-        fetch('/api/products').then(r=>r.json()),
-        fetch('/api/facets').then(r=>r.json())]);
-      var cfg=res[0]; ALL=res[1].products; FACETS=res[2];
-      var sb=$('#shopbar'); if(sb){ sb.innerHTML='Explore all '+ALL.length+' '+(cfg.line||'')+' designs at <b>Designer Wallcoverings</b> →'; }
-      chips(FACETS); colorbar(FACETS); fillPatterns(FACETS); render();
-    }catch(e){ console.warn('[astek] refresh failed', e); }
-  }
   async function syncMeta(){
     try{
       var m=await fetch('/api/meta').then(r=>r.json());
@@ -330,7 +254,7 @@ $('#footGate')&&$('#footGate').addEventListener('click',()=>openInquiry(null));
       meta=m;
       var t=new Date(m.lastRefresh).getTime()+m.intervalSec*1000;
       target = (t<=Date.now()) ? Date.now()+m.intervalSec*1000 : t; // clamp stale files to a fresh cycle
-      if(advanced) refreshData();
+      if(advanced) loadAll();
       return advanced;
     }catch(e){ return false; }
   }
@@ -343,5 +267,4 @@ $('#footGate')&&$('#footGate').addEventListener('click',()=>openInquiry(null));
   syncMeta().then(function(){ tick(); setInterval(tick,1000); });
 })();
 </script>
-</body>
-</html>
+</body></html>

← 9436002 auto-save: 2026-07-02T08:16:24 (1 files) — data/products.jso  ·  back to Astek Landing  ·  Add Pattern type-ahead filter covering ALL 4718 patterns (da 8328fe2 →