← back to Dw Substitute Viewer
index.html
573 lines
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Substitute Finder</title><style>
:root{--cols:5;--ink:#1c1a17;--muted:#8a8178;--line:#e7e1d8;--bg:#faf7f2;--accent:#b4531f}
*{box-sizing:border-box}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;color:var(--ink);background:var(--bg)}
header{padding:24px 32px 16px;border-bottom:1px solid var(--line);background:#fff}
h1{margin:0;font-size:21px;font-weight:600}.sub{color:var(--muted);font-size:13px}
.htop{max-width:1520px;margin:0 auto;padding:0 24px;display:flex;align-items:center;gap:16px}
input[type=search]{flex:1;max-width:400px;font:inherit;font-size:13px;padding:8px 12px;border:1px solid var(--line);border-radius:9px;background:#fff}
.sortwrap{font-size:12px;color:var(--muted);white-space:nowrap}
#sort{font:inherit;font-size:12.5px;padding:7px 9px;border:1px solid var(--line);border-radius:8px;background:#fff}
.topwrap{max-width:1520px;margin:0 auto;padding:16px 24px 0}
.wrap{max-width:1520px;margin:0 auto;padding:14px 24px 96px;display:flex;gap:18px;align-items:flex-start}
aside#rail{width:248px;flex:0 0 248px;position:sticky;top:14px;max-height:calc(100vh - 28px);overflow-y:auto;padding-bottom:40px}
main{flex:1;min-width:0}
/* collapsible facet sections */
details.sec{border:1px solid var(--line);border-radius:9px;margin-bottom:8px;background:#fff}
details.sec>summary{list-style:none;cursor:pointer;user-select:none;padding:9px 11px;font-size:10.5px;letter-spacing:.09em;text-transform:uppercase;color:var(--muted);display:flex;align-items:center;gap:7px}
details.sec>summary::-webkit-details-marker{display:none}
details.sec>summary::before{content:'▸';color:var(--accent);font-size:10px}
details.sec[open]>summary::before{content:'▾'}
details.sec>summary .n{margin-left:auto;background:var(--accent);color:#fff;border-radius:20px;padding:0 7px;font-size:10px;font-weight:700}
.fbody{padding:3px 10px 10px}
.fitem{display:flex;align-items:center;gap:8px;padding:3px 3px;cursor:pointer;font-size:12.5px;color:var(--ink);border-radius:5px}
.fitem:hover{background:var(--bg)}
.fitem .box{width:14px;height:14px;flex:0 0 14px;border:1px solid var(--muted);border-radius:4px;display:inline-flex;align-items:center;justify-content:center;font-size:10px}
.fitem.on .box{background:var(--accent);border-color:var(--accent);color:#fff;font-weight:700}
.fitem.on{color:var(--accent);font-weight:600}
.fitem .lbl{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.fitem .sw{position:static;width:11px;height:11px;border-radius:3px;flex:0 0 11px;border:1px solid rgba(0,0,0,.15);padding:0;background:transparent}
.fitem .ct{color:var(--muted);font-size:10.5px}
.more{color:var(--accent);font-size:11.5px;cursor:pointer;padding:4px 3px;display:inline-block;font-weight:600}
.ffind{width:100%;padding:5px 8px;font:inherit;font-size:11.5px;border:1px solid var(--line);border-radius:6px;margin:2px 0 6px}
.railtools{display:flex;gap:6px;margin-bottom:8px}
.railtools button{flex:1;font:inherit;font-size:11px;font-weight:600;color:var(--muted);background:#fff;border:1px solid var(--line);border-radius:7px;padding:6px 4px;cursor:pointer}
.railtools button:hover{border-color:var(--accent);color:var(--accent)}
.active{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px}
.afilter{background:#f3e7dd;border:1px solid var(--accent);color:var(--ink);border-radius:20px;padding:3px 9px;font-size:11px;cursor:pointer}
.afilter b{color:var(--accent);font-weight:700}
.clearall{background:none;border:1px solid var(--line);color:var(--muted);border-radius:20px;padding:3px 11px;font-size:11px;cursor:pointer}
.orig{display:flex;gap:26px;align-items:center;background:#fff;border:1px solid var(--line);border-left:5px solid var(--accent);border-radius:14px;padding:22px 26px}
.orig img{width:210px;height:210px;object-fit:cover;border-radius:10px;background:#eee}
.origleft{display:flex;flex-direction:column;gap:9px;width:210px;flex-shrink:0}
.gobtn.wide{width:100%;justify-content:center;text-align:center;padding:11px 6px;font-size:14px;background:var(--accent);color:#fff;border-color:var(--accent)}
.tagd{display:inline-block;background:var(--accent);color:#fff;font-size:12.5px;font-weight:700;letter-spacing:.5px;text-transform:uppercase;padding:5px 12px;border-radius:20px;margin-bottom:7px}
.tagd.disc{background:#c0392b}
.orig.disc{border-left-color:#c0392b}
.orig.disc h2{color:#c0392b}
.successor{margin:9px 0 2px;font-size:14px;font-weight:600;color:#c0392b}
.successor a{color:#c0392b;text-decoration:underline;font-weight:700}
.successor .arr{opacity:.7;margin:0 4px}
.catsimbtn{margin-left:8px;padding:5px 13px;font:inherit;font-size:13px;font-weight:600;color:#fff;background:#c0392b;border:1px solid #c0392b;border-radius:7px;cursor:pointer}
.catsimbtn:hover{background:#a5311f}
.catsimbtn:disabled{opacity:.6;cursor:wait}
#catSim{margin-top:14px}
.catsimhead{font-size:12px;font-weight:700;color:#c0392b;text-transform:uppercase;letter-spacing:.07em;margin-bottom:9px}
.catsimnone{font-size:13px;color:var(--muted);margin-top:6px}
.catsimgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:12px}
.catsimcard{background:#fff;border:1px solid var(--line);border-radius:10px;overflow:hidden;display:flex;flex-direction:column}
.catsimcard .csimg{width:100%;aspect-ratio:1/1;background:#eee center/cover no-repeat}
.catsimcard .csb{padding:7px 9px 9px;display:flex;flex-direction:column;gap:2px}
.catsimcard .cssku{font-size:11.5px;font-weight:700;color:var(--ink)}
.catsimcard .csttl{font-size:12px;color:var(--ink);line-height:1.25;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.catsimcard .csvt{font-size:10.5px;color:var(--muted)}
.catsimcard .csprice{font-size:12px;font-weight:700;color:var(--accent);margin-top:2px}
.orig h2{margin:3px 0 6px;font-size:25px;line-height:1.2}.orig .attrs{color:var(--muted);font-size:16px;line-height:1.65}
.imgurl{margin-top:9px;display:flex;gap:8px;align-items:center}
.imgurl input{font:inherit;font-size:14px;padding:8px 11px;border:1px solid var(--line);border-radius:8px;width:440px;max-width:52vw}
.imgurl span{font-size:13px;color:var(--muted);width:92px}
.gobtn{font:inherit;font-size:13px;font-weight:600;border:1px solid var(--line);background:#efe9e0;color:#1c1a17;border-radius:7px;padding:8px 13px;cursor:pointer;white-space:nowrap}
.gobtn:disabled{opacity:.4;cursor:default}
input[type=range]{width:100%;accent-color:var(--accent)}
select{padding:6px 10px;border:1px solid var(--line);border-radius:8px;background:#fff;font:inherit}
.prionote{font-size:10.5px;color:var(--muted);line-height:1.45;margin:1px 0 8px}
.clipstat{font-size:10.5px;color:var(--accent);font-weight:600;margin-top:7px;min-height:14px}
.hexrow{display:flex;align-items:center;gap:6px;margin-top:9px;font-size:11px;color:var(--muted)}
.hexrow input{width:78px;font:inherit;font-size:12px;font-family:ui-monospace,Menlo,monospace;padding:4px 7px;border:1px solid var(--line);border-radius:6px;text-transform:lowercase}
.hexrow .hexsw{width:18px;height:18px;border-radius:4px;border:1px solid rgba(0,0,0,.2);flex:0 0 18px}
.hexrow .hexclr{border:1px solid var(--line);background:#fff;color:var(--muted);border-radius:5px;width:20px;height:20px;cursor:pointer;font-size:10px;padding:0}
.prow{display:flex;align-items:center;gap:8px;padding:7px 8px;border:1px solid var(--line);border-radius:8px;margin-bottom:6px;background:var(--bg)}
.prow .rk{width:22px;height:22px;flex:0 0 22px;border-radius:50%;background:var(--accent);color:#fff;font-weight:700;font-size:12px;display:flex;align-items:center;justify-content:center}
.prow .pn{flex:1;font-size:13px;font-weight:600}
.prow .pnb{font-size:10.5px;color:var(--muted);font-weight:400}
.prow .ar{display:flex;flex-direction:column;gap:2px}
.prow .ar button{border:1px solid var(--line);background:#fff;color:var(--ink);border-radius:5px;width:22px;height:15px;line-height:1;cursor:pointer;font-size:10px;padding:0}
.prow .ar button:disabled{opacity:.3;cursor:default}
.dispctl{display:flex;flex-direction:column;gap:3px;margin-top:10px}
.dispctl label{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.4px;display:flex;justify-content:space-between}
.dispctl b{color:var(--accent);font-variant-numeric:tabular-nums}
.reset{margin-top:10px;width:100%;font-size:12px;color:var(--muted);background:none;border:1px solid var(--line);border-radius:8px;padding:7px 12px;cursor:pointer}
.count{color:var(--muted);font-size:13px;margin:0 0 10px}
.grid{display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:14px}
.meta{display:flex;flex-wrap:wrap;gap:4px 8px;align-items:center}
.m-hex .hx{display:inline-block;width:10px;height:10px;border-radius:2px;border:1px solid rgba(0,0,0,.15);vertical-align:-1px;margin-right:3px}
.m-sku{font-weight:700;color:var(--ink)}
/* MANUAL per-field toggles (Card fields panel) */
#grid.h-rank .rank,#grid.h-color .sw,#grid.h-status .st,#grid.h-vend .vend,#grid.h-title .title,#grid.h-hex .m-hex,#grid.h-specs .m-specs,#grid.h-price .m-price,#grid.h-sku .m-sku,#grid.h-score .scorebar,#grid.h-link .rowlink,#grid.h-urls .editimg{display:none}
/* AUTO density-driven progressive text reduction (more columns → less text, down to model # only) */
#grid.x-hex .m-hex,#grid.x-score .scorebar,#grid.x-title .title,#grid.x-vend .vend,#grid.x-specs .m-specs,#grid.x-price .m-price,#grid.x-status .st,#grid.x-color .sw{display:none}
#grid.x-title.x-vend.x-specs.x-price .body{padding:5px 7px}
.card{background:#fff;border:1px solid var(--line);border-radius:12px;overflow:hidden;display:flex;flex-direction:column;transition:box-shadow .15s,transform .15s;position:relative}
.card:hover{box-shadow:0 8px 22px rgba(0,0,0,.09);transform:translateY(-2px)}
.card.sel{outline:2px solid var(--accent);outline-offset:-2px}
.ph{position:relative;aspect-ratio:1/1;background:#efe9e0}.ph img{width:100%;height:100%;object-fit:cover;display:block}
.ph.drop,.orig img.drop{outline:3px dashed var(--accent);outline-offset:-5px}
.ph.drop::after{content:"Drop image to replace";position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(180,83,31,.22);color:#fff;font-size:12px;font-weight:700;text-shadow:0 1px 3px rgba(0,0,0,.6);pointer-events:none}
.rank{position:absolute;top:8px;left:38px;background:#1c1a17;color:#fff;font-size:12px;font-weight:600;min-width:24px;height:24px;padding:0 6px;border-radius:12px;display:flex;align-items:center;justify-content:center}
.pick{position:absolute;top:8px;left:8px;width:22px;height:22px;cursor:pointer;accent-color:var(--accent);z-index:2}
.sw{position:absolute;top:8px;right:8px;font-size:10px;font-weight:600;color:#fff;background:rgba(0,0,0,.55);padding:3px 7px;border-radius:20px}
.st{position:absolute;bottom:8px;left:8px;font-size:9.5px;font-weight:700;letter-spacing:.4px;color:#fff;padding:2px 6px;border-radius:4px}
.st.ACTIVE{background:#2e7d32}.st.DRAFT{background:#8a6d1f}
.clipb{position:absolute;top:8px;left:50%;transform:translateX(-50%);font-size:11px;font-weight:800;color:#fff;padding:3px 9px;border-radius:20px;box-shadow:0 1px 4px rgba(0,0,0,.3)}
.clipb.hi{background:#2e7d32} /* ≥85% — strong drop-in */
.clipb.mid{background:#c9861e} /* 65–85% — amber, verify */
.clipb.lo{background:#8a8178} /* <65% — grey, weak */
#grid.h-clip .clipb{display:none}
.editimg{position:absolute;bottom:8px;right:8px;font-size:11px;background:rgba(0,0,0,.55);color:#fff;border:none;border-radius:6px;padding:3px 7px;cursor:pointer}
.body{padding:10px 12px 12px}.vend{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.4px}
.title{font-size:13.5px;font-weight:600;margin:2px 0 4px;line-height:1.3}.meta{font-size:11.5px;color:#6b6255;line-height:1.5}
.scorebar{height:4px;background:#eee;border-radius:3px;margin-top:8px;overflow:hidden}.scorebar i{display:block;height:100%;background:var(--accent)}
.rowlink{display:inline-block;margin-top:8px;font-size:12px;color:var(--accent);text-decoration:none;font-weight:600}
.imgedit{margin-top:8px;display:none}.imgedit.show{display:flex;flex-direction:column;gap:6px}
.imgedit .urow{display:flex;gap:6px}
.imgedit input{font:inherit;font-size:11px;padding:4px 7px;border:1px solid var(--line);border-radius:6px;flex:1;min-width:0}
.thumbs{padding-top:2px}
.thumbs .thint{font-size:10.5px;color:var(--muted);margin-bottom:4px}
.thumbs .trow{display:flex;gap:6px;overflow-x:auto}
.thumbs .trow img{width:46px;height:46px;object-fit:cover;border-radius:6px;border:1px solid var(--line);cursor:pointer;flex:0 0 auto}
.thumbs .trow img:hover{outline:2px solid var(--accent)}
.imgurl .thumbs{margin-top:4px}
footer{color:var(--muted);font-size:12px;margin-top:28px;text-align:center}
/* sticky action bar */
.bar{position:fixed;left:0;right:0;bottom:0;background:#1c1a17;color:#fff;display:flex;align-items:center;gap:18px;padding:12px 32px;transform:translateY(110%);transition:transform .2s;z-index:20}
.bar.show{transform:none}
.bar b{font-size:15px}.bar .sp{margin-left:auto}
.bar button{font:inherit;font-weight:600;border:none;border-radius:8px;padding:9px 16px;cursor:pointer}
.bar .go{background:var(--accent);color:#fff}.bar .clr{background:transparent;color:#cfc7bd;border:1px solid #55504a}
/* modal */
.modal{position:fixed;inset:0;background:rgba(0,0,0,.5);display:none;align-items:center;justify-content:center;z-index:30;padding:20px}
.modal.show{display:flex}
.sheet{background:#fff;border-radius:14px;max-width:760px;width:100%;max-height:90vh;display:flex;flex-direction:column;overflow:hidden}
.sheet h3{margin:0;padding:16px 20px;border-bottom:1px solid var(--line);font-size:16px}
.sheet .in{padding:14px 20px;overflow:auto}
.frow{display:flex;gap:14px;flex-wrap:wrap;margin-bottom:12px}
.frow label{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.4px;display:flex;flex-direction:column;gap:4px;flex:1;min-width:180px}
.frow input{font:inherit;padding:7px 10px;border:1px solid var(--line);border-radius:8px}
.sheet textarea{width:100%;min-height:340px;font:13px/1.55 ui-monospace,Menlo,monospace;padding:12px;border:1px solid var(--line);border-radius:10px;resize:vertical}
.sheet .foot{display:flex;gap:10px;padding:14px 20px;border-top:1px solid var(--line);flex-wrap:wrap}
.sheet .foot button{font:inherit;font-weight:600;border:none;border-radius:8px;padding:9px 16px;cursor:pointer}
.sheet .foot .go{background:var(--accent);color:#fff}.sheet .foot .sec{background:#efe9e0;color:#1c1a17}
.sheet .foot .x{margin-left:auto;background:transparent;color:var(--muted);border:1px solid var(--line)}
</style></head><body>
<header><div class="htop"><h1>Substitute Finder</h1>
<input type="search" id="q" placeholder="Search title, SKU, vendor, color…">
<span class="sortwrap">Sort <select id="sort">
<option value="best">Best match</option>
<option value="color">Color closeness</option>
<option value="sku">SKU A→Z</option>
<option value="title">Title A→Z</option>
<option value="vendor">Vendor A→Z</option>
<option value="pricelo">Price ↑</option>
<option value="pricehi">Price ↓</option>
</select></span>
<span class="sub">ACTIVE / DRAFT only · archived never shown</span></div></header>
<div class="topwrap"><div class="orig" id="orig"></div><div id="catSim"></div></div>
<div class="wrap">
<aside id="rail">
<div class="railtools"><button id="expandAll">⤢ Expand all</button><button id="collapseAll">⤡ Collapse all</button></div>
<div class="active" id="activeRow"></div>
<details class="sec" open data-k="prio"><summary>Match priority</summary><div class="fbody">
<div class="prionote">Number by importance (1 = most). Reorder with ↑ ↓.</div>
<div id="prioList"></div>
<div class="hexrow">Match hex # <input id="hexIn" placeholder="auto" maxlength="7" spellcheck="false"><span class="hexsw" id="hexSw"></span><button class="hexclr" id="hexClr" title="use item's own color">✕</button></div>
<div class="clipstat" id="clipStat"></div>
<div class="dispctl"><label>Show <b id="vShow"></b></label><input type="range" id="show" min="10" max="50" step="5"></div>
<div class="dispctl"><label>Density <b id="vDen"></b></label><input type="range" id="den" min="3" max="12" step="1"></div>
<button class="reset" id="reset">Reset</button>
</div></details>
<details class="sec" data-k="fields"><summary>Card fields<span class="n" id="nFields"></span></summary><div class="fbody" id="fieldsRow"></div></details>
<details class="sec" open data-k="vendor"><summary>Vendor<span class="n" id="nVendor"></span></summary><div class="fbody" id="vendorRow"></div></details>
<details class="sec" data-k="material"><summary>Material<span class="n" id="nMaterial"></span></summary><div class="fbody" id="matRow"></div></details>
<details class="sec" data-k="style"><summary>Style<span class="n" id="nStyle"></span></summary><div class="fbody" id="styleRow"></div></details>
<details class="sec" data-k="color"><summary>Color<span class="n" id="nColor"></span></summary><div class="fbody" id="colorRow"></div></details>
<details class="sec" data-k="status"><summary>Status<span class="n" id="nStatus"></span></summary><div class="fbody" id="statusRow"></div></details>
</aside>
<main>
<div class="count" id="count"></div>
<div class="grid" id="grid"></div>
<footer>Ranked by your Match priority (Color · Style · Material). Tick 1+ cards → Draft letter. Cards link to the live product page.</footer>
</main>
</div>
<div class="bar" id="bar"><b id="barN">0 selected</b><span>substitutes chosen for the client letter</span>
<span class="sp"></span><button class="clr" id="barClr">Clear</button><button class="go" id="barGo">Draft letter →</button></div>
<div class="modal" id="modal"><div class="sheet">
<h3>Draft letter to client</h3>
<div class="in">
<div class="frow">
<label>Client name<input id="fClient" placeholder="e.g. Jane Doe"></label>
<label>Your name / signature<input id="fSender" value="Designer Wallcoverings"></label>
</div>
<textarea id="fBody"></textarea>
</div>
<div class="foot">
<button class="go" id="mCopy">Copy</button>
<button class="sec" id="mDl">Download .txt</button>
<button class="sec" id="mMail">Open in email</button>
<button class="x" id="mClose">Close</button>
</div>
</div></div>
<script>
let T={"sku": "", "title": "Milano Tessile - Midnight Weave Wallcovering | Phillipe Romano", "vendor": "Phillipe Romano", "status": "ARCHIVED", "color": "Off White", "hue": 0, "hex": "#f5f5f5", "material": "Textile / Linen", "style": "Traditional", "handle": "textile-wallcovering-midnight-weave-wallcovering-phillipe-romano-1", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/6_4ae25682-eee7-40f9-8b0d-f045d72b8aaf.jpg?v=1774894391", "tags": "{\"Architectural\",\"Burgundy\",\"color:Off White\",\"Commercial\",\"Dark Gray\",\"display_variant\",\"Light Gray\",\"Natural Wallcovering\",\"Navy Blue\",\"Needs-Image\",\"Non-Woven\",\"Paisley\",\"Textile Wallcovering\",\"Traditional\",\"TWIL Textile\",\"Victorian\",\"Wallcovering\",\"White\"}", "mfr": "MD5077T", "discontinued": true, "successor": {"sku": "DWJP-11901", "title": "Agate - Teal on White Paper Weave Repeat: 18\"(45.7cm) V x 17\"(43.2cm) H", "handle": "dwjp-11901-philip-jeffries-agate-teal-on-white-paper-weave-repeat-1845-7cm-v-x-1743-2cm-h-5934-b"}}, SUBS=[{"sku": "DWJP-11901", "vendor": "Phillip Jeffries", "title": "Agate - Teal on White Paper Weave Repeat: 18\"(45.7cm) V x 17\"(43.2cm) H", "status": "ACTIVE", "handle": "dwjp-11901-philip-jeffries-agate-teal-on-white-paper-weave-repeat-1845-7cm-v-x-1743-2cm-h-5934-b", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/PhillipJeffriesLogo_5d10de3e-e6e5-4d27-9bfb-66f13b4ab957.png?v=1722462409", "price": "", "material": "Textile / Linen", "style": "Traditional", "color": "Off White", "hue": 180, "hex": "#f5f5f5", "neutral": false}, {"sku": "DWJP-12198", "vendor": "Phillip Jeffries", "title": "Ellie's View - Aquatic on White Paperweave Repeat: 34\"(86.4cm) V x 34\"(86.4cm) H", "status": "ACTIVE", "handle": "dwjp-12198-philip-jeffries-ellies-view-aquatic-on-white-paperweave-repeat-3486-4cm-v-x-3486-4cm-h-7157", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/PhillipJeffriesLogo_95ebeae9-14a4-49af-afe1-2925c7612d53.png?v=1722461437", "price": "", "material": "Textile / Linen", "style": "Traditional", "color": "Off White", "hue": 190, "hex": "#f5f5f5", "neutral": false}, {"sku": "DWTA-65003-SAMPLE", "vendor": "Anna French", "title": "Barlow Linen - Flax Fabric | Anna French", "status": "ACTIVE", "handle": "dwta-65003designerwallcoverings-los-angeles", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/AT24576.jpg?v=1773962342", "price": "181.76", "material": "Textile / Linen", "style": "Traditional", "color": "Off White", "hue": 0, "hex": "#f5f5f5", "neutral": true}, {"sku": "DWJP-12200", "vendor": "Phillip Jeffries", "title": "Ellie's View - Navy on White Paperweave Repeat: 34\"(86.4cm) V x 34\"(86.4cm) H", "status": "ACTIVE", "handle": "dwjp-12200-philip-jeffries-ellies-view-navy-on-white-paperweave-repeat-3486-4cm-v-x-3486-4cm-h-7158", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/PhillipJeffriesLogo_06f7aa50-fda5-4048-8f15-14d1d3bc0540.png?v=1722461435", "price": "", "material": "Textile / Linen", "style": "Traditional", "color": "White", "hue": 220, "hex": "#f5f5f5", "neutral": false}, {"sku": "TXT-34598", "vendor": "Phillipe Romano", "title": "Ataga Bay Rayon Textile | Phillipe Romano", "status": "ACTIVE", "handle": "ataga-bay-rayon-textile-txt-34598", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/products/f7d12b8339e354643e2cbb5c9862a64f_37571276-347d-455e-a8f7-151e03a1eac3.jpg?v=1745456737", "price": "4.25", "material": "Textile / Linen", "style": "Traditional", "color": "Eggshell", "hue": 45, "hex": "#efead6", "neutral": true}, {"sku": "XXP-969342", "vendor": "Phillipe Romano", "title": "Geneva-Glowing Greige Durable Vinyl | Phillipe Romano", "status": "ACTIVE", "handle": "xxp-969342-designerwallcoverings-los-angeles", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-GM-06_8259d467-9104-40ed-8ebc-c0120f401cab.jpg?v=1733891133", "price": "", "material": "Textile / Linen", "style": "Traditional", "color": "Eggshell", "hue": 45, "hex": "#efead6", "neutral": true}, {"sku": "DWTA-65007-SAMPLE", "vendor": "Anna French", "title": "Barlow Linen - Light Blue Fabric | Anna French", "status": "ACTIVE", "handle": "dwta-65007designerwallcoverings-los-angeles", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/AT24578.jpg?v=1773962358", "price": "181.76", "material": "Textile / Linen", "style": "Traditional", "color": "Pearl", "hue": 210, "hex": "#e8e2d5", "neutral": false}, {"sku": "DWTA-65004-SAMPLE", "vendor": "Anna French", "title": "Barlow Linen - Linen Fabric | Anna French", "status": "ACTIVE", "handle": "dwta-65004designerwallcoverings-los-angeles", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/AT24577.jpg?v=1773962347", "price": "181.76", "material": "Textile / Linen", "style": "Traditional", "color": "Pearl", "hue": 45, "hex": "#e8e2d5", "neutral": true}, {"sku": "DWK-30124", "vendor": "Koroseal", "title": "Vive Alabaster - Off-White Commercial Wallcovering | Koroseal", "status": "ACTIVE", "handle": "dwk-30124-koroseal-stacy-garcia-sg14-02-vive-brownstone", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/VIVE_ALABASTER_SG14-02__KOROSEAL.jpg?v=1751952593", "price": "", "material": "Textile / Linen", "style": "Traditional", "color": "Cream", "hue": 345, "hex": "#f0e8d0", "neutral": false}, {"sku": "XOJ-33443", "vendor": "Phillipe Romano", "title": "Yuni Durable Vinyl", "status": "ACTIVE", "handle": "yuni-durable-vinyl-xoj-33443", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/products/0ca9e9e901edcdcef87846763e5e3d87_3e9e6f08-2d31-4622-a2cb-6a8a77847ecc.jpg?v=1572310354", "price": "4.25", "material": "Textile / Linen", "style": "Traditional", "color": "Bone", "hue": 45, "hex": "#e6dfcf", "neutral": true}, {"sku": "XTY-949529", "vendor": "Phillipe Romano", "title": "Legacy Linen Durable Vinyl | Phillipe Romano", "status": "ACTIVE", "handle": "xty-949529-designerwallcoverings-los-angeles", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/products/CM121-2441.jpg?v=1733888632", "price": "", "material": "Textile / Linen", "style": "Traditional", "color": "Bone", "hue": 45, "hex": "#e6dfcf", "neutral": true}, {"sku": "PRW-620355", "vendor": "Phillipe Romano", "title": "La Umbrelo Opaline Natural | Phillipe Romano", "status": "ACTIVE", "handle": "prw-620355-sample", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/MRO-01_6b1e46b8-6d66-4907-a36e-70ffc4421a15.jpg?v=1736207949", "price": "", "material": "Textile / Linen", "style": "Traditional", "color": "Bone", "hue": 42, "hex": "#d8cbb0", "neutral": true}, {"sku": "MTG-403654", "vendor": "Mind the Gap", "title": "The Undersea - White Wallcovering | Mind the Gap", "status": "ACTIVE", "handle": "the-undersea", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/products/MTG_403654_2_THE_UNDERSEA.jpg?v=1617224881", "price": "", "material": "Textile / Linen", "style": "Floral / Botanical", "color": "Teal", "hue": 180, "hex": "#f5f5f5", "neutral": false}, {"sku": "DWK-30070", "vendor": "Koroseal", "title": "Zendo Pepper - Off-White Fabric | Koroseal", "status": "ACTIVE", "handle": "dwk-30070-koroseal-stacy-garcia-sg29-08-zendo-brick", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/SG29-08_ZENDO_PEPPER.jpg?v=1751952724", "price": "", "material": "Textile / Linen", "style": "Geometric", "color": "Off White", "hue": 345, "hex": "#f5f5f5", "neutral": false}, {"sku": "EUR-70733", "vendor": "Osborne & Little", "title": "Rhodora rhododendron Floral Stripe | Osborne & Little Europe", "status": "ACTIVE", "handle": "eur-70733-designer-wallcoverings-los-angeles", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/w7022-01.webp?v=1738615579", "price": "", "material": "Textile / Linen", "style": "Floral / Botanical", "color": "Off White", "hue": 0, "hex": "#f5f5f5", "neutral": true}, {"sku": "DWK-30170", "vendor": "Koroseal", "title": "Market Street Lace - White Commercial Wallcovering | Koroseal", "status": "ACTIVE", "handle": "dwk-30170-koroseal-stacy-garcia-sg04-02-market-street-cashmere", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/MARKET-STREET_LACE_SG04-02__KOROSEAL.jpg?v=1751952499", "price": "", "material": "Textile / Linen", "style": "Textured / Solid", "color": "Rose", "hue": 345, "hex": "#f5f5f5", "neutral": false}, {"sku": "DWKK-119336", "vendor": "Clarke And Clarke", "title": "Nakuru - Mineral Turquoise By Clarke And Clarke | Clarke & Clarke Vintage |Botanical & Floral Texture Drapery Embroidery", "status": "ACTIVE", "handle": "dwkk-119336", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/F1547_05_CAC_74216266-c7ec-4cda-8a5a-1653ea87c27c.jpg?v=1725874330", "price": "", "material": "Textile / Linen", "style": "Floral / Botanical", "color": "Off White", "hue": 34, "hex": "#f5f5f5", "neutral": false}, {"sku": "DWKK-119371", "vendor": "Clarke And Clarke", "title": "Brigitte - Summer Multi By Clarke And Clarke | Clarke & Clarke Pavilion |Botanical & Floral Texture Drapery Embroidery", "status": "ACTIVE", "handle": "dwkk-119371", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/F1554_05_CAC_d97d5181-1ee8-4861-bdd3-f2012d4df6ee.jpg?v=1725874435", "price": "", "material": "Textile / Linen", "style": "Floral / Botanical", "color": "Off White", "hue": 34, "hex": "#f5f5f5", "neutral": false}, {"sku": "DWK-32420", "vendor": "Koroseal", "title": "Avro Lightest Grey - White Commercial Wallcovering | Koroseal", "status": "ACTIVE", "handle": "avro-lightest-grey-wallpaper-0204d106-republic-of-ii-by-iv-sample-architectural-wallcoverings", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/Avro_Lightest-Grey_0204D106__REPUBLIC_OF_II_BY_IV_FOR_KOROSEAL.jpg?v=1751967455", "price": "", "material": "Textile / Linen", "style": "Geometric", "color": "Rose", "hue": 345, "hex": "#f5f5f5", "neutral": false}, {"sku": "DWJP-14168", "vendor": "Phillip Jeffries", "title": "Mystic Weave-Silvered Mist | Phillip Jeffries", "status": "ACTIVE", "handle": "dwjp-14168-philip-jeffries-mystic-weave-silvered-mist-10051", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/PhillipJeffriesLogo_9c00a9ad-4b26-458e-86f6-62923d63eb9e.png?v=1722456067", "price": "", "material": "Textile / Linen", "style": "Traditional", "color": "Silver", "hue": 0, "hex": "#b8b8b8", "neutral": true}, {"sku": "DWTA-65000-SAMPLE", "vendor": "Anna French", "title": "Barlow Linen - Beige Fabric | Anna French", "status": "ACTIVE", "handle": "dwta-65000designerwallcoverings-los-angeles", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/AT24575.jpg?v=1773962335", "price": "181.76", "material": "Textile / Linen", "style": "Traditional", "color": "Off White", "hue": 40, "hex": "#c9b79c", "neutral": true}, {"sku": "DWKK-119162", "vendor": "Clarke And Clarke", "title": "Falena - Mineral/Rose Gold Teal By Clarke And Clarke | Clarke & Clarke Pomarium | Animal/Insects Drapery Embroidery", "status": "ACTIVE", "handle": "dwkk-119162", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/F1507_03_CAC_ac430cf1-1266-4bf4-9721-bf7792f537a0.jpg?v=1725873365", "price": "", "material": "Textile / Linen", "style": "Contemporary", "color": "Ivory", "hue": 45, "hex": "#efe9d8", "neutral": false}, {"sku": "DWKK-119209", "vendor": "Clarke And Clarke", "title": "Fruta - Summer Cotton Multi By Clarke And Clarke | Amazonia By Studio G For C&C |Botanical & Floral Multipurpose Print", "status": "ACTIVE", "handle": "dwkk-119209", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/F1516_01_CAC_8442bc4d-9dbf-4090-9655-ffbc5f648ff9.jpg?v=1725873625", "price": "", "material": "Textile / Linen", "style": "Floral / Botanical", "color": "Ivory", "hue": 34, "hex": "#efe9d8", "neutral": false}, {"sku": "DWKK-119215", "vendor": "Clarke And Clarke", "title": "Paradise - Russet Multi By Clarke And Clarke | Amazonia By Studio G For C&C |Botanical & Floral Multipurpose Print", "status": "ACTIVE", "handle": "dwkk-119215", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/F1519_03_CAC_9cffa605-f57c-4d1a-a1ff-7cc58fa08e01.jpg?v=1725873652", "price": "", "material": "Textile / Linen", "style": "Floral / Botanical", "color": "Ivory", "hue": 34, "hex": "#efe9d8", "neutral": false}, {"sku": "DWKK-119334", "vendor": "Clarke And Clarke", "title": "Nakuru - Ivory/Linen Beige By Clarke And Clarke | Clarke & Clarke Vintage |Botanical & Floral Texture Drapery Embroidery", "status": "ACTIVE", "handle": "dwkk-119334", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/F1547_03_CAC_4ea7fbdc-7b81-40d3-840f-e9b53d4674b9.jpg?v=1725874321", "price": "", "material": "Textile / Linen", "style": "Floral / Botanical", "color": "Alabaster", "hue": 34, "hex": "#efe9d8", "neutral": false}, {"sku": "DWKK-119367", "vendor": "Clarke And Clarke", "title": "Brigitte - Apple/Mineral Turquoise By Clarke And Clarke | Clarke & Clarke Pavilion |Botanical & Floral Texture Drapery Embroidery", "status": "ACTIVE", "handle": "dwkk-119367", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/F1554_01_CAC_a897a646-9572-41cc-9307-12c398d732f1.jpg?v=1725874411", "price": "", "material": "Textile / Linen", "style": "Floral / Botanical", "color": "Ivory", "hue": 34, "hex": "#efe9d8", "neutral": false}, {"sku": "DWJP-11892", "vendor": "Phillip Jeffries", "title": "Studs & Stripes - Vertical Brass on Oxford Linen Repeat: 1.25\" (3.2cm)V x 11.33\" (28.8cm)H", "status": "ACTIVE", "handle": "dwjp-11892-philip-jeffries-studs-stripes-vertical-brass-on-oxford-linen-repeat-1-25-3-2cmv-x-11-33-28-8cmh-5875-v", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/PhillipJeffriesLogo_aaf6725b-0934-44ab-948d-0f195990303c.png?v=1722462433", "price": "", "material": "Textile / Linen", "style": "Geometric", "color": "Eggshell", "hue": 45, "hex": "#efead6", "neutral": true}, {"sku": "DIG-17115 - Sample", "vendor": "Designer Wallcoverings", "title": "| DW Bespoke Studios", "status": "ACTIVE", "handle": "dig_17115_vintage_wallpaper_designer_wallcoverings", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/products/Vintage_Wallpaper_Reproduction_1917_Dig-17115_5cb2bcff-b024-4c7b-a051-f1a41486bc97.jpg?v=1756877174", "price": "", "material": "Textile / Linen", "style": "Floral / Botanical", "color": "Eggshell", "hue": 45, "hex": "#efead6", "neutral": true}, {"sku": "EUR-70593", "vendor": "Osborne & Little", "title": "Kayin | Osborne & Little Europe", "status": "ACTIVE", "handle": "eur-70593-designer-wallcoverings-los-angeles", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/4292.webp?v=1738615038", "price": "", "material": "Textile / Linen", "style": "Floral / Botanical", "color": "Eggshell", "hue": 45, "hex": "#efead6", "neutral": true}, {"sku": "EUR-70643", "vendor": "Osborne & Little", "title": "Habanera Cuban | Osborne & Little Europe", "status": "ACTIVE", "handle": "eur-70643-designer-wallcoverings-los-angeles", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/4252.webp?v=1738615208", "price": "", "material": "Textile / Linen", "style": "Floral / Botanical", "color": "Eggshell", "hue": 45, "hex": "#efead6", "neutral": true}, {"sku": "PRT-74202", "vendor": "Phillipe Romano", "title": "Nisce | Phillipe Romano", "status": "ACTIVE", "handle": "nisce-prt-74202", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/products/fa04f3747daff8aebb4c10193cf6a25c.jpg?v=1745455907", "price": "4.25", "material": "Textile / Linen", "style": "Textured / Solid", "color": "Eggshell", "hue": 45, "hex": "#efead6", "neutral": true}, {"sku": "DIG-2400090-SAMPLE", "vendor": "DW Bespoke Studio", "title": "Opal Light Mural Contemporary | Architectural Wallcoverings", "status": "ACTIVE", "handle": "opal-light-adrian", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/m2_8.jpg?v=1756707064", "price": "", "material": "Textile / Linen", "style": "Contemporary", "color": "Eggshell", "hue": 45, "hex": "#efead6", "neutral": true}, {"sku": "XTY-949481", "vendor": "Phillipe Romano", "title": "Sir Humphrey Striated Stripe Durable Vinyl | Phillipe Romano", "status": "ACTIVE", "handle": "xty-949481-designerwallcoverings-los-angeles", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/products/CM124-2499.jpg?v=1733888751", "price": "", "material": "Textile / Linen", "style": "Geometric", "color": "Eggshell", "hue": 45, "hex": "#efead6", "neutral": true}, {"sku": "XRF-34026", "vendor": "Phillipe Romano", "title": "Yoichi Durable Vinyl", "status": "ACTIVE", "handle": "yoichi-durable-vinyl-xrf-34026", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/products/3c1901160ff936b3957b2ec7d24f9026_4d4a4df7-a0a3-4dc2-9561-0a12712f5e6c.jpg?v=1572310393", "price": "4.25", "material": "Textile / Linen", "style": "Textured / Solid", "color": "Eggshell", "hue": 45, "hex": "#efead6", "neutral": true}, {"sku": "XRH-34073", "vendor": "Phillipe Romano", "title": "Rausu Durable Vinyl", "status": "ACTIVE", "handle": "rausu-durable-vinyl-xrh-34073", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/products/d5ce82787793073d9093c5ab377f913d_5367a055-6996-4a9f-be57-20fdf7f0c507.jpg?v=1572310395", "price": "4.25", "material": "Textile / Linen", "style": "Geometric", "color": "Eggshell", "hue": 45, "hex": "#efead6", "neutral": true}, {"sku": "DWDC-330647", "vendor": "Coordonn\u00e9", "title": "Raphia Glacier B00052 | Coordone Europe", "status": "ACTIVE", "handle": "dwdc-330647", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/B00052_01_2_2b37a09f-a9fd-413a-990a-dc28c290837a.jpg?v=1739302610", "price": "", "material": "Textile / Linen", "style": "Textured / Solid", "color": "Eggshell", "hue": 45, "hex": "#efead6", "neutral": true}, {"sku": "EUR-70828", "vendor": "Osborne & Little", "title": "Vernazza | Osborne & Little Europe", "status": "ACTIVE", "handle": "eur-70828-designer-wallcoverings-los-angeles", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/w7216-01_1_cff1a43a-b6e7-4de1-bd85-b2826c926ee3.webp?v=1738615980", "price": "", "material": "Textile / Linen", "style": "Geometric", "color": "Eggshell", "hue": 45, "hex": "#efead6", "neutral": true}, {"sku": "XWT-53480", "vendor": "Hollywood Wallcoverings", "title": "Hallandale Rice Paper Effect Durable | Hollywood Wallcoverings", "status": "ACTIVE", "handle": "hallandale-rice-paper-effect-durable-walls-xwt-53480", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/xwt-53480-sample-hallandale-rice-paper-effect-durable-hollywood-wallcoverings.jpg?v=1775715931", "price": "4.25", "material": "Textile / Linen", "style": "Textured / Solid", "color": "Eggshell", "hue": 45, "hex": "#efead6", "neutral": true}, {"sku": "EUR-70859", "vendor": "Osborne & Little", "title": "Deya Meadow Butterfly Panel | Osborne & Little Europe", "status": "ACTIVE", "handle": "eur-70859-designer-wallcoverings-los-angeles", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/w7265-02.webp?v=1738616131", "price": "", "material": "Textile / Linen", "style": "Floral / Botanical", "color": "Eggshell", "hue": 45, "hex": "#efead6", "neutral": true}, {"sku": "EUR-70906", "vendor": "Osborne & Little", "title": "Craquelure Vinyl | Osborne & Little Europe", "status": "ACTIVE", "handle": "eur-70906-designer-wallcoverings-los-angeles", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/w7350-05_2.webp?v=1738616353", "price": "", "material": "Textile / Linen", "style": "Geometric", "color": "Eggshell", "hue": 45, "hex": "#efead6", "neutral": true}, {"sku": "DWKK-119197", "vendor": "Clarke And Clarke", "title": "Acadia - Linen Grey By Clarke And Clarke | Amazonia By Studio G For C&C |Botanical & Floral Multipurpose Print", "status": "ACTIVE", "handle": "dwkk-119197", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/F1513_02_CAC_5f566282-2375-4b56-affe-e6ab487f4c65.jpg?v=1725873568", "price": "", "material": "Textile / Linen", "style": "Floral / Botanical", "color": "Pearl", "hue": 34, "hex": "#e8e2d5", "neutral": false}, {"sku": "DWKK-119213", "vendor": "Clarke And Clarke", "title": "Paradise - Autumn Multi By Clarke And Clarke | Amazonia By Studio G For C&C |Botanical & Floral Multipurpose Print", "status": "ACTIVE", "handle": "dwkk-119213", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/F1519_01_CAC_adb43c8f-6423-413d-bcde-1d0df545b781.jpg?v=1725873643", "price": "", "material": "Textile / Linen", "style": "Floral / Botanical", "color": "Cream", "hue": 34, "hex": "#f0e8d0", "neutral": false}, {"sku": "DWTT-72384-SAMPLE", "vendor": "Thibaut", "title": "Denmark (Printed Fabric) Red And Cream Fabric | Thibaut", "status": "DRAFT", "handle": "dwtt-72384-designer-wallcoverings-los-angeles", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/room-living_room-f96032.jpg?v=1775144476", "price": "3.50", "material": "Textile / Linen", "style": "Contemporary", "color": "Cream", "hue": 45, "hex": "#f0e8d0", "neutral": true}, {"sku": "DWK-30117", "vendor": "Koroseal", "title": "Vim Alabaster - Off-White Commercial Wallcovering | Koroseal", "status": "ACTIVE", "handle": "dwk-30117-koroseal-stacy-garcia-sg15-02-vim-strand", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/VIM_ALABASTER_SG15-02__KOROSEAL.jpg?v=1751952607", "price": "", "material": "Textile / Linen", "style": "Textured / Solid", "color": "Cream", "hue": 345, "hex": "#f0e8d0", "neutral": false}, {"sku": "DWK-30118", "vendor": "Koroseal", "title": "Vim Strand - Beige Commercial Wallcovering | Koroseal", "status": "ACTIVE", "handle": "dwk-30118-koroseal-stacy-garcia-sg15-03-vim-concrete", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/VIM_STRAND_SG15-03__KOROSEAL.jpg?v=1751952605", "price": "", "material": "Textile / Linen", "style": "Textured / Solid", "color": "Cream", "hue": 345, "hex": "#f0e8d0", "neutral": false}, {"sku": "DWKK-119330", "vendor": "Clarke And Clarke", "title": "Habitat - Lagoon Turquoise By Clarke And Clarke | Clarke & Clarke Vintage |Animal/Insects Botanical & Floral Multipurpose Print", "status": "ACTIVE", "handle": "dwkk-119330", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/F1546_02_CAC_02ae4c23-9fcb-4237-9901-ed8c84aeabaf.jpg?v=1725874304", "price": "", "material": "Textile / Linen", "style": "Floral / Botanical", "color": "Cream", "hue": 34, "hex": "#f0e8d0", "neutral": false}, {"sku": "DWKK-119168", "vendor": "Clarke And Clarke", "title": "Sorento - Multi/Linen Multi By Clarke And Clarke | Clarke & Clarke Pomarium |Botanical & Floral Multipurpose", "status": "ACTIVE", "handle": "dwkk-119168", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/F1509_04_CAC_3df566f0-3ecd-4463-ad24-8b927d632e48.jpg?v=1725873403", "price": "", "material": "Textile / Linen", "style": "Floral / Botanical", "color": "Bone", "hue": 34, "hex": "#e6dfcf", "neutral": false}, {"sku": "XXP-969281", "vendor": "Phillipe Romano", "title": "Jamestown-Rocky Mountain Durable Vinyl | Phillipe Romano", "status": "ACTIVE", "handle": "xxp-969281-designerwallcoverings-los-angeles", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-FD-02_c02f6995-e8ca-4112-b27f-d6db9d47c92c.jpg?v=1733891274", "price": "", "material": "Textile / Linen", "style": "Textured / Solid", "color": "Bone", "hue": 45, "hex": "#e6dfcf", "neutral": true}, {"sku": "XRF-34029", "vendor": "Phillipe Romano", "title": "Yoichi Durable Vinyl", "status": "ACTIVE", "handle": "yoichi-durable-vinyl-xrf-34029", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/products/ead53a0a309dd900d20dd5c6e1b51e18_bf107a4d-de19-450a-9f70-6983f36d1db0.jpg?v=1572310393", "price": "4.25", "material": "Textile / Linen", "style": "Textured / Solid", "color": "Bone", "hue": 45, "hex": "#e6dfcf", "neutral": true}, {"sku": "XRP-34142", "vendor": "Phillipe Romano", "title": "Matika", "status": "ACTIVE", "handle": "matika-xrp-34142", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/products/c41cf825719dc118d507b4758d4cebba_e2f422ce-e7a9-4a55-8192-8c5e4662b105.jpg?v=1572310410", "price": "4.25", "material": "Textile / Linen", "style": "Geometric", "color": "Bone", "hue": 45, "hex": "#e6dfcf", "neutral": true}, {"sku": "EUR-70830", "vendor": "Osborne & Little", "title": "Vernazza | Osborne & Little Europe", "status": "ACTIVE", "handle": "eur-70830-designer-wallcoverings-los-angeles", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/w7216-03_1_714d32d7-2612-49f7-bb6f-a958bdee2b70.webp?v=1738615989", "price": "", "material": "Textile / Linen", "style": "Geometric", "color": "Bone", "hue": 45, "hex": "#e6dfcf", "neutral": true}, {"sku": "XXP-969380", "vendor": "Phillipe Romano", "title": "Aloha Grass-Mahongany Durable Vinyl | Phillipe Romano", "status": "ACTIVE", "handle": "xxp-969380-designerwallcoverings-los-angeles", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-GR-14_bbd71735-a647-485b-95b8-1d5254920878.jpg?v=1733891056", "price": "", "material": "Textile / Linen", "style": "Geometric", "color": "Bone", "hue": 45, "hex": "#e6dfcf", "neutral": true}, {"sku": "XXP-969509", "vendor": "Phillipe Romano", "title": "Dover-Malted Milk Durable Vinyl | Phillipe Romano", "status": "ACTIVE", "handle": "xxp-969509-designerwallcoverings-los-angeles", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-LS-23_e919079d-df24-4dc1-8c77-1b777fa2324c.jpg?v=1733890736", "price": "", "material": "Textile / Linen", "style": "Geometric", "color": "Bone", "hue": 45, "hex": "#e6dfcf", "neutral": true}, {"sku": "PRW-920468", "vendor": "Phillipe Romano", "title": "Pilsen West Natural | Phillipe Romano", "status": "ACTIVE", "handle": "prw-920468-int-05", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/prw-920468-int-05.jpg?v=1781716038", "price": "", "material": "Textile / Linen", "style": "Floral / Botanical", "color": "Taupe", "hue": 42, "hex": "#d8cbb0", "neutral": true}, {"sku": "PRW-920885", "vendor": "Phillipe Romano", "title": "Willoughby Natural | Phillipe Romano", "status": "ACTIVE", "handle": "prw-920885-sum-223", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/prw-920885-sum-223.jpg?v=1781716783", "price": "", "material": "Textile / Linen", "style": "Geometric", "color": "Gunmetal", "hue": 42, "hex": "#d8cbb0", "neutral": true}, {"sku": "BELL-10330-SAMPLE", "vendor": "Phillipe Romano", "title": "Bellissima Natural Paper Weave with Fine Embroidery | Phillipe Romano", "status": "ACTIVE", "handle": "bellissima-natural-paper-weave-wallcovering-with-fine-embroidery-bell-10330", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/products/EWP-1068_809170bd-f6f5-4c63-816b-7ac14116681a.jpg?v=1745455314", "price": "", "material": "Textile / Linen", "style": "Geometric", "color": "Bone", "hue": 42, "hex": "#d8cbb0", "neutral": true}, {"sku": "DWKK-119234", "vendor": "Clarke And Clarke", "title": "Scintilla - Natural Beige By Clarke And Clarke | Clarke & Clarke Fusion | Modern Upholstery Velvet", "status": "ACTIVE", "handle": "dwkk-119234", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/F1525_04_CAC_5b3adbef-81e1-4e53-a4bc-1b5c0540273f.jpg?v=1725873749", "price": "", "material": "Textile / Linen", "style": "Contemporary", "color": "Beige", "hue": 42, "hex": "#d8cbb0", "neutral": true}, {"sku": "BELL-10530-SAMPLE", "vendor": "Phillipe Romano", "title": "Bellissima Natural Paper Weave with Fine Embroidery | Phillipe Romano", "status": "ACTIVE", "handle": "bellissima-natural-paper-weave-wallcovering-with-fine-embroidery-bell-10530", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/products/EWP-1113_ef448709-a243-45dc-89d4-bf14db801d61.jpg?v=1745455265", "price": "", "material": "Textile / Linen", "style": "Floral / Botanical", "color": "Light Gray", "hue": 42, "hex": "#d8cbb0", "neutral": true}, {"sku": "BELL-10550-SAMPLE", "vendor": "Phillipe Romano", "title": "Bellissima Natural Paper Weave with Fine Block Print | Phillipe Romano", "status": "ACTIVE", "handle": "bellissima-natural-paper-weave-wallcovering-with-fine-block-print-bell-10550", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/products/EWP-1116_cfad9ce2-f6ae-40ba-9750-81d860698c09.jpg?v=1745455260", "price": "", "material": "Textile / Linen", "style": "Geometric", "color": "Linen", "hue": 42, "hex": "#d8cbb0", "neutral": true}, {"sku": "BELL-10600-SAMPLE", "vendor": "Phillipe Romano", "title": "Bellissima Natural Paper Weave with Fine Embroidery | Phillipe Romano", "status": "ACTIVE", "handle": "bellissima-natural-paper-weave-wallcovering-with-fine-embroidery-bell-10600", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/RaisedEmbroidery-Chambray.jpg?v=1745455254", "price": "", "material": "Textile / Linen", "style": "Floral / Botanical", "color": "Ash", "hue": 42, "hex": "#d8cbb0", "neutral": true}, {"sku": "BELL-10510-SAMPLE", "vendor": "Phillipe Romano", "title": "Bellissima Natural Cotton with Fine Block Print | Phillipe Romano", "status": "ACTIVE", "handle": "bellissima-natural-cotton-wallcovering-with-fine-block-print-bell-10510", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/products/EWP-1110_15ddd439-ceb2-49fa-a80e-d0442b04b93a.jpg?v=1745455275", "price": "", "material": "Textile / Linen", "style": "Geometric", "color": "Pearl", "hue": 42, "hex": "#d8cbb0", "neutral": true}, {"sku": "PRW-620225", "vendor": "Phillipe Romano", "title": "La Arturo Hematite Natural | Phillipe Romano", "status": "ACTIVE", "handle": "prw-620225-sample", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/GEM-06_15f2b2f4-86a9-4536-b96d-893a10383e95.jpg?v=1736207453", "price": "", "material": "Textile / Linen", "style": "Geometric", "color": "Smoke", "hue": 42, "hex": "#d8cbb0", "neutral": true}, {"sku": "PRW-620351", "vendor": "Phillipe Romano", "title": "La Vitoro Camellia Natural | Phillipe Romano", "status": "ACTIVE", "handle": "prw-620351-sample", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/MNF-02_597c0da9-c3c8-4e9a-85e6-d83f8f35d729.jpg?v=1736207934", "price": "", "material": "Textile / Linen", "style": "Floral / Botanical", "color": "Brass", "hue": 36, "hex": "#d8cbb0", "neutral": false}, {"sku": "DWKK-119732", "vendor": "Clarke And Clarke", "title": "Carousel - Natural Grey By Clarke And Clarke | Formations By Studio G For C&C | Stripes Multipurpose Print", "status": "ACTIVE", "handle": "dwkk-119732", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/F1636_03_CAC_7bf47123-5bdb-415c-92c3-95addaa558e6.jpg?v=1725876115", "price": "", "material": "Textile / Linen", "style": "Geometric", "color": "Bone", "hue": 42, "hex": "#d8cbb0", "neutral": true}, {"sku": "DWKK-119374", "vendor": "Clarke And Clarke", "title": "Monique - Natural/Pewter Grey By Clarke And Clarke | Clarke & Clarke Pavilion |Botanical & Floral Texture Drapery Embroidery", "status": "ACTIVE", "handle": "dwkk-119374", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/F1555_03_CAC_04fbc42b-1b98-429a-8ecb-719190fa5980.jpg?v=1725874450", "price": "", "material": "Textile / Linen", "style": "Floral / Botanical", "color": "Ivory", "hue": 34, "hex": "#d8cbb0", "neutral": false}, {"sku": "LIN-33941", "vendor": "Phillipe Romano", "title": "Shibecha Poly Linen | Phillipe Romano", "status": "ACTIVE", "handle": "shibecha-poly-linen-walls-lin-33941", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/products/410c17f79b93558aaa4329dd7cbd1602_ef6043f0-c267-4428-993b-8ba957b62b36.jpg?v=1745457218", "price": "4.25", "material": "Textile / Linen", "style": "Textured / Solid", "color": "Ecru", "hue": 45, "hex": "#d6cbb0", "neutral": true}, {"sku": "XXP-969483", "vendor": "Phillipe Romano", "title": "Concord-Warm Mink Durable Vinyl | Phillipe Romano", "status": "ACTIVE", "handle": "xxp-969483-designerwallcoverings-los-angeles", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/products/T2-LL-08_cd730321-cff5-411d-8c49-a9d4afd801a2.jpg?v=1733890800", "price": "", "material": "Textile / Linen", "style": "Geometric", "color": "Ecru", "hue": 45, "hex": "#d6cbb0", "neutral": true}, {"sku": "DWKK-119149", "vendor": "Clarke And Clarke", "title": "Windsor - Chambray Light Blue By Clarke And Clarke | Clarke & Clarke Edgeworth | Plaid / Check Upholstery", "status": "ACTIVE", "handle": "dwkk-119149", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/F1505_02_CAC_6d45d553-e02e-4ff2-91a0-192403f83b92.jpg?v=1725873289", "price": "", "material": "Textile / Linen", "style": "Contemporary", "color": "Powder Blue", "hue": 200, "hex": "#a9c7e0", "neutral": false}, {"sku": "DWK-30076", "vendor": "Koroseal", "title": "Inwood Sky - Light Blue Commercial Wallcovering | Koroseal", "status": "ACTIVE", "handle": "dwk-30076-koroseal-stacy-garcia-sg23-04-inwood-canyon", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/INWOOD_SKY_SG23-04__24x24-1.jpg?v=1751952711", "price": "", "material": "Textile / Linen", "style": "Textured / Solid", "color": "Powder Blue", "hue": 200, "hex": "#a9c7e0", "neutral": false}, {"sku": "DWK-30460", "vendor": "Koroseal", "title": "Linsey Pvc Free Type Ii Linsey Pvc-free Type Ii Color 11 | Architectural Wallcoverings", "status": "ACTIVE", "handle": "dwk-30460-koroseal-koroseal-type-2-vinyl-se81-11-linsey-pvc-free-type-ii-woolsey", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/LINSEY_SKY_SE21-11.jpg?v=1751951873", "price": "", "material": "Textile / Linen", "style": "Textured / Solid", "color": "Powder Blue", "hue": 200, "hex": "#a9c7e0", "neutral": false}, {"sku": "DWKK-119148", "vendor": "Clarke And Clarke", "title": "Windsor - Blush Pink By Clarke And Clarke | Clarke & Clarke Edgeworth | Plaid / Check Upholstery", "status": "ACTIVE", "handle": "dwkk-119148", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/F1505_01_CAC_b7aa0c91-1fba-4247-8ff4-efec08ade38e.jpg?v=1725873284", "price": "", "material": "Textile / Linen", "style": "Contemporary", "color": "Linen", "hue": 350, "hex": "#e0b4b4", "neutral": false}, {"sku": "DWKK-119160", "vendor": "Clarke And Clarke", "title": "Falena - Blush/Gold Pink By Clarke And Clarke | Clarke & Clarke Pomarium | Animal/Insects Drapery Embroidery", "status": "ACTIVE", "handle": "dwkk-119160", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/F1507_01_CAC_28487c7f-4579-43eb-84a8-42e648d2a0b7.jpg?v=1725873354", "price": "", "material": "Textile / Linen", "style": "Contemporary", "color": "Ivory", "hue": 350, "hex": "#e0b4b4", "neutral": false}]; const BASE="https://www.designerwallcoverings.com/products/";
const D={prio:['visual','color','style','material'],den:5,show:10,sort:'best'}; // prio[0]=most important
const PRIONAMES={visual:['Visual match','CLIP image look-alike'],color:['Color / Hue','perceptual color + tone'],style:['Style','pattern family'],material:['Material','faux-leather, grasscloth…']};
const S=Object.assign({},D, JSON.parse(localStorage.getItem('subfinder')||'{}'));
const IMG=JSON.parse(localStorage.getItem('subfinder_img')||'{}'); // sku -> image override url
const PURL=JSON.parse(localStorage.getItem('subfinder_purl')||'{}'); // sku -> product page override url
const picked=new Set();
const el=id=>document.getElementById(id);
function imgOf(sku,fallback){return IMG[sku]||fallback||'';}
function setImg(sku,url){if(url){IMG[sku]=url;}else{delete IMG[sku];}localStorage.setItem('subfinder_img',JSON.stringify(IMG));}
function purlOf(sku,fallback){return PURL[sku]||fallback||'';}
function setPurl(sku,url){if(url){PURL[sku]=url;}else{delete PURL[sku];}localStorage.setItem('subfinder_purl',JSON.stringify(PURL));}
function prodUrl(c){return PURL[c.sku]||(BASE+c.handle);}
function openUrl(u){if(u)window.open(u,'_blank','noopener');}
function dropUrlFrom(dt){
// 1) canonical dragged resource URL — the most reliable signal for the ACTUAL image dropped
const uris=(dt.getData('text/uri-list')||'').split(/\r?\n/).map(s=>s.trim()).filter(s=>s&&!s.startsWith('#'));
if(uris.length)return uris[0];
// 2) html fragment — a drag can carry multiple <img>; take the last real one, srcset-largest, skip data:
const html=dt.getData('text/html')||'';
if(html){let best='';const re=/<img\b[^>]*?(?:srcset=["']([^"']+)["'][^>]*?)?src=["']([^"']+)["']/gi;let m;
while((m=re.exec(html))){let u=m[2];if(m[1]){const p=m[1].split(',').map(s=>s.trim().split(/\s+/));if(p.length)u=p[p.length-1][0];}if(u&&!u.startsWith('data:'))best=u;}
if(best)return best;}
// 3) plain-text url
const p=(dt.getData('text/plain')||'').trim(); if(/^https?:\/\//.test(p))return p;
return '';
}
function syncImgField(sku,val){
if(sku===T.sku){const f=document.getElementById('origUrl');if(f)f.value=val;return;}
const card=document.querySelector(`.card[data-sku="${sku}"]`); if(card){const f=card.querySelector('.iurl');if(f)f.value=val;}
}
// replace a card/orig image by drop: local file (data-URL) OR image dragged from a web page (url)
// setting the ORIGINAL image (drop/thumbnail) keeps it as the target image and re-runs CLIP —
// it must NOT re-resolve the target (that would clobber the just-set image with the catalog image)
function onOrigImageChanged(){CLIP_KEY='';if(CLIP_READY)runClip();}
function applyDrop(dt,sku,imgEl){
const after=()=>{if(sku===T.sku)onOrigImageChanged();};
if(dt.files&&dt.files.length&&dt.files[0].type.startsWith('image/')){
const r=new FileReader();r.onload=()=>{setImg(sku,r.result);imgEl.src=r.result;syncImgField(sku,r.result);after();};r.readAsDataURL(dt.files[0]);return true;
}
const url=dropUrlFrom(dt);
if(url){setImg(sku,url);imgEl.src=url;syncImgField(sku,url);after();return true;}
return false;
}
function wireDrop(zone,imgEl,sku){
zone.addEventListener('dragover',e=>{e.preventDefault();zone.classList.add('drop');});
zone.addEventListener('dragleave',e=>{if(e.target===zone)zone.classList.remove('drop');});
zone.addEventListener('drop',e=>{e.preventDefault();zone.classList.remove('drop');applyDrop(e.dataTransfer,sku,imgEl);});
}
// headless extraction sidecar (only when viewer is served, not file://)
let EXTRACT_READY=false;
function probeExtract(){return fetch('/api/extract?ping=1').then(r=>r.ok?r.json():null).then(j=>{EXTRACT_READY=!!(j&&j.ok);return EXTRACT_READY;}).catch(()=>false);}
// ---- CLIP visual re-rank ----
let CLIP_READY=false, CLIP_BUSY=false, CLIP_KEY='';
function probeClip(){return fetch('/api/clip?ping=1').then(r=>r.ok?r.json():null).then(j=>{CLIP_READY=!!(j&&j.ok);return CLIP_READY;}).catch(()=>false);}
function clipStatus(t){const e=el('clipStat');if(e)e.textContent=t;}
function runClip(){
if(!CLIP_READY||CLIP_BUSY)return;
const tImg=(el('origImg')&&el('origImg').src)||imgOf(T.sku,T.img); if(!tImg)return;
const key=tImg+'|'+SUBS.length; if(key===CLIP_KEY)return; // already ranked this target+pool
CLIP_BUSY=true; clipStatus('◐ computing visual match…');
const payload={target:tImg,cands:SUBS.map(c=>({sku:c.sku,img:imgOf(c.sku,c.img)}))};
fetch('/api/clip',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)})
.then(r=>r.json()).then(j=>{CLIP_BUSY=false;
if(j&&j.ok&&j.ranked){const m={};j.ranked.forEach(x=>m[x.sku]=x.sim);SUBS.forEach(c=>c.clip=m[c.sku]||0);CLIP_KEY=key;clipStatus('✓ visual match on');draw();}
else clipStatus('visual match unavailable');
}).catch(()=>{CLIP_BUSY=false;clipStatus('visual match failed');});
}
function fillThumbs(container,sku,imgEl,images){
if(!images.length){container.innerHTML='';return;}
container.innerHTML='<div class="thint">'+images.length+' product image'+(images.length>1?'s':'')+' — click one to use</div>'
+'<div class="trow">'+images.map(u=>`<img src="${u}" alt="">`).join('')+'</div>';
container.querySelectorAll('.trow img').forEach(t=>t.addEventListener('click',()=>{const u=t.getAttribute('src');setImg(sku,u);imgEl.src=u;syncImgField(sku,u);if(sku===T.sku)onOrigImageChanged();}));
}
// open the product page headless, pull images, replace the loaded image with the first + show the rest
const COLORWORDS=['burnt orange','powder blue','hunter green','terracotta','rust','clay','copper','cedar','pumpkin','spice','amber','bronze','caramel','saddle','tobacco','cognac','chestnut','walnut','espresso','mocha','coffee','khaki','wheat','camel','tan','garnet','burgundy','wine','crimson','ruby','orange','gold','honey','mustard','ochre','brown','yellow','lime','olive','sage','juniper','emerald','forest','green','teal','aqua','indigo','navy','steel','ocean','blue','purple','violet','lavender','plum','aubergine','mauve','pink','rose','blush','coral','salmon','gray','grey','slate','charcoal','pewter','silver','taupe','mushroom','bone','ivory','cream','eggshell','beige','sand','pearl','ecru','oatmeal','stone','natural','black','white','onyx'];
function colorHint(title,color){const t=(title||'').toLowerCase();for(const w of COLORWORDS)if(t.includes(w))return w;return (color||'').replace(/^color:/,'').trim();}
function extractInto(btn,prodUrlStr,sku,imgEl,thumbsEl,isOrig,color){
if(!prodUrlStr){return;}
const label=btn.textContent;btn.disabled=true;btn.textContent='…';
fetch('/api/extract?url='+encodeURIComponent(prodUrlStr)+(color?'&color='+encodeURIComponent(color):'')).then(r=>r.json()).then(j=>{
btn.disabled=false;btn.textContent=label;
const imgs=(j&&j.images)||[];
// onload of a product URL on the ORIGINAL: it's a new item — replace the title/description
if(isOrig&&j&&j.title){T.title=j.title;const h=el('origTitle');if(h)h.textContent=j.title;}
if(imgs.length){setImg(sku,imgs[0]);imgEl.src=imgs[0];syncImgField(sku,imgs[0]);if(thumbsEl)fillThumbs(thumbsEl,sku,imgEl,imgs);}
else if(!isOrig){btn.textContent='none';setTimeout(()=>btn.textContent=label,1200);}
if(isOrig)scheduleSearch(); // new item loaded → reload substitutes
}).catch(()=>{btn.disabled=false;btn.textContent=label;});
}
function drawOrig(){
el('orig').innerHTML=
`<div class="origleft">
<img id="origImg" src="${imgOf(T.sku,T.img)}" alt="">
<button class="gobtn wide" id="findSubs">🔍 Find subs</button>
</div>
<div><span class="tagd${T.discontinued?' disc':''}">${T.discontinued?'⛔ Discontinued':(T.status==='NEW'?'New item':T.status)} · needs substitute</span>
<h2 id="origTitle">${T.title}</h2>
<div class="attrs" id="origAttrs">${T.vendor} · ${T.sku}<br>Material ${T.material} · Style ${T.style} · Color ${T.color} · Hue ${T.hue}°</div>
${T.successor?`<div class="successor">Now available as<span class="arr">→</span><a href="${BASE+(T.successor.handle||'')}" target="_blank" rel="noopener">${T.successor.sku} · ${T.successor.title}</a></div>`:(T.discontinued?`<div class="successor">No live successor identified —<button class="catsimbtn" id="catSimBtn">Search the catalog for a similar pattern</button></div>`:'')}
<div class="imgurl"><span>Image URL</span><input id="origUrl" placeholder="paste an image URL…" value="${imgOf(T.sku,'')}"></div>
<div class="imgurl"><span>Product URL</span><input id="origPurl" placeholder="paste the product page URL…" value="${purlOf(T.sku,'')}"><button class="gobtn" id="origPurlGo">⤵ Get images</button></div>
<div class="thumbs" id="origThumbs"></div>
</div>`;
el('orig').className='orig'+(T.discontinued?' disc':'');
el('origUrl').addEventListener('input',e=>{setImg(T.sku,e.target.value.trim());el('origImg').src=imgOf(T.sku,T.img);});
el('origPurl').addEventListener('input',e=>{setPurl(T.sku,e.target.value.trim());});
el('origPurlGo').addEventListener('click',()=>extractInto(el('origPurlGo'),el('origPurl').value.trim(),T.sku,el('origImg'),el('origThumbs'),true,colorHint(T.title,T.color)));
el('findSubs').addEventListener('click',()=>reSearch(true));
var _csb=el('catSimBtn'); if(_csb)_csb.addEventListener('click',()=>loadCatSim(T.sku||T.mfr||''));
wireDrop(el('origImg'),el('origImg'),T.sku);
}
// "Search the catalog for a similar pattern" → proxied all.dw /api/similar → attribute-scored
// LIVE substitutes rendered as a card grid below the orig card (shown only when a discontinued
// item has NO resolved successor). One-shot: results replace the call-to-action button.
function loadCatSim(sku){
var box=el('catSim'); if(!box)return;
var btn=el('catSimBtn');
if(btn){btn.disabled=true;btn.textContent='searching…';}
fetch('/api/similar?sku='+encodeURIComponent(sku)+'&limit=12').then(r=>r.json()).then(d=>{
box.innerHTML='';
var rows=(d&&d.rows)||[];
if(!rows.length){box.innerHTML='<div class="catsimnone">No similar live patterns found.</div>';}
else{
var h=document.createElement('div');h.className='catsimhead';h.textContent='Similar live patterns ('+rows.length+')';box.appendChild(h);
var g=document.createElement('div');g.className='catsimgrid';box.appendChild(g);
rows.forEach(function(r){
var c=document.createElement('div');c.className='catsimcard';
var price=r.price?('$'+Number(r.price).toLocaleString(undefined,{minimumFractionDigits:2,maximumFractionDigits:2})):'';
var img=r.image||(r.images&&r.images[0])||'';
c.innerHTML='<div class="csimg"></div>'
+'<div class="csb"><div class="cssku">'+(r.sku||'—')+'</div>'
+'<div class="csttl" title="'+String(r.title||'').replace(/"/g,'"')+'">'+(r.title||'')+'</div>'
+'<div class="csvt">'+[r.vendor,r.type].filter(Boolean).join(' · ')+'</div>'
+(price?'<div class="csprice">'+price+'</div>':'')+'</div>';
if(img){var th=c.querySelector('.csimg');th.style.backgroundImage='url("'+img+'")';}
if(r.url){c.style.cursor='pointer';c.addEventListener('click',()=>window.open(r.url,'_blank','noopener,noreferrer'));}
g.appendChild(c);
});
}
if(btn)btn.remove();
}).catch(()=>{
if(btn){btn.disabled=false;btn.textContent='Search the catalog for a similar pattern';}
box.innerHTML='<div class="catsimnone">Similarity search unavailable — try again.</div>';
});
}
// re-run the substitute search for the current item (auto when the left image changes; manual via Find subs)
let searchTimer=null;
function scheduleSearch(){clearTimeout(searchTimer);searchTimer=setTimeout(()=>reSearch(false),450);}
function reSearch(manual){
const q=(T.title||'').trim(); if(!q)return;
const b=el('findSubs'), lbl=b?b.textContent:'';
if(b){b.disabled=true;b.textContent='finding…';}
const prevImg=el('origImg')?el('origImg').src:'';
fetch('/api/subs?q='+encodeURIComponent(q)).then(r=>r.json()).then(res=>{
if(b){b.disabled=false;b.textContent=lbl;}
if(res&&res.target){ T=res.target; if(prevImg)setImg(T.sku,prevImg); // keep the shown image as the target (never revert to catalog img)
SUBS=res.subs||[]; picked.clear(); drawOrig(); draw(); syncBar(); runClip(); }
}).catch(()=>{if(b){b.disabled=false;b.textContent=lbl;}});
}
function hdist(a,b){let d=Math.abs(a-b)%360;return Math.min(d,360-d);}
function rgb(h){h=h||'#8a8a8a';return [parseInt(h.slice(1,3),16),parseInt(h.slice(3,5),16),parseInt(h.slice(5,7),16)];}
function cdist(a,b){const x=rgb(a),y=rgb(b);return Math.sqrt((x[0]-y[0])**2+(x[1]-y[1])**2+(x[2]-y[2])**2);}
let HEXTARGET=''; // user-typed hex# overrides the item's own color for the Color dimension
function colorTarget(){return HEXTARGET||T.hex;}
function updHexSw(){const sw=el('hexSw');if(sw)sw.style.background=colorTarget();const hi=el('hexIn');if(hi&&!HEXTARGET)hi.placeholder=(T.hex||'auto');}
function dimMatch(c,dim){
if(dim==='visual') return (typeof c.clip==='number')?c.clip:0; // CLIP image similarity 0..1
if(dim==='color') return 1-cdist(c.hex,colorTarget())/441; // perceptual color+tone (vs typed hex# or item)
if(dim==='style') return c.style===T.style?1:0;
return c.material===T.material?1:0; // material
}
function score(c){
// weight by priority rank: 1st=N, 2nd=N-1, …
let num=0,den=0; const N=S.prio.length;
S.prio.forEach((dim,i)=>{const w=N-i; num+=w*dimMatch(c,dim); den+=w;});
return den?num/den:0;
}
let RANKED=[];
// ---------- left-rail facet filters ----------
const FSETS={vendor:new Set(),material:new Set(),style:new Set(),color:new Set(),status:new Set()};
let QTEXT='';
const rowUI={vendorRow:{find:'',expand:false},matRow:{},styleRow:{},colorRow:{},statusRow:{}};
function loadFacets(){try{const s=JSON.parse(localStorage.getItem('subfinder_facets')||'{}');for(const k in FSETS)if(s[k])s[k].forEach(v=>FSETS[k].add(v));}catch(e){}}
function saveFacets(){const o={};for(const k in FSETS)o[k]=[...FSETS[k]];localStorage.setItem('subfinder_facets',JSON.stringify(o));}
const dimVal={vendor:c=>c.vendor,material:c=>c.material,style:c=>c.style,color:c=>c.color,status:c=>c.status};
function passes(c){
if(QTEXT){const h=(c.title+' '+c.sku+' '+c.vendor+' '+c.color+' '+c.material+' '+c.style).toLowerCase();if(!h.includes(QTEXT))return false;}
for(const k in FSETS){if(FSETS[k].size && !FSETS[k].has(dimVal[k](c)))return false;}
return true;
}
function counts(items,fn){const m={};items.forEach(c=>{const v=fn(c);if(v)m[v]=(m[v]||0)+1;});return m;}
function facetList(rowId,cnts,set,cap){
const row=el(rowId); if(!row)return; row.innerHTML=''; const ui=rowUI[rowId]||(rowUI[rowId]={find:'',expand:false});
let entries=Object.entries(cnts).sort((a,b)=>b[1]-a[1]);
const big=entries.length>12;
if(big){const f=document.createElement('input');f.className='ffind';f.placeholder='find…';f.value=ui.find||'';
f.oninput=()=>{ui.find=f.value;renderFacets();const g=el(rowId).querySelector('.ffind');if(g){g.focus();g.setSelectionRange(g.value.length,g.value.length);}};row.appendChild(f);}
if(ui.find)entries=entries.filter(([v])=>v.toLowerCase().includes(ui.find.toLowerCase()));
let shown=entries,hidden=0; const CAP=cap||8;
if(!ui.expand&&entries.length>CAP){shown=entries.slice(0,CAP);hidden=entries.length-CAP;}
const sv=new Set(shown.map(([v])=>v)); for(const v of set)if(!sv.has(v)){const e=entries.find(([x])=>x===v);shown.push(e||[v,0]);}
if(!shown.length){row.insertAdjacentHTML('beforeend','<div style="font-size:10px;color:#b0a89c;padding:2px">none</div>');return;}
shown.forEach(([val,ct])=>{const d=document.createElement('div');d.className='fitem'+(set.has(val)?' on':'')+(ct?'':' zero');
d.innerHTML=`<span class="box">${set.has(val)?'✓':''}</span><span class="lbl" title="${(''+val).replace(/"/g,'"')}">${val}</span><span class="ct">${ct}</span>`;
d.onclick=()=>{set.has(val)?set.delete(val):set.add(val);saveFacets();draw();};row.appendChild(d);});
if(hidden>0){const m=document.createElement('span');m.className='more';m.textContent=`See ${hidden} more ▾`;m.onclick=()=>{ui.expand=true;renderFacets();};row.appendChild(m);}
else if(ui.expand&&entries.length>CAP){const m=document.createElement('span');m.className='more';m.textContent='See less ▴';m.onclick=()=>{ui.expand=false;renderFacets();};row.appendChild(m);}
}
function renderFacets(){
const base=SUBS.filter(c=>{const q=QTEXT; if(q){const h=(c.title+' '+c.sku+' '+c.vendor+' '+c.color+' '+c.material+' '+c.style).toLowerCase();if(!h.includes(q))return false;}return true;});
facetList('vendorRow',counts(base,dimVal.vendor),FSETS.vendor,10);
facetList('matRow',counts(base,dimVal.material),FSETS.material,10);
facetList('styleRow',counts(base,dimVal.style),FSETS.style,10);
facetList('colorRow',counts(base,dimVal.color),FSETS.color,14);
facetList('statusRow',counts(base,dimVal.status),FSETS.status,4);
// section count badges
const SEC={vendor:'nVendor',material:'nMaterial',style:'nStyle',color:'nColor',status:'nStatus'};
for(const k in SEC){const b=el(SEC[k]);if(b){b.textContent=FSETS[k].size||'';b.style.display=FSETS[k].size?'':'none';}}
renderActive();
}
function renderActive(){
const row=el('activeRow');row.innerHTML='';let n=0;
const LBL={vendor:'Vendor',material:'Material',style:'Style',color:'Color',status:'Status'};
for(const k in FSETS)FSETS[k].forEach(v=>{n++;const s=document.createElement('span');s.className='afilter';s.innerHTML=`<b>${LBL[k]}:</b> ${v} ✕`;s.onclick=()=>{FSETS[k].delete(v);saveFacets();draw();};row.appendChild(s);});
if(n){const c=document.createElement('button');c.className='clearall';c.textContent='Clear all';c.onclick=()=>{for(const k in FSETS)FSETS[k].clear();saveFacets();draw();};row.appendChild(c);}
}
// ---------- 1/2/3 priority ranker ----------
function renderPrio(){
const box=el('prioList');if(!box)return;box.innerHTML='';
S.prio.forEach((dim,i)=>{const nm=PRIONAMES[dim];const d=document.createElement('div');d.className='prow';
d.innerHTML=`<span class="rk">${i+1}</span><span class="pn">${nm[0]}<br><span class="pnb">${nm[1]}</span></span>
<span class="ar"><button ${i===0?'disabled':''} data-u="${i}">▲</button><button ${i===S.prio.length-1?'disabled':''} data-d="${i}">▼</button></span>`;
box.appendChild(d);});
box.querySelectorAll('button[data-u]').forEach(b=>b.onclick=()=>{const i=+b.dataset.u;[S.prio[i-1],S.prio[i]]=[S.prio[i],S.prio[i-1]];saveS();renderPrio();draw();});
box.querySelectorAll('button[data-d]').forEach(b=>b.onclick=()=>{const i=+b.dataset.d;[S.prio[i+1],S.prio[i]]=[S.prio[i],S.prio[i+1]];saveS();renderPrio();draw();});
}
function saveS(){localStorage.setItem('subfinder',JSON.stringify(S));}
// ---------- card-field visibility: manual toggles + auto density-driven reduction ----------
const FIELDROWS=[['rank','#'],['clip','Visual %'],['color','Color badge'],['status','Status'],['vend','Vendor'],['title','Title'],['hex','Hex'],['specs','Specs'],['price','Price'],['sku','Model #'],['score','Match bar'],['link','Link'],['urls','URL editor']];
const FIELDS=Object.assign(Object.fromEntries(FIELDROWS.map(([k])=>[k,true])),JSON.parse(localStorage.getItem('subfinder_fields')||'{}'));
function applyFieldClasses(){const g=el('grid');FIELDROWS.forEach(([k])=>g.classList.toggle('h-'+k,!FIELDS[k]));}
function updFieldsN(){const n=FIELDROWS.filter(([k])=>!FIELDS[k]).length;const b=el('nFields');if(b){b.textContent=n||'';b.style.display=n?'':'none';}}
function renderFields(){const box=el('fieldsRow');if(!box)return;box.innerHTML='';
FIELDROWS.forEach(([k,lbl])=>{const on=FIELDS[k];const d=document.createElement('div');d.className='fitem'+(on?' on':'');
d.innerHTML=`<span class="box">${on?'✓':''}</span><span class="lbl">${lbl}</span>`;
d.onclick=()=>{FIELDS[k]=!FIELDS[k];localStorage.setItem('subfinder_fields',JSON.stringify(FIELDS));applyFieldClasses();renderFields();};box.appendChild(d);});
updFieldsN();}
// as columns increase, remove text in order (hex→score→name→vendor→specs→price→status→color); model # + image always kept
const DENSORDER=['hex','score','title','vend','specs','price','status','color'];
function applyDensityFields(cols){const g=el('grid');const hide=cols<=4?0:Math.min(DENSORDER.length,cols-4);
DENSORDER.forEach((k,i)=>g.classList.toggle('x-'+k,i<hide));}
function draw(){
document.documentElement.style.setProperty('--cols',S.den);
applyDensityFields(S.den);applyFieldClasses();updHexSw();
el('vDen').textContent=S.den;el('vShow').textContent=S.show;
renderFacets();
let arr=SUBS.filter(passes).map(c=>({c,s:score(c)}));
const sk=S.sort||'best', pf=v=>{const n=parseFloat(v);return isNaN(n)?null:n;};
if(sk==='sku')arr.sort((a,b)=>(''+a.c.sku).localeCompare(''+b.c.sku));
else if(sk==='title')arr.sort((a,b)=>(''+a.c.title).localeCompare(''+b.c.title));
else if(sk==='vendor')arr.sort((a,b)=>(''+a.c.vendor).localeCompare(''+b.c.vendor)||b.s-a.s);
else if(sk==='pricelo')arr.sort((a,b)=>((pf(a.c.price)??1e12)-(pf(b.c.price)??1e12)));
else if(sk==='pricehi')arr.sort((a,b)=>((pf(b.c.price)??-1)-(pf(a.c.price)??-1)));
else if(sk==='color')arr.sort((a,b)=>cdist(a.c.hex,colorTarget())-cdist(b.c.hex,colorTarget()));
else arr.sort((a,b)=>b.s-a.s); // best match (score)
RANKED=arr.slice(0,S.show);
const nf=arr.length;
el('count').textContent=nf+' match your filters — showing top '+RANKED.length;
el('grid').innerHTML=RANKED.map((r,i)=>{const c=r.c;const on=picked.has(c.sku);return`
<div class="card ${on?'sel':''}" data-sku="${c.sku}">
<div class="ph">
<input type="checkbox" class="pick" ${on?'checked':''} title="Select for letter">
<span class="rank">${i+1}</span><span class="sw">${c.color}</span><span class="st ${c.status}">${c.status}</span>
${(typeof c.clip==='number'&&c.clip>0)?`<span class="clipb ${c.clip>=.85?'hi':c.clip>=.65?'mid':'lo'}" title="CLIP visual similarity">${Math.round(c.clip*100)}% look</span>`:''}
<img loading="lazy" src="${imgOf(c.sku,c.img)}" alt="">
<button class="editimg" title="Override image / product URL">✎ URLs</button>
</div>
<div class="body"><div class="vend">${c.vendor}</div><div class="title">${c.title}</div>
<div class="meta">
<span class="m-hex"><i class="hx" style="background:${c.hex}"></i>${c.hex}</span>
<span class="m-specs">${c.material} · ${c.style}</span>
${c.price?`<span class="m-price">$${c.price}</span>`:''}
<span class="m-sku">${c.sku}</span>
</div>
<div class="scorebar"><i style="width:${Math.round(r.s*100)}%"></i></div>
<a class="rowlink" href="${prodUrl(c)}" target="_blank" rel="noopener">View product ↗</a>
<div class="imgedit">
<div class="urow"><input class="iurl" placeholder="image URL for ${c.sku}…" value="${imgOf(c.sku,'')}"></div>
<div class="urow"><input class="purl" placeholder="product page URL for ${c.sku}…" value="${purlOf(c.sku,'')}"><button class="gobtn gop">⤵ Get images</button></div>
<div class="thumbs"></div>
</div>
</div></div>`;}).join('');
el('grid').querySelectorAll('.card').forEach(card=>{
const sku=card.dataset.sku, cc=RANKED.find(r=>r.c.sku===sku).c;
card.querySelector('.pick').addEventListener('change',e=>{e.target.checked?picked.add(sku):picked.delete(sku);card.classList.toggle('sel',e.target.checked);syncBar();});
card.querySelector('.editimg').addEventListener('click',()=>card.querySelector('.imgedit').classList.toggle('show'));
card.querySelector('.imgedit .iurl').addEventListener('input',e=>{setImg(sku,e.target.value.trim());card.querySelector('.ph img').src=imgOf(sku,cc.img);});
card.querySelector('.imgedit .purl').addEventListener('input',e=>{setPurl(sku,e.target.value.trim());card.querySelector('.rowlink').href=prodUrl(cc);});
card.querySelector('.imgedit .gop').addEventListener('click',e=>extractInto(e.target,card.querySelector('.purl').value.trim()||prodUrl(cc),sku,card.querySelector('.ph img'),card.querySelector('.imgedit .thumbs'),false,colorHint(cc.title,cc.color)));
wireDrop(card.querySelector('.ph'),card.querySelector('.ph img'),sku);
});
}
function syncBar(){const n=picked.size;el('barN').textContent=n+' selected';el('bar').classList.toggle('show',n>0);}
function bind(id,key){const e=el(id);e.value=S[key];e.addEventListener('input',()=>{S[key]=+e.value;localStorage.setItem('subfinder',JSON.stringify(S));draw();});}
// ---- letter ----
function letterText(){
const client=el('fClient').value.trim()||'[Client name]';
const sender=el('fSender').value.trim()||'Designer Wallcoverings';
const chosen=SUBS.map(c=>({c,s:score(c)})).sort((a,b)=>b.s-a.s).map(r=>r.c).filter(c=>picked.has(c.sku));
const lines=chosen.map((c,i)=>` ${i+1}. ${c.title} — ${c.vendor}\n Color: ${c.color} · ${c.material}\n ${prodUrl(c)}`).join('\n\n');
const plural=chosen.length===1?'a comparable alternative':'the following comparable alternatives';
return `Dear ${client},
Thank you for your interest in ${T.title} (${T.sku}). Unfortunately, this item has been discontinued and is no longer available.
We'd be glad to recommend ${plural}, chosen to closely match the original in material, color, and style:
${lines}
Each of these is currently available, and we'd be happy to send samples or answer any questions to help you decide.
Warm regards,
${sender}
Designer Wallcoverings`;
}
function openModal(){el('fBody').value=letterText();el('modal').classList.add('show');}
['fClient','fSender'].forEach(id=>el(id).addEventListener('input',()=>el('fBody').value=letterText()));
el('barGo').addEventListener('click',openModal);
el('barClr').addEventListener('click',()=>{picked.clear();syncBar();draw();});
el('mClose').addEventListener('click',()=>el('modal').classList.remove('show'));
el('modal').addEventListener('click',e=>{if(e.target===el('modal'))el('modal').classList.remove('show');});
el('mCopy').addEventListener('click',()=>{navigator.clipboard.writeText(el('fBody').value);el('mCopy').textContent='Copied ✓';setTimeout(()=>el('mCopy').textContent='Copy',1200);});
el('mDl').addEventListener('click',()=>{const b=new Blob([el('fBody').value],{type:'text/plain'});const a=document.createElement('a');a.href=URL.createObjectURL(b);a.download=`substitute-letter-${T.sku}.txt`;a.click();});
el('mMail').addEventListener('click',()=>{const sub=`Substitute recommendations for ${T.sku}`;location.href=`mailto:?subject=${encodeURIComponent(sub)}&body=${encodeURIComponent(el('fBody').value)}`;});
// sanitize prio (must be exactly the 4 dims, no dupes/missing after code changes)
{const want=['visual','color','style','material'];if(!Array.isArray(S.prio))S.prio=want.slice();
S.prio=S.prio.filter(x=>want.includes(x));want.forEach(x=>{if(!S.prio.includes(x))S.prio.push(x);});S.prio=S.prio.slice(0,4);}
loadFacets();
drawOrig();renderPrio();renderFields();
function bind(id,key){const e=el(id);e.value=S[key];e.addEventListener('input',()=>{S[key]=+e.value;saveS();draw();});}
bind('show','show');bind('den','den');
{let qt;el('q').addEventListener('input',e=>{clearTimeout(qt);qt=setTimeout(()=>{QTEXT=e.target.value.trim().toLowerCase();draw();},220);});}
{const so=el('sort');so.value=S.sort||'best';so.addEventListener('change',e=>{S.sort=e.target.value;saveS();draw();});}
{const hi=el('hexIn');hi.addEventListener('input',()=>{let v=hi.value.trim().toLowerCase();if(v&&v[0]!=='#')v='#'+v;
if(/^#[0-9a-f]{6}$/.test(v)){HEXTARGET=v;}else if(v===''||v==='#'){HEXTARGET='';}else{return;}draw();});}
el('hexClr').addEventListener('click',()=>{HEXTARGET='';el('hexIn').value='';draw();});
el('expandAll').addEventListener('click',()=>{document.querySelectorAll('#rail details.sec').forEach(d=>d.open=true);for(const k in rowUI)rowUI[k].expand=true;renderFacets();});
el('collapseAll').addEventListener('click',()=>{document.querySelectorAll('#rail details.sec').forEach(d=>d.open=false);for(const k in rowUI)rowUI[k].expand=false;renderFacets();});
el('reset').addEventListener('click',()=>{S.prio=D.prio.slice();S.den=D.den;S.show=D.show;S.sort=D.sort;el('sort').value=S.sort;saveS();
for(const k in FSETS)FSETS[k].clear();saveFacets();QTEXT='';el('q').value='';HEXTARGET='';el('hexIn').value='';
FIELDROWS.forEach(([k])=>FIELDS[k]=true);localStorage.setItem('subfinder_fields',JSON.stringify(FIELDS));
el('show').value=S.show;el('den').value=S.den;renderPrio();renderFields();draw();});
draw();syncBar();
probeExtract().then(ok=>{if(ok){drawOrig();renderPrio();draw();}});
probeClip().then(ok=>{if(ok)runClip();});
</script></body></html>