Skip to content

feat(themes): custom themes + Light/Dark/System appearance mode#112

Closed
brooksc wants to merge 17 commits into
johannesjo:mainfrom
brooksc:feat/custom-themes
Closed

feat(themes): custom themes + Light/Dark/System appearance mode#112
brooksc wants to merge 17 commits into
johannesjo:mainfrom
brooksc:feat/custom-themes

Conversation

@brooksc
Copy link
Copy Markdown
Contributor

@brooksc brooksc commented May 15, 2026

Summary

This PR adds two related theme features to Parallel Code:

1. Custom Themes

  • New Custom Themes section in Settings → Themes tab
  • Create themes via a YAML editor with an AI-generated prompt to help design them
  • Edit, clone, and delete custom themes
  • Themes stored as individual YAML files in userData/themes/ for portability
  • Custom themes support all CSS variables plus a terminal background color
  • WCAG AA contrast validation (4.5:1 ratio) on save with inline error messages
  • Path traversal guard on the IPC handler for theme file names

2. Light / Dark / System Appearance Mode

  • New three-way mode selector at the top of the Themes tab: Light, Dark, System
  • System mode auto-follows prefers-color-scheme via a reactive window.matchMedia listener
  • Each mode remembers its own preset and custom theme independently
  • Backward-compatible: existing users with a saved preset are migrated to the correct slot on first launch

3. Built-in Theme Contrast Fixes

Three built-in themes had --accent/--border-focus colors that failed WCAG AA (4.5:1 contrast on their respective backgrounds):

  • classic: #4c6fff#4267ff (4.17 → 4.55:1 on #0d1117)
  • islands-dark: #548af7#286cf5 (3.30 → 4.61:1 on #0f1923)
  • islands-light: #3574f0#2c6def (4.27 → 4.60:1 on #f5f7fa)

4. Tests

  • 8 unit tests for presetsForTone and defaultPresetForTone helpers
  • 16 unit tests for applyAppearanceMode routing and slot cleanup on delete
  • WCAG contrast audit test covering all 12 built-in presets

Files Changed

Area Files
Types/validation src/lib/custom-theme.ts, src/lib/look.ts, src/lib/os-appearance.ts
Store src/store/types.ts, src/store/core.ts, src/store/ui.ts, src/store/store.ts
Persistence src/store/persistence.ts, src/store/autosave.ts
UI src/components/SettingsDialog.tsx, src/components/CustomThemeDialog.tsx
Terminal/app src/App.tsx, src/components/TerminalView.tsx, src/lib/theme.ts
IPC electron/ipc/channels.ts, electron/ipc/register.ts, electron/ipc/persistence.ts
CSS src/styles.css
Tests src/lib/look.test.ts, src/store/appearance-mode.test.ts, src/lib/custom-theme-contrast.test.ts
Tooling eslint.config.js, .prettierignore (exclude .remember/ and .parallel-code/ local dirs)

Test Plan

  • npm run typecheck — zero errors
  • npm run lint — zero warnings
  • npm run test — all tests pass
  • Open Settings → Themes tab; verify General/Themes tab navigation works
  • Create a custom theme via "Create New" button; verify YAML editor and AI prompt
  • Toggle Light/Dark/System modes; verify preset grids filter correctly
  • In System mode: use DevTools → Rendering → Emulate prefers-color-scheme: light/dark — verify live switch
  • Close and relaunch — verify mode and theme restored correctly
  • Existing user with no appearanceMode in state: verify backward-compat migration

🤖 Generated with Claude Code

brooksc and others added 17 commits May 14, 2026 19:47
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>
@brooksc
Copy link
Copy Markdown
Contributor Author

brooksc commented May 15, 2026

image image

When you create a new custom theme and select show/copy prompt, it generates the one below. I fed this to claude code with my description of what I wanted and it generated a suitable theme!

You are a UI theme designer for Parallel Code, a dark-mode terminal multiplexer and AI coding assistant.

Help me create a custom color theme by asking about my aesthetic preferences, then filling in the YAML template.

