Skip to content

gp-sphinx: Prevent flash of wrong theme on initial load#23

Merged
tony merged 6 commits intomainfrom
tn-color-schemes
Apr 27, 2026
Merged

gp-sphinx: Prevent flash of wrong theme on initial load#23
tony merged 6 commits intomainfrom
tn-color-schemes

Conversation

@tony
Copy link
Copy Markdown
Member

@tony tony commented Apr 26, 2026

Summary

  • Adds _inject_fowt_prevention() html-page-context callback that emits a synchronous <head> snippet via Furo's metatags slot
  • Snippet sets html.style.colorScheme (so the html canvas paints in the user's preferred scheme on first paint) and a gp-sphinx-theme-pending gating class on <html>
  • New inline CSS rule hides body content until Furo's existing inline body-script sets body[data-theme] — any paint that lands in that window is invisible, while the html canvas is already correctly themed
  • DOMContentLoaded failsafe sets body[data-theme] from the head-resolved value if Furo's body-script ever stops running, so a future Furo refactor can't leave body permanently hidden

Eliminates the flicker that was visible when localStorage.theme disagreed with OS prefers-color-scheme (e.g., user toggled to dark on a light-mode OS), or on slower networks/CPUs where Furo's body-script lost the race against first paint.

The visibility-hide approach reads zero Furo internals, so Furo can ship CSS-variable changes without dragging gp-sphinx along — there's no mirror to keep in sync.

Test plan

  • uv run ruff check . --fix --show-fixes — all checks passed
  • uv run ruff format . — 181 files left unchanged
  • uv run mypy — Success, no issues found in 176 source files
  • uv run py.test --reruns 0 -vvv — 1226 passed, 3 skipped
  • just build-docs — build succeeded
  • Built docs/_build/html/index.html contains the snippet at line 14, before all stylesheet <link> tags
  • Browser smoke test (Playwright against local build):
    • localStorage=null, OS=light → light bg from frame 1, html.style.colorScheme="light"
    • localStorage="dark", OS=light (worst-case mismatch) → dark bg rgb(19,20,22) from frame 1, html.style.colorScheme="dark"
    • localStorage="light", OS=light → light bg, html.style.colorScheme="light"
    • SPA navigation across pages preserves body[data-theme], no regression
    • Theme-toggle button: still cycles auto → dark/light → auto correctly via Furo's existing handler
  • Live test on deployed docs (will follow in a temporary deploy commit on this branch)

why: When localStorage.theme and OS prefers-color-scheme disagree
(e.g., user toggled to dark on a light-mode OS), the page briefly
paints the OS-default scheme before settling on the stored choice.
Furo's inline body-script runs after <body> opens, and the meta
color-scheme tag defers canvas color to OS, so the wrong scheme can
leak into first paint on slower networks/CPUs.

what:
- Add _inject_fowt_prevention() html-page-context callback that
  injects a synchronous <head> snippet via metatags
- Snippet sets html.style.colorScheme to the resolved theme (canvas
  paints correctly) and adds a gp-sphinx-theme-pending gating class
- New CSS rule hides body until Furo's body-script sets data-theme,
  so any pre-script body paint is invisible
- DOMContentLoaded failsafe sets body[data-theme] from the
  head-resolved value if Furo's body-script ever stops running
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Apr 26, 2026

Codecov Report

❌ Patch coverage is 50.00000% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.19%. Comparing base (05ddc4b) to head (28ed3a8).
⚠️ Report is 7 commits behind head on main.

Files with missing lines Patch % Lines
packages/gp-sphinx/src/gp_sphinx/config.py 50.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #23      +/-   ##
==========================================
- Coverage   90.20%   90.19%   -0.02%     
==========================================
  Files         163      163              
  Lines       13876    13880       +4     
==========================================
+ Hits        12517    12519       +2     
- Misses       1359     1361       +2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

tony added a commit that referenced this pull request Apr 26, 2026
why: Allow visual verification of the flash-of-wrong-theme fix at
gp-sphinx.git-pull.com before merging PR #23. The live deploy uses
the same prod S3 bucket + CloudFront distribution as main, so this
will temporarily replace production docs.

what:
- Add tn-color-schemes to docs workflow push-trigger branches
- TEMPORARY: revert this commit (or drop the line) before merging
  the FOWT fix to main
tony added 4 commits April 26, 2026 20:26
why: Cloudflare Rocket Loader rewrites every inline <script> to a
private MIME type and runs it asynchronously after page load. On
gp-sphinx.git-pull.com the FOWT-prevention head-script was being
deferred past first paint — gating class never applied, body
visibility:hidden never matched, and the flicker remained visible.
Furo's own inline body-script suffers the same fate, so we can't
rely on it to set body[data-theme] synchronously either.

what:
- Add data-cfasync="false" to the FOWT <script> so Rocket Loader
  leaves it alone — script now runs synchronously as written
- Replace the single DOMContentLoaded body[data-theme] setter with
  a requestAnimationFrame + DOMContentLoaded pair, so we set the
  attribute as soon as document.body exists rather than waiting
  for Furo's now-async body-script
- Update docstring to document the Rocket Loader bypass
why: _inject_copybutton_bridge sets window.GP_SPHINX_COPYBUTTON_SELECTOR
in an inline <script> that spa-nav.js reads when re-creating copy
buttons after SPA swaps. Without data-cfasync="false" the script is
deferred by Cloudflare Rocket Loader, so the global is undefined when
spa-nav.js runs and the override falls back to the default
"div.highlight pre" — projects that customize copybutton_selector
silently lose copy buttons on prompt admonitions etc.

what:
- Add data-cfasync="false" to the bridge <script> tag, matching the
  same Rocket Loader bypass already in _inject_fowt_prevention
…flicker

why: Cloudflare Rocket Loader defers external scripts too — furo.js
runs well after page load, and Furo's CSS rule
".no-js .theme-toggle-container {display: none}" keeps the
light/dark/auto toggle invisible until furo.js strips the no-js
class on DCL. The toggle then appears suddenly mid-load, which
reads as a flicker.

what:
- In _inject_fowt_prevention's head-script (already opted out of
  Rocket Loader via data-cfasync="false"), strip the no-js class
  from <html> right after adding gp-sphinx-theme-pending — the
  toggle is now visible from first paint
