Dark Reader looks for `<meta name="color-scheme">` first. Finding none, it waits for a stylesheet and then samples text and background colours at several points of the screen, plus some further heuristics, to decide whether the page needs darkening. On the dark palette that verdict is wrong: it inverts an app that is already dark, a second time. So the tag now states which scheme is actually live. It ships in the markup as `light dark`, before any script runs; the pre-paint script narrows it to the one in use, and `applyResolvedTheme` keeps it there when the toggle moves — the same two-signal write as the `.dark` class, in both places. `var dark` moved out of the `try` in that script. It has to be readable after a throwing localStorage in order to set the meta, and false is the right value there: it is what `readThemePreference` -> "system" -> `prefersDark` resolves to on the module side, so the two still agree in the one case that cannot be read. Both setters create the tag when it is absent rather than skipping the write. A silent no-op leaves Dark Reader guessing, which is the bug. The drift guard now runs the real script, lifted out of the HTML, instead of matching its text — it asserts class and meta together for a stored dark, a stored light, and no preference at all. Text matching would have passed a script that declared the tag and never set it. Not `<meta name="darkreader-lock">`: that switches the extension off wholesale, including in light mode, where a user who installed it wants it working. Only the dark palette asks it to stand down. Verified in Chrome across stored dark, stored light, system-dark and a live toggle with no reload: one tag, content and computed `color-scheme` agreeing each time. Whether the extension honours it needs a human with Dark Reader installed. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
50 lines
2.1 KiB
HTML
50 lines
2.1 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>Budget 2.0</title>
|
|
<!-- Relative, NOT root-absolute: the dev server's HTML bundler resolves
|
|
every <link href> from disk and has no `external` option, so "/x"
|
|
fails to resolve and the whole page 500s. build.ts rewrites these to
|
|
the deploy base for production. -->
|
|
<link rel="manifest" href="manifest.webmanifest" />
|
|
<link rel="icon" href="icon.svg" type="image/svg+xml" />
|
|
<meta name="theme-color" content="#0f172a" />
|
|
<!-- The live scheme, stated before any script runs. The pre-paint script
|
|
below narrows it to the one actually in use; src/ui/theme.ts keeps it
|
|
in step from then on. Dark Reader reads this tag to decide whether the
|
|
page already has a dark theme, and inverts our own dark palette a
|
|
second time when it finds nothing. -->
|
|
<meta name="color-scheme" content="light dark" />
|
|
<!--
|
|
Applies the saved theme BEFORE the stylesheet paints, so a dark-theme
|
|
user never sees a flash of the light palette. It cannot import
|
|
src/ui/theme.ts (it has to run before any module loads), so it repeats
|
|
the key and the fallback; src/ui/theme.test.ts fails if the two drift.
|
|
Wrapped because localStorage throws outright in some configurations.
|
|
-->
|
|
<script>
|
|
var dark = false;
|
|
try {
|
|
var saved = localStorage.getItem("budget2:theme");
|
|
dark =
|
|
saved === "dark" ||
|
|
((saved === null || saved === "system") &&
|
|
window.matchMedia("(prefers-color-scheme: dark)").matches);
|
|
} catch (e) {}
|
|
if (dark) document.documentElement.classList.add("dark");
|
|
var scheme = document.querySelector('meta[name="color-scheme"]');
|
|
if (!scheme) {
|
|
scheme = document.head.appendChild(document.createElement("meta"));
|
|
scheme.name = "color-scheme";
|
|
}
|
|
scheme.content = dark ? "dark" : "light";
|
|
</script>
|
|
<script type="module" src="./frontend.tsx" async></script>
|
|
</head>
|
|
<body>
|
|
<div id="root"></div>
|
|
</body>
|
|
</html>
|