feat(theme): apply saved theme before app startup

This commit is contained in:
t8y2 2026-07-21 15:58:04 +08:00
parent 01b5f31740
commit 28a5b22e7e
2 changed files with 137 additions and 0 deletions

View File

@ -5,6 +5,44 @@
<link rel="icon" type="image/png" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DBX</title>
<style>
html,
body,
#root {
background-color: rgb(255 255 255);
}
html.dark,
html.dark body,
html.dark #root {
background-color: rgb(19 20 22);
}
</style>
<script data-dbx-startup-theme>
(() => {
const root = document.documentElement;
let mode = null;
// localStorage can throw when WebView storage is unavailable; keep the existing light fallback.
try {
mode = localStorage.getItem("dbx-theme");
} catch {}
const followsSystem = mode === "system" || mode === "soft-system";
let dark = mode === "dark" || mode === "soft-dark";
if (followsSystem) {
try {
dark = window.matchMedia("(prefers-color-scheme: dark)").matches;
} catch {
dark = false;
}
}
root.classList.toggle("dark", dark);
root.style.colorScheme = dark ? "dark" : "light";
})();
</script>
</head>
<body>
<div id="root">

View File

@ -0,0 +1,99 @@
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { describe, expect, it, vi } from "vitest";
const indexHtml = readFileSync(fileURLToPath(new URL("../../../../index.html", import.meta.url)), "utf8");
const startupScript = indexHtml.match(/<script data-dbx-startup-theme>([\s\S]*?)<\/script>/)?.[1];
if (!startupScript) throw new Error("Startup theme script not found");
type StartupThemeOptions = {
mode?: string | null;
prefersDark?: boolean;
storageError?: boolean;
};
function runStartupTheme({ mode = null, prefersDark = false, storageError = false }: StartupThemeOptions = {}) {
const toggle = vi.fn();
const root = { classList: { toggle }, style: { colorScheme: "" } };
const localStorage = {
getItem: vi.fn(() => {
if (storageError) throw new DOMException("Storage unavailable", "SecurityError");
return mode;
}),
};
const matchMedia = vi.fn(() => ({ matches: prefersDark }));
Function("document", "localStorage", "window", startupScript)({ documentElement: root }, localStorage, { matchMedia });
return { matchMedia, root, toggle };
}
describe("startup theme", () => {
it("applies dark mode before the application mounts", () => {
const { matchMedia, root, toggle } = runStartupTheme({ mode: "dark" });
expect(toggle).toHaveBeenCalledWith("dark", true);
expect(root.style.colorScheme).toBe("dark");
expect(matchMedia).not.toHaveBeenCalled();
});
it("keeps explicit light mode light", () => {
const { matchMedia, root, toggle } = runStartupTheme({ mode: "light", prefersDark: true });
expect(toggle).toHaveBeenCalledWith("dark", false);
expect(root.style.colorScheme).toBe("light");
expect(matchMedia).not.toHaveBeenCalled();
});
it.each([
[true, "dark"],
[false, "light"],
])("resolves system mode when prefersDark is %s", (prefersDark, appearance) => {
const { matchMedia, root, toggle } = runStartupTheme({ mode: "system", prefersDark });
expect(toggle).toHaveBeenCalledWith("dark", prefersDark);
expect(root.style.colorScheme).toBe(appearance);
expect(matchMedia).toHaveBeenCalledWith("(prefers-color-scheme: dark)");
});
it("uses the current light default when no preference exists", () => {
const { root, toggle } = runStartupTheme();
expect(toggle).toHaveBeenCalledWith("dark", false);
expect(root.style.colorScheme).toBe("light");
});
it("falls back to light when localStorage is unavailable", () => {
const { root, toggle } = runStartupTheme({ storageError: true, prefersDark: true });
expect(toggle).toHaveBeenCalledWith("dark", false);
expect(root.style.colorScheme).toBe("light");
});
it("falls back to light for an invalid persisted mode", () => {
const { root, toggle } = runStartupTheme({ mode: "invalid", prefersDark: true });
expect(toggle).toHaveBeenCalledWith("dark", false);
expect(root.style.colorScheme).toBe("light");
});
it("falls back to light when system appearance detection is unavailable", () => {
const toggle = vi.fn();
const root = { classList: { toggle }, style: { colorScheme: "" } };
Function("document", "localStorage", "window", startupScript)({ documentElement: root }, { getItem: () => "system" }, {});
expect(toggle).toHaveBeenCalledWith("dark", false);
expect(root.style.colorScheme).toBe("light");
});
it.each([
["soft-dark", true],
["soft-light", false],
])("preserves legacy %s mode", (mode, dark) => {
const { toggle } = runStartupTheme({ mode });
expect(toggle).toHaveBeenCalledWith("dark", dark);
});
});