- No-JS users still get the "no-js" class because the head-script
  itself never runs for them, so the toggle stays correctly hidden
- Update docstring to document the behaviour
why: Each rule had `transition: background <duration>` for hover
smoothing. CSS has no per-trigger transition control, so the same
animation runs when the theme-toggle flips a CSS variable's value —
producing a visible mid-blend (e.g., grey → blue lerp) on every
dark/light swap. Snap on hover is acceptable; snap on theme swap is
the goal.

what:
- Delete `transition: background 100ms ease-out` from
  api_style.css (dl.py headers, both top-level and nested) and
  layout.css (rST/directive API headers and card-shell headers)
- Add `.sig:not(.sig-inline) { transition: none }` to override
  Furo's compiled `transition: background .1s ease-out` on the same
  selector — added in sphinx-gp-theme's theme custom.css for
  downstream consumers AND in docs/_static/css/custom.css because
  the project file shadows the theme file at the same path
@tony tony force-pushed the tn-color-schemes branch from 006aa4f to 88f7a8b Compare April 27, 2026 01:26
@tony tony merged commit 75f2d77 into main Apr 27, 2026
10 checks passed
@tony tony deleted the tn-color-schemes branch April 27, 2026 01:29
tony added a commit to tmux-python/libtmux that referenced this pull request Apr 27, 2026
why: gp-sphinx cut 0.0.1a11 covering a theme-flicker fix that
addresses three independent failure modes on Furo + Cloudflare
deploys. (1) Flash of the wrong colour scheme on initial load when
localStorage.theme and OS prefers-color-scheme disagree. (2)
Toggle-icon "pop-in" mid-load caused by furo.js being deferred by
Cloudflare Rocket Loader, leaving <html class="no-js"> on the page
and the .theme-toggle-container hidden until furo.js eventually
ran. (3) Animated mid-blend on .sig and dl.py headers during
runtime theme toggle, from CSS transitions on background-on-hover
that also ran on every CSS-variable swap. Tracking the new alpha
picks all three fixes up on the next docs build at
https://libtmux.git-pull.com/. See
git-pull/gp-sphinx#23 for the source-side
detail.

what:
- pyproject.toml: gp-sphinx, sphinx-autodoc-api-style, and
  sphinx-autodoc-pytest-fixtures all move 0.0.1a10 -> 0.0.1a11 in
  every dependency group that pins them (one row in
  [project.optional-dependencies], one row in [dependency-groups]
  per package).
- uv.lock regenerated against the new pins; gp-sphinx workspace
  siblings co-resolved to 0.0.1a11.
tony added a commit to vcs-python/libvcs that referenced this pull request Apr 27, 2026
why: gp-sphinx cut 0.0.1a11 covering a theme-flicker fix that
addresses three independent failure modes on Furo + Cloudflare
deploys. (1) Flash of the wrong colour scheme on initial load when
localStorage.theme and OS prefers-color-scheme disagree. (2)
Toggle-icon "pop-in" mid-load caused by furo.js being deferred by
Cloudflare Rocket Loader, leaving <html class="no-js"> on the page
and the .theme-toggle-container hidden until furo.js eventually
ran. (3) Animated mid-blend on .sig and dl.py headers during
runtime theme toggle, from CSS transitions on background-on-hover
that also ran on every CSS-variable swap. Tracking the new alpha
picks all three fixes up on the next docs build at
https://libvcs.git-pull.com/. See
git-pull/gp-sphinx#23 for the source-side
detail.

what:
- pyproject.toml: gp-sphinx, sphinx-autodoc-api-style, and
  sphinx-autodoc-pytest-fixtures all move 0.0.1a10 -> 0.0.1a11 in
  every dependency group that pins them (one row in
  [project.optional-dependencies], one row in [dependency-groups]
  per package).
- uv.lock regenerated against the new pins; gp-sphinx workspace
  siblings co-resolved to 0.0.1a11.
tony added a commit to vcs-python/vcspull that referenced this pull request Apr 27, 2026
why: gp-sphinx cut 0.0.1a11 covering a theme-flicker fix that
addresses three independent failure modes on Furo + Cloudflare
deploys. (1) Flash of the wrong colour scheme on initial load when
localStorage.theme and OS prefers-color-scheme disagree. (2)
Toggle-icon "pop-in" mid-load caused by furo.js being deferred by
Cloudflare Rocket Loader, leaving <html class="no-js"> on the page
and the .theme-toggle-container hidden until furo.js eventually
ran. (3) Animated mid-blend on .sig and dl.py headers during
runtime theme toggle, from CSS transitions on background-on-hover
that also ran on every CSS-variable swap. Tracking the new alpha
picks all three fixes up on the next docs build at
https://vcspull.git-pull.com/. See
git-pull/gp-sphinx#23 for the source-side
detail.

what:
- pyproject.toml: gp-sphinx, sphinx-autodoc-argparse, and
  sphinx-autodoc-api-style all move 0.0.1a10 -> 0.0.1a11 in every
  dependency group that pins them (one row in
  [project.optional-dependencies], one row in [dependency-groups]
  per package).
- uv.lock regenerated against the new pins; gp-sphinx workspace
  siblings co-resolved to 0.0.1a11.
