view mrjunejune/src/index.js @ 264:04fee26ecce0

add authenticated JRPG conversation platform Add reusable auth/session storage, owned conversation recovery, guest quotas, admin workflows, URL-routed conversation UI, mobile frame support, and parallel browser acceptance. Co-authored-by: Copilot <[email protected]>
author MrJuneJune <me@mrjunejune.com>
date Fri, 07 Aug 2026 07:34:12 -0700
parents 60a876c4587a
children
line wrap: on
line source

const THEME_STORAGE_KEY = 'theme-preference';
const THEMES = ['auto', 'paper', 'ink', 'playful'];
const THEME_LABELS = {
  auto: 'Auto',
  paper: 'Paper',
  ink: 'Ink',
  playful: 'Playful',
};

function normalizeTheme(value) {
  if (value === 'light') return 'paper';
  if (value === 'dark') return 'ink';
  return THEMES.includes(value) ? value : 'auto';
}

function getThemePreference() {
  return normalizeTheme(localStorage.getItem(THEME_STORAGE_KEY));
}

function updateThemeColor() {
  const probe = document.createElement('span');
  probe.style.background = 'var(--zenbu-sys-color-surface-page)';
  document.body.append(probe);
  const color = getComputedStyle(probe).backgroundColor;
  probe.remove();
  document.querySelector('meta[name="theme-color"]')
    ?.setAttribute('content', color);
}

function updateThemeControl(preference) {
  const toggle = document.querySelector('#themeToggle');
  const label = document.querySelector('#themeName');
  if (label) label.textContent = THEME_LABELS[preference];
  toggle?.setAttribute(
    'aria-label',
    `Change site theme. Current theme: ${THEME_LABELS[preference]}`,
  );
}

function applyTheme(value, persist = false) {
  const preference = normalizeTheme(value);
  const root = document.documentElement;
  root.classList.remove('light-mode', 'dark', 'auto');
  if (preference === 'auto') delete root.dataset.zenTheme;
  else root.dataset.zenTheme = preference;
  if (persist) localStorage.setItem(THEME_STORAGE_KEY, preference);
  updateThemeControl(preference);
  requestAnimationFrame(updateThemeColor);
  document.dispatchEvent(new CustomEvent('zen-theme-change', {
    detail: { theme: preference },
  }));
  return preference;
}

const currentPreference = applyTheme(getThemePreference());
if (localStorage.getItem(THEME_STORAGE_KEY) !== currentPreference) {
  localStorage.setItem(THEME_STORAGE_KEY, currentPreference);
}

function enhanceLinks(root = document) {
  const links = root instanceof HTMLAnchorElement
    ? [root]
    : [...root.querySelectorAll?.('a') || []];
  for (const link of links) {
    if (link.closest('zen-link, zen-button') ||
        link.dataset.zenLinkEffect === 'none') {
      continue;
    }
    const wrapper = document.createElement('zen-link');
    wrapper.setAttribute('effect', 'paw');
    link.before(wrapper);
    wrapper.append(link);
  }
}

window.addEventListener('DOMContentLoaded', () => {
  enhanceLinks();
  const observer = new MutationObserver(records => {
    for (const record of records) {
      for (const node of record.addedNodes) {
        if (node instanceof Element) enhanceLinks(node);
      }
    }
  });
  observer.observe(document.body, { childList: true, subtree: true });
});

document.querySelector('#themeToggle')?.addEventListener('click', () => {
  const current = getThemePreference();
  const next = THEMES[(THEMES.indexOf(current) + 1) % THEMES.length];
  applyTheme(next, true);
});

matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
  if (getThemePreference() === 'auto') updateThemeColor();
});

window.MrJuneJuneTheme = Object.freeze({
  apply: theme => applyTheme(theme, true),
  current: getThemePreference,
  themes: [...THEMES],
});