← back to Dw Pairs Well
public/preview.html
466 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Design Coordinate — local preview</title>
<style>
:root {
--gutter: 40px; --text: #1a1a1a; --muted: #777; --line: #e7e7e7;
--cols: 6; /* density slider sets this */
}
* { box-sizing: border-box; }
body { margin: 0; color: var(--text); background: #fafafa;
font: 16px/1.45 -apple-system, BlinkMacSystemFont, "Helvetica Neue", Arial, sans-serif; }
header.shop { border-bottom: 1px solid var(--line); background: #fff;
padding: 14px var(--gutter); display: flex; justify-content: space-between; align-items: center; }
header.shop .logo { font-weight: 600; letter-spacing: 0.04em; }
header.shop .preview-tag { font-size: 11px; color: #b54708; background: #fff5d6;
padding: 4px 10px; border-radius: 999px; letter-spacing: 0.06em; text-transform: uppercase; }
main { max-width: 1480px; margin: 0 auto; padding: 32px var(--gutter); }
.source-picker { background: #fff; border: 1px solid var(--line); border-radius: 6px;
padding: 14px 18px; margin-bottom: 24px; display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.source-picker label { font-size: 12px; color: var(--muted); letter-spacing: 0.06em; text-transform: uppercase; }
.source-picker input { font: inherit; padding: 8px 12px; border: 1px solid var(--line); border-radius: 4px; min-width: 180px; }
.source-picker button { font: inherit; padding: 8px 14px; background: #1a1a1a; color: #fff; border: 0; border-radius: 4px; cursor: pointer; }
.source-picker .quick { display: flex; gap: 6px; flex-wrap: wrap; }
.source-picker .quick button { background: #fff; color: #333; border: 1px solid var(--line); padding: 6px 10px; font-size: 12px; }
.source-picker .quick button:hover { background: #f4ede4; border-color: #c9b48a; }
.product { display: grid; gap: 36px; grid-template-columns: 1.2fr 0.8fr;
background: #fff; border: 1px solid var(--line); border-radius: 6px; padding: 32px; margin-bottom: 24px; }
@media (max-width: 900px) { .product { grid-template-columns: 1fr; } }
.product .photo { background: #f4f4f4; border-radius: 4px; overflow: hidden; aspect-ratio: 1/1; }
.product .photo img { width: 100%; height: 100%; object-fit: cover; display: block; }
.product .meta h1 { margin: 0 0 6px; font-size: 24px; font-weight: 500; line-height: 1.2; }
.product .meta .vendor { color: var(--muted); font-size: 13px; letter-spacing: 0.08em; text-transform: uppercase; margin-bottom: 20px; }
.product .meta .price { font-size: 20px; margin-bottom: 18px; }
.product .meta .add { display: block; width: 100%; padding: 14px; background: #111; color: #fff; border: 0; font-size: 12px; letter-spacing: 0.12em; text-transform: uppercase; cursor: pointer; border-radius: 4px; margin-bottom: 22px; }
.src-palette { margin-top: 6px; }
.src-palette h4 { margin: 0 0 8px; font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); }
.src-palette table { border-collapse: collapse; width: 100%; font-size: 12px; }
.src-palette th, .src-palette td { text-align: left; padding: 6px 8px; border-bottom: 1px solid #f0ebe2; }
.src-palette th { font-weight: 500; color: var(--muted); font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase; }
.src-palette td.swatch { width: 28px; }
.swatch-dot { display: inline-block; width: 20px; height: 20px; border-radius: 50%; border: 1px solid rgba(0,0,0,0.12); vertical-align: middle; }
.swatch-square { display: inline-block; width: 14px; height: 14px; border-radius: 3px; border: 1px solid rgba(0,0,0,0.12); vertical-align: middle; margin-right: 4px; }
.paint-code { color: var(--muted); font-variant-numeric: tabular-nums; font-size: 11px; }
.vendor-picker { margin-top: 6px; margin-bottom: 14px; }
.vendor-picker h4 { margin: 0 0 8px; font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); }
.vendor-picker .chips { display: flex; flex-wrap: wrap; gap: 6px; }
.vendor-picker label {
font-size: 12px; padding: 5px 10px; border: 1px solid var(--line); border-radius: 999px;
cursor: pointer; background: #fff; user-select: none; display: inline-flex; gap: 6px; align-items: center;
}
.vendor-picker label:has(input:checked) { background: #1a1a1a; color: #fff; border-color: #1a1a1a; }
.vendor-picker input { accent-color: #fff; }
/* === Design Coordinate block === */
.dc-section { margin-top: 32px; }
.dc-section .dc-header {
display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap;
gap: 12px; margin-bottom: 14px;
}
.dc-section h2 {
margin: 0; font: 500 16px/1.2 ui-serif, Georgia, serif;
}
.dc-section h2 .count { color: var(--muted); font-size: 13px; margin-left: 6px; }
.dc-controls {
display: flex; align-items: center; gap: 12px; font-size: 12px; color: var(--muted);
text-transform: uppercase; letter-spacing: 0.06em;
}
.dc-controls input[type=range] { width: 180px; accent-color: #1a1a1a; }
.dc-controls .colcount {
font-variant-numeric: tabular-nums; color: var(--text);
min-width: 1.5em; text-align: right; font-weight: 600;
}
.pairs-well__cta {
display: inline-flex; align-items: center; justify-content: center; gap: 8px;
width: auto; max-width: 100%; padding: 10px 18px; margin: 0;
background: #111; color: #fff; border: 0; border-radius: 999px;
font-size: 12px; font-weight: 500; letter-spacing: 0.06em;
text-transform: uppercase; cursor: pointer;
transition: background 160ms ease, transform 120ms ease;
}
.pairs-well__cta:hover { background: #2a2a2a; }
.pairs-well__cta[aria-expanded="true"] .pairs-well__cta-icon { transform: rotate(45deg); }
.pairs-well__cta-icon { display: inline-block; font-size: 22px; line-height: 1; transition: transform 200ms ease; }
.pairs-well__grid {
display: grid; gap: 14px; margin-top: 18px;
grid-template-columns: repeat(var(--cols), minmax(0, 1fr));
}
/* On narrow screens the slider value is clamped */
@media (max-width: 1200px) { .pairs-well__grid { grid-template-columns: repeat(min(6, var(--cols)), minmax(0, 1fr)); } }
@media (max-width: 900px) { .pairs-well__grid { grid-template-columns: repeat(min(4, var(--cols)), minmax(0, 1fr)); } }
@media (max-width: 600px) { .pairs-well__grid { grid-template-columns: repeat(min(2, var(--cols)), minmax(0, 1fr)); } }
.pairs-well__card { display: block; text-decoration: none; color: inherit;
background: #fff; border: 1px solid #e7e7e7; border-radius: 6px;
overflow: hidden; transition: box-shadow 160ms ease, transform 160ms ease; }
.pairs-well__card:hover { box-shadow: 0 6px 20px rgba(0,0,0,0.08); transform: translateY(-2px); }
.pairs-well__img-wrap { aspect-ratio: 1 / 1; background: #f4f4f4; overflow: hidden; }
.pairs-well__img { width: 100%; height: 100%; object-fit: cover; display: block; }
.pairs-well__meta { padding: 10px 12px 12px; }
.pairs-well__title { margin: 0 0 4px; font-size: 13px; font-weight: 600; line-height: 1.3;
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.pairs-well__vendor { margin: 0 0 6px; font-size: 10px; color: #777; letter-spacing: 0.04em; text-transform: uppercase; }
.pairs-well__why { display: inline-block; padding: 2px 7px; background: #f4ede4;
color: #5a4628; border-radius: 999px; font-size: 10px; line-height: 1.4; margin-bottom: 8px; }
.pairs-well__palette { display: flex; gap: 3px; margin: 0 0 8px; flex-wrap: wrap; }
.pairs-well__palette .swatch-dot { width: 12px; height: 12px; }
.pairs-well__paints { border-top: 1px dashed #ebe4d8; margin-top: 4px; padding-top: 6px;
font-size: 10px; line-height: 1.5; color: #444; }
.pairs-well__paints .label { color: #888; font-weight: 600; margin-right: 4px; letter-spacing: 0.04em; }
.pairs-well__paints .row { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* AI-generated section variants */
.ai-coords .pairs-well__card { border-style: dashed; border-color: #d9c7a8; }
.ai-coords .pairs-well__why { background: #efe6f5; color: #5b3478; }
.ai-badge {
display: inline-block; padding: 3px 8px; background: #2a2a2a; color: #fff;
border-radius: 999px; font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase;
margin-left: 8px;
}
.pairs-well__skeleton {
background: linear-gradient(90deg, #eee 0%, #f6f6f6 50%, #eee 100%);
background-size: 200% 100%; animation: pwShim 1.2s linear infinite;
aspect-ratio: 1 / 1; border-radius: 6px;
}
@keyframes pwShim { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
.pairs-well__empty { padding: 18px; color: #777; font-size: 14px; grid-column: 1 / -1; }
</style>
</head>
<body>
<header class="shop">
<div class="logo">DESIGNER WALLCOVERINGS</div>
<div class="preview-tag">Local preview · service :9813</div>
</header>
<main>
<div class="source-picker">
<label for="sku">SKU or handle</label>
<input id="sku" type="text" value="DWRW-74991" autocomplete="off" placeholder="DWA-91351 or izara-beige-..." style="min-width:340px">
<button id="load">Load</button>
<div class="quick">
<button data-sku="izara-beige-residential-wallpaper-jeffrey-stevens">Izara Beige (repro 2026-05-13)</button>
<button data-sku="izara-light-green-residential-wallpaper-jeffrey-stevens">Izara Light Green</button>
<button data-sku="DWRW-74991">DWRW-74991 (Rebel Walls mural)</button>
<button data-sku="DWPT-200315">DWPT-200315 (1838 Floribunda Midnight)</button>
<button data-sku="DWPT-200312">DWPT-200312 (1838 Lavender Dream)</button>
<button data-sku="DWWC-508630">DWWC-508630 (Phillipe Romano Bongol)</button>
</div>
</div>
<article class="product">
<div class="photo"><img id="product-photo" alt="" src=""></div>
<div class="meta">
<h1 id="product-title">—</h1>
<p class="vendor" id="product-vendor">—</p>
<p class="price">From $4.25 / sample</p>
<button class="add">Add Sample to Cart</button>
<div class="vendor-picker" id="vendor-picker">
<h4>Paint brands</h4>
<div class="chips">
<label><input type="checkbox" value="sw" checked> Sherwin-Williams</label>
<label><input type="checkbox" value="de" checked> Dunn-Edwards</label>
<label><input type="checkbox" value="bm" checked> Benjamin Moore</label>
<label><input type="checkbox" value="fb" checked> Farrow & Ball</label>
<label><input type="checkbox" value="behr" > Behr</label>
<label><input type="checkbox" value="ppg" > PPG</label>
<label><input type="checkbox" value="ral" > RAL</label>
</div>
</div>
<div class="src-palette" id="src-palette" hidden>
<h4>Color story</h4>
<table>
<thead><tr id="src-palette-head"></tr></thead>
<tbody id="src-palette-body"></tbody>
</table>
</div>
</div>
</article>
<!-- CATALOG COORDINATES -->
<section id="pairs-well-with" class="dc-section"
data-pairs-endpoint="/api/pairs"
data-product-sku="DWRW-74991">
<button type="button" class="pairs-well__cta" aria-expanded="false">
<span class="pairs-well__cta-text">Design Coordinate</span>
<span class="pairs-well__cta-icon">+</span>
</button>
<div class="dc-header" id="dc-header-catalog" hidden>
<h2>Stripes & Plaids from our catalog <span class="count" id="catalog-count"></span></h2>
<div class="dc-controls">
<label for="catalog-cols">Columns</label>
<input id="catalog-cols" type="range" min="3" max="12" value="6" step="1">
<span class="colcount" id="catalog-cols-val">6</span>
</div>
</div>
<div id="catalog-grid" class="pairs-well__grid" hidden></div>
</section>
<!-- AI-GENERATED COORDINATES -->
<section id="ai-coords-section" class="dc-section ai-coords" hidden>
<div class="dc-header">
<h2>Auto Generated <span class="ai-badge">Preview · 6 options</span></h2>
<div class="dc-controls">
<label for="ai-cols">Columns</label>
<input id="ai-cols" type="range" min="3" max="12" value="6" step="1">
<span class="colcount" id="ai-cols-val">6</span>
</div>
</div>
<div id="ai-grid" class="pairs-well__grid"></div>
</section>
</main>
<script>
function esc(s) {
return String(s == null ? '' : s)
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
.replace(/"/g, '"').replace(/'/g, ''');
}
// Relative paths — work whether served from localhost:9813 or pairs.designerwallcoverings.com
var endpointPairs = '/api/pairs';
var endpointAi = '/api/ai-coordinates';
var picker = document.getElementById('sku');
var loadBtn = document.getElementById('load');
var photoEl = document.getElementById('product-photo');
var titleEl = document.getElementById('product-title');
var vendorEl = document.getElementById('product-vendor');
var quickBtns = document.querySelectorAll('.quick button');
var section = document.getElementById('pairs-well-with');
var cta = section.querySelector('.pairs-well__cta');
var catalogHeader = document.getElementById('dc-header-catalog');
var catalogGrid = document.getElementById('catalog-grid');
var catalogCols = document.getElementById('catalog-cols');
var catalogColsVal = document.getElementById('catalog-cols-val');
var catalogCount = document.getElementById('catalog-count');
var aiSection = document.getElementById('ai-coords-section');
var aiGrid = document.getElementById('ai-grid');
var aiCols = document.getElementById('ai-cols');
var aiColsVal = document.getElementById('ai-cols-val');
var srcPalette = document.getElementById('src-palette');
var srcPaletteBody = document.getElementById('src-palette-body');
// ---- density sliders ----
function wireSlider(input, valEl, gridEl) {
function apply() {
var n = Math.max(3, Math.min(12, parseInt(input.value, 10) || 8));
valEl.textContent = n;
gridEl.style.setProperty('--cols', n);
try { localStorage.setItem('dc.cols.' + input.id, n); } catch (_) {}
}
try {
var stored = parseInt(localStorage.getItem('dc.cols.' + input.id), 10);
if (!Number.isNaN(stored) && stored >= 3 && stored <= 12) input.value = stored;
} catch (_) {}
input.addEventListener('input', apply);
apply();
}
wireSlider(catalogCols, catalogColsVal, catalogGrid);
wireSlider(aiCols, aiColsVal, aiGrid);
// ---- source palette render ----
// ===== paint vendor selection =====
var VENDOR_LABEL = { sw:'Sherwin-Williams', de:'Dunn-Edwards', bm:'Benjamin Moore', fb:'Farrow & Ball', behr:'Behr', ppg:'PPG', ral:'RAL' };
var VENDOR_DEFAULT = ['sw','de','bm','fb'];
var vendorChecks = document.querySelectorAll('#vendor-picker input[type=checkbox]');
function selectedVendors() {
var out = [];
vendorChecks.forEach(function (cb) { if (cb.checked) out.push(cb.value); });
return out.length ? out : VENDOR_DEFAULT.slice();
}
try {
var stored = JSON.parse(localStorage.getItem('dc.vendors') || 'null');
if (Array.isArray(stored) && stored.length) {
vendorChecks.forEach(function (cb) { cb.checked = stored.indexOf(cb.value) !== -1; });
}
} catch (_) {}
function persistVendors() {
try { localStorage.setItem('dc.vendors', JSON.stringify(selectedVendors())); } catch (_) {}
}
vendorChecks.forEach(function (cb) {
cb.addEventListener('change', function () {
persistVendors();
// Re-render with the new vendor set.
if (section._catalogPairs && !document.getElementById('catalog-grid').hidden) {
renderGrid(catalogGrid, section._catalogPairs);
}
if (section._sourcePalette) renderSourcePalette(section._sourcePalette);
// AI grid doesn't carry paint matches yet (placeholder posters)
});
});
function renderSourcePalette(palette) {
if (!palette || !palette.length) { srcPalette.hidden = true; return; }
section._sourcePalette = palette;
var vendors = selectedVendors();
document.getElementById('src-palette-head').innerHTML =
'<th></th>' + vendors.map(function (v) { return '<th>' + esc(VENDOR_LABEL[v]) + '</th>'; }).join('');
srcPaletteBody.innerHTML = palette.map(function (c) {
var cells = '<td class="swatch"><span class="swatch-dot" style="background:' + esc(c.hex) + '"></span></td>';
vendors.forEach(function (v) {
var p = c.paints && c.paints[v];
cells += '<td>' + (p
? '<span class="swatch-square" style="background:' + esc(p.hex) + '"></span>' + esc(p.name) + ' <span class="paint-code">' + esc(p.code) + '</span>'
: '—') + '</td>';
});
return '<tr>' + cells + '</tr>';
}).join('');
srcPalette.hidden = false;
}
function renderCard(p, opts) {
opts = opts || {};
var img = p.image_url ? esc(p.image_url) : '';
var why = (p.why && p.why[0]) ? esc(p.why[0]) : (p.motif ? esc(p.motif) : '');
var palette = (p.palette || []).map(function (c) {
var hex = typeof c === 'string' ? c : c.hex;
var sw = c && c.paints && c.paints.sw, de = c && c.paints && c.paints.de;
var tip = (sw ? 'SW ' + sw.name + ' ' + sw.code : '') + (de ? ' · DE ' + de.name + ' ' + de.code : '');
return '<span class="swatch-dot" style="background:' + esc(hex) + '" title="' + esc(tip) + '"></span>';
}).join('');
var dom = (p.palette && p.palette[0]) || null;
var paintsBlock = '';
if (dom && dom.paints) {
var vendors = selectedVendors();
var rows = vendors.map(function (v) {
var paint = dom.paints[v];
if (!paint) return '';
var lbl = ({ sw:'SW', de:'DE', bm:'BM', fb:'F&B', behr:'Behr', ppg:'PPG', ral:'RAL' })[v] || v.toUpperCase();
return '<span class="row"><span class="label">' + lbl + '</span>'
+ '<span class="swatch-square" style="background:' + esc(paint.hex) + '"></span>'
+ esc(paint.name) + ' <span class="paint-code">' + esc(paint.code) + '</span></span>';
}).filter(Boolean).join('');
if (rows) paintsBlock = '<div class="pairs-well__paints">' + rows + '</div>';
}
var href = p.url ? esc(p.url) : (p.handle ? '#' + esc(p.handle) : '#');
return '<a class="pairs-well__card" href="' + href + '" title="' + esc((p.why || []).join(' · ')) + '">'
+ '<div class="pairs-well__img-wrap">' + (img ? '<img class="pairs-well__img" src="' + img + '" alt="' + esc(p.title) + '" loading="lazy">' : '') + '</div>'
+ '<div class="pairs-well__meta">'
+ '<p class="pairs-well__title">' + esc(p.title || '') + '</p>'
+ '<p class="pairs-well__vendor">' + esc(p.vendor || (opts.aiBadge || '')) + '</p>'
+ (why ? '<span class="pairs-well__why">' + why + '</span>' : '')
+ (palette ? '<div class="pairs-well__palette">' + palette + '</div>' : '')
+ paintsBlock
+ '</div>'
+ '</a>';
}
function renderGrid(grid, items, opts) {
if (!items || !items.length) {
grid.innerHTML = '<div class="pairs-well__empty">No coordinates found yet for this pattern.</div>';
return;
}
grid.innerHTML = items.map(function (p) { return renderCard(p, opts); }).join('');
}
function showSkeletons(grid, n) {
var html = '';
for (var i = 0; i < n; i++) html += '<div class="pairs-well__skeleton"></div>';
grid.innerHTML = html;
}
// ---- load source product + catalog pairs (deferred until CTA click) + AI ----
// `key` can be a dw_sku (e.g. DWA-91351) OR a Shopify handle (e.g. izara-beige-...).
// We auto-detect: if it contains a hyphen + non-uppercase letters, treat as handle.
function buildQuery(key) {
var isHandle = /[a-z]/.test(key) && key.indexOf('-') !== -1 && !/^DW[A-Z]?-/.test(key);
return (isHandle ? 'handle=' : 'dw_sku=') + encodeURIComponent(key);
}
async function loadSource(key) {
picker.value = key;
section.setAttribute('data-product-sku', key);
cta.setAttribute('aria-expanded', 'false');
catalogHeader.hidden = true;
catalogGrid.hidden = true;
catalogGrid.innerHTML = '';
srcPalette.hidden = true;
aiSection.hidden = true;
aiGrid.innerHTML = '';
var qs = buildQuery(key);
try {
var r = await fetch(endpointPairs + '?' + qs + '&limit=6');
if (!r.ok) throw new Error('http ' + r.status);
var d = await r.json();
if (d && d.ok === false) throw new Error(d.error || 'bad payload');
if (d && d.source) {
photoEl.src = d.source.image_url || '';
photoEl.alt = d.source.title || '';
titleEl.textContent = d.source.title || '—';
vendorEl.textContent = d.source.vendor || '—';
renderSourcePalette(d.source.palette || []);
section._catalogPairs = d.pairs || [];
catalogCount.textContent = '(' + (d.pairs || []).length + ')';
} else {
titleEl.textContent = 'Not found: ' + key;
vendorEl.textContent = '';
photoEl.removeAttribute('src');
}
} catch (e) {
console.error('[preview] catalog fetch failed:', e, 'url=', endpointPairs + '?' + qs);
titleEl.textContent = 'Catalog fetch failed: ' + (e && e.message ? e.message : 'unknown') + ' (key: ' + key + ')';
vendorEl.textContent = '';
}
// Kick off AI generation in parallel — show its section as soon as it loads
showSkeletons(aiGrid, 6);
aiSection.hidden = false;
try {
var ar = await fetch(endpointAi + '?' + qs + '&limit=6');
if (!ar.ok) throw new Error('http ' + ar.status);
var ad = await ar.json();
if (ad && ad.ok === false) throw new Error(ad.error || 'bad payload');
if (ad && ad.items) renderGrid(aiGrid, ad.items, { aiBadge: 'Auto Generated' });
else aiGrid.innerHTML = '<div class="pairs-well__empty">Auto Generated coordinates not ready yet.</div>';
} catch (e) {
console.error('[preview] ai fetch failed:', e, 'url=', endpointAi + '?' + qs);
aiGrid.innerHTML = '<div class="pairs-well__empty">AI fetch failed: ' + esc(e && e.message ? e.message : 'unknown') + ' (key: ' + esc(key) + ')</div>';
}
}
loadBtn.addEventListener('click', function () { loadSource(picker.value.trim()); });
picker.addEventListener('keydown', function (e) { if (e.key === 'Enter') loadSource(picker.value.trim()); });
quickBtns.forEach(function (b) { b.addEventListener('click', function () { loadSource(b.getAttribute('data-sku')); }); });
// Deep-link: ?handle=... or ?sku=... or ?key=... auto-loads on page open.
// Sets `window.__deepLinkLoaded` so the default loadSource init at the bottom
// doesn't race + overwrite this load (caught 2026-05-13 — deep-link Izara
// fetched correctly but was clobbered by the trailing DWRW-74991 default).
(function () {
var params = new URLSearchParams(window.location.search);
var k = params.get('handle') || params.get('sku') || params.get('key');
if (k) { window.__deepLinkLoaded = true; loadSource(k.trim()); }
})();
cta.addEventListener('click', function () {
var open = cta.getAttribute('aria-expanded') === 'true';
if (open) {
cta.setAttribute('aria-expanded', 'false');
catalogHeader.hidden = true;
catalogGrid.hidden = true;
return;
}
cta.setAttribute('aria-expanded', 'true');
catalogHeader.hidden = false;
catalogGrid.hidden = false;
if (section._catalogPairs) renderGrid(catalogGrid, section._catalogPairs);
});
if (!window.__deepLinkLoaded) loadSource('DWRW-74991');
</script>
</body>
</html>