← back to Contractwallpaper

public/dw-theme-nav.js

124 lines

/* 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);
})();