Add a reusable theme system with a Neonflux theme

Introduce a theme layer on top of the existing CSS custom properties.
All color tokens now live in themes.css, scoped by a data-theme
attribute on <html>, so a theme is just a new token block plus a
dropdown option - no code changes needed. The Default theme preserves
the existing light/dark behavior; Neonflux adds an electric-cyan dark
theme.

The active theme is chosen via a Theme selector in the sidebar footer
and persisted in a new main-process settings file (userData
settings.json) through getTheme/setTheme IPC, so it survives restarts.
Hard-coded on-accent, selection, shadow and backdrop colors across the
renderer were converted to semantic tokens so every theme stays
consistent.
This commit is contained in:
avi 2026-08-18 12:42:09 -05:00
commit ad0329bbf9
12 changed files with 346 additions and 52 deletions

View file

@ -714,6 +714,57 @@ const BOOK3_DIR = __BOOKDIR3_VALUE__;
out.navBreakCount = navBreaks.length;
out.navBreaksOk = navBreaks.length >= 1 && navBreaks.some((el) => el.textContent.startsWith("# Two"));
// --- theme system: selector, immediate switching, persistence ---
const themeSelect = document.getElementById("themeSelect");
const themeVar = (name) =>
getComputedStyle(document.documentElement).getPropertyValue(name).trim();
const docBeforeTheme = navBook.state.doc.toString();
out.themeSelectPresent = !!themeSelect;
out.themeSelectOptions = themeSelect
? Array.from(themeSelect.options).map((o) => o.value)
: [];
out.themeHasDefault = out.themeSelectOptions.includes("default");
out.themeHasNeonflux = out.themeSelectOptions.includes("neonflux");
out.themeInitialAttr = document.documentElement.getAttribute("data-theme");
out.themeInitialStored = await window.folio.getTheme();
// Switch to Neonflux: applies immediately and persists via IPC.
themeSelect.value = "neonflux";
themeSelect.dispatchEvent(new Event("change", { bubbles: true }));
await navWait(200);
out.themeNeonfluxAttr = document.documentElement.getAttribute("data-theme");
out.themeNeonfluxStored = await window.folio.getTheme();
const neonBg = themeVar("--color-bg");
const neonAccent = themeVar("--color-accent");
out.themeNeonfluxBg = neonBg;
out.themeNeonfluxAccent = neonAccent;
out.themeNeonfluxApplied =
out.themeNeonfluxAttr === "neonflux" &&
out.themeNeonfluxStored === "neonflux" &&
neonBg.toLowerCase() === "#0a0d14" &&
neonAccent.toLowerCase() === "#22d3ee";
// Neonflux is dark: computed background of the app should be near-black.
const bodyBg = getComputedStyle(document.body).backgroundColor;
out.themeNeonfluxDark = /rgb\((10|11|12|9|8),/.test(bodyBg);
// Switching themes must not touch the manuscript content.
out.themeContentUnchanged = navBook.state.doc.toString() === docBeforeTheme;
// Switch back to Default: applies immediately and persists.
themeSelect.value = "default";
themeSelect.dispatchEvent(new Event("change", { bubbles: true }));
await navWait(200);
out.themeDefaultAttr = document.documentElement.getAttribute("data-theme");
out.themeDefaultStored = await window.folio.getTheme();
const defaultBg = themeVar("--color-bg");
out.themeDefaultBg = defaultBg;
out.themeDefaultApplied =
out.themeDefaultAttr === "default" &&
out.themeDefaultStored === "default" &&
(defaultBg.toLowerCase() === "#ffffff" || defaultBg.toLowerCase() === "#0f0f12");
out.themeContentUnchanged2 = navBook.state.doc.toString() === docBeforeTheme;
const okFlags = [
"dndMoved",
"kbMoved",
@ -781,6 +832,14 @@ const BOOK3_DIR = __BOOKDIR3_VALUE__;
"navFollowsNewOrder",
"navNextAfterReorderOk",
"navBreaksOk",
"themeSelectPresent",
"themeHasDefault",
"themeHasNeonflux",
"themeNeonfluxApplied",
"themeNeonfluxDark",
"themeContentUnchanged",
"themeDefaultApplied",
"themeContentUnchanged2",
];
out.all = okFlags.every((f) => out[f] === true);
} catch (e) {