← back to Quadrille Showroom

public/proto/proto-chrome.js

142 lines

/* ============================================================================
 * proto-chrome.js — shared overlay chrome injected into every V6–V10 proto.
 *
 * Each proto (Color River / Mood-Board Table / Swipe Tower / Walk-In Room /
 * Concierge Nook) is a STANDALONE full-page experience that the Quadrille House
 * version picker opens FULL-SCREEN in an overlay <iframe> over the 3D canvas.
 * This script adds the two things every proto needs to live inside the picker:
 *
 *   1. a big, senior-friendly "‹ Back to Showroom" button (top-left) that
 *      postMessage's the parent to close the overlay and return to the picker.
 *   2. a NUMBERED-ELEMENTS legend (top-right, collapsible) so the picker's
 *      select-and-choose tray still works — clicking a number posts it to the
 *      parent, which adds {Vn.N label} to the Chosen Elements tray.
 *
 * Self-contained, zero-dependency, idempotent. The proto declares its version
 * key + element list via window.PROTO_META = { key:'V6', elements:[{n,label}] }
 * BEFORE loading this script. If opened directly (not in an iframe) the Back
 * button simply does history.back()/closes — harmless standalone.
 * ========================================================================== */
(function () {
  'use strict';
  if (window.__protoChromeLoaded) return;
  window.__protoChromeLoaded = true;

  var META = window.PROTO_META || { key: '?', label: '', elements: [] };
  var inFrame = (function () { try { return window.self !== window.top; } catch (e) { return true; } })();

  function post(msg) {
    try { (window.parent || window).postMessage(Object.assign({ __qhProto: true }, msg), '*'); } catch (e) {}
  }

  // ---- styles (scoped, high z so they sit above the proto's own UI) ----------
  var css = document.createElement('style');
  css.textContent = [
    '#qh-proto-back{position:fixed;top:16px;left:16px;z-index:2147483000;',
    'display:inline-flex;align-items:center;gap:10px;min-height:56px;padding:0 26px;',
    'font:700 19px/1 Georgia,"Times New Roman",serif;letter-spacing:.4px;cursor:pointer;',
    'color:#1a1206;background:linear-gradient(180deg,#e3cb95,#c19a44);',
    'border:2px solid #b9933f;border-radius:30px;box-shadow:0 10px 34px -8px rgba(0,0,0,.6);',
    '-webkit-user-select:none;user-select:none;transition:transform .12s,box-shadow .12s;}',
    '#qh-proto-back:hover{transform:translateY(-1px);box-shadow:0 14px 40px -8px rgba(0,0,0,.7);}',
    '#qh-proto-back:active{transform:translateY(1px);}',
    '#qh-proto-back .arr{font-size:24px;line-height:1;}',
    '#qh-proto-legend{position:fixed;top:16px;right:16px;z-index:2147483000;',
    'width:300px;max-width:46vw;background:rgba(14,13,18,.93);color:#f0e6cf;',
    'border:1.5px solid rgba(201,169,110,.45);border-radius:14px;',
    'box-shadow:0 18px 50px -12px rgba(0,0,0,.7);backdrop-filter:blur(12px);',
    'font-family:-apple-system,Segoe UI,Roboto,sans-serif;overflow:hidden;}',
    '#qh-proto-legend.collapsed .qpl-body{display:none;}',
    '.qpl-head{display:flex;align-items:center;justify-content:space-between;',
    'padding:11px 14px;cursor:pointer;border-bottom:1px solid rgba(201,169,110,.22);}',
    '.qpl-head.only{border-bottom:none;}',
    '.qpl-title{font-family:Georgia,serif;font-size:15px;letter-spacing:.5px;color:#e9dcbf;}',
    '.qpl-chev{color:#c9a96e;font-size:12px;transition:transform .15s;}',
    '#qh-proto-legend.collapsed .qpl-chev{transform:rotate(180deg);}',
    '.qpl-body{max-height:60vh;overflow:auto;padding:8px 10px 12px;}',
    '.qpl-row{display:flex;align-items:flex-start;gap:10px;padding:7px 8px;border-radius:9px;cursor:pointer;',
    'transition:background .12s;}',
    '.qpl-row:hover{background:rgba(201,169,110,.14);}',
    '.qpl-row.chosen{background:rgba(201,169,110,.22);}',
    '.qpl-badge{flex:0 0 auto;width:26px;height:26px;border-radius:50%;',
    'display:flex;align-items:center;justify-content:center;font-weight:700;font-size:14px;',
    'background:#c9a96e;color:#1a1206;border:1.5px solid #1a1206;}',
    '.qpl-row.chosen .qpl-badge{background:#2e7d4f;color:#fff;}',
    '.qpl-label{font-size:13.5px;line-height:1.35;color:#e8e1d2;padding-top:3px;}',
    '.qpl-hint{padding:2px 10px 10px;font-size:11px;color:#a7a08f;line-height:1.4;}',
    '@media(max-width:760px){#qh-proto-legend{width:230px;}#qh-proto-back{min-height:50px;font-size:17px;padding:0 18px;}}'
  ].join('');
  document.head.appendChild(css);

  // ---- Back button -----------------------------------------------------------
  var back = document.createElement('button');
  back.id = 'qh-proto-back';
  back.innerHTML = '<span class="arr">‹</span> Back to Showroom';
  back.addEventListener('click', function () {
    if (inFrame) post({ type: 'back' });
    else { try { history.back(); } catch (e) { window.location.href = '/'; } }
  });
  document.body.appendChild(back);
  // ESC also returns
  window.addEventListener('keydown', function (e) {
    if (e.key === 'Escape') { if (inFrame) post({ type: 'back' }); }
  });

  // ---- Numbered-elements legend ----------------------------------------------
  var chosenSet = {}; // key -> true (n)
  var legend = document.createElement('div');
  legend.id = 'qh-proto-legend';
  legend.className = 'collapsed';
  var rows = (META.elements || []).map(function (el) {
    return '<div class="qpl-row" data-n="' + el.n + '">' +
      '<span class="qpl-badge">' + el.n + '</span>' +
      '<span class="qpl-label">' + escapeHtml(el.label) + '</span></div>';
  }).join('');
  legend.innerHTML =
    '<div class="qpl-head"><span class="qpl-title">' + escapeHtml(META.label || ('Version ' + META.key)) + '</span>' +
    '<span class="qpl-chev">▲</span></div>' +
    '<div class="qpl-body">' +
      '<div class="qpl-hint">Tap a number to add that element to your Chosen list.</div>' +
      rows +
    '</div>';
  document.body.appendChild(legend);

  legend.querySelector('.qpl-head').addEventListener('click', function () {
    legend.classList.toggle('collapsed');
  });
  legend.querySelectorAll('.qpl-row').forEach(function (row) {
    row.addEventListener('click', function () {
      var n = parseInt(row.getAttribute('data-n'), 10);
      var el = (META.elements || []).find(function (e) { return e.n === n; });
      if (!el) return;
      var key = META.key + '.' + n;
      var nowChosen = !chosenSet[key];
      chosenSet[key] = nowChosen;
      row.classList.toggle('chosen', nowChosen);
      post({ type: 'choose', version: META.key, n: n, label: el.label, chosen: nowChosen });
    });
  });

  // parent can sync which elements are already chosen (so the legend reflects the tray)
  window.addEventListener('message', function (ev) {
    var d = ev.data; if (!d || !d.__qhPicker) return;
    if (d.type === 'sync-chosen' && Array.isArray(d.keys)) {
      chosenSet = {};
      d.keys.forEach(function (k) { chosenSet[k] = true; });
      legend.querySelectorAll('.qpl-row').forEach(function (row) {
        var key = META.key + '.' + row.getAttribute('data-n');
        row.classList.toggle('chosen', !!chosenSet[key]);
      });
    }
  });

  // tell the parent we're ready (parent then sends the current chosen set)
  post({ type: 'ready', version: META.key });

  function escapeHtml(s) {
    return String(s).replace(/[&<>"']/g, function (m) {
      return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[m];
    });
  }
})();