← back to Quadrille Showroom
Capture visitor age as a plain data point on the Sample Tray (replaces Age View)
59eede3b0253081cb5ddf4a03463ffbe310af62b · 2026-07-01 10:53:43 -0700 · Steve
Per Steve's call: age is no longer an in-showroom control — it's a plain field on the
sample-request surface. Added an optional 'Your age' number input to the Sample Tray
(#tray-age), persisted to localStorage('qh_visitor_age'), clamped 0–120, exposed via
window._qh.visitorAge so a future request submit can include it. Styled to match the tray.
Also fixed a PRE-EXISTING bug this surfaced: the dock stows #sample-tray on boot with
.qh-stowed (display:none!important), which the tray's own .visible (display:block) could
never override — so the Sample Tray never actually appeared even when samples were added.
updateSampleTray() now clears qh-stowed when populated (re-adds it when empty).
Verified (Playwright): field renders + is visible when the tray populates; value persists
across reload; _qh.visitorAge reflects it; clamps 999→120; blank clears the key to null;
0 console errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Files touched
M package-lock.jsonM public/css/showroom.cssM public/js/showroom.jsM public/showroom.html
Diff
commit 59eede3b0253081cb5ddf4a03463ffbe310af62b
Author: Steve <steve@designerwallcoverings.com>
Date: Wed Jul 1 10:53:43 2026 -0700
Capture visitor age as a plain data point on the Sample Tray (replaces Age View)
Per Steve's call: age is no longer an in-showroom control — it's a plain field on the
sample-request surface. Added an optional 'Your age' number input to the Sample Tray
(#tray-age), persisted to localStorage('qh_visitor_age'), clamped 0–120, exposed via
window._qh.visitorAge so a future request submit can include it. Styled to match the tray.
Also fixed a PRE-EXISTING bug this surfaced: the dock stows #sample-tray on boot with
.qh-stowed (display:none!important), which the tray's own .visible (display:block) could
never override — so the Sample Tray never actually appeared even when samples were added.
updateSampleTray() now clears qh-stowed when populated (re-adds it when empty).
Verified (Playwright): field renders + is visible when the tray populates; value persists
across reload; _qh.visitorAge reflects it; clamps 999→120; blank clears the key to null;
0 console errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
package-lock.json | 4 ++--
public/css/showroom.css | 6 ++++++
public/js/showroom.js | 27 ++++++++++++++++++++++++++-
public/showroom.html | 4 ++++
4 files changed, 38 insertions(+), 3 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 90d796b..c06af6d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "quadrille-showroom",
- "version": "1.2.0",
+ "version": "1.2.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "quadrille-showroom",
- "version": "1.2.0",
+ "version": "1.2.1",
"license": "ISC",
"dependencies": {
"compression": "^1.8.1",
diff --git a/public/css/showroom.css b/public/css/showroom.css
index 6ef5695..da73e94 100644
--- a/public/css/showroom.css
+++ b/public/css/showroom.css
@@ -133,6 +133,12 @@ body { overflow: hidden; background: #0a0a0f; font-family: 'Segoe UI', system-ui
#tray-count { background: #c9a96e; color: #0a0a0f; width: 18px; height: 18px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; font-size: 10px; font-weight: 700; }
#tray-items { display: flex; gap: 6px; overflow-x: auto; max-width: 500px; }
.tray-item { width: 40px; height: 40px; border-radius: 4px; border: 1px solid rgba(201,169,110,0.3); cursor: pointer; flex-shrink: 0; }
+/* Optional visitor age — a plain data point that rides along with the sample request. */
+#tray-age { display: flex; align-items: center; gap: 8px; margin-top: 8px; padding-top: 7px; border-top: 1px solid rgba(201,169,110,0.12); font-size: 11px; color: #888; }
+#tray-age label { white-space: nowrap; }
+.tray-age-opt { color: #666; }
+#tray-age-input { width: 56px; background: rgba(0,0,0,0.35); border: 1px solid rgba(201,169,110,0.3); border-radius: 6px; color: #e8e2d4; font-size: 12px; padding: 4px 7px; text-align: center; }
+#tray-age-input:focus { outline: none; border-color: #c9a96e; }
/* Bottom Bar */
#bottom-bar {
diff --git a/public/js/showroom.js b/public/js/showroom.js
index cf44df1..6e72310 100644
--- a/public/js/showroom.js
+++ b/public/js/showroom.js
@@ -54,6 +54,7 @@ const BOOT_PITCH = -0.12;
// ============================================================
let scene, camera, renderer, controls, clock, raycaster, mouse;
let wingBoards = [], wingRacks = [], sampleTray = [], products = [], vendors = [];
+let visitorAge = null; // optional visitor age captured on the Sample Tray (plain data point)
let focusedWing = null, animatingWings = [], frameCount = 0, lastFpsTime = 0;
let restingOpenWing = null; // Slice-1: the middle board, open-at-angle on boot as the resting visual (NOT a focus/selection)
// BOOT RACE GUARD (contrarian polish): the canvas click listener is wired at init() but
@@ -3901,6 +3902,23 @@ function initHUD() {
});
}
+ // Optional visitor AGE on the Sample Tray — a plain data point that rides along with the
+ // sample request. No server submit exists yet, so it persists to localStorage and is read
+ // back via window._qh.visitorAge when a request flow is added. (Replaces the removed
+ // in-showroom Age View: age is captured on the form, it no longer drives the visuals.)
+ const ageInput = document.getElementById('tray-age-input');
+ if (ageInput) {
+ const saved = localStorage.getItem('qh_visitor_age');
+ if (saved !== null && saved !== '') { ageInput.value = saved; visitorAge = parseInt(saved) || null; }
+ ageInput.addEventListener('input', () => {
+ const v = ageInput.value.trim();
+ const n = v === '' ? null : Math.max(0, Math.min(120, parseInt(v, 10) || 0));
+ visitorAge = n;
+ if (n == null) localStorage.removeItem('qh_visitor_age');
+ else localStorage.setItem('qh_visitor_age', String(n));
+ });
+ }
+
// REVEAL slider — how much of each closed board's pattern shows in the packed arc.
// Live: re-rakes every board's closed yaw via QHRack.relayout (no full rebuild), so
// the open hero stays open while the slivers widen/narrow underneath. Persisted.
@@ -4119,10 +4137,14 @@ function updateSampleTray() {
const tray = document.getElementById('sample-tray'), items = document.getElementById('tray-items');
document.getElementById('tray-count').textContent = sampleTray.length;
if (sampleTray.length > 0) {
+ // The dock stows the tray on boot with .qh-stowed (display:none!important); clear it so
+ // the tray can actually surface when a sample is added (its .visible display:block can't
+ // beat !important on its own). Without this the Sample Tray — and its age field — never show.
+ tray.classList.remove('qh-stowed');
tray.classList.add('visible'); items.innerHTML = '';
const cc = {'Navy':'#1a2744','Sage':'#6b7f5e','Cream':'#f0ead6','Gold':'#c9a96e','Silver':'#b8b8c0','Blush':'#d4a0a0','Charcoal':'#3a3a42','Ivory':'#f5f0e8','Slate':'#5a6068','Teal':'#2a6b6b','Coral':'#cd6858','Burgundy':'#6b2040'};
sampleTray.forEach(p => { const d = document.createElement('div'); d.className = 'tray-item'; d.style.background = cc[p.color]||'#d8d4cc'; d.title = `${p.pattern_name} - ${p.color}`; items.appendChild(d); });
- } else tray.classList.remove('visible');
+ } else { tray.classList.remove('visible'); tray.classList.add('qh-stowed'); }
}
function applyLightingMode(mode) {
@@ -5127,6 +5149,9 @@ window._qh = {
// avatar's back across the whole frame.
setAvatarVisible(v) { avatarWanted = !!v; if (avatarRig) avatarRig.visible = avatarWanted; },
get avatarVisible() { return avatarWanted; },
+ // Optional visitor age captured on the Sample Tray form — a plain data point for a future
+ // request submit to include; null if the visitor left it blank.
+ get visitorAge() { return visitorAge; },
// ---- PHASE 3 "At the Table" surface — the SEATED third-person figure + consultation
// set, and the over-the-shoulder camera pose. Lazy-built on first show. Gated to the
// 'table' view mode only (separate from the first-person avatarWanted).
diff --git a/public/showroom.html b/public/showroom.html
index 39108f3..46054db 100644
--- a/public/showroom.html
+++ b/public/showroom.html
@@ -299,6 +299,10 @@
<div id="sample-tray">
<div class="tray-header"><span>Sample Tray</span><span id="tray-count">0</span></div>
<div id="tray-items"></div>
+ <div id="tray-age">
+ <label for="tray-age-input">Your age <span class="tray-age-opt">(optional)</span></label>
+ <input id="tray-age-input" type="number" min="0" max="120" step="1" inputmode="numeric" placeholder="—" autocomplete="off">
+ </div>
</div>
<canvas id="minimap" width="160" height="128"></canvas>
← 88e4cd8 chore: v1.2.0 → v1.2.1 (session close — /5x fixes + verifica
·
back to Quadrille Showroom
·
auto-save: 2026-07-01T11:11:52 (2 files) — public/js/showroo df1dc51 →