[object Object]

← 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

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 →