Skip to content

Add a dark theme, chosen in the settings or following the system - #642

Merged
zaerl merged 4 commits into
trunkfrom
feat/dark-mode
Oct 6, 2026
Merged

zaerl merged 4 commits into
trunkfrom
feat/dark-mode

Conversation

@zaerl

@zaerl zaerl commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator

Why

#560, and the last row of #559's dialog. The window painted light only, and said so: its colour-scheme declaration was held to light by a test, since the older component library ships light styles and a promise of dark that only reached the native form controls gave charcoal inputs on white cards. A contributor whose machine is in dark mode got a white window all evening, with no way to say otherwise. This adds a dark theme, chosen in the settings or following the system.

What changes

Theme, on the General tab, under Appearance: Light, Dark or System, which is the default and follows the operating system. A change is on screen as it is made; nothing running is touched. The prototype's fourth answer, a custom pair of colours, is not offered.

One source of truth. Main gives the choice to Electron's nativeTheme, read from the store before the window is made and set again as the setting changes. Chromium answers the page's prefers-color-scheme from it and paints the window's chrome and the native form controls to match, so the window only reads that answer: a wrapper around the design system's provider seeds the dark ramp from the prototype's dark background when the scheme is dark, and passes no colour when it is light, so the light theme is pixel for pixel what it was and no picture in the guide moves. The provider builds every token from the seed, for its own components, for the older library's through the variables it maps for them, and for the app's own styles; the diff and log panes follow through their tokens.

What the provider does not reach. The terminal takes its colours as values and reads them again when the scheme changes. The older library's dialogs and popovers are painted white in its own stylesheet, with no token behind the colour, and are painted here with tokens; the popover's ring and shadow are restated in the dark scheme only.

No white before the page paints. A dark window is made in the dark colour and kept in it as the theme changes, and the page paints its body the same colour under the dark scheme until the provider has mounted, which it marks on the document. color-scheme.test.cjs now holds the declaration to light dark and that colour to the theme module.

The pictures and the journeys. Playwright holds a page to the light scheme whatever the machine says, so the guide's pictures are pinned to light through the app's own setting (SHOTS_THEME=dark for a dark one), and every journey stays light; the theme journey alone lets the page follow the app.

Not in this pull request:

  • The patch and Trac windows, which are made without a theme and paint their own bar white. Follow-up.
  • A custom theme.
  • Mail rendering is unchanged here and tracked separately.

How to test this

Platforms: any. The journeys drive this on macOS and Windows. From the repository root, after npm ci and npm run build:once:

npx playwright test --project=journeys settings.spec i18n.spec terminal.spec logs.spec
  • settings.spec.js, the theme journey: stored dark, the window starts dark, Electron is told, the body and the terminal are painted with the dark tokens; Light chosen is kept, Electron is told, the page, the body, the terminal and the window's own colour are light at once with no relaunch; System is kept and the window is the colour of whichever theme Electron then says; started again with dark kept, the window is made dark and the control says so.
  • i18n.spec.js: the control in the pseudo-locale.
  • terminal.spec.js, logs.spec.js: the terminal's and the panes' colours are the tokens, as before.

