fix(reader): restore night-mode coverage of extension overlays - #260
Open
Gnathonic wants to merge 1 commit into
Open
fix(reader): restore night-mode coverage of extension overlays#260Gnathonic wants to merge 1 commit into
Gnathonic wants to merge 1 commit into
Conversation
The night-mode red filter stopped tinting browser-extension popups. The
regression is in Chrome, not in this repo: from Chrome 151, an SVG
*reference* filter is no longer painted onto cross-origin or restricted
frames (csswg-drafts#13846). Yomitan renders its dictionary popup as a
chrome-extension:// iframe, so `html { filter: url(#night-mode-filter) }`
skips it. Measured: Chromium 145 and 150 tint the frame, Chrome 151 does
not, and no placement of a url() filter — on the root or directly on the
iframe — brings it back.
Replace the root filter with a pair of blend layers: a `saturation` pass
then a `multiply` pass against pure red. Blending, unlike `filter`, still
composites over cross-origin frames. On greys the result is identical to
the old matrix (#000000 -> 0,0,0, #ffffff -> 255,0,0, every ramp step
exact); it differs only on fully saturated colour, where the CSS blending
spec's luma is Rec.601 rather than Rec.709 — a mean error of ~1/255 over
real manga covers.
Both layers carry the maximum z-index and are kept as the last two
children of <html>. Extensions routinely inject at z-index 2147483647;
anything landing between the passes would be reddened but never
desaturated, collapsing its greens and blues to black. Ties resolve by
tree order, so a MutationObserver on the root re-asserts the pair when an
extension injects. This is also why the layers cannot be pseudo-elements:
a ::before box is the first child and loses every tie.
Firefox now takes the same path. Its overlay hack existed because Gecko
silently drops url() filters on the root element (Bugzilla 1769223, still
NEW in 153.0.1 — confirmed against inline, external and data: references,
and every stacking-context workaround). Since the blend layers are what
both engines use, the isFirefox branch and its dialog MutationObserver are
deleted rather than ported.
Also fixes three surfaces that the root filter never reached:
- `[popover]` top-layer content — every Flowbite dropdown, tooltip and
popover — was untinted. Now covered via `:popover-open`, along with
`::backdrop` and non-root `:fullscreen`.
- The bare `dialog` rule double-filtered non-modal dialogs. Flowbite's
Drawer calls show(), not showModal(), so it stays in normal flow and was
already tinted; the matrix is not idempotent, so the Settings drawer
rendered ~5x too dark. Narrowed to `dialog:modal`.
- On light themes the page background escaped entirely, because a root
filter does not apply to the propagated canvas background. The
full-viewport blend layers cover it.
Verified in Chrome 151.0.7922.71 and Firefox 153.0 against the running
app: every surface tints exactly once, and turning night mode off restores
the original pixels. e2e/night-mode.spec.ts fails on the previous code
with the reported symptom.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Night mode's red filter stopped tinting browser-extension popups. The regression is in Chrome, not in this repo.
From Chrome 151, an SVG reference filter is no longer painted onto cross-origin or restricted frames (csswg-drafts#13846). Yomitan renders its dictionary popup as a
chrome-extension://iframe, sohtml { filter: url(#night-mode-filter) }skips it.Measured on the version boundary:
No placement of a
url()filter brings it back — blocked on the root and directly on the iframe. Any SVG filter in the ancestor chain also suppresses that frame's own function filters.The fix
The root filter is replaced by a pair of blend layers on
<html>: asaturationpass, then amultiplypass against pure red. Blending, unlikefilter, still composites over cross-origin frames. The SVG matrix survives, used only for top-layer content.Colour is preserved where it matters. Greys are pixel-identical to the old matrix —
#000000→0,0,0,#ffffff→255,0,0, every ramp step exact. Only fully saturated colours differ, by ≤32/255, because the CSS blending spec fixes its luma function at Rec.601 while the SVG matrix is Rec.709. Over five real manga covers that is a mean error of 1.3/255.Both layers carry the maximum z-index and are kept as the last two children of
<html>. This is load-bearing. Extensions routinely inject atz-index: 2147483647(Yomitan included); anything landing between the two passes is reddened but never desaturated, collapsing its greens and blues to black. Ties resolve by tree order, so aMutationObserveron the root re-asserts the pair when an extension injects. It is also why the layers cannot be pseudo-elements — a::beforebox is the first child and loses every tie.Firefox now takes the same path. Its overlay hack existed because Gecko silently drops
url()filters on the root element (Bugzilla 1769223, still NEW — reconfirmed on 153.0.1 against inline, external anddata:references and every stacking-context workaround). Since the blend layers are what both engines use, theisFirefoxbranch and its dialogMutationObserverare deleted rather than ported. Net −169 lines in that component.Three more surfaces the root filter never reached
[popover]top-layer content — every Flowbite dropdown, tooltip and popover — was untinted. Now covered via:popover-open, along with::backdropand non-root:fullscreen.dialogrule double-filtered non-modal dialogs. Flowbite's Drawer callsshow(), notshowModal(), so it stays in normal flow and was already tinted; the matrix is not idempotent, so the Settings drawer rendered ~5× too dark. Narrowed todialog:modal.Rejected alternatives
grayscale(1)operates on greys afterward, where every remaining function scales all three channels equally. Best fit found had RMS error 0.32 and crushed tonal range to ~43%.filteron<body>orhtml > *makes it a containing block forposition: fixeddescendants — the reader HUD unpins and scrolls away. Measured in both engines.backdrop-filteris exact and reaches extension frames in Chrome, but Firefox does not paint it at all (confirmed on system Firefox 153.0.1, not just the Playwright build) and it costs ~2× the frame time.Cost
~25 ms/frame vs a 16.7 ms baseline on a 1920×1080 pan in Chrome (Firefox: 19 vs 17), and only while night mode is on. Accepted as the price of covering extension popups.
Verification
Chrome 151.0.7922.71 and Firefox 153.0, driven against the running app with the real setting — every surface tints exactly once, and turning night mode off restores the original pixels.
New
e2e/night-mode.spec.tscovers the surfaces, the exactly-once requirement, the max-z-index layering, and thatposition: fixedstays pinned. It fails on the previous code with the reported symptom, and its z-index case fails on a split-z-index layering.svelte-check0 errors · 960 unit tests pass · 17/17 e2e pass (zoom suite unaffected) · ESLint clean on changed files.Note
CHANGELOG is intentionally untouched — happy to add entries in whatever split you prefer, since the drawer, dropdown and light-theme-background fixes are arguably separate user-visible lines.
🤖 Generated with Claude Code