← back to Reidwitlin Landing
Retrofit front page to the internal-line-viewer spec (astek :9944 parity): left panel with search + pattern type-ahead + 7 collapsed facet tables (Books/Styles/Colors/Patterns/Colorways/Width/Repeat; Styles derived from tags — no style column in rwltd_catalog; Material/Length/Match skipped, 0/996 populated), value+count click-to-filter with teal summary badge + Clear, Card Fields toggles down to pure image grid, density slider drives --cols AND --fs type scale, tight 1px/2px/8px cards, data-href + single delegated click (no <a>-wrapped cards), rwltd.* localStorage namespace, location.origin fetches; memo-sample modal + PDP flow kept. E2E-verified Chromium + WebKit creds-in-URL: 120 grid children == 120 cards, 0 empty anchors, all tables collapsed, filter 996→341→996, imgonly clean, --fs 1.18→0.50, 0 console/page errors.
98a3a2a4f3fe4e0ecb71da66d21dfa8c3b451708 · 2026-07-02 17:26:08 -0700 · Steve Abrams
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Files touched
Diff
commit 98a3a2a4f3fe4e0ecb71da66d21dfa8c3b451708
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Jul 2 17:26:08 2026 -0700
Retrofit front page to the internal-line-viewer spec (astek :9944 parity): left panel with search + pattern type-ahead + 7 collapsed facet tables (Books/Styles/Colors/Patterns/Colorways/Width/Repeat; Styles derived from tags — no style column in rwltd_catalog; Material/Length/Match skipped, 0/996 populated), value+count click-to-filter with teal summary badge + Clear, Card Fields toggles down to pure image grid, density slider drives --cols AND --fs type scale, tight 1px/2px/8px cards, data-href + single delegated click (no <a>-wrapped cards), rwltd.* localStorage namespace, location.origin fetches; memo-sample modal + PDP flow kept. E2E-verified Chromium + WebKit creds-in-URL: 120 grid children == 120 cards, 0 empty anchors, all tables collapsed, filter 996→341→996, imgonly clean, --fs 1.18→0.50, 0 console/page errors.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---
public/index.html | 694 +++++++++++++++++++++++++++++++-----------------------
1 file changed, 394 insertions(+), 300 deletions(-)
diff --git a/public/index.html b/public/index.html
index 256db2e..c2a62a3 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1,109 +1,195 @@
-<!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>Reid Witlin · Catalog · dw_unified</title>
+<style>
+:root{--cols:6;--fs:1;--bg:#0c0d12;--panel:#15171f;--card:#12141b;--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:10px 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:6px;padding:6px 9px;font-size:13px}
+.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:6px;padding:6px 12px;cursor:pointer;font-size:13px;text-decoration:none}
+.btn:hover{border-color:var(--acc)}
+/* ── left panel: filters + field tables, ALL collapsed on load ── */
+#layout{display:flex;align-items:flex-start}
+aside{position:sticky;width:230px;min-width:230px;max-height:calc(100vh - 52px);overflow:auto;
+ padding:10px 10px 28px;border-right:1px solid var(--line);background:#0e0f15}
+aside input[type=text]{width:100%;margin-bottom:7px;font-size:12.5px;padding:5px 8px}
+aside details{border-bottom:1px solid var(--line)}
+aside summary{cursor:pointer;list-style:none;display:flex;align-items:center;gap:6px;padding:8px 4px;
+ font-size:10.5px;font-weight:700;color:var(--mut);text-transform:uppercase;letter-spacing:.6px;user-select:none}
+aside summary::-webkit-details-marker{display:none}
+aside summary::before{content:'▸';font-size:9px;color:var(--mut);transition:transform .12s}
+aside details[open] summary::before{transform:rotate(90deg)}
+aside summary:hover{color:var(--ink)}
+aside summary .cur{margin-left:auto;color:var(--acc);text-transform:none;letter-spacing:0;font-weight:600;
+ max-width:100px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
+.fbody{padding:2px 0 8px}
+.frow{display:flex;justify-content:space-between;align-items:center;gap:8px;padding:3px 8px;border-radius:6px;
+ cursor:pointer;color:var(--mut);font-size:12.5px;user-select:none}
+.frow:hover{background:var(--panel);color:var(--ink)}
+.frow.active{background:var(--panel);color:var(--ink);outline:1px solid var(--acc);outline-offset:-1px}
+.frow .fl{display:inline-flex;align-items:center;gap:7px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
+.frow .n{font-variant-numeric:tabular-nums;font-size:11px;color:var(--mut);flex:none}
+.frow .dot{width:10px;height:10px;border-radius:50%;border:1px solid #0008;display:inline-block;flex:none}
+.frow input[type=checkbox]{accent-color:var(--acc);margin:0 2px 0 0}
+.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}
+ 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 */
+ 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}
+.card{background:var(--card);border:1px solid var(--line);border-radius:2px;overflow:hidden;display:flex;flex-direction:column;position:relative;cursor:pointer}
+.card:hover{border-color:#414a5e}
+.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:6px;right:6px;z-index:2;background:#000a;color:#cbd5e1;font-size:9px;font-weight:700;border-radius:4px;padding:1px 6px;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:calc(6px*var(--fs)) calc(8px*var(--fs)) calc(7px*var(--fs));display:flex;flex-direction:column;gap:calc(3px*var(--fs))}
+.vend{font-size:calc(10px*var(--fs));color:var(--mut);font-weight:600;letter-spacing:.2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+.ttl{font-weight:600;font-size:calc(12.5px*var(--fs));line-height:1.3;letter-spacing:-.1px}
+.sku{font-family:ui-monospace,Menlo,monospace;font-size:calc(11px*var(--fs));color:var(--mut)}
+/* field visibility toggles (grid view only — list view columns stay intact) */
+body.f-off-vend .grid:not(.list) .vend{display:none}
+body.f-off-ttl .grid:not(.list) .ttl{display:none}
+body.f-off-chips .grid:not(.list) .chips{display:none}
+body.imgonly .grid:not(.list) .card .b{display:none}
+/* ── chips (minimal) ── */
+.chips{display:flex;flex-wrap:wrap;gap:5px;align-items:center}
+.chip{font-size:calc(10px*var(--fs));font-weight:600;border-radius:999px;padding:1px calc(8px*var(--fs));letter-spacing:.2px;white-space:nowrap;border:1px solid transparent}
+.chip.cl{border-color:transparent;color:var(--mut);background:transparent;display:inline-flex;align-items:center;gap:5px;padding-left:0}
+.chip.cl .dot{width:calc(9px*var(--fs));height:calc(9px*var(--fs));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 (direct child of .card so list-mode span works) ── */
+.info{display:none;flex-direction:column;gap:7px;border-top:1px dashed var(--line);padding:8px 8px 9px;cursor:default}
+.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 (.b uses display:contents so its
+ children become columns of the card grid). Fixed type sizes (1 column). */
+:root{--listcols:42px 110px minmax(120px,1.2fr) minmax(90px,1fr) 92px minmax(110px,1fr) 112px minmax(110px,1.2fr) 64px 110px 92px}
+.grid.list{grid-template-columns:1fr;gap:4px;padding:4px 12px 60px}
+.grid.list .card{
+ display:grid;align-items:center;border-radius:2px;
+ 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:3px;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;font-size:12.5px}
+.grid.list .chips{flex-wrap:nowrap;gap:6px;overflow:hidden}
+.grid.list .chip{font-size:10px;padding:1px 8px}
+.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,.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 24px 6px 20px;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)}
+/* ── memo-sample / inquiry modal (dark theme; kept from the vendor-landing build) ── */
+.modal-back{position:fixed;inset:0;z-index:60;background:#000a;display:none;align-items:center;justify-content:center;padding:16px}
+.modal-back.on{display:flex}
+.modal{position:relative;background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:22px;width:min(440px,100%);max-height:92vh;overflow:auto}
+.modal h3{margin:2px 0 4px;font-size:16px}
+.modal .msub{color:var(--mut);font-size:12px;margin-bottom:10px}
+.modal label{display:block;font-size:10.5px;font-weight:700;color:var(--mut);text-transform:uppercase;letter-spacing:.5px;margin:10px 0 4px}
+.modal input,.modal textarea{width:100%;background:var(--bg);color:var(--ink);border:1px solid var(--line);border-radius:6px;padding:8px 10px;font-size:13px;font-family:inherit}
+.modal textarea{min-height:64px;resize:vertical}
+.mclose{position:absolute;top:8px;right:10px;background:none;border:none;color:var(--mut);font-size:20px;cursor:pointer}
+.mclose:hover{color:var(--ink)}
+.modal .cta{margin-top:14px;width:100%;background:var(--acc);color:#062a24;border:none;border-radius:8px;padding:10px;font-size:13px;font-weight:700;cursor:pointer}
+.modal .msg{font-size:12px;margin-top:8px;min-height:14px}
+.modal .msg.ok{color:var(--pub)}
+.modal .msg.err{color:var(--arch)}
+</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"> </h1>
- <div class="sub" id="heroSub">Architectural surfaces, made to order — curated by Designer Wallcoverings.</div>
- <div class="hero-kicker" id="heroKicker"> </div>
- </div>
- <a class="enter" href="#catalog">Explore the Collection</a>
+<header>
+ <h1>Reid Witlin · 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>
+ <option value="style">Style</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="20" 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="20" step="1" value="4">
+<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>
+ <details id="fieldsbox"><summary>Card fields</summary>
+ <div class="fbody">
+ <label class="frow"><span class="fl"><input type="checkbox" data-fv="vend" checked>Book · series</span></label>
+ <label class="frow"><span class="fl"><input type="checkbox" data-fv="ttl" checked>Title / color</span></label>
+ <label class="frow"><span class="fl"><input type="checkbox" data-fv="chips" checked>Chips · details</span></label>
</div>
- <div class="ctl">
- <button class="viewbtn" id="view" type="button">▤ List view</button>
- </div>
- </div>
-</div>
-
-<!-- COLOR FILTER -->
-<div class="colorbar" id="colorbar"></div>
-
-<!-- GRID -->
-<main class="grid-wrap">
- <div class="lhead" id="listHead" hidden></div>
+ </details>
+ <div id="facetbox"><!-- one collapsed <details> table per data field --></div>
+ <button class="btn" id="clearf">✕ Clear filters</button>
+</aside>
+<main>
+ <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>
</main>
+</div>
-<!-- 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 & 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 Reid Witlin collection at <b>Designer Wallcoverings</b> →</a>
-
-<!-- MEMO-SAMPLE / INQUIRY MODAL -->
+<!-- MEMO-SAMPLE / INQUIRY MODAL (kept from the vendor-landing build; PDP also uses __openInquiry) -->
<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">
@@ -114,32 +200,121 @@
<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>
+ <button class="cta" id="inqSend">Request Memo Sample</button>
</div>
</div>
<script>
-const $=s=>document.querySelector(s), grid=$('#grid');
-let ALL=[], view=[], activeBook=null, activeColor=null, activeSeries=null, FACETS=null, LINE='';
-const LS={sort:'reidwitlin.sort', dens:'reidwitlin.density', view:'reidwitlin.view'};
-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 $=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',teal:'#3f8a86',blue:'#4f7fb5',purple:'#8a6fb5'};
+function esc(s){return String(s==null?'':s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));}
+// ── every data field this catalog ACTUALLY carries is a left-panel table ──
+// (material / length / match are 0/996 in rwltd_catalog — skipped, not hidden-empty)
+const FIELDS=[
+ ['book','Books'],['style','Styles'],['color_bucket','Colors'],['series','Patterns'],['color','Colorways'],
+ ['width','Width'],['repeat','Repeat'],
+];
+// style is derived from tags (interior-style lexicon) at load time
+const STYLE_VOCAB=['Contemporary','Traditional','Transitional','Modern','Minimalist','Scandinavian','Rustic',
+ 'Industrial','Coastal','Classic','Eclectic','Farmhouse','Bohemian','Organic Modern'];
+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) — rwltd.* namespace
+$('#sort').value=localStorage.getItem('rwltd.sort')||'newest';
+// ── density also drives type scale: fewer/larger cards → bigger type, 20/row → smallest ──
+function setDensity(v){
+ v=Number(v);
+ const fs=Math.max(.5,Math.min(1.2, v<=6 ? 1+(6-v)*.06 : 1-(v-6)*.042));
+ document.documentElement.style.setProperty('--cols',v);
+ document.documentElement.style.setProperty('--fs',fs.toFixed(3));
+}
+const dens=localStorage.getItem('rwltd.density')||'6';
+$('#density').value=dens; setDensity(dens);
+$('#density').oninput=()=>{setDensity($('#density').value);localStorage.setItem('rwltd.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('rwltd.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('rwltd.fieldvis',JSON.stringify(FVIS));
+}
+document.querySelectorAll('#fieldsbox input[data-fv]').forEach(cb=>{
+ cb.onchange=()=>{FVIS[cb.dataset.fv]=cb.checked;applyFieldVis();};
+});
+applyFieldVis();
+// NOTE: no <a> wrapper around the card — nesting anchors splits the DOM (adoption
+// agency) and litters the grid with empty <a> cells. Navigation is delegated click.
+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 `<div class="card" data-href="/product/${encodeURIComponent(p.handle)}" data-handle="${esc(p.handle)}">
+ ${img}<div class="b">
+ <span class="vend">${esc(p.book||'Reid Witlin')}${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">
+ ${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.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>
+ <span class="lc" title="Pattern">${esc(p.series||'')}</span>
+ <span class="lc" title="Width">${esc(p.width||'')}</span>
+ <span class="lc" title="Repeat">${esc(p.repeat||'')}</span>
+ <span class="lc" title="Added">${fmtAdded(p.published_at)}</span>
+ </div>
+ <div class="info" onclick="event.stopPropagation()">
+ ${kv('DW SKU',p.dw_sku)}${kv('Vendor SKU',p.sku)}${kv('Pattern',p.series)}${kv('Book',p.book)}
+ ${kv('Style',p.style)}${kv('Colorway',p.color)}${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>`:''}
+ <button class="act" onclick="__openInquiry('${esc(p.inquiry_sku||p.dw_sku||p.sku)}','${esc((p.series||'')+(p.color?(' — '+p.color):''))}')" title="request a memo sample">✉ Memo</button>
+ <a class="lnk" href="/product/${encodeURIComponent(p.handle)}">open ↗</a>
+ </div>
+ </div>
+ </div>`;
+}
+function fmtAdded(d){
+ if(!d) return '';
+ const t=new Date(d); if(isNaN(t)) return '';
+ return t.toLocaleDateString(undefined,{year:'numeric',month:'short',day:'numeric'});
+}
+// delegated card navigation — anything not interactive opens the PDP
+grid.addEventListener('click',e=>{
+ if(e.target.closest('.chip.toggle,.info,a,button'))return;
+ const c=e.target.closest('.card[data-href]');
+ if(c)location.href=c.dataset.href;
+});
+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);
});
- 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);
}
-
+window.copyTxt=copyTxt;
+// color sort key — white/grey/black first (by value), then chromatic by hue
function colorKey(p){
const b=p.color_bucket, hue=p.hue??999;
if(b==='white')return [0, p.val??1];
@@ -147,179 +322,105 @@ function colorKey(p){
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;
-}
-
-// ── list view: sortable columns over every non-null data field ──
-const COLS=[
- ['book','Book'],['series','Pattern'],['display_name','Name'],['color','Color'],
- ['color_bucket','Bucket'],['sku','SKU'],['dw_sku','DW SKU'],['width','Width'],
- ['repeat','Repeat'],['published_at','Added']
-];
-let colSort=null; // {f:field, dir:1|-1}
-function colCompare(f,dir){
- return (x,y)=>{
- let a=x[f], b=y[f];
- const ea=(a==null||a===''), eb=(b==null||b==='');
- if(ea&&eb) return 0; if(ea) return 1; if(eb) return -1; // empties last either way
- if(f==='published_at') return (new Date(a)-new Date(b))*dir;
- return String(a).localeCompare(String(b),undefined,{numeric:true,sensitivity:'base'})*dir;
- };
-}
-function renderListHead(){
- const h=$('#listHead');
- h.innerHTML='<span></span>'+COLS.map(([f,l])=>{
- const on=colSort&&colSort.f===f;
- return `<button class="lh ${on?'on':''}" data-f="${f}" type="button">${l}${on?(colSort.dir>0?' ▲':' ▼'):''}</button>`;
- }).join('');
- h.querySelectorAll('.lh').forEach(b=>b.onclick=()=>{
- const f=b.dataset.f;
- colSort = (colSort&&colSort.f===f) ? {f,dir:-colSort.dir} : {f,dir:1};
- renderListHead(); render();
+function applyFilters(){
+ const q=$('#q').value.trim().toLowerCase(), pat=$('#pattern').value.trim().toLowerCase();
+ VIEW=ALL.filter(p=>{
+ for(const [k] of FIELDS){ if(FIL[k]&&String(p[k]??'')!==FIL[k])return false; }
+ if(pat&&!String(p.series||p.display_name||'').toLowerCase().includes(pat))return false;
+ if(q&&![p.display_name,p.series,p.sku,p.dw_sku,p.color,p.book].some(v=>v&&String(v).toLowerCase().includes(q)))return false;
+ return true;
});
+ const s=$('#sort').value, cmp={
+ newest:(a,b)=>new Date(b.published_at||0)-new Date(a.published_at||0),
+ color:(a,b)=>{const kx=colorKey(a),ky=colorKey(b);return kx[0]-ky[0]||kx[1]-ky[1];},
+ style:(a,b)=>{const av=a.style||'',bv=b.style||'';return av.localeCompare(bv)||String(a.series||'').localeCompare(String(b.series||''));},
+ 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(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
+ if(k==='published_at')return dir*(new Date(av)-new Date(bv));
+ 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';
+ renderSide();
}
-function fmtAdded(d){
- if(!d) return '';
- const t=new Date(d); if(isNaN(t)) return '';
- return t.toLocaleDateString(undefined,{year:'numeric',month:'short',day:'numeric'});
-}
-
-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);
- if(colSort) items = [...items].sort(colCompare(colSort.f,colSort.dir)); // full filtered set, not just rendered rows
- 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,'"')}" 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>
- <span class="lc">${p.book||''}</span>
- <span class="lc">${p.series||''}</span>
- <span class="lc lc-nm">${p.display_name||p.title||''}</span>
- <span class="lc">${p.color||''}</span>
- <span class="lc">${p.color_bucket?`<span class="dot" style="background:${BUCKET_HEX[p.color_bucket]||'#ccc'}"></span>`:''}${p.color_bucket||''}</span>
- <span class="lc mono">${p.sku||''}</span>
- <span class="lc mono">${p.dw_sku||''}</span>
- <span class="lc">${p.width||''}</span>
- <span class="lc">${p.repeat||''}</span>
- <span class="lc">${fmtAdded(p.published_at)}</span>
- </div>
- </div>`).join('');
-}
-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 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';
}
-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();});
+function buildFacetCache(){
+ FCOUNTS={};
+ for(const [k] of FIELDS){
+ const m={};
+ 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}));
+ $('#patternlist').innerHTML=series.map(s=>`<option value="${esc(s)}">`).join('');
}
-
-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);colSort=null;renderListHead();render();});
-
-// grid / list view toggle (persisted)
-function applyView(){
- const list=localStorage.getItem(LS.view)==='list';
- grid.classList.toggle('list',list);
- $('#listHead').hidden=!list;
- $('#view').textContent=list?'▦ Grid view':'▤ List view';
- const c=document.querySelector('.controls');
- if(c) document.documentElement.style.setProperty('--ctlh',c.offsetHeight+'px');
+// left panel — one collapsed <details> table per field; open state survives re-render
+function renderSide(){
+ const openNow=new Set([...document.querySelectorAll('#facetbox details[open]')].map(d=>d.dataset.f));
+ $('#facetbox').innerHTML=FIELDS.map(([k,label])=>{
+ const rows=(FCOUNTS[k]||[]).slice(0,FCAP).map(([v,n])=>{
+ const dot=k==='color_bucket'?`<span class="dot" style="background:${BUCKET_HEX[v]||'#666'}"></span>`:'';
+ return `<div class="frow${FIL[k]===String(v)?' active':''}" onclick="pickF('${k}',this.dataset.v)" data-v="${esc(v)}">
+ <span class="fl">${dot}${esc(v)}</span><span class="n">${n.toLocaleString()}</span></div>`;
+ }).join('');
+ const more=(FCOUNTS[k]||[]).length>FCAP?`<div class="fmore">+ ${(FCOUNTS[k].length-FCAP).toLocaleString()} more — use search</div>`:'';
+ const cur=FIL[k]?`<span class="cur" title="${esc(FIL[k])}">${esc(FIL[k])}</span>`:'';
+ return `<details data-f="${k}"${openNow.has(k)?' open':''}><summary>${label}${cur}</summary><div class="fbody">${rows}${more}</div></details>`;
+ }).join('');
+ $('#clearf').style.display=(Object.values(FIL).some(Boolean)||$('#q').value||$('#pattern').value)?'block':'none';
}
-$('#view').onclick=()=>{localStorage.setItem(LS.view,localStorage.getItem(LS.view)==='list'?'grid':'list');applyView();};
-$('#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 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('rwltd.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) — order mirrors the card's .b children ──
+const LIST_COLS=[[null,''],['book','Book'],['display_name','Name'],['color','Color'],['color_bucket','Bucket'],
+ ['sku','SKU'],['dw_sku','DW SKU'],['series','Pattern'],['width','Width'],['repeat','Repeat'],['published_at','Added']];
+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('');
+ syncSticky();
}
-
-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 colSort(k){
+ if(COLSORT.key===k)COLSORT.dir=-COLSORT.dir; else{COLSORT.key=k;COLSORT.dir=1;}
+ renderListHead(); applyFilters();
}
-
-// 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){}
- });
+window.colSort=colSort;
+function syncSticky(){
+ const h=document.querySelector('header').offsetHeight;
+ $('#listhead').style.top=h+'px';
+ document.querySelector('aside').style.top=h+'px';
+ document.querySelector('aside').style.maxHeight=`calc(100vh - ${h}px)`;
}
-
-// memo-sample modal wiring (homepage uses generic collection inquiry; PDP passes a sku)
-function openInquiry(sku, label){
+addEventListener('resize',syncSticky);
+function applyView(){const list=localStorage.getItem('rwltd.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('rwltd.view',localStorage.getItem('rwltd.view')==='list'?'grid':'list');applyView();};
+applyView();
+addEventListener('scroll',()=>{if(shown<VIEW.length&&innerHeight+scrollY>=document.body.offsetHeight-600)renderMore();});
+// ── memo-sample modal wiring (kept feature; PDP calls __openInquiry too) ──
+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.`
@@ -327,6 +428,7 @@ function openInquiry(sku, label){
$('#inqMsg').textContent=''; $('#inqMsg').className='msg';
$('#inqBack').classList.add('on');
}
+window.__openInquiry=openInquiry;
$('#inqClose').onclick=()=>$('#inqBack').classList.remove('on');
$('#inqBack').addEventListener('click',e=>{if(e.target.id==='inqBack')$('#inqBack').classList.remove('on');});
$('#inqSend').onclick=async()=>{
@@ -343,22 +445,14 @@ $('#inqSend').onclick=async()=>{
}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);
- // location.origin prefix: pages opened via user:pass@ URLs break relative fetches in WebKit
- const [cfg,pd,fc]=await Promise.all([
- fetch(location.origin+'/api/config').then(r=>r.json()),
- fetch(location.origin+'/api/products').then(r=>r.json()),
- fetch(location.origin+'/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); renderListHead(); applyView(); render();
-})();
+async function loadAll(){
+ const [pd]=await Promise.all([
+ fetch(location.origin+'/api/products').then(r=>r.json())]);
+ ALL=pd.products;
+ // derive style from tags (interior-style lexicon; rwltd_catalog has no style column)
+ for(const p of ALL){ p.style=(p.tags||[]).find(t=>STYLE_VOCAB.includes(t))||null; }
+ buildFacetCache(); applyFilters();
+}
+loadAll();
</script>
-</body>
-</html>
+</body></html>
← 41474dc chore: addendum gate clean, v0.2.1 (session close)
·
back to Reidwitlin Landing
·
chore: pin port + stable .env credential via pm2, guarded do a206855 →