Repository navigation
Let people choose light, dark or Auto in Settings › Appearance - #491
Merged
Merged
Conversation
…etting. The dark theme lived under `prefers-color-scheme` media queries, so it could only ever follow the device. To let someone choose a theme in the app, the stylesheet now keys every dark rule off `[data-theme='dark']` on `<html>`, and `theme.ts` keeps that attribute set to the theme in effect: the stored choice, or the device's answer when there is none, kept current as the device or another tab changes it. An inline script in `index.html` sets the attribute before the first frame, since the app's module runs after it and a dark reader would otherwise see a white flash on every load. A test runs that script against the module's rule so the two cannot drift. The front and features pages' pictures, the features clips, and the colour the Android and iOS apps paint around the page follow the same store. With nothing chosen everything resolves exactly as before, so this commit changes nothing anyone sees. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The app only followed the device's theme, with no way to read in dark on a light laptop or the reverse. Appearance is a new Settings page with three choices, each drawn as the app in miniature, and a line under them saying which theme Auto is showing right now. Three states rather than the two-state toggle often recommended for a header switch: that argument is about a control that is always on screen. On a settings page, "go back to following my device" has to be something you can pick, or someone who chose Dark on a dark device could never follow it into light again. The choice is kept per device, like the device's own setting. The page sits beside Offline, the other page about this device rather than the account. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…m the phone's. `enableEdgeToEdge` picks the bars' icon colour from the phone's night mode, while the strips behind them are painted in the page's own colour. Those always agreed until Settings › Appearance let the page choose a theme of its own: Dark on a light phone put dark icons on a dark strip, and the clock and battery vanished. The icons now take their contrast from the colour the page reports, as iOS's status bar already does from the same message. Before the page has reported, the system's answer stands, since the strip is still the app's own surface then. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e's. The Turnstile widget was rendered with Cloudflare's `theme: 'auto'`, which follows `prefers-color-scheme`. The theme choice is per device and outlives a sign-out, so someone who picked Light on a dark laptop got a dark check box on a light sign-in form. It now takes the theme in effect from `theme.ts`, read once at mount, since the widget is drawn once and the theme does not change on that screen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…uery. Every dark rule used to sit under `@media (prefers-color-scheme: dark)`, and that is the habit the next one will copy. Written that way now, a rule follows the device past the theme chosen in Settings › Appearance, and nothing else would notice. A test reads the stylesheet for such a query, and AGENTS.md says which selector to use instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`setThemeChoice` assigned `fallback` above its `let`. It worked, since the function only runs after the module has loaded, but it read as a temporal dead zone waiting to happen. The declaration now sits with the module's other state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three paths in `theme.ts` had no test: the `storage` listener that carries a choice made in one tab to the others, the three answers `useDarkSourceMedia` gives every front and features picture, and the choice held for the page when storage refuses the write. Each is pinned now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tile rules went in between `.offline-storage` and `.offline-kept`, which put 120 lines between the kept notebooks' comment and the rules it describes. They now follow the Offline rules as a section of their own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each tile carried a hand-copied set of four colours from its theme, with a comment asking whoever changed a theme to change its tile too. The two token blocks now also apply to `.theme-mini[data-theme]`, so a tile is drawn with exactly the colours its theme uses and cannot drift from them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Focusing a tile from the keyboard drew the tile's accent outline and the radio's own ring beside the word. The tile's is the larger and clearer of the two, so the radio's is dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`Turnstile` worked out the theme itself from storage, which skipped the choice `theme.ts` holds for a page whose storage refused the write. `currentTheme` is that one answer, exported for script that draws once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… chosen on the web. The Android and iOS sign-in screens load `/turnstile` in a web view and draw it inside a native screen that follows the phone. The web view keeps the site's storage across a sign-out, so a theme chosen in Settings › Appearance reached that page: Dark on a light phone put a dark box in the middle of a light sign-in screen. The page now follows the device. `followDeviceTheme` in `theme.ts` holds the page to the device's answer while the route is entered, and both the inline script and `theme.ts` start that way on a load of `/turnstile`, so no frame is drawn in the chosen theme first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Preview: https://unbent-pr-491.of2668.workers.devSign in with Continue on the dev instance: a preview cannot register Runs against the shared Worker |
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.
The app followed the device's light or dark setting and nothing else. This adds a choice: Settings › Appearance, with Auto, Light and Dark.
Where it lives, and why three states
It is a settings page, not a switch in the chrome. A theme is set once and then left alone, so a control on every view would take up room on every visit for a decision made once. The account menu is for the account, not the app.
The CSS-Tricks piece on two-state toggles argues against a third state for a control that is always on screen. It also says a settings panel is where all three belong, and that is this case: Auto has to be something you can pick again after choosing Light or Dark.
The three choices are tiles that each draw a small picture of the app in their theme, with Auto's tile split in half. They are real radio buttons, so arrow keys and screen readers treat them as one choice of three. A line underneath says what Auto is showing right now ("Follows this device, which is dark now"), or that a pinned choice is kept on this device only.
How it works
[data-theme='dark']on<html>, not@media (prefers-color-scheme). A test fails if a media-query dark rule comes back, and AGENTS.md says so.index.htmlsets the attribute before the first paint, so there is no flash of the wrong theme. A test runs that script againsttheme.ts's rules so the two cannot drift.theme.tsholds the choice inlocalStorage(unbent.theme; Auto is no key at all). It follows the device while Auto is chosen and picks up a choice made in another tab. When private browsing refuses the write, the choice still holds until the page closes.<picture>sources and clips follow the choice, not the device./turnstilepage that the apps' native sign-in screens embed always follows the phone instead, because those screens do.Not covered
./gradlew testpasses, but it has not been checked on a device. To check it: on a phone in light mode, choose Dark (and the reverse), and see that the clock and battery stay readable.Checks
The web suite passes (2,763 tests) and
tscis clean for both the app and the service worker. I checked the page in the browser at desktop and phone widths, in both themes.The release note for Android mentions the new setting. To fit it beside main's newer lines, I tightened the wording, and the note is now 499 characters.
🤖 Generated with Claude Code