← 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 { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[m];
});
}
})();