I broke six claims one at a time and each was caught: the stored theme not applied at startup, the setting not telling Electron, the window not made dark, the terminal not re-reading its colours, the page not seeded dark, the window not painted on a setting change (the last by ipc-wiring.test.cjs; the journey is kept green by Electron's own updated for that case). tests/unit/theme.test.cjs, settings.test.cjs, settings-view.test.cjs and color-scheme.test.cjs hold the model, the control's entries and the page's declaration.

Worth a look by hand (any platform, the current head): Settings → General → Theme. Press Dark: the whole window, the dialog you are in and the footer's Give feedback popover are dark at once; the site menu, the create-site dialog and the review dialog's diff are dark when opened. Open the Terminal tray before and after: it follows. Press System and change your machine's appearance: the window follows. Quit with Dark kept and open the app: it opens dark, with no white flash.

What must not have happened: the light theme looking any different from before (the guide's pictures are the reference); a white window or page in the moment before a dark page paints; the terminal keeping the old colours after a switch; a server or build stopped or started by the change.

What I could not test: Windows, where the journeys run in CI; the system's theme changing under System by hand, which the unit test drives through Electron's updated with a stubbed answer, and which I could not stage on the Mac without changing its appearance under the running suite. The pictures of the dark theme in this description were taken with SHOTS_THEME=dark and are not in the diff: the guide shows the light theme.

Risks and limitations

  • Review: 2 passes, 6 findings fixed here, 1 follow-up left (the patch and Trac windows), every style note applied. Details below.
  • The patch and Trac windows stay white under Dark, as they were. Follow-up.
  • The dark theme is the design system's ramp from one seed, not a hand-made palette: a surface or a stroke that reads wrong in dark is the ramp's to fix, and the one token the app sets by hand is the pre-mount body colour, held to the seed by a test.
  • The older library's buttons keep a handful of literal colours for a disabled or a destructive state, readable in dark but not the ramp's.

Related

Closes #560. Part of #559, which is part of #542. Follows #641.


Design decisions and alternatives considered
  • nativeTheme as the one place the choice is made. The prototype resolves 'system' in the page with matchMedia. Here main resolves nothing: it gives Electron the setting, and the page reads prefers-color-scheme, which Electron answers from it. One chain, setting → Electron → page → tokens, and the window's chrome and the native controls come with it.
  • No seed in the light scheme. Generating the light ramp from a seed moves every colour by a hair; the tokens stylesheet already holds the light values, and the guide's pictures show them.
  • The window's own colour set from the setting, not left to updated. Electron promises the event for a change to light or dark, not to system; the listener stays for the system's theme changing under 'system'.
  • A pre-mount body rule in index.html, scoped to before the provider mounts. The page's first paint is before its script runs; the rule stops at the attribute the provider sets, so the ramp's surface is the body from then on. The colour is the seed, and a test holds the two together.
  • Three themes, not four. A custom pair of colours is a feature the prototype has and nobody has asked for.
Review outcome (required — see AGENTS.md)

Pass 1: 3 [fix here] · 1 [follow-up] · 5 style notes. Pass 2 (on the fix-up): 3 [fix here] · 0 [follow-up] · 1 style note. All 6 fixed here, every style note applied, 1 follow-up left.

  • Review: completed — a separate agent context, read-only; head 8a7457b / base 3866e1a; the store rule checked against the startup read and the handler, the provider's effect ordering against React's commit phases, the pre-mount rule's specificity, the deep-link path that can open the window before the ready path, the old library's rules the shell overrides, translation extraction; ESLint and the unit files run; 3 [fix here], 1 [follow-up].
    • Fixed: the window's colour was set once and went stale on a theme change, an OS change and a deep link's early window; the journey read the light body before the app had repainted; the popover's ring and shadow were restated in the light theme too, and a menu-group rule matched nothing.
    • Follow-up left: the patch and Trac windows are not themed.
    • Style notes, applied: a stale comment on the store's first read; translator context for the theme's words; SHOTS_THEME checked; the tests naming the colours by their constants; a unit test that restated constants.
  • Review: completed — a second separate agent context, read-only; head 64cad1a / base 3866e1a; the updated listener against Electron 43's documentation, the stub's assumptions, the light theme against the library's and the tokens' values; ESLint and the unit files run; 3 [fix here].
    • Fixed: nothing showed Electron emits updated for a change to 'system', so the window is painted from the setting itself and the journey reads the window's colour; a test's name claimed a deep link's window it did not exercise; SHOTS_THEME=system passed the check that exists to keep the system's theme out of the pictures.
    • Style note, applied: the dark popover's soft layer is a glow, and the comment says so.
  • Since review: 64cad1a → HEAD is those three and the note, not reviewed again. On the head: npm run lint, npm test, npm run test:electron (2029), all 124 journeys, the packaged smoke and the docs build pass.
Implementation notes
  • src/theme.cjs: THEMES, themeColorSeeds, windowBackground, the two seeds. src/settings.cjs: theme. src/main.js: applyStoredTheme, paintWindowForTheme, the updated listener, backgroundColor on the window, settings:set.
  • src/renderer/components/app-theme.jsx: AppTheme, useDarkScheme. src/renderer/index.jsx: mounted under it. src/renderer/hooks/use-site-terminal.jsx: the re-read. src/renderer/terminal-theme.cjs: the header. src/renderer/shell.css: the old library's dialogs and popovers. src/renderer/index.html: color-scheme and the pre-mount rule.
  • src/renderer/settings-view.cjs: themeItems. src/renderer/components/settings-dialog.jsx: ThemeControl.
  • scripts/screenshots/fixtures.cjs: SHOTS_THEME. scripts/screenshots/capture.cjs, tests/e2e/helpers/app.cjs: colorScheme.
  • Tests: tests/unit/theme.test.cjs, color-scheme.test.cjs, settings.test.cjs, settings-view.test.cjs, ipc-wiring.test.cjs (the electron stub's nativeTheme); tests/e2e/journeys/settings.spec.js.
Screenshots or recording

Not attached: I have no way to upload images from the command line. SHOTS_THEME=dark npm run shots takes the guide's pictures in the dark theme into docs/public/screenshots/ for a look (and leaves them changed; restore them after).

🤖 Generated with Claude Code

zaerl and others added 3 commits October 6, 2026 17:25
The window painted light only, and said so in its color-scheme declaration (#560). Now it paints both: a Theme control on the General tab offers Light, Dark and System, and System, the default, follows the operating system.

Main gives the choice to Electron's nativeTheme, read from the store before the window is made and set again as the setting is changed; Chromium answers the page's prefers-color-scheme from it and paints the window's chrome and the native form controls to match. The window reads only that answer: a wrapper around the design system's provider seeds the dark ramp from the prototype's dark background when the scheme is dark, and passes no colour in the light scheme, so the light theme is pixel for pixel what it was. The terminal, which takes its colours as values, reads them again when the scheme changes. The older component library's dialogs and popovers, painted white in its own stylesheet, are painted with tokens.

A dark window is made in the dark colour, and index.html paints the body the same colour under the dark scheme until the provider has mounted, so nothing white shows before the page has its tokens; color-scheme.test.cjs holds the declaration and that colour to the theme module.

The screenshot fixtures pin the pictures to the light theme whatever the maintainer's machine is set to, with SHOTS_THEME=dark for a dark one, and the harness lets the page follow the app's theme rather than Playwright's light emulation; a journey can ask for the same with colorScheme: null, and the theme journey does.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
From the first review pass. The colour the window was made with is given again whenever Electron says the theme changed: a setting changed, the system's theme under 'system', or a deep link that opened the window before the stored theme was read. The popover's ring and shadow are restated in the dark scheme only, so the light theme is what it was, and a rule that matched nothing is gone. The theme journey reads the light body once the app has repainted. The theme's words carry a translator's context, SHOTS_THEME is checked against the themes, and the tests name the colours by their constants.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… in the shots

From the second review pass. The window's colour is set as the theme setting is changed, and when the stored theme is applied, rather than left to Electron's `updated`, which is not promised for a change to 'system'; the listener stays for the system's theme changing under 'system'. The theme journey reads the window's own colour after each choice. SHOTS_THEME takes light or dark and not the system's, which is what the pin keeps out of the pictures. The dark popover's soft layer is said to be the glow it is.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Repository: WordPress/contributor-toolkit/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: e80f978e-8790-4a51-bd28-792ceadf3b4a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@zaerl zaerl self-assigned this Oct 6, 2026
@zaerl
zaerl merged commit 9fd167e into trunk Oct 6, 2026
10 checks passed
@zaerl
zaerl deleted the feat/dark-mode branch October 6, 2026 16:20
zaerl added a commit that referenced this pull request Oct 7, 2026
…nothing opens (#649)

## Why

The follow-up of
#642: the two
windows the app makes besides its own were white under the dark theme.
One of them turned out not to exist for anyone.

## What changes

**The Trac window is made in the app's theme.** It is the window Trac's
proof-of-work check runs in, shown when the check needs a click, and it
is now made in the colour of the theme the app is in, so its frame is
not white where Trac's page does not paint. Trac's own page is Trac's
and stays as Trac draws it.

**The patch window is gone.** It had no caller: the review dialog
replaced it in the window, and only the preload bridge and the smoke's
list of the bridge still named it; the git history shows no renderer
ever called it. A window nobody opens cannot be the wrong colour, so it
is removed with its channel and its bridge entry, and the review
standard's window invariant names the two windows that exist, the main
one and Trac's.

**Not in this pull request:** a dark Trac page; mail rendering is
unchanged here and tracked separately.

## How to test this

Platforms: any; the Trac window needs the network. From the repository
root, after `npm ci`:

```
node --test tests/unit/trac-view.test.cjs tests/unit/ipc-wiring.test.cjs
```

- `trac-view.test.cjs`: the Trac window is made in the dark colour under
the dark theme and the light colour under the light one; the test is red
on trunk, where the window is made with no colour.
- `ipc-wiring.test.cjs`: every handler main registers is still one the
suite names, with `git:create-patch` gone from both.
- The packaged smoke holds the bridge to its list, with
`createPatchWindow` gone from both.

This change has no journey of its own: no journey opens the Trac window,
since it needs Trac's check, and the patch window is removed, not
changed.

**Worth a look by hand** (any platform, the current head, with the
network): set the theme to **Dark** in Settings → General, link a Trac
ticket and press **Show Trac attachments** on a ticket whose check Trac
escalates to the "I am human" checkbox, so the window appears: its frame
is dark around Trac's page. On a ticket whose check clears by itself the
window never shows, as before.

**What must not have happened:** anything in the window offering a patch
in a window of its own (nothing did before either); the Trac window
failing to show when the check needs a click; the attachment list not
arriving.

**What I could not test:** the escalated check by hand, which Trac
decides; I could not make it ask for the click.

## Risks and limitations

- Review: 2 passes, 2 findings fixed here, every style note applied,
none left. Details below.
- The bridge loses a function (`createPatchWindow`) nothing called; a
fork of the renderer that did call it would find it gone.
- A pre-existing journey, the Gutenberg pull request card in
`i18n.spec.js` from
#643, passes in the
full suite only on a retry and fails when run alone, on trunk as here.
Not touched here.

## Related

Follow-up of #642,
part of #560 and #542.

---

<details>
<summary>Review outcome (required — see AGENTS.md)</summary>

**Pass 1: 2 [fix here] · 0 [follow-up] · 3 style notes. Pass 2 (on the
fix-up): 1 [fix here] · 0 [follow-up] · 2 style notes. All applied
here.**

- **Review:** completed — a separate agent context, read-only; head
`1d0673d` / base `dca4bde`; the claim that nothing opens the patch
window checked against the renderer, the docs, the menu, the deep-link
path and the git history (no renderer ever called it); the module loader
of the Trac window's tests; the coverage guard of the wiring suite;
ESLint and the two unit files run; 2 [fix here].
- Fixed: the review standard's window invariant and three comments named
the patch window.
- Style notes, applied: the Trac window's comment overstated when its
colour shows; the new test slept through the poll; the save-patch test's
name implied a comparison it did not make.
- **Review:** completed — a second separate agent context, read-only;
head `99efecf` / base `dca4bde`; the zero deadline traced through
`openAndScrape`, the renamed test against what it reads, the standard's
sentence against the two windows that exist; ESLint and the two unit
files run; 1 [fix here], wording.
- Fixed: the save-patch test's name said "first read" of a read the
handler makes second.
- Style notes, applied: the stand-in window's variable still called it a
patch; a deadline "already passed" that is a deadline of now.
- **Since review:** `99efecf → 5131c7c` is that wording, in the two unit
files only. On `1d0673d`: `npm run lint`, `npm test`, `npm run
test:electron`, all 124 journeys and the packaged smoke pass; on
`99efecf`: `npm run test:electron` (2038); on the head: `npm run lint`
and the two unit files.

</details>

<details>
<summary>Implementation notes</summary>

- `src/trac-view.js`: `backgroundColor` from
`windowBackground(nativeTheme.shouldUseDarkColors)`.
- `src/main.js`: `buildPatchHtml` and `git:create-patch` removed.
`src/preload.js`: `createPatchWindow` removed.
`tests/e2e/packaged/smoke.spec.js`, `tests/unit/ipc-wiring.test.cjs`:
their lists.
- `tests/unit/trac-view.test.cjs`: the colour under each theme.
- `.github/instructions/code-review.instructions.md`: the window
invariant names the main and the Trac window.

</details>

<details>
<summary>Screenshots or recording</summary>

Not attached: the Trac window shows only when Trac escalates its check,
which I could not make it do.

</details>

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
zaerl added a commit that referenced this pull request Oct 7, 2026
…tor's own (#650)

## Why

The prototype's dialog has a fourth theme, and the dark theme of
#642 left it out: a
background and an accent colour of the contributor's own, from which the
design system builds every other colour as it builds the dark theme from
its one seed. This adds it, under the name the design system gives the
second colour: primary. Part of #560.

## What changes

**Custom, on the General tab.** Under **Theme**, a fourth answer;
chosen, two fields appear beneath it: **Background** and **Primary**,
each typed as hex or picked with the system's colour picker, which is
the swatch before the field. A colour is kept as six lowercase hex
digits from three or six as typed; one that is not a colour is refused
in main's words and the field goes back to what is kept. The picker's
choice is kept when the picker is done, not at every step of a drag,
since each keep is a write to the store; the field shows the colour
under the pointer meanwhile. The colours start as the light theme's.

**A custom theme is a scheme too.** Main gives Electron light or dark by
whether the background reads better with white text than with black, the
crossover the design system's own ramp turns at, so the native form
controls and the window's chrome match the page; and the window is made
and painted in the background chosen, as is the Trac window, which main
now hands its colour. The window reads the settings above the design
system's provider, since the theme is one of them, and gives the
provider the two colours; the terminal re-reads its colours whenever the
theme as painted changes, by a name that changes with it.

**When the colours do not read.** The design system says when it could
not reach the contrast it wants between two of the colours it built, and
under Custom the dialog passes that on in one sentence. The colours are
kept all the same: a contributor who chose them can read them, and the
sentence is for the one who did not mean to.

**Not in this pull request:**

- The moment before the settings arrive: a custom theme shows the
standard theme of its scheme for one round trip to main at launch, then
its own. Removing it means handing the renderer the theme before its
first paint, new bridge surface for a separate change.
- Mail rendering is unchanged here and tracked separately.

## How to test this

Platforms: any. The journeys drive this on macOS and Windows. From the
repository root, after `npm ci` and `npm run build:once`:

```
npx playwright test --project=journeys settings.spec i18n.spec
```

- `settings.spec.js`, the custom theme journey: the colours are asked
for under Custom alone and start as the light theme's; a dark background
typed and entered is kept, Electron is told the scheme, and the page,
the body and the window are painted in it at once; a primary colour in
three digits is kept in six and the brand surface is built from it; a
colour spelled another way shows as kept; a colour picked with the
picker is kept; a colour that is not one is refused in main's words and
what is kept stays; a light background makes a light theme again;
started again, the window is made in the background and the fields show
the colours kept.
- `i18n.spec.js`: the control in the pseudo-locale.

Six claims were broken one at a time and each turned the journey red:
the page not seeded from the colours, Electron not told the scheme, a
refusal leaving the typed text, three digits not expanded, the window
not painted in the background, and the picker driven from what is kept
rather than the draft, which was the bug the first review pass found.
`tests/unit/theme.test.cjs` holds the resolution, the crossover and the
hex forms; `settings.test.cjs` what is accepted; `ipc-wiring.test.cjs`
what Electron, the window and the Trac window are given.

**Worth a look by hand** (any platform, the current head): Settings →
General → Theme → **Custom**. Type `102030` into Background and press
Enter: the whole window is navy, the dialog included, and the native
controls (the folder field's button, a select's list) are dark. Press
the Primary swatch, drag in the picker: the field follows the pointer;
close the picker: the buttons take the colour. Type `navy`: refused
under the fields, the field back to the colour kept. Type `fff8e1`: a
cream theme with dark text and light controls. Quit and open the app: it
opens in the cream, after a moment of the standard light theme.

**What must not have happened:** the standard themes looking any
different (the guide's pictures are the reference); a picker drag
writing the store at every step; a colour kept that the field does not
show; a server or build touched by a change.

**What I could not test:** Windows and Linux pickers, which the OS
draws; the journeys drive the picker through its value, as a picker does
when it is done.

## Risks and limitations

- Review: 2 passes, 6 findings fixed here, 1 follow-up left (the flash
at launch), every style note applied. Details below.
- **A custom theme flashes the standard theme of its scheme at launch**,
for one round trip to main. See above.
- **The pre-paint of a dark window** is the dark seed, not the custom
background, for the moment before the page's script runs; then the
standard dark theme, then the custom one.
- **Two colours can be chosen that read badly together.** The dialog
says so and keeps them; the standard themes are one press away.

## Related

Part of #560, which is part of #542. Follows
#642 and
#649.

---

<details>
<summary>Design decisions and alternatives considered</summary>

- **"Primary", not the prototype's "accent".** It is what the design
system calls the seed, and what the provider's prop is named.
- **The scheme from the background's luminance**, by the rule the design
system's ramp uses to pick its text direction: white where it contrasts
better than black. One rule, so the native controls and the ramp agree
except within a hair of the crossover.
- **The picker commits when done, not as it is dragged.** A drag is tens
of events a second; each keep is a store write and an IPC round trip.
- **The picker driven from the draft.** A controlled input is put back
to its prop after every step of a drag, and the picker's closing then
reads the old colour: the first review pass found the picker never
saving.
- **The settings read above the provider.** The theme is a setting, so
the component that reads the settings has to sit above the one that
paints with them; the app is handed what was read.
- **The warning passed on, the colours kept.** The design system's
answer is the authority on contrast; the contributor is the authority on
what they want to look at.

</details>

<details>
<summary>Review outcome (required — see AGENTS.md)</summary>

**Pass 1: 4 [fix here] · 1 [follow-up] · 4 style notes. Pass 2 (on the
fix-up): 2 [fix here] · 0 [follow-up] · 3 style notes. All 6 fixed here,
every style note applied, 1 follow-up left.**

- **Review:** completed — a separate agent context, read-only; head
`8b3658a` / base `59afb0c`; the store rule against the changed handlers,
the theme's ordering in main against Electron's `themeSource`, the
luminance maths against the design system's own ramp direction, React's
controlled-input restore against the colour picker's `change`, the
design system's input and the provider's warnings; ESLint and the unit
files run; 4 [fix here], 1 [follow-up].
- Fixed: a colour picked with the picker was never kept, since a
controlled input is put back to its prop after every step of a drag and
the picker's closing read the old colour; the wiring test's Trac
assertion never opened a Trac window; the fallback before the settings
arrive was a branch in the component; the hex fields' monospace rule
reached nothing.
- Follow-up left: the standard theme of the scheme shows for one round
trip at launch.
- Style notes, applied: the crossover pinned; a colour spelled another
way shown as kept; a dead prop; the translator comment checked.
- **Review:** completed — a second separate agent context, read-only;
head `7fdf4e0` / base `59afb0c`; the picker's event order on a real
picker and under Playwright's fill, the listener's double-commit, the
accent ramp's first step for the journey's exact colours, the crossover
recomputed; ESLint and the unit files run; 2 [fix here].
- Fixed: a keep that finished late overwrote newer typing in the field;
what the field shows after a keep, and what the picker holds while the
field holds no colour, were decided in the component.
- Style notes, applied: the Trac assertion built a repository it never
opened; the journey's two exact colours say where they come from; a
comment on the swatch.
- **Since review:** `7fdf4e0 → 0a9965d` is those two and the notes, not
reviewed again; `31c125c` stands the swatch in the design system's
prefix slot as a centred square, from the maintainer's look at the
dialog, not reviewed again. On `0a9965d`: `npm run lint`, `npm test`,
`npm run test:electron`, all 133 journeys, the packaged smoke and the
docs build pass; on the head: `npm run lint`, `npm run test:electron`
(2045) and all 133 journeys, the packaged smoke not run since the
maintainer's own app held the single-instance lock. The commit after it
fixes a Windows-only cleanup race in `tests/unit/log-tail.test.cjs`, a
test this branch does not otherwise touch, which failed the Windows unit
job on `31c125c`: the read helper now settles on the stream's `close`,
so the folder is removed after the file is.

</details>

<details>
<summary>Implementation notes</summary>

- `src/theme.cjs`: `THEMES` with 'custom', `THEME_KEYS`, `resolveTheme`,
`nativeThemeSource`, `normalizeHexColor`, `isHexColor`, `isDarkColor`,
`PRIMARY`. `src/settings.cjs`: `customBackground`, `customPrimary`,
`acceptColor`.
- `src/main.js`: `themeSettings`, `applyTheme`, `currentTheme`,
`paintWindowForTheme`; `settings:set` applies for any theme key; the
Trac window is handed `backgroundColor`. `src/trac-view.js`:
`deps.backgroundColor`.
- `src/renderer/components/app-theme.jsx`: `AppTheme({ settings })`,
`useThemeKey`, `useThemeWarnings`. `src/renderer/index.jsx`: `Root`
reads the settings above the provider.
`src/renderer/hooks/use-site-terminal.jsx`: re-reads on the theme's
name.
- `src/renderer/components/settings-dialog.jsx`: `ColorField`, the
fourth option, the warning. `src/renderer/settings-view.cjs`:
`themeItems`. `src/renderer/shell.css`: `.theme-colors`, `.color-field`,
`.color-swatch`.
- Tests: `tests/unit/theme.test.cjs`, `settings.test.cjs`,
`settings-view.test.cjs`, `ipc-wiring.test.cjs`, `trac-view.test.cjs`;
`tests/e2e/journeys/settings.spec.js`.

</details>

<details>
<summary>Screenshots or recording</summary>

Not attached: I have no way to upload images from the command line. The
custom theme journey drives everything that changed; `SHOTS_THEME` takes
light or dark only, so a picture of a custom theme is a hand's work.

</details>

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
@zaerl zaerl added this to the v2.0.0 milestone Oct 7, 2026
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.

Add dark mode

1 participant