Skip to content

[codex] add system appearance theme selection#3344

Merged
Yeraze merged 2 commits into
Yeraze:mainfrom
wilhel1812:codex/system-appearance-theme
Jun 8, 2026
Merged

[codex] add system appearance theme selection#3344
Yeraze merged 2 commits into
Yeraze:mainfrom
wilhel1812:codex/system-appearance-theme

Conversation

@wilhel1812

@wilhel1812 wilhel1812 commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a new appearance preference model with system, dark, and light modes. System mode follows prefers-color-scheme and switches automatically when the browser/OS appearance changes.

The theme slots are persisted as darkTheme and lightTheme, while the existing theme setting remains the effective/current theme for backward compatibility.

Behavior

  • New users default to system appearance with Mocha as the dark theme and Latte as the light theme.
  • Existing Mocha users migrate to the same system/Mocha/Latte default.
  • Existing non-Mocha users keep their current behavior by staying in explicit dark mode with their prior theme assigned to both slots.
  • Custom theme cards now expose Apply as Dark and Apply as Light; those buttons update only the corresponding slot and do not change appearance mode.

Validation

  • Passed: npm run test:run src/contexts/SettingsContext.test.tsx src/server/server.settings-persistence.test.ts src/components/CustomThemeManagement.test.tsx
  • Passed: npm run typecheck
  • Passed: npm run build
  • Passed: targeted ESLint on touched source/test files; warnings only
  • npm run lint still reports pre-existing unrelated repository lint/parser issues outside this change

@wilhel1812 wilhel1812 marked this pull request as ready for review June 7, 2026 09:59

@Yeraze Yeraze left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — system appearance theme selection

Solid, well-structured feature. CI is green and it's cleanly mergeable. The migration logic is careful and the per-slot model is implemented consistently across SettingsContext and SettingsTab. No crashes or blocking bugs found, but two real behavioral issues and a couple of nits are worth addressing before merge.

What it does well

  • Clean state model. appearanceMode: 'system' | 'dark' | 'light' + darkTheme/lightTheme slots, with the legacy theme key retained as the effective theme for backward compat. New keys are correctly added to VALID_SETTINGS_KEYS (the silent-save trap is avoided).
  • Migration matches the stated behavior and is mirrored in both the localStorage initializer (getInitialThemePreferences) and the server-load path: new users → system/mocha/latte; mocha users → same; non-mocha users → explicit dark with their theme in both slots.
  • System listener is correctmatchMedia('(prefers-color-scheme: dark)') with proper addEventListener/cleanup, and a separate effect recomputes the effective theme. No render loop (the apply-effect depends on appearanceMode/darkTheme/lightTheme/systemIsDark, not on theme, which is what applyTheme mutates).
  • SettingsTab wiring is complete — local state, the handleSave dependency array, and reset-to-defaults are all updated; the dark/light selects share renderThemeOptions().

Issues

1. Theme gallery (ThemeDocumentation.tsx) silently breaks the new model — main concern.
It still calls the legacy setTheme(id), which this PR redefined to force appearanceMode='dark' and overwrite both slots:

const setTheme = (newTheme) => {
  setAppearanceModeState('dark');
  setDarkThemeState(newTheme);
  setLightThemeState(newTheme);  // clobbers the user's light slot
  ...
}

So a user in system mode who clicks any card in the theme gallery is silently kicked into explicit dark mode and loses their separate light theme; picking a light theme there lands it in the dark slot. This contradicts the PR's own design principle for the custom-theme cards ("buttons update only the corresponding slot and do not change appearance mode"). Suggest giving the gallery Apply-as-Dark/Light affordances, or at minimum not flipping the mode — or explicitly scoping it out.

2. onThemeChange is now a dead prop. SettingsTab removed it from destructuring and replaced it with direct context setters, but it's still declared in SettingsTabProps (line 84) and still passed by App.tsx:4976 and GlobalSettingsPage.tsx:132. Harmless, but worth removing to avoid confusion.

Nits

  • Migration asymmetry: the localStorage path derives appearanceMode/lightTheme without the !hasNewThemePreferences guard the server-load path uses. Only reachable with partial state (slot keys present but appearanceMode missing), which never happens since they're written together — defensive, not a real bug, but the two paths would ideally share a helper.
  • getInitialThemePreferences() writes to localStorage inside a useState initializer (a side effect in render). It's idempotent and matches the existing pattern in this file, so acceptable.
  • Good test coverage on the custom-theme-card slot behavior; nothing covers the gallery/setTheme mode-flip in issue #1.

Recommendation

Approve pending #1 (genuine UX regression) and ideally #2 cleanup. Everything else is polish.

🤖 Generated with Claude Code

…prop

Addresses review feedback on PR Yeraze#3344.

Yeraze#1 Theme gallery (ThemeDocumentation) no longer calls the legacy setTheme,
which forced appearanceMode='dark' and overwrote both theme slots — silently
kicking system-mode users into dark mode and clobbering their light theme.
Each card now offers "Apply as Dark" / "Apply as Light" that update only the
corresponding slot and never change the appearance mode, matching the custom
theme cards. Adds regression tests for the per-slot behavior.

Yeraze#2 Remove the now-dead onThemeChange prop from SettingsTabProps and its two
pass-sites (App.tsx, GlobalSettingsPage.tsx), plus the now-unused setTheme
destructuring.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Yeraze

Yeraze commented Jun 8, 2026

Copy link
Copy Markdown
Owner

Pushed ae581da4 to address the review feedback:

  • Issue "Claude PR Assistant workflow" #1 (UX regression): The theme gallery (ThemeDocumentation) no longer calls the legacy setTheme, which was forcing appearanceMode='dark' and overwriting both slots. Each card now has Apply as Dark / Apply as Light buttons that update only the corresponding slot and never change the appearance mode — consistent with the custom-theme cards. Assigned slots show a badge and disable their button. Added ThemeDocumentation.test.tsx covering the per-slot behavior (incl. asserting setAppearanceMode/setTheme are not called).
  • Issue feat: traceroute highlighting and UI improvements #2 (dead prop): Removed onThemeChange from SettingsTabProps and its two pass-sites (App.tsx, GlobalSettingsPage.tsx), plus the now-unused setTheme destructuring.

Local: full Vitest suite green (6082 passed), tsc clean.

I left the migration-asymmetry nit alone since it's defensive-only and not reachable in practice — happy to factor out a shared helper if you'd prefer.

@Yeraze Yeraze merged commit bebe13e into Yeraze:main Jun 8, 2026
20 checks passed
Yeraze added a commit that referenced this pull request Jun 10, 2026
The [4.9.4] section captured only the entries present in [Unreleased] when
the release branch was cut. Add the remaining changes that shipped in
v4.9.4 but merged without their own CHANGELOG entries: {DATE}/{TIME}
tokens (#3382), MeshCore JSONL export (#3391), system theme (#3344),
Map Pin Style fix (#3364), MeshCore connecting state (#3380), the Map
Analysis security gate (#3365/#3366), and the 11 dependency bumps.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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.

2 participants