[object Object]

← back to Vinylwallpaper

admin: add dw-theme-nav 20-theme navigator (upper-right, admin-only)

f292080629ac5a38200ddad1af639340e74efc37 · 2026-06-01 16:38:44 -0700 · Steve Abrams

Files touched

Diff

commit f292080629ac5a38200ddad1af639340e74efc37
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Jun 1 16:38:44 2026 -0700

    admin: add dw-theme-nav 20-theme navigator (upper-right, admin-only)
---
 public/dw-theme-nav.js | 123 +++++++++++++++++++++++++++++++++++++++++++++++++
 public/index.html      |   1 +
 2 files changed, 124 insertions(+)

diff --git a/public/dw-theme-nav.js b/public/dw-theme-nav.js
new file mode 100644
index 0000000..5edf346
--- /dev/null
+++ b/public/dw-theme-nav.js
@@ -0,0 +1,123 @@
+/* dw-theme-nav.js — admin-only theme navigator (drop-in).
+ *
+ * Lets a logged-in admin step ‹ back / next › through a library of ~20 color
+ * themes, live, from a pill in the UPPER-RIGHT corner. Each theme rewrites the
+ * site's CSS custom properties (--bg/--paper/--muted/--line/--accent/--bg-soft),
+ * so it previews any palette instantly. Choice persists per-site in localStorage.
+ *
+ * Drop-in:  <script src="/dw-theme-nav.js" defer></script>
+ * Admin gate: shows ONLY when localStorage.dwAdmin === '1' (same gate as
+ *   dw-header.js; set via ?admin=1). Public visitors never see it.
+ * Optional override BEFORE load:
+ *   window.DwThemeNavConfig = { themes: [...], extraVars: {'--card':'#..'} }
+ *
+ * Idempotent · self-styled · zero deps.
+ */
+(function () {
+  'use strict';
+  if (window.__dwThemeNavLoaded) return;
+  window.__dwThemeNavLoaded = true;
+
+  // admin gate (mirror dw-header.js)
+  var isAdmin = false;
+  try {
+    var p = new URLSearchParams(location.search);
+    if (p.get('admin') === '1') localStorage.setItem('dwAdmin', '1');
+    if (p.get('admin') === '0') localStorage.removeItem('dwAdmin');
+    isAdmin = localStorage.getItem('dwAdmin') === '1';
+  } catch (e) {}
+  if (!isAdmin) return;
+
+  var CFG = window.DwThemeNavConfig || {};
+  var KEY = 'dwTheme:' + location.hostname.replace(/^www\./, '');
+
+  // vars order: [bg, paper(text), muted, line, accent, bg-soft]
+  var THEMES = CFG.themes || [
+    { n: 'Classic Cream',     v: ['#f5f2eb', '#1c1a16', '#8b7866', 'rgba(28,26,22,.12)', '#9c7a4d', '#ece6d8'] },
+    { n: 'Victorian Oxblood', v: ['#f4ece0', '#2a1714', '#8a6f5e', 'rgba(42,23,20,.12)', '#7b2d26', '#ece0cf'] },
+    { n: 'Belle Époque Sage', v: ['#f1ede2', '#2f2c22', '#877f63', 'rgba(47,44,34,.12)', '#6f7d57', '#e6e0cd'] },
+    { n: 'Edwardian Pale',    v: ['#f7f3ec', '#34302a', '#978b78', 'rgba(52,48,42,.10)', '#9aa489', '#eee7d8'] },
+    { n: 'Art Deco Gold',     v: ['#f3eee2', '#15140f', '#8a7c55', 'rgba(21,20,15,.14)', '#b8932e', '#e7ddc4'] },
+    { n: 'Streamline Teal',   v: ['#eef1f2', '#1c2429', '#7c8990', 'rgba(28,36,41,.12)', '#3f6e7d', '#dde5e8'] },
+    { n: 'Atomic Turquoise',  v: ['#f6f2e9', '#26241f', '#8c8275', 'rgba(38,36,31,.12)', '#2f9e96', '#ece4d3'] },
+    { n: 'Mod Orange',        v: ['#f7f2e6', '#221d18', '#8a7e66', 'rgba(34,29,24,.13)', '#e2622c', '#efe6d2'] },
+    { n: 'Earthy 70s',        v: ['#f3ebdb', '#281f17', '#8a7a5e', 'rgba(40,31,23,.13)', '#b5651d', '#e9ddc4'] },
+    { n: 'Memphis Pink',      v: ['#faf7f2', '#14110f', '#8a8a8a', 'rgba(20,17,15,.14)', '#ff2d8b', '#efeae3'] },
+    { n: 'Noir',              v: ['#0f0f0f', '#f4f4f2', '#9a9a9a', 'rgba(255,255,255,.14)', '#e8e8e8', '#1a1a1a'] },
+    { n: 'Forest',            v: ['#f1f3ee', '#1a241c', '#7d8a7e', 'rgba(26,36,28,.12)', '#2f6b45', '#e0e8e0'] },
+    { n: 'Royal Navy',        v: ['#eef0f5', '#14192a', '#7e8499', 'rgba(20,25,42,.12)', '#27407a', '#dde2ee'] },
+    { n: 'Burgundy',          v: ['#f5efee', '#241416', '#8c7376', 'rgba(36,20,22,.12)', '#7a263a', '#ece0e0'] },
+    { n: 'Slate Modern',      v: ['#eef0f1', '#1b2024', '#7f8890', 'rgba(27,32,36,.12)', '#52606b', '#dee2e5'] },
+    { n: 'Desert Sand',       v: ['#f6efe3', '#2c2419', '#9a8a6e', 'rgba(44,36,25,.12)', '#c08a45', '#ece0c9'] },
+    { n: 'Emerald Luxe',      v: ['#101814', '#eef3ee', '#84a394', 'rgba(235,250,242,.12)', '#3fd09a', '#1a2720'] },
+    { n: 'Plum',              v: ['#f4f0f4', '#241827', '#897689', 'rgba(36,24,39,.12)', '#7b4a8a', '#e9e0ec'] },
+    { n: 'Copper',            v: ['#f5efe9', '#241a14', '#8c7a6a', 'rgba(36,26,20,.12)', '#b5651d', '#ece0d4'] },
+    { n: 'Ocean',             v: ['#eef3f5', '#15252b', '#7d949c', 'rgba(21,37,43,.12)', '#1f7d96', '#dde9ed'] },
+  ];
+
+  var VARS = ['--bg', '--paper', '--muted', '--line', '--accent', '--bg-soft'];
+
+  function apply(i) {
+    var t = THEMES[(i % THEMES.length + THEMES.length) % THEMES.length];
+    var root = document.documentElement.style;
+    for (var k = 0; k < VARS.length; k++) root.setProperty(VARS[k], t.v[k]);
+    if (CFG.extraVars) for (var ev in CFG.extraVars) root.setProperty(ev, CFG.extraVars[ev]);
+    var lbl = document.getElementById('dwtn-label');
+    if (lbl) lbl.textContent = t.n + '  ·  ' + ((i % THEMES.length + THEMES.length) % THEMES.length + 1) + '/' + THEMES.length;
+  }
+
+  var idx = 0;
+  try { var s = parseInt(localStorage.getItem(KEY), 10); if (!isNaN(s)) idx = s; } catch (e) {}
+
+  function go(d) {
+    idx = (idx + d % THEMES.length + THEMES.length) % THEMES.length;
+    apply(idx);
+    try { localStorage.setItem(KEY, String(idx)); } catch (e) {}
+  }
+
+  // ---- styles ----
+  var css = document.createElement('style');
+  css.textContent = [
+    '#dwtn{position:fixed;top:14px;right:18px;z-index:9999;display:flex;align-items:center;gap:2px;',
+      'background:rgba(10,10,10,.82);backdrop-filter:blur(8px);border:1px solid rgba(245,241,232,.18);',
+      'border-radius:999px;padding:3px 4px;font-family:-apple-system,system-ui,sans-serif;box-shadow:0 6px 20px rgba(0,0,0,.4)}',
+    '#dwtn button{display:flex;align-items:center;justify-content:center;width:26px;height:26px;border:0;background:transparent;',
+      'color:#f5f1e8;font-size:15px;line-height:1;cursor:pointer;border-radius:50%;transition:background .15s}',
+    '#dwtn button:hover{background:rgba(245,241,232,.14)}',
+    '#dwtn #dwtn-label{font-size:11px;letter-spacing:.04em;color:#f5f1e8;min-width:120px;text-align:center;padding:0 6px;white-space:nowrap;opacity:.92}',
+    '#dwtn .dwtn-reset{font-size:12px;opacity:.6}',
+  ].join('');
+  document.head.appendChild(css);
+
+  // ---- widget ----
+  function build() {
+    var el = document.createElement('div');
+    el.id = 'dwtn';
+    el.setAttribute('aria-label', 'Theme navigator (admin)');
+    el.innerHTML =
+      '<button type="button" id="dwtn-prev" aria-label="Previous theme" title="Previous theme (←)">‹</button>' +
+      '<span id="dwtn-label">—</span>' +
+      '<button type="button" id="dwtn-next" aria-label="Next theme" title="Next theme (→)">›</button>' +
+      '<button type="button" class="dwtn-reset" id="dwtn-reset" aria-label="Reset to site default" title="Reset to site default">↺</button>';
+    document.body.appendChild(el);
+    document.getElementById('dwtn-prev').addEventListener('click', function () { go(-1); });
+    document.getElementById('dwtn-next').addEventListener('click', function () { go(1); });
+    document.getElementById('dwtn-reset').addEventListener('click', function () {
+      try { localStorage.removeItem(KEY); } catch (e) {}
+      var root = document.documentElement.style;
+      for (var k = 0; k < VARS.length; k++) root.removeProperty(VARS[k]);
+      idx = 0;
+      var lbl = document.getElementById('dwtn-label'); if (lbl) lbl.textContent = 'Site default';
+    });
+    document.addEventListener('keydown', function (e) {
+      if (e.target && /input|textarea|select/i.test(e.target.tagName)) return;
+      if (e.key === 'ArrowLeft' && e.altKey) { go(-1); }
+      if (e.key === 'ArrowRight' && e.altKey) { go(1); }
+    });
+    // restore saved theme on load
+    try { if (localStorage.getItem(KEY) !== null) apply(idx); else { var l = document.getElementById('dwtn-label'); if (l) l.textContent = 'Site default'; } } catch (e) {}
+  }
+
+  if (document.body) build();
+  else document.addEventListener('DOMContentLoaded', build);
+})();
diff --git a/public/index.html b/public/index.html
index 3852f2d..0c4199c 100644
--- a/public/index.html
+++ b/public/index.html
@@ -928,6 +928,7 @@ document.addEventListener('DOMContentLoaded', function(){ try { renderRailSectio
 <!-- ============================================================
      DW UNIVERSAL CONTACT MODULE — fashion-house UX
      Inject before   <script src="/hero-4grid.js" defer></script>
+<script src="/dw-theme-nav.js" defer></script>
 </body> in every DW-family sister site.
      Self-contained: CSS + 4 modals + JS submission handlers.
      Uses host site's CSS vars (--bg, --fg, --gold, --rule).

← c5cac51 fleet rebuild: dw-header (hamburgers UL + centered name) + u  ·  back to Vinylwallpaper  ·  a11y: add aria-label to contact/sample form inputs (zero vis 2ac3d7e →