Add a light theme with an Appearance setting - #1
Merged
Merged
Conversation
The panel has one palette. On a machine set to light it is a dark rectangle matching nothing around it. Three inputs decide the palette: high contrast wins, then an explicit override, then Windows own preference as the default. High contrast keeps winning because a user who turned it on did not mean "unless the app has a light theme". The control reuses the three-segment component the noise mode row already uses, rather than inventing a third settings idiom. The panel teaches that pattern once and gets a second use free; the existing pill toggles cannot express three states anyway. Its subtitle states what actually resolved, so AUTO is not a black box on a machine whose Windows is dark. Seven colours are sampled from the supplied mockups, by analysing colour frequency rather than reading fixed coordinates - guessed coordinates were tried first and landed on background, because these images are 363x457 and the dark ones were 358x521. Three further greys are present but not yet located, and the spec says to locate the feature rather than guess which grey is which. Four appear in neither mockup and are derived, marked as derived, because theme.rs opens by claiming every value in it was measured. The warning banner is called out: in the dark palette it is almost indistinguishable from the card, because the warning is carried by the glyph and the wording. Light keeps that relationship rather than inventing an amber nobody specified. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R5ZQjfGZPMQaG6vZrc9YE2
Five tasks. The palette indirection already exists from the high-contrast work, so this is a third palette plus a resolution function plus one settings row - not a rewrite. The precedence rule gets an exhaustive table test because it is the part most likely to be wrong: three appearance choices times two Windows preferences times high contrast on and off. Task 3 factors the segmented control out of the noise row before reusing it, rather than copying it, and checks the dark fixtures are byte-identical afterwards - that is what proves the refactor changed nothing. Nine palette values are already sampled and listed. Three text colours still need the glyph runs located, and the plan carries the script plus the reason the rule inverts: the dark palette took peak luminance within a glyph run because the ink was light, and on a light background you want the darkest pixel instead. Three more appear in neither mockup and are marked derived, because theme.rs claims everything in it was measured. Gate checks use exit codes throughout. Grepping test output for FAILED matches the word "failed" in every passing line, which is how a red build reached main earlier today. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R5ZQjfGZPMQaG6vZrc9YE2
Twelve of fifteen colours are sampled from the supplied mockups by locating each feature and reading it. Fixed coordinates were tried first and returned background for four of six, because these images are 363x457 while the dark ones are 358x521. Text is sampled as the DARKEST pixel in the glyph run, inverting the rule at the top of theme.rs. That rule takes peak luminance because light ink anti-aliases toward a dark background; here dark ink anti-aliases toward a light one. Following it unchanged would have sampled near-white and produced invisible text that still passes a naive contrast check. It also corrects a first reading: frequency analysis suggested #9397AB for text_secondary, but that is the anti-aliased body - the stroke core is #6E7283. Three values appear in neither mockup and are marked derived rather than measured, because this file opens by claiming everything in it was sampled. accent_label is among them: no end label is highlighted in the mockup, since sidetone sits at 14 of 15. Precedence is a pure function of (choice, Windows preference, high contrast) with an exhaustive table test, because that is the part most likely to be got wrong. High contrast still wins over everything - someone who turned it on did not mean "unless the app has a light theme". Storage follows the existing settings pattern exactly; read_dword and set_string were already there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R5ZQjfGZPMQaG6vZrc9YE2
A three-segment AUTO / DARK / LIGHT row in Settings, reusing the component the noise mode row already uses rather than inventing a third settings idiom. The existing pill toggles cannot express three states, and the panel teaches the segmented pattern once already. The segmented control was extracted from noise_section before being reused, not copied - and the extraction was verified by rendering every fixture and confirming all twelve came back byte-identical. The row subtitle names the theme that actually resolved, so AUTO is not a black box on a machine whose Windows is dark - which is this one. One real bug found by looking at the render: the toggle knob used text_primary, which is near-white in the dark palette by coincidence and near-black in the light one, where the knob vanished into its own track. It now has its own palette role, which is what it always was. Worth flagging rather than silently resolving: the light mockup draws the ON toggle differently from the implementation - a pale #D7D5E7 track with an accent knob, where the code uses an accent track with a light knob. That was not adopted, because matching it would mean maintaining two toggle designs for one visual difference. Easy to change if the mockup is the intent. Verified on the machine: the default is System, an override persists across a restart, and an unrecognised registry value leaves the tray drawing rather than failing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R5ZQjfGZPMQaG6vZrc9YE2
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.
Adds a light theme, following the Windows setting by default with a manual override.
Design:
docs/history/specs/2026-08-02-light-theme-design.mdPlan:
docs/history/plans/2026-08-02-light-theme.mdWhat this changes
A three-segment AUTO / DARK / LIGHT row in Settings, reusing the same component the noise mode row uses rather than inventing a third settings idiom — the pill toggles cannot express three states, and the panel already teaches the segmented pattern. The row subtitle names the theme that actually resolved (
Following Windows — dark), soAUTOis not a black box.Precedence: high contrast > explicit override > Windows preference. Someone who turned high contrast on did not mean "unless the app has a light theme".
How it was verified
theme.rs. That rule takes peak luminance because light ink anti-aliases toward a dark background; here it is the reverse. Following it unchanged would have sampled near-white and produced invisible text that still passes a naive contrast check. It also corrected a first reading — frequency analysis suggested#9397ABfortext_secondary, but that is the anti-aliased body; the stroke core is#6E7283.theme.rsopens by claiming everything in it was sampled.settings.pngchanges, because that view gained a row.One bug found
The toggle knob used
text_primary, which is near-white in the dark palette by coincidence and near-black in the light one, where the knob vanished into its own track. It now has its own palette role, which is what it always was. Found by looking at the render, not by a test.One thing left for you
The light mockup draws the ON toggle differently from the implementation — a pale
#D7D5E7track with an accent knob, where the code uses an accent track with a light knob. I did not adopt it, because matching it would mean maintaining two toggle designs for one visual difference. Easy to change if the mockup is the intent.Not verified
Switching Windows to light and watching an open panel follow. That needs your display settings, and
WM_SETTINGCHANGEis wired up for it.