[object Object]

← back to Wallco Ai

PDP theme switcher: working public dropdown (Classic + 17 variants) in .switcher; fix broken ← Classic link (route via ?classic=1)

08fb7a1203fb6f19455f6d9deb33db0c3757fd93 · 2026-06-01 19:53:45 -0700 · Steve Abrams

Files touched

Diff

commit 08fb7a1203fb6f19455f6d9deb33db0c3757fd93
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Jun 1 19:53:45 2026 -0700

    PDP theme switcher: working public dropdown (Classic + 17 variants) in .switcher; fix broken ← Classic link (route via ?classic=1)
---
 public/theme-variants/theme-variant-nav.js | 100 +++++++++++++++++++++++++----
 1 file changed, 88 insertions(+), 12 deletions(-)

diff --git a/public/theme-variants/theme-variant-nav.js b/public/theme-variants/theme-variant-nav.js
index 1ca47b5..be201ae 100644
--- a/public/theme-variants/theme-variant-nav.js
+++ b/public/theme-variants/theme-variant-nav.js
@@ -1,24 +1,26 @@
-/* theme-variant-nav.js — admin-only ‹ prev / next › theme stepper, upper-right.
+/* theme-variant-nav.js — PDP theme switchers for the 17 variants.
  *
  * Drop-in (ABSOLUTE path — variants are served at /design/:id/:slug):
  *   <script src="/theme-variants/theme-variant-nav.js" defer></script>
- * Admin gate: shows ONLY when localStorage.dwAdmin === '1' (set via ?admin=1).
- * Steps through all 17 PDP theme variants while preserving the design id + query,
- * so you stay on the same design. Marks the LIVE theme (from /data/pdp-theme.json).
- * Self-styled · idempotent · zero deps. Alt+←/→ also step.
+ *
+ * Two switchers:
+ *  (1) PUBLIC theme dropdown — turns each variant's static .switcher bar
+ *      ("Theme: X  ← Classic") into a working <select> of Classic + all 17
+ *      variants. Runs for everyone (the bar is already public). Also fixes the
+ *      broken "← Classic" link (its inline href points at the bare /design/:id
+ *      which serves the LIVE theme, so when a variant is the live default it
+ *      loops back to itself — we route Classic through ?classic=1).
+ *  (2) ADMIN ‹ prev / next › stepper pill (upper-right) — shows ONLY when
+ *      localStorage.dwAdmin === '1' (set via ?admin=1). Alt+←/→ also step.
+ * Marks the LIVE theme (from /api/pdp-theme). Self-styled · idempotent · 0 deps.
  */
 (function () {
   'use strict';
   if (window.__themeVariantNavLoaded) return;
   window.__themeVariantNavLoaded = true;
 
-  // admin gate
-  try {
-    var q = new URLSearchParams(location.search);
-    if (q.get('admin') === '1') localStorage.setItem('dwAdmin', '1');
-    if (q.get('admin') === '0') localStorage.removeItem('dwAdmin');
-    if (localStorage.getItem('dwAdmin') !== '1') return;
-  } catch (e) { return; }
+  // NOTE: the admin gate is DOWN below — it wraps ONLY the ‹ › stepper pill.
+  // The public dropdown runs for everyone.
 
   // ordered route slugs (match server /design/:id/:slug + smoke test) → display name
   var THEMES = [
@@ -73,6 +75,80 @@
 
   var LIVE_IDX = -1;
 
+  // ───────────────────────────────────────────────────────────────────────
+  // (1) PUBLIC theme dropdown (everyone). Classic routes through ?classic=1
+  // (server forces the monolith) so it works even when a variant is the live
+  // default; variants route through /design/:id/<slug> (always that variant).
+  // ───────────────────────────────────────────────────────────────────────
+  function classicUrl() {
+    if (mode !== 'route') return '/';                 // file-mode dev: no monolith
+    var p = new URLSearchParams(location.search);
+    p.set('classic', '1');
+    return '/design/' + encodeURIComponent(designId) + '?' + p.toString();
+  }
+  function buildDropdown() {
+    var sw = document.querySelector('header .switcher') || document.querySelector('.switcher');
+    if (!sw || sw.querySelector('.tvn-dd')) return;   // no bar, or already built
+    if (!document.getElementById('tvn-dd-css')) {
+      var st = document.createElement('style');
+      st.id = 'tvn-dd-css';
+      st.textContent = '.switcher .tvn-dd{font:inherit;font-size:11px;line-height:1.4;color:var(--ink,#1a1611);'
+        + 'background:transparent;border:1px solid rgba(0,0,0,.28);border-radius:6px;padding:2px 6px;'
+        + 'cursor:pointer;max-width:230px;vertical-align:middle}';
+      document.head.appendChild(st);
+    }
+    var sel = document.createElement('select');
+    sel.className = 'tvn-dd';
+    sel.setAttribute('aria-label', 'Switch PDP theme');
+    var oc = document.createElement('option');
+    oc.value = '__classic__';
+    oc.textContent = 'Classic (full PDP)';
+    sel.appendChild(oc);
+    THEMES.forEach(function (t, i) {
+      var o = document.createElement('option');
+      o.value = t[0];
+      o.textContent = 'V' + (i + 1) + ' · ' + t[1] + (i === LIVE_IDX ? '  • live' : '');
+      if (i === idx) o.selected = true;
+      sel.appendChild(o);
+    });
+    sel.addEventListener('change', function () {
+      location.href = (sel.value === '__classic__') ? classicUrl() : urlFor(SLUGS.indexOf(sel.value));
+    });
+    // Repurpose the existing "← Classic" link (inline href is the broken bare
+    // URL). Force it through classicUrl() so it works even if the page's own
+    // inline script resets the href afterwards.
+    var back = sw.querySelector('#back-to-classic');
+    if (back) {
+      back.href = classicUrl();
+      back.addEventListener('click', function (e) { e.preventDefault(); location.href = classicUrl(); });
+    }
+    sw.appendChild(document.createTextNode(' '));
+    sw.appendChild(sel);
+  }
+  function tvnInit() {
+    // Resolve live theme (to flag "• live"), then build. Build regardless of
+    // fetch outcome — the dropdown must work even if /api/pdp-theme is down.
+    fetch('/api/pdp-theme', { credentials: 'same-origin' })
+      .then(function (r) { return r.ok ? r.json() : null; })
+      .then(function (j) {
+        var s = j && typeof j.theme === 'string' ? j.theme.toLowerCase().trim() : '';
+        var li = SLUGS.indexOf(s);
+        if (li !== -1) LIVE_IDX = li;
+      })
+      .catch(function () {})
+      .then(buildDropdown);
+  }
+  if (document.body) tvnInit();
+  else document.addEventListener('DOMContentLoaded', tvnInit);
+
+  // ───────────── (2) ADMIN ‹ prev / next › stepper pill (admin-only) ─────────────
+  try {
+    var q = new URLSearchParams(location.search);
+    if (q.get('admin') === '1') localStorage.setItem('dwAdmin', '1');
+    if (q.get('admin') === '0') localStorage.removeItem('dwAdmin');
+    if (localStorage.getItem('dwAdmin') !== '1') return;
+  } catch (e) { return; }
+
   // ---- styles ----
   var css = document.createElement('style');
   css.textContent = [

← 833bd9f genComfy: COMFY_SAMPLER/COMFY_SCHEDULER env knob (seam inves  ·  back to Wallco Ai  ·  follow-ups #2 + #3: settlement trigger on all_designs (post- 05e0e01 →