feat(theme): apply saved theme before app startup
This commit is contained in:
parent
01b5f31740
commit
28a5b22e7e
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue