From 8ed007a75f786635bc42751eb136deaa5c67d434 Mon Sep 17 00:00:00 2001 From: Connor Johnstone Date: Tue, 8 Sep 2026 18:00:01 -0400 Subject: [PATCH] Add a theme toggle to the nav, and centralise the palette Three states rather than two: system (follow the OS), light, dark. A two-state toggle cannot express "just do what my laptop does", which is the state most people actually want. Two details that are easy to get wrong and were both handled deliberately: - The dark rules are guarded with :root:not([data-theme="light"]). Without that guard, choosing light does nothing on a machine whose OS is set to dark, because the media query keeps winning -- the toggle appears to work in one direction only. - The stored theme is applied by an inline script placed ahead of any rendered content, so there is no flash of the wrong palette on load. A deferred script would repaint after first paint, visibly. localStorage access is wrapped in try/catch: it throws outright in some private-browsing modes, and a theme preference is not worth a broken page. The palette moved out of all four templates into _theme.html, which also removes four near-identical copies of the nav CSS that had already started to drift. Tests assert the toggle, the guard, the pre-paint ordering, and that no page redeclares the palette locally. --- news/templates/_nav.html | 25 ++++++++++++++++++++++ news/templates/_theme.html | 44 ++++++++++++++++++++++++++++++++++++++ news/templates/config.html | 14 +----------- news/templates/digest.html | 11 +--------- news/templates/triage.html | 14 +----------- news/templates/tune.html | 12 +---------- tests/test_delivery.py | 24 +++++++++++++++++++++ 7 files changed, 97 insertions(+), 47 deletions(-) create mode 100644 news/templates/_theme.html diff --git a/news/templates/_nav.html b/news/templates/_nav.html index 6f0304e..037a0ee 100644 --- a/news/templates/_nav.html +++ b/news/templates/_nav.html @@ -4,4 +4,29 @@ Digest Tune Config + + diff --git a/news/templates/_theme.html b/news/templates/_theme.html new file mode 100644 index 0000000..14ecd19 --- /dev/null +++ b/news/templates/_theme.html @@ -0,0 +1,44 @@ +{#- Shared palette, theme toggle and nav styling. + + Three states, not two: system (follow the OS), light, dark. The dark rules + are guarded with :not([data-theme="light"]) so an explicit light choice wins + over a dark OS setting -- without that guard the toggle only works one way. +-#} + + diff --git a/news/templates/config.html b/news/templates/config.html index 700babe..1e62cf4 100644 --- a/news/templates/config.html +++ b/news/templates/config.html @@ -2,15 +2,8 @@ Config +{% include "_theme.html" %}
diff --git a/news/templates/digest.html b/news/templates/digest.html index 2b9f29f..b0910b1 100644 --- a/news/templates/digest.html +++ b/news/templates/digest.html @@ -2,12 +2,8 @@ Briefing — {{ day }} +{% include "_theme.html" %}
diff --git a/news/templates/triage.html b/news/templates/triage.html index 9c427f3..4c23cf4 100644 --- a/news/templates/triage.html +++ b/news/templates/triage.html @@ -2,15 +2,8 @@ Triage +{% include "_theme.html" %} diff --git a/news/templates/tune.html b/news/templates/tune.html index 171d4d7..7cc6920 100644 --- a/news/templates/tune.html +++ b/news/templates/tune.html @@ -2,13 +2,8 @@ Tune +{% include "_theme.html" %}
diff --git a/tests/test_delivery.py b/tests/test_delivery.py index d4e4148..8228fb7 100644 --- a/tests/test_delivery.py +++ b/tests/test_delivery.py @@ -243,3 +243,27 @@ def test_every_page_carries_the_nav(client, path, active): for target in ("/tune", "/config", "/digest"): assert f'href="{target}"' in html, f"{path} is missing a link to {target}" assert f'class="here">{active}<' in html, f"{path} does not mark itself active" + + +@pytest.mark.parametrize("path", ["/", "/tune", "/config", "/digest"]) +def test_every_page_has_a_working_theme_toggle(client, path): + """Three states (system/light/dark), no flash, and the guard that makes the + light choice win over a dark OS setting.""" + c, _ = client + html = c.get(path).text + assert 'id="theme"' in html, "theme button missing" + assert '[data-theme="dark"]' in html, "explicit dark rules missing" + assert ':root:not([data-theme="light"])' in html, ( + "without this guard, choosing light does nothing under a dark OS theme") + # The theme must be applied before first paint, so its script has to appear + # ahead of any rendered content. + assert html.index("localStorage.getItem('theme')") < html.index("