Add a built-in Light theme

Replaces the old Default theme (which followed the OS color scheme) with a
fixed warm, writing-focused Light theme: off-white editor surface, charcoal
text, subtle warm-gray borders, and a restrained deep teal-green accent. The
selector now offers Light and Neonflux. Old saves storing "default" are
migrated to "light" in both the renderer and the main process, and the e2e
scenario now covers Light + Neonflux application, light/dark checks, theme
round-trips, and content preservation.
This commit is contained in:
avi 2026-08-18 17:15:10 -05:00
commit 247f16baa5
5 changed files with 103 additions and 75 deletions

View file

@ -432,10 +432,13 @@ ipcMain.handle("folio:getClipboardText", () => {
});
// Theme preference. Stored as a plain string so future themes can be added
// without touching the main process.
ipcMain.handle("folio:getTheme", () => getSetting("theme", "default"));
// without touching the main process. "default" from older builds maps to
// "light" in the renderer, so it is normalized here too.
ipcMain.handle("folio:getTheme", () => getSetting("theme", "light"));
ipcMain.handle("folio:setTheme", (_e, id: unknown) => {
setSetting("theme", typeof id === "string" && id ? id : "default");
const theme =
typeof id === "string" && id && id !== "default" ? id : "light";
setSetting("theme", theme);
return { ok: true };
});

View file

@ -30,7 +30,7 @@
<div class="theme-control">
<label class="theme-label" for="themeSelect">Theme</label>
<select id="themeSelect" class="theme-select" title="Interface theme">
<option value="default">Default</option>
<option value="light">Light</option>
<option value="neonflux">Neonflux</option>
</select>
</div>

View file

@ -6,28 +6,37 @@ export interface ThemeApi {
setTheme: (id: string) => Promise<unknown>;
}
function selector(): HTMLSelectElement {
export function selector(): HTMLSelectElement {
return document.getElementById("themeSelect") as HTMLSelectElement;
}
// Older builds stored the theme as "default"; that now maps to the Light
// theme, which replaced it as Folio's default light appearance.
const LIGHT = "light";
function normalizeTheme(id: unknown): string {
if (typeof id !== "string" || !id || id === "default") return LIGHT;
return id;
}
export function applyTheme(id: string): void {
const theme = typeof id === "string" && id ? id : "default";
const theme = normalizeTheme(id);
document.documentElement.setAttribute("data-theme", theme);
const sel = selector();
if (sel && sel.value !== theme) sel.value = theme;
}
export function currentTheme(): string {
return document.documentElement.getAttribute("data-theme") || "default";
return normalizeTheme(document.documentElement.getAttribute("data-theme"));
}
// Load the saved theme and wire the selector. Unknown stored values fall back
// to Default (the :root base), so adding a theme later is safe for old saves.
// to Light (the :root base), so adding a theme later is safe for old saves.
export async function initTheme(api: ThemeApi): Promise<void> {
let saved = "default";
let saved = LIGHT;
try {
const value = await api.getTheme();
if (typeof value === "string" && value) saved = value;
if (typeof value === "string" && value) saved = normalizeTheme(value);
} catch {
// keep the default on any IPC failure
}

View file

@ -4,9 +4,8 @@
just a block of variable values — adding one means dropping a
`:root[data-theme="..."]` block here plus an <option> in the theme selector.
Default mirrors Folio's original appearance: light, with an automatic dark
variant that follows the OS color scheme. Neonflux is a fixed dark,
futuristic theme. */
Light is Folio's default: a warm, writing-focused light scheme. Neonflux is
a fixed dark, futuristic theme. */
/* ---- shared (non-color) tokens ---- */
:root {
@ -17,58 +16,35 @@
--gap: 16px;
}
/* ---- Default (light) ---- */
/* ---- Light (default) ----
A clean, professional writing theme. Warm off-white chrome keeps the app
soft instead of harsh, while a warm-white editor surface and charcoal text
stay comfortable for long-form reading. Borders and dividers are subtle
warm grays; the accent is a restrained deep teal-green so interactive and
selected elements stay clearly visible without shouting. */
:root,
:root[data-theme="default"] {
--color-bg: #ffffff;
--color-panel: #f7f7f8;
--color-border: #e2e2e7;
--color-text: #1a1a1d;
--color-muted: #6b6b76;
--color-accent: #2563eb;
--color-accent-hover: #1d4ed8;
:root[data-theme="light"] {
--color-bg: #fffdf9;
--color-panel: #f5f1e9;
--color-border: #e4e0d5;
--color-text: #2b2925;
--color-muted: #7c786d;
--color-accent: #2f6b5f;
--color-accent-hover: #26584e;
--color-on-accent: #ffffff;
--color-on-accent-soft: rgba(255, 255, 255, 0.15);
--color-on-accent-border: rgba(255, 255, 255, 0.3);
--color-on-accent-soft: rgba(255, 255, 255, 0.18);
--color-on-accent-border: rgba(255, 255, 255, 0.35);
--color-selection: rgba(37, 99, 235, 0.25);
--color-selection-strong: rgba(37, 99, 235, 0.5);
--color-selection: rgba(47, 107, 95, 0.22);
--color-selection-strong: rgba(47, 107, 95, 0.45);
--color-shadow: rgba(0, 0, 0, 0.25);
--color-shadow-strong: rgba(0, 0, 0, 0.3);
--color-backdrop: rgba(0, 0, 0, 0.45);
--color-shadow: rgba(45, 40, 32, 0.18);
--color-shadow-strong: rgba(45, 40, 32, 0.26);
--color-backdrop: rgba(45, 40, 32, 0.4);
--color-scrollbar: rgba(0, 0, 0, 0.18);
--color-scrollbar-hover: rgba(0, 0, 0, 0.32);
}
/* ---- Default (dark, follows OS preference) ---- */
@media (prefers-color-scheme: dark) {
:root:not([data-theme]),
:root[data-theme="default"] {
--color-bg: #0f0f12;
--color-panel: #18181e;
--color-border: #2a2a32;
--color-text: #e6e6ec;
--color-muted: #9a9aa6;
--color-accent: #4a94f0;
--color-accent-hover: #6ab0ff;
--color-on-accent: #ffffff;
--color-on-accent-soft: rgba(255, 255, 255, 0.15);
--color-on-accent-border: rgba(255, 255, 255, 0.3);
--color-selection: rgba(74, 148, 240, 0.25);
--color-selection-strong: rgba(74, 148, 240, 0.5);
--color-shadow: rgba(0, 0, 0, 0.55);
--color-shadow-strong: rgba(0, 0, 0, 0.65);
--color-backdrop: rgba(0, 0, 0, 0.6);
--color-scrollbar: rgba(255, 255, 255, 0.15);
--color-scrollbar-hover: rgba(255, 255, 255, 0.3);
}
--color-scrollbar: rgba(45, 40, 32, 0.22);
--color-scrollbar-hover: rgba(45, 40, 32, 0.38);
}
/* ---- Neonflux ----