From d69e896c4c7208a3e56b2bb47219132539b35dfb Mon Sep 17 00:00:00 2001 From: Lucas Winther Date: Wed, 2 Sep 2026 23:10:09 +0200 Subject: [PATCH] fix(theme): declare the live colour scheme, so Dark Reader stands down MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dark Reader looks for `` 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 ``: 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) --- AGENTS.md | 9 ++++- src/index.html | 17 +++++++- src/ui/theme.test.ts | 95 +++++++++++++++++++++++++++++++++++++++++--- src/ui/theme.ts | 25 ++++++++++++ 4 files changed, 137 insertions(+), 9 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 52ce6aa..93b5672 100755 --- a/AGENTS.md +++ b/AGENTS.md @@ -96,8 +96,13 @@ ship twice — re-introducing one is a regression, not a style choice. reference elsewhere, so this one still needs your attention. - **The pre-paint theme script in `index.html` and `src/ui/theme.ts` must agree.** The script cannot import the module — it has to run before any module - loads — so it repeats the storage key and the fallback logic. - → `src/ui/theme.test.ts` + loads — so it repeats the storage key and the fallback logic. Both set two + things, not one: the `.dark` class and ``, which + must also match the `color-scheme` declarations in `src/index.css`. The meta + tag is what Dark Reader reads to decide the app already has a dark theme; + without it, it samples the painted colours and inverts our dark palette a + second time. → `src/ui/theme.test.ts`, which runs the real script out of the + HTML rather than matching its text. - **Anything derived from the base path is asserted for both a root and a subpath deploy:** the manifest (`src/manifest.test.ts`), the font URLs (`src/fontCss.test.ts`), the icons (`src/icons.test.ts`), and the service diff --git a/src/index.html b/src/index.html index 232f5c6..8e94235 100644 --- a/src/index.html +++ b/src/index.html @@ -11,6 +11,12 @@ + + diff --git a/src/ui/theme.test.ts b/src/ui/theme.test.ts index d37adfc..1423e75 100644 --- a/src/ui/theme.test.ts +++ b/src/ui/theme.test.ts @@ -10,6 +10,7 @@ import { THEME_KEY, applyResolvedTheme, isThemePreference, + prefersDark, readThemePreference, resolveTheme, storeThemePreference, @@ -18,8 +19,27 @@ import { beforeEach(() => { localStorage.clear(); document.documentElement.classList.remove("dark"); + for (const meta of document.querySelectorAll('meta[name="color-scheme"]')) { + meta.remove(); + } }); +function colorSchemeMeta(): string | null { + return document.querySelector('meta[name="color-scheme"]')?.getAttribute("content") ?? null; +} + +/** + * Lifts the inline pre-paint script out of index.html and runs it for real, + * rather than matching its text. The module script below it carries + * attributes, so a bare `