[object Object]

← back to Quadrille Showroom

NOW VIEWING navigator HUD: museum media-player strip (swatch thumb + serif name + tracked colorway + 27x27 spec + Back/Peruse-play-pause/Next + 'N of N' brass progress). Vanilla CSS/HTML, brass-hairline museum glass per ART-DIRECTION; reuses gotoBoardOffset+togglePeruse, syncs play glyph to tour state. FPS 72/min51, errorCount 0. Figma/Magic MCP design phase tool-gated this session (noted)

a38c66c4ad345814af59c3e2a783c60e467f6cfa · 2026-06-27 12:16:56 -0700 · Steve

Files touched

Diff

commit a38c66c4ad345814af59c3e2a783c60e467f6cfa
Author: Steve <steve@designerwallcoverings.com>
Date:   Sat Jun 27 12:16:56 2026 -0700

    NOW VIEWING navigator HUD: museum media-player strip (swatch thumb + serif name + tracked colorway + 27x27 spec + Back/Peruse-play-pause/Next + 'N of N' brass progress). Vanilla CSS/HTML, brass-hairline museum glass per ART-DIRECTION; reuses gotoBoardOffset+togglePeruse, syncs play glyph to tour state. FPS 72/min51, errorCount 0. Figma/Magic MCP design phase tool-gated this session (noted)
---
 public/css/showroom.css | 92 +++++++++++++++++++++++++++++++++++++++++++++++++
 public/js/showroom.js   | 76 ++++++++++++++++++++++++++++++++++++++--
 public/showroom.html    | 21 +++++++++++
 3 files changed, 186 insertions(+), 3 deletions(-)

