feat(v0.5.0-prep): manual theme toggle (Phase 2)

Adds an Auto / Dark / Light radio group to the Options page that
overrides the OS `prefers-color-scheme` setting. Stored in
chrome.storage.local under tuner.theme. Defaults to Auto.

Architecture:
- src/lib/theme.js (new, ~55 lines) — getTheme/setTheme/applyTheme/initTheme
  helpers. applyTheme sets/removes `data-theme` attribute on <html>;
  initTheme reads storage + applies.
- popup.js + newtab.js + options.js: call initTheme() FIRST in their
  init() so the theme paints before anything else. All three also
  listen for chrome.storage.onChanged on the THEME_KEY and live-apply
  changes — pick Light in Options, popup + newtab flip instantly.
- options.html: new 'Appearance' card with 3 radio buttons (Auto/Dark/
  Light) above the existing Playback card.
- options.css: styled radio group (pill-shaped, accent border on
  :checked, hover state). Plus the Auto/Dark/Light CSS overrides
  themselves.
- popup.css, newtab.css, options.css: each gets html[data-theme=light]
  and html[data-theme=dark] blocks that override :root vars. Attribute
  selector specificity beats both :root and the @media :root, so the
  manual override wins when set.

UX:
- Default = Auto (follows OS via existing @media block)
- Pick Dark → overrides OS, forces dark palette
- Pick Light → overrides OS, forces light palette
- Selection persists across reloads, syncs across all three surfaces

7 files, ~150 lines added. No new permissions, no new dependencies.

Bundled with v0.4.1-prep back-link + Phase 1 OS-follow light mode for
the v0.5.0 release.
This commit is contained in:
2026-06-10 00:13:07 +01:00
parent 5510cebde1
commit d0d5e76abe
8 changed files with 240 additions and 11 deletions
+55
View File
@@ -0,0 +1,55 @@
// RangerHQ Tuner — Theme helpers (v0.5.0)
//
// Three modes:
// auto — follow OS via @media (prefers-color-scheme: light)
// dark — force dark regardless of OS
// light — force light regardless of OS
//
// Stored in chrome.storage.local under `tuner.theme`. Default: auto.
//
// Application strategy: set/remove `data-theme` attribute on <html>.
// - "auto" → attribute removed → @media in each stylesheet decides
// - "dark" → data-theme="dark" → CSS forces dark vars
// - "light" → data-theme="light" → CSS forces light vars
//
// Used from popup.js, newtab.js, and options.js on init + on cross-surface
// sync.
export const THEME_KEY = 'tuner.theme';
export const THEME_AUTO = 'auto';
export const THEME_DARK = 'dark';
export const THEME_LIGHT = 'light';
export const THEME_DEFAULT = THEME_AUTO;
export const VALID_THEMES = [THEME_AUTO, THEME_DARK, THEME_LIGHT];
/** Read the stored theme preference, falling back to "auto". */
export async function getTheme() {
if (typeof chrome === 'undefined' || !chrome.storage?.local) return THEME_DEFAULT;
const o = await chrome.storage.local.get(THEME_KEY);
const v = o[THEME_KEY];
return VALID_THEMES.includes(v) ? v : THEME_DEFAULT;
}
/** Persist a theme preference. Any cross-surface listeners will pick it up. */
export async function setTheme(theme) {
if (!VALID_THEMES.includes(theme)) theme = THEME_DEFAULT;
if (typeof chrome === 'undefined' || !chrome.storage?.local) return theme;
await chrome.storage.local.set({ [THEME_KEY]: theme });
return theme;
}
/** Apply a theme to the current document. Pass "auto" to clear the override. */
export function applyTheme(theme) {
const t = VALID_THEMES.includes(theme) ? theme : THEME_DEFAULT;
const html = document.documentElement;
if (t === THEME_AUTO) {
html.removeAttribute('data-theme');
} else {
html.setAttribute('data-theme', t);
}
}
/** Convenience: read the stored theme and apply it. */
export async function initTheme() {
applyTheme(await getTheme());
}