feat(themes): custom themes + Light/Dark/System appearance mode#112
Closed
brooksc wants to merge 17 commits into
Closed
feat(themes): custom themes + Light/Dark/System appearance mode#112brooksc wants to merge 17 commits into
brooksc wants to merge 17 commits into
Conversation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
YAML preserves AI-generated comments through the copy-paste round-trip. Adds parseThemeYaml() with syntax and structural validation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Injects a <style> tag with custom CSS vars, sets data-look=custom:id on both html and app-shell, and passes the custom terminalBackground to xterm. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> # Conflicts: # src/store/persistence.ts
…t/delete Moves built-in presets to a dedicated Themes tab and adds a Custom Themes section with a CustomThemeDialog for AI-assisted YAML theme creation, live validation feedback, and save/apply in one step. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Clone button on each preset card reads live CSS vars via getComputedStyle and pre-fills the YAML editor with accurate hex/gradient values - generateThemePrompt(existingYaml?) returns an edit-focused prompt when YAML is present, so Copy Prompt adapts to the current textarea content - themeToYaml() exported as a shared serializer - readCssVarsForPreset() / terminalBackground exported from theme.ts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…a/themes/ Each custom theme is now saved as ~/Library/Application Support/parallel-code/themes/<id>.yaml (or the platform equivalent), independent of state.json. Themes survive app reinstalls and are easy to back up, share, or manually edit. - New IPC: LoadCustomThemes / SaveCustomTheme / DeleteCustomTheme - loadCustomThemes() called at startup, replaces state.json blob loading - saveCustomTheme / deleteCustomTheme write/delete files immediately - One-time migration: themes found in legacy state.json are written to files Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> # Conflicts: # src/store/persistence.ts
… audit - checkThemeContrast() checks 4 key pairs (fg/bg-elevated, fg-muted/bg-elevated, fg/bg-selected, accent-text/accent) against WCAG AA minimums - CustomThemeDialog shows live contrast warnings with amber border; theme still saves - Audit test (custom-theme-contrast.test.ts) reports all built-in theme issues Findings — 4 themes have failures: classic: accent-text on accent: 4.17:1 (#fff on #4c6fff) minimal: fg on bg-selected: 1.55:1 (#ececec on rgba(200,191,160,0.22)) islands-dark: accent-text on accent: 3.30:1 (#fff on #548af7) islands-light: accent-text on accent: 4.27:1 (#fff on #3574f0) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> # Conflicts: # package-lock.json # package.json
Contrast audit (checkThemeContrast) flagged 4 failures across 3 themes. Fixed by darkening accent colors to meet 4.5:1 ratio on white/elevated bg. BEFORE → AFTER (ratio): - classic: --accent/#border-focus #4c6fff → #4267ff (4.17→4.55:1) - islands-dark: --accent/--border-focus #548af7 → #286cf5 (3.30→4.61:1) - islands-light: --accent/--border-focus #3574f0 → #2c6def (4.27→4.60:1) (--link left at #3574f0 — link text on white is decorative/non-critical) Also fixed a false positive for the 'minimal' theme: --bg-selected uses rgba(200,191,160,0.22) which colord compared against white, giving 1.55:1. Added blendOver() to composite semi-transparent backgrounds against --bg-elevated before contrast calculation; actual blended ratio is 8.66:1. All 13 built-in-theme contrast audit tests now pass with zero warnings. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a three-way appearance mode selector to the Themes tab in Settings: - Light: always uses the chosen light theme - Dark: always uses the chosen dark theme - System: auto-follows OS prefers-color-scheme, switching between a chosen dark theme and a chosen light theme Architecture: - New AppearanceMode type + tone field on LookPresetOption in look.ts - 5 new persisted fields: appearanceMode, lightThemePreset/CustomId, darkThemePreset/CustomId — effective themePreset/activeCustomThemeId remain as computed outputs, unchanged for all other consumers - src/lib/os-appearance.ts: singleton osIsDark() signal wrapping window.matchMedia, safe for Node/test environments - applyAppearanceMode() in ui.ts derives effective theme from mode + OS state; called from a createEffect in App.tsx on OS changes - Backward compat: existing themePreset in state.json is migrated into the appropriate dark/light slot on first load UI changes in SettingsDialog Themes tab: - Light/Dark/System pill selector at top - Single filtered preset grid in Light or Dark mode - Two labeled preset grids in System mode (Dark theme / Light theme) - Custom theme rows show "Dark" / "Light" assign buttons in System mode Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1. CustomThemeDialog.handleSave: replace activateCustomTheme() with setDarkTheme/setLightTheme so the slot fields are updated — without this, a mode switch or relaunch would silently drop the active theme. 2. deleteCustomTheme: clear darkThemeCustomId/lightThemeCustomId when they reference the deleted ID, then re-apply the mode — without this, a later OS switch would set data-look to a deleted custom ID, leaving the app with no CSS variables. 3. Backward-compat migration in loadState: carry activeCustomThemeId into the appropriate slot so users with a custom theme active before upgrading don't lose their selection on first launch. 4. IPC handlers for SaveCustomTheme/DeleteCustomTheme: validate that the theme id matches /^[\w-]+$/ before using it as a filename, preventing path traversal (e.g. id = "../../.bashrc") from a compromised renderer. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- look.test.ts: presetsForTone() and defaultPresetForTone() — verifies the tone filter covers all presets exactly once and islands-light is the only light preset - appearance-mode.test.ts: 16 tests covering applyAppearanceMode() routing (dark/light/system × OS state × custom themes), setDarkTheme/ setLightTheme slot updates, and deleteCustomTheme slot cleanup (the bug found in code review where dangling slot refs survived deletion) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Contributor
Author
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.


Summary
This PR adds two related theme features to Parallel Code:
1. Custom Themes
userData/themes/for portability2. Light / Dark / System Appearance Mode
prefers-color-schemevia a reactivewindow.matchMedialistener3. Built-in Theme Contrast Fixes
Three built-in themes had
--accent/--border-focuscolors that failed WCAG AA (4.5:1 contrast on their respective backgrounds):#4c6fff→#4267ff(4.17 → 4.55:1 on#0d1117)#548af7→#286cf5(3.30 → 4.61:1 on#0f1923)#3574f0→#2c6def(4.27 → 4.60:1 on#f5f7fa)4. Tests
presetsForToneanddefaultPresetForTonehelpersapplyAppearanceModerouting and slot cleanup on deleteFiles Changed
src/lib/custom-theme.ts,src/lib/look.ts,src/lib/os-appearance.tssrc/store/types.ts,src/store/core.ts,src/store/ui.ts,src/store/store.tssrc/store/persistence.ts,src/store/autosave.tssrc/components/SettingsDialog.tsx,src/components/CustomThemeDialog.tsxsrc/App.tsx,src/components/TerminalView.tsx,src/lib/theme.tselectron/ipc/channels.ts,electron/ipc/register.ts,electron/ipc/persistence.tssrc/styles.csssrc/lib/look.test.ts,src/store/appearance-mode.test.ts,src/lib/custom-theme-contrast.test.tseslint.config.js,.prettierignore(exclude.remember/and.parallel-code/local dirs)Test Plan
npm run typecheck— zero errorsnpm run lint— zero warningsnpm run test— all tests passprefers-color-scheme: light/dark— verify live switchappearanceModein state: verify backward-compat migration🤖 Generated with Claude Code