diff --git a/public/css/showroom.css b/public/css/showroom.css
index 142347f..5490519 100644
--- a/public/css/showroom.css
+++ b/public/css/showroom.css
@@ -188,3 +188,95 @@ body { overflow: hidden; background: #0a0a0f; font-family: 'Segoe UI', system-ui
 ::-webkit-scrollbar { width: 4px; }
 ::-webkit-scrollbar-track { background: transparent; }
 ::-webkit-scrollbar-thumb { background: rgba(201,169,110,0.3); border-radius: 2px; }
+
+/* ============================================================
+   NOW VIEWING — museum media-player navigator strip
+   Shown only when a wingboard is focused. Brass-hairline + museum
+   glass; conforms to ART-DIRECTION.md (refined, calm, not a dashboard).
+   Sits bottom-centre, above the window/sort bar. Augments the detail
+   card's browse row with a player-style transport + N-of-N progress.
+   ============================================================ */
+#now-viewing {
+  position: fixed; left: 50%; bottom: 96px; transform: translateX(-50%);
+  z-index: 28; width: min(460px, 70vw);
+  background: linear-gradient(180deg, rgba(20,20,26,0.92), rgba(12,12,17,0.95));
+  border: 1px solid rgba(201,169,110,0.28);
+  border-radius: 14px; overflow: hidden;
+  box-shadow: 0 16px 44px -14px rgba(0,0,0,0.72), inset 0 1px 0 rgba(255,255,255,0.04);
+  backdrop-filter: blur(16px);
+  opacity: 1; transition: opacity .35s ease, transform .35s ease;
+}
+#now-viewing.hidden { opacity: 0; transform: translateX(-50%) translateY(8px); pointer-events: none; }
+
+/* Thin brass progress rail across the very top — the "track position" */
+.nv-progress { height: 2px; background: rgba(201,169,110,0.14); width: 100%; }
+.nv-progress-fill {
+  height: 100%; width: 0%;
+  background: linear-gradient(90deg, #b9933f, #e8cf93);
+  box-shadow: 0 0 6px rgba(201,169,110,0.5);
+  transition: width .45s cubic-bezier(.22,.61,.36,1);
+}
+
+.nv-body { display: flex; align-items: center; gap: 14px; padding: 11px 16px 12px; }
+
+/* Swatch thumbnail — small framed chip of the actual wallcovering */
+.nv-swatch {
+  width: 46px; height: 46px; flex-shrink: 0; border-radius: 7px; overflow: hidden;
+  background: #15151c; border: 1px solid rgba(201,169,110,0.35);
+  box-shadow: 0 2px 8px rgba(0,0,0,0.5);
+}
+.nv-swatch img { width: 100%; height: 100%; object-fit: cover; display: block; }
+
+/* Meta block — serif name, tracked colorway + spec */
+.nv-meta { flex: 1; min-width: 0; }
+.nv-eyebrow {
+  font-size: 8.5px; letter-spacing: 2.6px; text-transform: uppercase;
+  color: #c9a96e; opacity: .82; margin-bottom: 3px;
+}
+.nv-name {
+  font-family: Georgia, 'Times New Roman', serif; font-size: 17px; font-weight: 600;
+  color: #f0e6cf; line-height: 1.1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
+}
+.nv-sub {
+  display: flex; align-items: center; gap: 7px; margin-top: 3px;
+  font-size: 11px; color: #a7a299; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
+}
+.nv-color { letter-spacing: 1.4px; text-transform: uppercase; font-size: 10px; color: #b6b0a4; }
+.nv-dot { color: rgba(201,169,110,0.5); }
+.nv-spec { font-variant-numeric: tabular-nums; color: #cfc7b8; }
+
+/* Transport — Back / Peruse play-pause / Next, brass hairline circles */
+.nv-transport { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
+.nv-btn {
+  width: 30px; height: 30px; border-radius: 50%;
+  border: 1px solid rgba(201,169,110,0.34);
+  background: rgba(201,169,110,0.07); color: #c9a96e;
+  font-size: 11px; cursor: pointer; line-height: 1;
+  display: flex; align-items: center; justify-content: center;
+  transition: background .18s, border-color .18s, transform .12s;
+}
+.nv-btn:hover { background: rgba(201,169,110,0.2); transform: translateY(-1px); }
+.nv-btn:active { transform: translateY(0); }
+.nv-play {
+  width: 38px; height: 38px; font-size: 13px;
+  background: linear-gradient(180deg, rgba(216,189,132,0.22), rgba(185,147,63,0.18));
+  border-color: rgba(201,169,110,0.5);
+}
+.nv-play.playing {
+  background: linear-gradient(180deg, #d8bd84, #b9933f); color: #1a1206;
+  box-shadow: 0 0 12px rgba(201,169,110,0.45);
+}
+
+/* N of 50 progress count — tabular, brass accent on the index */
+.nv-count {
+  display: flex; align-items: baseline; gap: 4px; flex-shrink: 0;
+  font-variant-numeric: tabular-nums; font-size: 11px; color: #837d72;
+  letter-spacing: .5px; padding-left: 2px;
+}
+.nv-count #nv-index { color: #e8cf93; font-size: 14px; font-weight: 600; }
+.nv-count .nv-of { font-size: 9px; letter-spacing: 1.5px; text-transform: uppercase; opacity: .7; }
+
+@media (max-width: 720px) {
+  #now-viewing { bottom: 150px; width: 88vw; }
+  .nv-count { display: none; }
+}
diff --git a/public/js/showroom.js b/public/js/showroom.js
index cc7682f..359635e 100644
--- a/public/js/showroom.js
+++ b/public/js/showroom.js
@@ -1263,6 +1263,7 @@ function unfocusWing(skipPanel) {
   // skipPanel=true and the next cladWalls overwrites, so no neutral flicker.
   if (!skipPanel) revertWalls();
   if (!skipPanel) document.getElementById('wing-detail').classList.add('hidden');
+  if (!skipPanel) hideNowViewing();   // tuck the navigator strip away on a true exit
 
   // Unlock first so smoothCameraTo can move
   unlockControls();
@@ -1340,6 +1341,65 @@ function showWingDetail(product) {
   // CTA label by mode
   const cta = document.getElementById('btn-view-dw');
   if (cta) cta.textContent = product.cta_mode === 'live' ? 'View on Designer Wallcoverings' : 'Request a Memo Sample';
+
+  // Feed the NOW VIEWING media-player strip in lock-step with the detail card.
+  updateNowViewing(product);
+}
+
+// NOW VIEWING navigator — populate the museum media-player strip from the focused
+// board: swatch thumb + serif pattern name + tracked colorway + 27×27 spec + the
+// global N-of-N progress (across the whole window, not just the visible boards).
+function updateNowViewing(product) {
+  const nv = document.getElementById('now-viewing');
+  if (!nv) return;
+  nv.classList.remove('hidden');
+
+  const { pattern, colorway } = splitPatternColor(product.pattern_name || product.name);
+  const nm = document.getElementById('nv-name');
+  if (nm) nm.textContent = pattern || 'Pattern';
+  // Colorway: real colorway > derived from title > collection name (never the literal
+  // placeholder word). Hide the separator dot if there's genuinely nothing to show.
+  const col = document.getElementById('nv-color');
+  const dot = document.querySelector('#now-viewing .nv-dot');
+  const cw = product.color || colorway || product.collection || '';
+  if (col) col.textContent = cw;
+  if (dot) dot.style.display = cw ? '' : 'none';
+
+  // Spec: width × repeat (China Seas straight-match = 27″ × 27″). Falls back gracefully.
+  const fmtIn = (n) => (n || n === 0) ? `${(Math.round(n * 10) / 10)}″` : '';
+  const w = product.width_inches, r = product.repeat_inches;
+  const specEl = document.getElementById('nv-spec');
+  if (specEl) specEl.textContent = (w && r) ? `${fmtIn(w)} × ${fmtIn(r)}` : (w ? fmtIn(w) : '27″ × 27″');
+
+  // Swatch thumb — the actual wallcovering image (not the room shot).
+  const thumb = document.getElementById('nv-thumb');
+  const src = product.image || product.rawImage || product.room;
+  if (thumb && src) thumb.src = src.charAt(0) === '/' ? src : ('/api/proxy/image?url=' + encodeURIComponent(src));
+
+  // N of N — global position across the active window/collection.
+  const idx = (typeof windowOffset === 'number' ? windowOffset : 0) +
+              (focusedWing && focusedWing.userData ? focusedWing.userData.index : 0) + 1;
+  const total = windowTotal || products.length || 0;
+  const iEl = document.getElementById('nv-index'); if (iEl) iEl.textContent = idx;
+  const tEl = document.getElementById('nv-total'); if (tEl) tEl.textContent = total || '—';
+  const fill = document.getElementById('nv-fill');
+  if (fill) fill.style.width = (total ? Math.max(2, Math.min(100, (idx / total) * 100)) : 2) + '%';
+
+  syncNowViewingPlay();
+}
+
+// Keep the strip's play/pause glyph in sync with the Peruse tour state.
+function syncNowViewingPlay() {
+  const pb = document.getElementById('nv-play');
+  if (!pb) return;
+  pb.classList.toggle('playing', !!peruseActive);
+  pb.innerHTML = peruseActive ? '❚❚' : '▷';
+  pb.title = peruseActive ? 'Pause the tour' : 'Peruse (auto-tour)';
+}
+
+function hideNowViewing() {
+  const nv = document.getElementById('now-viewing');
+  if (nv) nv.classList.add('hidden');
 }
 
 function updateOpenBtnState() {
@@ -1643,9 +1703,11 @@ function stopPeruse(keepFocus) {
 
 function setPeruseBtn(on) {
   const b = document.getElementById('btn-peruse');
-  if (!b) return;
-  b.classList.toggle('active', on);
-  b.innerHTML = on ? '■ Stop Perusing' : '▷ Peruse Collection';
+  if (b) {
+    b.classList.toggle('active', on);
+    b.innerHTML = on ? '■ Stop Perusing' : '▷ Peruse Collection';
+  }
+  syncNowViewingPlay();   // keep the navigator strip's play glyph in lock-step
 }
 
 // ============================================================
@@ -1661,6 +1723,14 @@ function initHUD() {
   document.getElementById('close-detail').addEventListener('click', () => unfocusWing());
   document.getElementById('btn-fan-left').addEventListener('click', () => gotoBoardOffset(-1));   // Back
   document.getElementById('btn-fan-right').addEventListener('click', () => gotoBoardOffset(1));    // Next
+
+  // NOW VIEWING navigator transport — reuses the same board-flip + Peruse engine.
+  const nvBack = document.getElementById('nv-back');
+  const nvNext = document.getElementById('nv-next');
+  const nvPlay = document.getElementById('nv-play');
+  if (nvBack) nvBack.addEventListener('click', () => { stopPeruse(true); gotoBoardOffset(-1); });
+  if (nvNext) nvNext.addEventListener('click', () => { stopPeruse(true); gotoBoardOffset(1); });
+  if (nvPlay) nvPlay.addEventListener('click', () => togglePeruse());
   document.getElementById('btn-back-wing').addEventListener('click', () => unfocusWing());
 
   document.getElementById('btn-music').addEventListener('click', () => document.getElementById('music-player').classList.toggle('hidden'));
diff --git a/public/showroom.html b/public/showroom.html
index 2c7599d..c1a5836 100644
--- a/public/showroom.html
+++ b/public/showroom.html
@@ -140,6 +140,27 @@
       </span>
     </div>
 
+    <!-- NOW VIEWING navigator — museum media-player strip (shown only on focus).
+         Brass-hairline + museum glass, conforms to ART-DIRECTION.md. Augments the
+         detail card's browse row: swatch · serif name · colorway · spec · transport. -->
+    <div id="now-viewing" class="hidden" aria-label="Now viewing navigator">
+      <div class="nv-progress"><div class="nv-progress-fill" id="nv-fill"></div></div>
+      <div class="nv-body">
+        <div class="nv-swatch"><img id="nv-thumb" alt=""></div>
+        <div class="nv-meta">
+          <div class="nv-eyebrow">Now Viewing</div>
+          <div class="nv-name" id="nv-name">Pattern</div>
+          <div class="nv-sub"><span id="nv-color" class="nv-color">Colorway</span><span class="nv-dot">&middot;</span><span id="nv-spec" class="nv-spec">27&Prime; &times; 27&Prime;</span></div>
+        </div>
+        <div class="nv-transport">
+          <button id="nv-back" class="nv-btn" title="Back (previous wingboard)" aria-label="Back">&#9664;</button>
+          <button id="nv-play" class="nv-btn nv-play" title="Peruse (play / pause the tour)" aria-label="Peruse play-pause">&#9655;</button>
+          <button id="nv-next" class="nv-btn" title="Next wingboard" aria-label="Next">&#9654;</button>
+        </div>
+        <div class="nv-count"><span id="nv-index">1</span><span class="nv-of">of</span><span id="nv-total">50</span></div>
+      </div>
+    </div>
+
     <div id="bottom-bar">
       <span id="info-text">WASD to walk up to a wing &middot; within 4 ft it opens to the full continuous design &middot; Click to inspect &middot; P to Peruse</span>
       <span id="fps-counter">60 FPS</span>

← 1e55735 PJ hero framing: narrow FOV (55->30) on focus + back off pro  ·  back to Quadrille Showroom  ·  WALL realism: derive subtle normal+roughness maps from wall- bb93cda →