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:
@@ -9,6 +9,10 @@ import {
|
||||
getHistoryCap, setHistoryCap,
|
||||
clearHistory, clearFavourites, clearAll,
|
||||
} from '../lib/history.js';
|
||||
import {
|
||||
THEME_KEY, THEME_DEFAULT, VALID_THEMES,
|
||||
getTheme, setTheme, applyTheme,
|
||||
} from '../lib/theme.js';
|
||||
|
||||
const STORAGE_KEYS = {
|
||||
stations: 'tuner.stationsCache',
|
||||
@@ -39,6 +43,11 @@ init().catch(err => {
|
||||
|
||||
async function init() {
|
||||
els.version.textContent = `v${chrome.runtime.getManifest().version}`;
|
||||
|
||||
// Theme — apply stored preference + wire the radio group + listen for
|
||||
// cross-surface changes (popup/newtab pick a theme → Options reflects).
|
||||
await initThemeUI();
|
||||
|
||||
await refreshStats();
|
||||
await refreshPlayback();
|
||||
|
||||
@@ -86,6 +95,12 @@ async function init() {
|
||||
// Refresh stats live if storage changes from another surface
|
||||
chrome.storage.onChanged.addListener((changes, area) => {
|
||||
if (area !== 'local') return;
|
||||
if (changes[THEME_KEY]) {
|
||||
const v = changes[THEME_KEY].newValue || THEME_DEFAULT;
|
||||
applyTheme(v);
|
||||
const radio = document.querySelector(`input[name="opt-theme"][value="${v}"]`);
|
||||
if (radio) radio.checked = true;
|
||||
}
|
||||
if (Object.keys(changes).some(k => k.startsWith('tuner.'))) {
|
||||
refreshStats();
|
||||
refreshPlayback();
|
||||
@@ -93,6 +108,23 @@ async function init() {
|
||||
});
|
||||
}
|
||||
|
||||
/** Apply the stored theme, check the matching radio, wire change handlers. */
|
||||
async function initThemeUI() {
|
||||
const current = await getTheme();
|
||||
applyTheme(current);
|
||||
const checked = document.querySelector(`input[name="opt-theme"][value="${current}"]`);
|
||||
if (checked) checked.checked = true;
|
||||
for (const r of document.querySelectorAll('input[name="opt-theme"]')) {
|
||||
r.addEventListener('change', async (e) => {
|
||||
const v = e.target.value;
|
||||
if (!VALID_THEMES.includes(v)) return;
|
||||
await setTheme(v);
|
||||
applyTheme(v);
|
||||
toast(`Theme: ${v}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshStats() {
|
||||
const [history, favs, all] = await Promise.all([
|
||||
getHistory(),
|
||||
|
||||
Reference in New Issue
Block a user