VARIABLES AND THEIR ROLES:
• --bg: App-wide page background. Can be a hex color or CSS gradient (e.g. "radial-gradient(130% 120% at 18% 0%, #202044 0%, #171c30 58%, #12151f 100%)")
• --bg-elevated: Raised surfaces: panels, dropdowns, tooltips
• --bg-input: Input fields and code editor backgrounds
• --bg-hover: Hover state background for buttons and list items
• --bg-selected: Selected item background (active task, highlighted row)
• --bg-selected-subtle: Subtle selected state — same hue as --bg-selected with ~25% alpha (e.g. "#2d2b5840")
• --border: Primary border for panels and inputs
• --border-subtle: Softer secondary borders
• --border-focus: Focus ring color when a field is focused (usually matches accent)
• --fg: Primary text color — must be readable on --bg-elevated
• --fg-muted: Secondary text, less important labels
• --fg-subtle: Tertiary text, placeholders, disabled states
• --accent: Primary interactive color — buttons, checkboxes, active indicators
• --accent-hover: Lighter/brighter version of accent for hover states
• --accent-text: Text color ON accent-colored backgrounds (usually white or near-black)
• --link: Hyperlink color (often a lighter, more saturated accent)
• --success: Success states, positive indicators (usually green-ish)
• --error: Error states, destructive actions (usually red-ish)
• --warning: Warning states, caution indicators (usually amber/orange)
• --island-bg: Background of task column "islands" — typically 1-2 shades darker than bg-elevated
• --island-border: Border around task column islands
• --island-radius: Corner radius for islands (e.g. "12px", "8px", "0px" for sharp)
• --task-container-bg: Background of the task list container within an island
• --task-panel-bg: Content panel backgrounds inside tasks (conceptually matches terminalBackground)

• terminalBackground: Opaque hex color for the terminal emulator (hex only, no gradients — should match --task-panel-bg conceptually)

Please:

  1. Ask me about my aesthetic preferences (mood, accent color, reference themes I like, light vs dark)
  2. Generate a complete theme in this exact YAML format when ready (keep the comments — they help the user understand each value):

name: My Theme Name
terminalBackground: "#hex"
vars:
--bg: "..." # App-wide page background. Can be a hex color or CSS gradient (e.g. "radial-gradient(130% 120% at 18% 0%, #202044 0%, #171c30 58%, #12151f 100%)")
--bg-elevated: "..." # Raised surfaces: panels, dropdowns, tooltips
--bg-input: "..." # Input fields and code editor backgrounds
--bg-hover: "..." # Hover state background for buttons and list items
--bg-selected: "..." # Selected item background (active task, highlighted row)
--bg-selected-subtle: "..." # Subtle selected state — same hue as --bg-selected with ~25% alpha (e.g. "#2d2b5840")
--border: "..." # Primary border for panels and inputs
--border-subtle: "..." # Softer secondary borders
--border-focus: "..." # Focus ring color when a field is focused (usually matches accent)
--fg: "..." # Primary text color — must be readable on --bg-elevated
--fg-muted: "..." # Secondary text, less important labels
--fg-subtle: "..." # Tertiary text, placeholders, disabled states
--accent: "..." # Primary interactive color — buttons, checkboxes, active indicators
--accent-hover: "..." # Lighter/brighter version of accent for hover states
--accent-text: "..." # Text color ON accent-colored backgrounds (usually white or near-black)
--link: "..." # Hyperlink color (often a lighter, more saturated accent)
--success: "..." # Success states, positive indicators (usually green-ish)
--error: "..." # Error states, destructive actions (usually red-ish)
--warning: "..." # Warning states, caution indicators (usually amber/orange)
--island-bg: "..." # Background of task column "islands" — typically 1-2 shades darker than bg-elevated
--island-border: "..." # Border around task column islands
--island-radius: "..." # Corner radius for islands (e.g. "12px", "8px", "0px" for sharp)
--task-container-bg: "..." # Background of the task list container within an island
--task-panel-bg: "..." # Content panel backgrounds inside tasks (conceptually matches terminalBackground)

RULES:

  • All --bg-* and --fg-* values must be hex colors (no gradients)
  • --bg may be a CSS gradient if the aesthetic calls for it
  • --bg-selected-subtle should be the same hue as --bg-selected with ~25% opacity appended (e.g. "#2d2b5840")
  • --island-radius should be "12px", "8px", or "0px"
  • Ensure sufficient contrast: --fg on --bg-elevated should meet WCAG AA (4.5:1 ratio)
  • terminalBackground must be an opaque hex value
  • Wrap any value containing a colon or special characters in double quotes

@brooksc brooksc closed this May 15, 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.

1 participant