← 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=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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>