Skip to content

Add a light theme with an Appearance setting - #1

Merged
cunningorb merged 4 commits into
mainfrom
light-theme
Aug 3, 2026
Merged

cunningorb merged 4 commits into
mainfrom
light-theme

Conversation

@cunningorb

Copy link
Copy Markdown
Owner

Adds a light theme, following the Windows setting by default with a manual override.

Design: docs/history/specs/2026-08-02-light-theme-design.md
Plan: docs/history/plans/2026-08-02-light-theme.md

What 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), so AUTO is 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

  • 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 the light mockups 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 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 #9397AB for text_secondary, but that is the anti-aliased body; the stroke core is #6E7283.
  • Three values are marked derived rather than measured, because they appear in neither mockup and theme.rs opens by claiming everything in it was sampled.
  • Precedence has an exhaustive table test over every combination of choice, Windows preference, and high contrast.
  • The segmented control was extracted before reuse, not copied, and all twelve rendered fixtures came back byte-identical afterwards — that is what proves the extraction changed nothing. Only settings.png changes, because that view gained a row.
  • 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.

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 #D7D5E7 track 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_SETTINGCHANGE is wired up for it.

micmancg and others added 4 commits August 2, 2026 19:10
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
@cunningorb
cunningorb merged commit 4a994a1 into main Aug 3, 2026
1 check passed
@cunningorb
cunningorb deleted the light-theme branch August 3, 2026 02:33
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.

1 participant