tony added a commit to tmux-python/tmuxp that referenced this pull request Apr 27, 2026
why: gp-sphinx cut 0.0.1a11 covering a theme-flicker fix that
addresses three independent failure modes on Furo + Cloudflare
deploys. (1) Flash of the wrong colour scheme on initial load when
localStorage.theme and OS prefers-color-scheme disagree. (2)
Toggle-icon "pop-in" mid-load caused by furo.js being deferred by
Cloudflare Rocket Loader, leaving <html class="no-js"> on the page
and the .theme-toggle-container hidden until furo.js eventually
ran. (3) Animated mid-blend on .sig and dl.py headers during
runtime theme toggle, from CSS transitions on background-on-hover
that also ran on every CSS-variable swap. Tracking the new alpha
picks all three fixes up on the next docs build at
https://tmuxp.git-pull.com/. See
git-pull/gp-sphinx#23 for the source-side
detail.

what:
- pyproject.toml: gp-sphinx, sphinx-autodoc-argparse, and
  sphinx-autodoc-api-style all move 0.0.1a10 -> 0.0.1a11 in every
  dependency group that pins them (one row in
  [project.optional-dependencies], one row in [dependency-groups]
  per package).
- uv.lock regenerated against the new pins; gp-sphinx workspace
  siblings co-resolved to 0.0.1a11.
tony added a commit to git-pull/gp-libs that referenced this pull request Apr 27, 2026
why: gp-sphinx cut 0.0.1a11 covering a theme-flicker fix that
addresses three independent failure modes on Furo + Cloudflare
deploys. (1) Flash of the wrong colour scheme on initial load when
localStorage.theme and OS prefers-color-scheme disagree. (2)
Toggle-icon "pop-in" mid-load caused by furo.js being deferred by
Cloudflare Rocket Loader, leaving <html class="no-js"> on the page
and the .theme-toggle-container hidden until furo.js eventually
ran. (3) Animated mid-blend on .sig and dl.py headers during
runtime theme toggle, from CSS transitions on background-on-hover
that also ran on every CSS-variable swap. Tracking the new alpha
picks all three fixes up on the next docs build at
https://gp-libs.git-pull.com/. See
git-pull/gp-sphinx#23 for the source-side
detail.

what:
- pyproject.toml: gp-sphinx and sphinx-autodoc-api-style move
  0.0.1a10 -> 0.0.1a11 in every dependency group that pins them
  (one row in [project.optional-dependencies], one row in
  [dependency-groups] per package).
- uv.lock regenerated against the new pins; gp-sphinx workspace
  siblings co-resolved to 0.0.1a11.
tony added a commit to tony/django-docutils that referenced this pull request Apr 27, 2026
why: gp-sphinx cut 0.0.1a11 covering a theme-flicker fix that
addresses three independent failure modes on Furo + Cloudflare
deploys. (1) Flash of the wrong colour scheme on initial load when
localStorage.theme and OS prefers-color-scheme disagree. (2)
Toggle-icon "pop-in" mid-load caused by furo.js being deferred by
Cloudflare Rocket Loader, leaving <html class="no-js"> on the page
and the .theme-toggle-container hidden until furo.js eventually
ran. (3) Animated mid-blend on .sig and dl.py headers during
runtime theme toggle, from CSS transitions on background-on-hover
that also ran on every CSS-variable swap. Tracking the new alpha
picks all three fixes up on the next docs build at
https://django-docutils.git-pull.com/. See
git-pull/gp-sphinx#23 for the source-side
detail.

what:
- pyproject.toml: gp-sphinx and sphinx-autodoc-api-style move
  0.0.1a10 -> 0.0.1a11 in every dependency group that pins them
  (one row in [project.optional-dependencies], one row in
  [dependency-groups] per package).
- uv.lock regenerated against the new pins; gp-sphinx workspace
  siblings co-resolved to 0.0.1a11.
