← back to Wallco Ai
public/theme-variants/v17-roomshow.html
314 lines
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>wallco.ai — Room-Left Showcase PDP</title>
<link rel="icon" href="/static/favicon.ico" type="image/x-icon">
<style>
:root {
--bg: #f7f3ea;
--ink: #1a160d;
--muted: #8a8276;
--panel: #ece3d0;
--bar: rgba(247,243,234,.92);
--accent: #c2a14e;
--on-accent: #1a160d;
--split: 1.2fr 1fr;
--radius: 16px;
--shadow: 0 20px 55px rgba(0,0,0,.16);
--brandfont: var(--serif);
--titlefont: 500 32px/1.12 var(--serif);
--paneltitle: 500 18px/1 var(--serif);
--room-ar: 4/3;
--pat-max: 380px;
--pat-border: 10px solid #fff;
--room-pos: static;
--line: rgba(0,0,0,.10);
--sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif;
--serif: "Cormorant Garamond", Georgia, serif;
--mono: ui-monospace, Menlo, "SF Mono", monospace;
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body { background: var(--bg); color: var(--ink); font: 14px/1.5 var(--sans); -webkit-font-smoothing: antialiased; }
a { color: inherit; } button { font: inherit; cursor: pointer; }
header.topbar { position: sticky; top: 0; z-index: 40; background: var(--bar);
backdrop-filter: blur(10px); border-bottom: 1px solid var(--line); padding: 10px 24px;
display: flex; align-items: center; gap: 20px; }
header .brand { font: 600 18px/1 var(--brandfont); letter-spacing: .04em; }
header nav { font-size: 12.5px; color: var(--muted); display: flex; gap: 16px; flex-wrap: wrap; }
header nav a { text-decoration: none; }
header .grow { flex: 1; }
header .switcher { font-size: 11px; color: var(--muted); }
header .switcher a { text-decoration: underline; color: var(--ink); margin-left: 8px; }
.adjust-btn { display: inline-flex; align-items: center; gap: 8px; border: 1px solid var(--line);
background: var(--accent); color: var(--on-accent); border-radius: 999px; padding: 9px 16px;
font: 700 12px/1 var(--sans); letter-spacing: .06em; text-transform: uppercase; }
.adjust-btn .bars { display: inline-flex; flex-direction: column; gap: 2.5px; }
.adjust-btn .bars i { width: 15px; height: 2px; background: currentColor; border-radius: 2px; display: block; }
main.roomleft { max-width: 1640px; margin: 0 auto; padding: 22px 24px 60px;
display: grid; grid-template-columns: var(--split); gap: 30px; align-items: start; }
/* LEFT — room setting */
.col-room { min-width: 0; position: var(--room-pos, static); top: 80px; }
.room-main { aspect-ratio: var(--room-ar, 4/3); background: var(--panel); border-radius: var(--radius);
overflow: hidden; position: relative; box-shadow: var(--shadow); }
.room-main img { width: 100%; height: 100%; object-fit: cover; display: block; }
.room-main .tag { position: absolute; left: 14px; bottom: 14px; background: var(--bar);
border: 1px solid var(--line); border-radius: 999px; padding: 6px 13px;
font: 600 10.5px/1 var(--sans); letter-spacing: .06em; text-transform: uppercase; }
.room-thumbs { display: flex; gap: 8px; margin-top: 12px; }
.room-thumbs .t { flex: 1; aspect-ratio: 1/1; border-radius: calc(var(--radius) - 4px);
background: var(--panel); border: 1px solid var(--line); background-size: cover; background-position: center; cursor: pointer; }
.room-cap { margin-top: 12px; font-size: 12px; color: var(--muted); }
/* RIGHT — pattern + buy */
.col-pattern { min-width: 0; }
.pattern-art { aspect-ratio: 1/1; max-width: var(--pat-max, 420px); background: var(--panel);
border-radius: var(--radius); overflow: hidden; box-shadow: var(--shadow); border: var(--pat-border, 0); }
.pattern-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
h1.title { font: var(--titlefont); margin: 18px 0 4px; }
.cat { color: var(--muted); font-size: 13px; margin-bottom: 16px; }
.cta-row { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 18px; }
.cta { border-radius: 999px; padding: 13px 20px; font: 700 12px/1 var(--sans);
letter-spacing: .07em; text-transform: uppercase; border: 1px solid transparent; }
.cta.sample { background: var(--accent); color: var(--on-accent); }
.cta.buy { background: var(--ink); color: var(--bg); }
.mini-specs { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 26px; max-width: 460px;
border-top: 1px solid var(--line); padding-top: 16px; }
.mini-specs .k { font: 600 10px/1.3 var(--sans); letter-spacing: .07em; text-transform: uppercase; color: var(--muted); }
.mini-specs .v { font-family: var(--mono); font-size: 12.5px; margin-top: 1px; }
/* adjustments overlay (menu + panel) — placement driven by body[data-mode] */
.ov { position: fixed; inset: 0; z-index: 60; display: none; }
.ov.open { display: block; }
.ov .scrim { position: absolute; inset: 0; background: rgba(20,17,14,.45); backdrop-filter: blur(3px); }
.menu, .panel { position: absolute; background: var(--bg); border: 1px solid var(--line);
box-shadow: 0 24px 80px rgba(0,0,0,.28); }
.menu { border-radius: 14px; padding: 8px; min-width: 250px; }
.menu h4 { margin: 6px 12px 8px; font: 600 10.5px/1 var(--sans); letter-spacing: .12em; text-transform: uppercase; color: var(--muted); }
.menu button { display: flex; width: 100%; align-items: center; gap: 11px; border: 0; background: transparent;
text-align: left; padding: 10px 12px; border-radius: 9px; color: var(--ink); font-size: 13.5px; }
.menu button:hover, .menu button.on { background: var(--panel); }
.menu button .ic { width: 22px; text-align: center; opacity: .7; }
.panel { border-radius: 14px; width: min(440px, 92vw); max-height: 78vh; overflow: auto; }
.panel header { display: flex; justify-content: space-between; align-items: center;
padding: 16px 20px; border-bottom: 1px solid var(--line); position: sticky; top: 0; background: var(--bg); }
.panel header h3 { margin: 0; font: var(--paneltitle); }
.panel header .x { background: transparent; border: 0; font-size: 22px; color: var(--muted); }
.panel .pbody { padding: 18px 20px 24px; }
/* mode: dropdown — menu under the button, panel as small popover top-right */
[data-mode="dropdown"] .menu { top: 62px; right: 24px; }
[data-mode="dropdown"] .panel { top: 62px; right: 24px; }
/* mode: drawer — slide in from right */
[data-mode="drawer"] .menu { top: 0; right: 0; height: 100%; border-radius: 0; min-width: 320px; padding: 18px; }
[data-mode="drawer"] .panel { top: 0; right: 0; height: 100%; border-radius: 0; width: min(440px,94vw); max-height: 100%; }
/* mode: sheet — slide up from bottom */
[data-mode="sheet"] .menu { left: 0; right: 0; bottom: 0; border-radius: 18px 18px 0 0; min-width: 0;
display: grid; grid-template-columns: repeat(auto-fill,minmax(150px,1fr)); gap: 6px; padding: 18px 22px 28px; }
[data-mode="sheet"] .menu h4 { grid-column: 1/-1; }
[data-mode="sheet"] .panel { left: 0; right: 0; bottom: 0; border-radius: 18px 18px 0 0; width: auto; max-height: 70vh; }
/* mode: modal — centered */
[data-mode="modal"] .menu { top: 50%; left: 50%; transform: translate(-50%,-50%); }
[data-mode="modal"] .panel { top: 50%; left: 50%; transform: translate(-50%,-50%); }
/* mode: palette — command-palette near top-center */
[data-mode="palette"] .menu { top: 84px; left: 50%; transform: translateX(-50%); width: min(520px,92vw); }
[data-mode="palette"] .panel { top: 84px; left: 50%; transform: translateX(-50%); }
/* mode: fab — floating action button, menu pops above it */
[data-mode="fab"] .menu { right: 24px; bottom: 86px; }
[data-mode="fab"] .panel { right: 24px; bottom: 86px; }
.fab-btn { position: fixed; right: 24px; bottom: 24px; z-index: 55; width: 60px; height: 60px;
border-radius: 50%; background: var(--accent); color: var(--on-accent); border: 0;
box-shadow: 0 10px 30px rgba(0,0,0,.3); font-size: 22px; display: none; align-items: center; justify-content: center; }
body[data-mode="fab"] .fab-btn { display: flex; }
body[data-mode="fab"] header .adjust-btn { display: none; }
/* adjustment panel content */
.swatches { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 6px; }
.sw { width: 34px; height: 34px; border-radius: 50%; border: 2px solid #fff; box-shadow: 0 0 0 1px var(--line); }
.sliders label { display: block; font-size: 12px; color: var(--muted); margin: 10px 0 3px; }
.sliders input[type=range] { width: 100%; }
.roomprev { height: 220px; border-radius: 10px; background: var(--panel); background-size: cover; background-position: center; }
.pairg { display: grid; grid-template-columns: repeat(3,1fr); gap: 6px; }
.pairg .p { aspect-ratio: 1/1; border-radius: 6px; background: var(--panel); background-size: cover; background-position: center; }
.pills { display: flex; flex-wrap: wrap; gap: 6px; }
.pill { font-size: 11px; padding: 5px 10px; border-radius: 999px; background: var(--panel); border: 1px solid var(--line); text-decoration: none; }
.calc .r { display: grid; grid-template-columns: repeat(3,1fr); gap: 8px; }
.calc label { font-size: 11px; color: var(--muted); display: block; margin-bottom: 2px; }
.calc input, .calc select { width: 100%; padding: 7px 9px; border: 1px solid var(--line); border-radius: 7px; font: inherit; background: #fff; }
.calc .tot { margin-top: 10px; padding: 10px 12px; background: var(--panel); border-radius: 8px; }
.calc .tot b { font: 500 17px var(--serif); }
.spectab .row { display: grid; grid-template-columns: 150px 1fr; padding: 6px 0; border-bottom: 1px solid var(--line); font-size: 12.5px; }
.spectab .row .k { color: var(--muted); font: 600 10px/1.4 var(--sans); letter-spacing: .06em; text-transform: uppercase; }
.spectab .row .v { font-family: var(--mono); }
.tile-btns { display: flex; flex-wrap: wrap; gap: 6px; }
.tile-btn { flex: 1; min-width: 100px; background: var(--panel); border: 1px solid var(--line); padding: 10px; border-radius: 8px; font: 600 12px/1.2 var(--sans); text-align: center; text-decoration: none; }
pre.prompt { font: 11px/1.45 var(--mono); background: var(--panel); padding: 12px; border-radius: 8px; max-height: 200px; overflow: auto; margin: 0; white-space: pre-wrap; word-break: break-word; }
footer.site { border-top: 1px solid var(--line); padding: 24px; text-align: center; color: var(--muted); font-size: 12px; background: var(--panel); }
footer.site .h { font: 500 15px/1.4 var(--serif); color: var(--ink); margin-bottom: 4px; }
.loading { display: flex; align-items: center; justify-content: center; height: 60vh; color: var(--muted); font-style: italic; }
@media (max-width: 1000px) { main.roomleft { grid-template-columns: 1fr; } .col-room { position: static; } }
@media (max-width: 640px) { header.topbar { flex-wrap: wrap; padding: 8px 14px; } main.roomleft { padding: 16px 14px; } }
</style>
</head>
<body data-mode="fab">
<header class="topbar">
<a href="/" class="brand">wallco.ai</a>
<nav><a href="/designs?sort=newest">Designs</a><a href="/designs?cat=monterey-mural">Murals</a><a href="/samples">Samples</a><a href="/basket">Basket</a></nav>
<span class="grow"></span>
<button class="adjust-btn" id="adjust-open"><span class="bars"><i></i><i></i><i></i></span> Adjust</button>
<span class="switcher">Theme: Room-Left Showcase (V17) <a id="back-to-classic" href="#">← Classic</a></span>
</header>
<button class="fab-btn" id="fab-open" aria-label="Adjust"><span style="font-size:24px">≡</span></button>
<div id="root" class="loading">Loading design…</div>
<div class="ov" id="ov">
<div class="scrim" id="ov-scrim"></div>
<div class="menu" id="adj-menu" role="menu" aria-label="Adjustments"></div>
<div class="panel" id="adj-panel" role="dialog" hidden>
<header><h3 id="adj-panel-title">Adjust</h3><button class="x" id="adj-panel-close">×</button></header>
<div class="pbody" id="adj-panel-body"></div>
</div>
</div>
<script>
(async function () {
const m = location.pathname.match(/\/design\/(\d+)/);
const qid = new URLSearchParams(location.search).get('id');
const id = m ? parseInt(m[1], 10) : (qid ? parseInt(qid, 10) : null);
if (!Number.isFinite(id) || id < 1) { document.getElementById('root').textContent = 'No id'; return; }
document.getElementById('back-to-classic').href = '/design/' + id;
let design;
try {
const r = await fetch('/api/design/' + id, { credentials: 'same-origin' });
if (!r.ok) throw new Error('HTTP ' + r.status);
design = (await r.json()).design;
} catch (e) { document.getElementById('root').textContent = 'Failed: ' + e.message; return; }
const title = design.title || ('Design No.' + id);
const cat = design.category || 'wallpaper';
const catDisplay = (cat || '').replace(/ · /g, ' — ').replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
const isMural = design.kind === 'mural_panel' || design.kind === 'mural';
const kindLabel = design.kind === 'seamless_tile' ? 'Seamless Tile (24×24")' : isMural ? 'Mural Panel' : (design.kind || 'pattern');
const palette = Array.isArray(design.palette) ? design.palette : [];
const dominant = design.dominant_hex || (palette[0] && palette[0].hex) || '#999';
const img = design.image_url || ('/designs/img/by-id/' + id);
const handle = design.handle || ('wallco-' + id);
const generator = design.generator || 'wallco.ai';
const created = design.created_at ? new Date(design.created_at) : null;
const roomMockup = (design.room_mockups && (Array.isArray(design.room_mockups) ? design.room_mockups[0] : Object.keys(design.room_mockups)[0])) || null;
const roomImgUrl = roomMockup ? ('/designs/room/design_' + id + '_' + roomMockup + '.png') : null;
const roomLabel = roomMockup ? roomMockup.replace(/_/g, ' ') : 'room preview';
// ambient tint from dominant (used by showcase mode via --amb)
document.documentElement.style.setProperty('--amb', dominant + '22');
const root = document.getElementById('root');
root.className = '';
root.innerHTML = `
<main class="roomleft">
<section class="col-room">
<div class="room-main">
<img loading="eager" src="${roomImgUrl || img}" alt="${escapeAttr(title)} in a room">
<span class="tag">${roomImgUrl ? 'See in a room — ' + escapeHtml(roomLabel) : 'Pattern preview'}</span>
</div>
<div class="room-thumbs">
<div class="t" ${roomImgUrl ? `style="background-image:url('${roomImgUrl}')"` : `style="background-image:url('${img}')"`}></div>
<div class="t" style="background-image:url('${img}')"></div>
<div class="t" style="background-image:url('${img}'); background-size:55%"></div>
<div class="t" style="background-image:url('${img}'); background-size:30%"></div>
</div>
<div class="room-cap">Room setting shown left · the wallcovering applied to a styled interior. Tap a thumbnail to switch the view.</div>
</section>
<section class="col-pattern">
<div class="pattern-art"><img loading="eager" src="${img}" alt="${escapeAttr(title)}"></div>
<h1 class="title">${escapeHtml(title)}</h1>
<div class="cat">${escapeHtml(catDisplay)} · ${escapeHtml(handle)} · by ${escapeHtml(generator)}</div>
<div class="cta-row">
<button class="cta sample" onclick="alert('Sample added (stub)')">Add sample · $4.25</button>
<button class="cta buy" id="adjust-open-2">Adjust & buy</button>
</div>
<div class="mini-specs">
<div><div class="k">Kind</div><div class="v">${escapeHtml(kindLabel)}</div></div>
<div><div class="k">Dominant</div><div class="v">${escapeHtml(dominant)}</div></div>
<div><div class="k">Pattern No.</div><div class="v">${id}</div></div>
<div><div class="k">Status</div><div class="v">${design.is_published ? 'Live' : 'Curating'}</div></div>
${created ? `<div><div class="k">Created</div><div class="v" title="${escapeAttr(design.created_at)}">${created.toLocaleDateString(undefined,{year:'numeric',month:'short',day:'numeric'})}</div></div>` : ''}
<div><div class="k">Edges</div><div class="v">PASS</div></div>
</div>
</section>
</main>
<footer class="site"><div class="h">wallco.ai</div><div>A Designer Wallcoverings company · Sherman Oaks, CA</div></footer>`;
// ---- adjustments: the hamburger menu of things to select from ----
const w = 24, h = 24;
const ADJ = [
['palette','◐','Palette & recolor', () => `<div class="swatches">${(palette.length?palette.slice(0,6):[{hex:dominant}]).map(p=>`<div class="sw" style="background:${escapeAttr(p.hex)}" title="${escapeAttr(p.hex)}"></div>`).join('')}</div><p style="color:var(--muted);font-size:12.5px">Recolor this design — drag a swatch onto the pattern to remap that ink (full recolor in Classic).</p>`],
['image','✦','Image adjustments', () => `<div class="sliders">${['Hue','Saturation','Brightness','Contrast','Sepia'].map(s=>`<label>${s}</label><input type="range" min="0" max="100" value="50">`).join('')}</div>`],
['room','▣','See in a room', () => `<div class="roomprev" style="${roomImgUrl?`background-image:url('${roomImgUrl}')`:''}"></div><p style="color:var(--muted);font-size:12.5px;margin-top:10px">${roomImgUrl?('Residential preview — '+escapeHtml(roomLabel)):'No room render yet for this design.'}</p>`],
['pairs','▥','Pairs well with', () => `<div class="pairg">${Array.from({length:6}).map(()=>`<div class="p" style="background-image:url('${img}');background-size:${40+Math.floor(Math.random()*40)}%"></div>`).join('')}</div>`],
['paints','◇','Coordinate paints', () => `<div class="pills">${['Sherwin-Williams','Dunn-Edwards','Benjamin Moore','Farrow & Ball','Behr','PPG','RAL'].map(b=>`<a class="pill" href="#">${b}</a>`).join('')}</div>`],
['specs','≡','Full specifications', () => `<div class="spectab">${[['Category',catDisplay],['Kind',kindLabel],['Width',(isMural?'36"':'24"')],['Sold by',isMural?'Per panel (36"×11ft)':'Per roll (27 ft)'],['Repeat',w+'" straight match'],['Material','Type II vinyl · class A'],['Handle',handle],['Designed by',generator],['Settlement','OK (logged)'],['Edges scan','PASS · 6-lens']].map(([k,v])=>`<div class="row"><span class="k">${escapeHtml(k)}</span><span class="v">${escapeHtml(v)}</span></div>`).join('')}</div>`],
['calc','▦','Roll calculator', () => `<div class="calc"><div class="r"><div><label>Wall W (ft)</label><input id="cw" type="number" value="10" min="1"></div><div><label>Wall H (ft)</label><input id="ch" type="number" value="9" min="1"></div><div><label>Roll W</label><select id="crw"><option>24</option><option>36</option><option>52</option></select></div></div><div class="tot" id="ctot">— rolls · $—</div></div>`],
['share','↗','Share · Furnish · Trade', () => `<div class="tile-btns"><a class="tile-btn" href="#" onclick="copyLink();return false">Copy link</a><a class="tile-btn" href="#">Pinterest</a><a class="tile-btn" href="#">Instagram</a><a class="tile-btn" href="/design/${id}/tearsheet" target="_blank">Tear sheet</a><a class="tile-btn" href="#">Furnish (AI)</a><a class="tile-btn" href="#">Email client</a></div>`],
['gen','⌘','Generator + prompt', () => `<pre class="prompt">${escapeHtml((design.prompt||'wallco.ai original · seed '+(design.seed||'—')).slice(0,1200))}</pre>`],
];
const menu = document.getElementById('adj-menu');
menu.innerHTML = '<h4>Adjust this design</h4>' + ADJ.map(([k,ic,label]) =>
`<button data-adj="${k}" role="menuitem"><span class="ic">${ic}</span> ${label}</button>`).join('');
const ov = document.getElementById('ov');
const panel = document.getElementById('adj-panel');
const menuEl = document.getElementById('adj-menu');
function openMenu(){ ov.classList.add('open'); panel.hidden = true; menuEl.style.display=''; }
function closeOv(){ ov.classList.remove('open'); panel.hidden = true; }
function showPanel(key){
const item = ADJ.find(a => a[0] === key); if(!item) return;
document.getElementById('adj-panel-title').textContent = item[2];
document.getElementById('adj-panel-body').innerHTML = item[3]();
panel.hidden = false; menuEl.style.display='none'; ov.classList.add('open');
menu.querySelectorAll('button').forEach(b => b.classList.toggle('on', b.dataset.adj===key));
if (key === 'calc') wireCalc();
}
['adjust-open','adjust-open-2','fab-open'].forEach(idn => { const el=document.getElementById(idn); if(el) el.addEventListener('click', openMenu); });
document.getElementById('ov-scrim').addEventListener('click', closeOv);
document.getElementById('adj-panel-close').addEventListener('click', () => { panel.hidden=true; menuEl.style.display=''; });
menu.addEventListener('click', e => { const b=e.target.closest('button[data-adj]'); if(b) showPanel(b.dataset.adj); });
document.addEventListener('keydown', e => { if(e.key==='Escape') closeOv(); });
function wireCalc(){
const cw=document.getElementById('cw'), ch=document.getElementById('ch'), crw=document.getElementById('crw'), tot=document.getElementById('ctot');
if(!cw) return;
function calc(){ const a=+cw.value||0,b=+ch.value||0,rw=+crw.value||24; if(a<1||b<1){tot.textContent='— rolls · $—';return;}
if(isMural){const p=Math.ceil((a*12)/rw);tot.innerHTML='<b>'+p+'</b> panels · <b>$'+(p*245).toLocaleString()+'</b>';}
else{const wp=Math.ceil((a*12)/rw),r=wp*Math.ceil(b/27);tot.innerHTML='<b>'+r+'</b> rolls · <b>$'+(r*165).toLocaleString()+'</b>';} }
[cw,ch,crw].forEach(el=>el.addEventListener('input',calc)); calc();
}
// thumbnail → swap room main image
document.querySelectorAll('.room-thumbs .t').forEach(t => t.addEventListener('click', () => {
const bg = t.style.backgroundImage; const im = document.querySelector('.room-main img');
const u = bg.slice(5,-2); if(im && u) im.src = u;
}));
})();
window.copyLink = async function(){ try{ await navigator.clipboard.writeText(location.href.replace(/\/[a-z0-9-]+$/,'/'+location.pathname.match(/design\/(\d+)/)[1])); alert('Link copied'); }catch{ alert('Copy failed'); } };
function escapeHtml(v){ if(v==null) return ''; return String(v).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
function escapeAttr(v){ return escapeHtml(v); }
</script>
<script src="/js/color-dots.js" defer></script>
<script src="/theme-variants/theme-variant-nav.js" defer></script>
<script src="/theme-variants/theme-section-rearrange.js" defer></script>
</body>
</html>