tony added a commit to cihai/cihai that referenced this pull request Apr 27, 2026
why: gp-sphinx cut 0.0.1a11 covering a theme-flicker fix that
addresses three independent failure modes on Furo + Cloudflare
deploys. (1) Flash of the wrong colour scheme on initial load when
localStorage.theme and OS prefers-color-scheme disagree. (2)
Toggle-icon "pop-in" mid-load caused by furo.js being deferred by
Cloudflare Rocket Loader, leaving <html class="no-js"> on the page
and the .theme-toggle-container hidden until furo.js eventually
ran. (3) Animated mid-blend on .sig and dl.py headers during
runtime theme toggle, from CSS transitions on background-on-hover
that also ran on every CSS-variable swap. Tracking the new alpha
picks all three fixes up on the next docs build at
https://cihai.git-pull.com/. See
git-pull/gp-sphinx#23 for the source-side
detail.

what:
- pyproject.toml: gp-sphinx and sphinx-autodoc-api-style move
  0.0.1a10 -> 0.0.1a11 in every dependency group that pins them
  (one row in [project.optional-dependencies], one row in
  [dependency-groups] per package).
- uv.lock regenerated against the new pins; gp-sphinx workspace
  siblings co-resolved to 0.0.1a11.
tony added a commit to cihai/cihai-cli that referenced this pull request Apr 27, 2026
why: gp-sphinx cut 0.0.1a11 covering a theme-flicker fix that
addresses three independent failure modes on Furo + Cloudflare
deploys. (1) Flash of the wrong colour scheme on initial load when
localStorage.theme and OS prefers-color-scheme disagree. (2)
Toggle-icon "pop-in" mid-load caused by furo.js being deferred by
Cloudflare Rocket Loader, leaving <html class="no-js"> on the page
and the .theme-toggle-container hidden until furo.js eventually
ran. (3) Animated mid-blend on .sig and dl.py headers during
runtime theme toggle, from CSS transitions on background-on-hover
that also ran on every CSS-variable swap. Tracking the new alpha
picks all three fixes up on the next docs build at
https://cihai-cli.git-pull.com/. See
git-pull/gp-sphinx#23 for the source-side
detail.

what:
- pyproject.toml: gp-sphinx, sphinx-autodoc-argparse, and
  sphinx-autodoc-api-style all move 0.0.1a10 -> 0.0.1a11 in every
  dependency group that pins them (one row in
  [project.optional-dependencies], one row in [dependency-groups]
  per package).
- uv.lock regenerated against the new pins; gp-sphinx workspace
  siblings co-resolved to 0.0.1a11.
tony added a commit to cihai/unihan-db that referenced this pull request Apr 27, 2026
why: gp-sphinx cut 0.0.1a11 covering a theme-flicker fix that
addresses three independent failure modes on Furo + Cloudflare
deploys. (1) Flash of the wrong colour scheme on initial load when
localStorage.theme and OS prefers-color-scheme disagree. (2)
Toggle-icon "pop-in" mid-load caused by furo.js being deferred by
Cloudflare Rocket Loader, leaving <html class="no-js"> on the page
and the .theme-toggle-container hidden until furo.js eventually
ran. (3) Animated mid-blend on .sig and dl.py headers during
runtime theme toggle, from CSS transitions on background-on-hover
that also ran on every CSS-variable swap. Tracking the new alpha
picks all three fixes up on the next docs build at
https://unihan-db.git-pull.com/. See
git-pull/gp-sphinx#23 for the source-side
detail.

what:
- pyproject.toml: gp-sphinx and sphinx-autodoc-api-style move
  0.0.1a10 -> 0.0.1a11 in every dependency group that pins them
  (one row in [project.optional-dependencies], one row in
  [dependency-groups] per package).
- uv.lock regenerated against the new pins; gp-sphinx workspace
  siblings co-resolved to 0.0.1a11.
tony added a commit to cihai/unihan-etl that referenced this pull request Apr 27, 2026
why: gp-sphinx cut 0.0.1a11 covering a theme-flicker fix that
addresses three independent failure modes on Furo + Cloudflare
deploys. (1) Flash of the wrong colour scheme on initial load when
localStorage.theme and OS prefers-color-scheme disagree. (2)
Toggle-icon "pop-in" mid-load caused by furo.js being deferred by
Cloudflare Rocket Loader, leaving <html class="no-js"> on the page
and the .theme-toggle-container hidden until furo.js eventually
ran. (3) Animated mid-blend on .sig and dl.py headers during
runtime theme toggle, from CSS transitions on background-on-hover
that also ran on every CSS-variable swap. Tracking the new alpha
picks all three fixes up on the next docs build at
https://unihan-etl.git-pull.com/. See
git-pull/gp-sphinx#23 for the source-side
detail.

what:
- pyproject.toml: gp-sphinx, sphinx-autodoc-argparse,
  sphinx-autodoc-api-style, and sphinx-autodoc-pytest-fixtures all
  move 0.0.1a10 -> 0.0.1a11 in every dependency group that pins
  them (one row in [project.optional-dependencies], one row in
  [dependency-groups] per package).
- uv.lock regenerated against the new pins; gp-sphinx workspace
  siblings co-resolved to 0.0.1a11.
tony added a commit to tmux-python/libtmux-mcp that referenced this pull request Apr 27, 2026
why: gp-sphinx cut 0.0.1a11 covering a theme-flicker fix that
addresses three independent failure modes on Furo + Cloudflare
deploys. (1) Flash of the wrong colour scheme on initial load when
localStorage.theme and OS prefers-color-scheme disagree. (2)
Toggle-icon "pop-in" mid-load caused by furo.js being deferred by
Cloudflare Rocket Loader, leaving <html class="no-js"> on the page
and the .theme-toggle-container hidden until furo.js eventually
ran. (3) Animated mid-blend on .sig and dl.py headers during
runtime theme toggle, from CSS transitions on background-on-hover
that also ran on every CSS-variable swap. Tracking the new alpha
picks all three fixes up on the next docs build at
https://libtmux-mcp.git-pull.com/. See
git-pull/gp-sphinx#23 for the source-side
detail.

what:
- pyproject.toml: gp-sphinx, sphinx-autodoc-api-style, and
  sphinx-autodoc-fastmcp all move 0.0.1a10 -> 0.0.1a11 in every
  dependency group that pins them (one row in
  [project.optional-dependencies], one row in [dependency-groups]
  per package).
- uv.lock regenerated against the new pins; gp-sphinx workspace
  siblings co-resolved to 0.0.1a11.
tony added a commit to tony/django-slugify-processor that referenced this pull request Apr 27, 2026
why: gp-sphinx cut 0.0.1a11 covering a theme-flicker fix that
addresses three independent failure modes on Furo + Cloudflare
deploys. (1) Flash of the wrong colour scheme on initial load when
localStorage.theme and OS prefers-color-scheme disagree. (2)
Toggle-icon "pop-in" mid-load caused by furo.js being deferred by
Cloudflare Rocket Loader, leaving <html class="no-js"> on the page
and the .theme-toggle-container hidden until furo.js eventually
ran. (3) Animated mid-blend on .sig and dl.py headers during
runtime theme toggle, from CSS transitions on background-on-hover
that also ran on every CSS-variable swap. Tracking the new alpha
picks all three fixes up on the next docs build at
https://django-slugify-processor.git-pull.com/. See
git-pull/gp-sphinx#23 for the source-side
detail.

what:
- pyproject.toml: gp-sphinx and sphinx-autodoc-api-style move
  0.0.1a10 -> 0.0.1a11 in every dependency group that pins them
  (one row in [project.optional-dependencies], one row in
  [dependency-groups] per package).
- uv.lock regenerated against the new pins; gp-sphinx workspace
  siblings co-resolved to 0.0.1a11.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants