Skip to content

feat(web): add app theme toggle (default / dark / system / blue) and theme support#605

Open
donnfelker wants to merge 4 commits into
ColeMurray:mainfrom
donnfelker:upstream-app-theme-toggle
Open

feat(web): add app theme toggle (default / dark / system / blue) and theme support#605
donnfelker wants to merge 4 commits into
ColeMurray:mainfrom
donnfelker:upstream-app-theme-toggle

Conversation

@donnfelker
Copy link
Copy Markdown
Contributor

@donnfelker donnfelker commented May 9, 2026

  • feat(web): add app theme toggle (default / dark / system / blue)
CleanShot.2026-05-09.at.17.40.41.mp4

Adds an App theme section to Settings → Appearance, above "Code highlighting". The CSS variables and next-themes provider were already wired — this is the missing UI plus a small registry pattern so deployments can ship branded palettes without touching component code.

  • Theme registry at packages/web/src/lib/app-themes.ts — a list of {id, label, colorScheme} tuples. Built-ins: Default (light), Dark, System. A sample "Blue" palette ships as a worked example; rename, replace, or remove freely.
  • Configurable deploy-time default via NEXT_PUBLIC_APP_DEFAULT_THEME (and a matching app_default_theme Terraform variable, validated against the registry). Lets a deployment land new visitors on a branded theme out of the box; users can still switch and their choice persists.
  • Picker UI — a <select> matching the existing Code Highlighting rows. Scales cleanly as palettes are added.
  • Syntax highlighting picks the right hljs stylesheet for a custom palette via the registry's colorScheme field.
  • Small audit pass — fixes a few avatar borders and a toggle thumb that hardcoded bg-white/border-white and broke in dark mode.
  • New docs/THEMING.md documents tokens, runtime switching, the registry, the deploy-time default, and a 4-step "add a branded theme" recipe.

No breaking changes: existing localStorage.theme values keep working;unset or unknown NEXT_PUBLIC_APP_DEFAULT_THEME falls back to "system"so a deploy-config typo can't ship a broken UI.

Summary by CodeRabbit

  • Documentation

    • Added a comprehensive theming guide and linked it from setup; documents token-based theming, branded themes, and theme registration.
  • New Features

    • App theme picker in Appearance settings with light/dark/system and custom themes (example "Blue").
    • Deploy-time default theme support via configuration.
  • Style

    • UI borders and toggle styles updated to use theme-aware tokens.
  • Tests

    • Added tests covering the app theme picker behavior and regressions.

Review Change Stack

* feat(web): add app theme toggle (default / dark / system)

Adds an "App theme" section to Settings → Appearance, above the
existing "Code highlighting" group. Default / Dark / System map to
next-themes' setTheme(), which already handled the .dark class on
<html>, localStorage persistence, and cross-tab sync — this PR is
the missing UI on top of that.

Also fixes a small set of components that bypassed the design tokens
and broke in dark mode:

- participants-section.tsx, session/[id]/page.tsx: avatar borders
  swapped from `border-white` → `border-background` so they remain
  visible against either palette.
- sandbox-settings.tsx: toggle thumb swapped from `bg-white` →
  `bg-foreground` for high contrast in both modes.

ToggleGroup `value` is always a string (`theme ?? "system"`) to avoid
Radix's "changing from uncontrolled to controlled" warning during
hydration. New regression test guards that.

Adds docs/THEMING.md describing the design token system, the
runtime switching mechanism, and how to add a future themed palette
via `[data-theme="..."]` selectors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(web): named themes + configurable deploy-time default

Builds on the App theme toggle: themes are now a registry of named
entries, the picker lists all of them, and a deployer can pick which
one is applied on first load via a new tfvar. Useful for shipping a
company-branded theme out of the box — users can still switch in
Settings → Appearance and their choice persists.

- packages/web/src/lib/app-themes.ts: new registry of {id, label,
  colorScheme} tuples. Built-ins: light/dark/system. Ships a sample
  "Blue" palette as a worked example so the feature is verifiable
  out of the box; deployers replace or remove it.
- providers.tsx: passes themes={APP_THEME_IDS} and
  defaultTheme={APP_DEFAULT_THEME} to next-themes.
- site-config.ts: reads NEXT_PUBLIC_APP_DEFAULT_THEME, validated via
  resolveDefaultAppTheme — an unknown id falls back to "system" so
  a tfvars typo never ships a broken UI.
- appearance-settings.tsx: ToggleGroup → <select> listing every
  registered theme. Matches the existing Code Highlighting <select>
  styling and scales as more palettes are added.
- syntax-highlight-theme.tsx: when the active theme is a named
  palette ("blue", etc.) and Code Highlighting is on "system", reads
  the palette's colorScheme from the registry so hljs picks the
  matching light/dark stylesheet.
- globals.css: .blue rule for the sample palette with comments
  explaining how to override or remove it.

Terraform plumbing mirrors the existing app_name/app_icon pattern:
- variables.tf: app_default_theme (default "system") with a
  contains([...]) validation block that gates unknown ids at plan
  time; doc string explains the company-theme use case.
- web-cloudflare.tf: NEXT_PUBLIC_APP_DEFAULT_THEME wired into both
  the build env and the wrangler.production.toml [vars] block.
- web-vercel.tf: corresponding Vercel project env entry.
- terraform.tfvars.example: commented example with the
  company-theme rationale and list of valid ids.

docs/THEMING.md updated with three new sections: the registry,
"Setting a default theme on deploy", and "Adding a new branded
theme" (4-step recipe covering CSS, registry, the Terraform
validation list, and the optional tfvar default).

Tests: appearance-settings.test.tsx now asserts against the
registry contents and the dropdown UI; controlled/uncontrolled
regression guard preserved. 28 files / 214 web tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(web): explain colorScheme field on AppTheme

Move the rationale next to the type definition so it's visible at
the point of use. Reviewer feedback on PR #3 — the field-level
comment is more discoverable than the file-level docblock when a
maintainer hovers the type or reads it inline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 9, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cb41c710-43d8-4c23-8477-772480092246

📥 Commits

Reviewing files that changed from the base of the PR and between 7e232fb and 089a270.

📒 Files selected for processing (1)
  • docs/THEMING.md
✅ Files skipped from review due to trivial changes (1)
  • docs/THEMING.md

📝 Walkthrough

Walkthrough

This PR implements a token-based theming system: theme registry/types, a blue branded CSS theme, ThemeProvider wiring with explicit themes and deploy-time default, settings UI and tests for an App theme picker, syntax-highlight integration, component token updates, documentation, and Terraform wiring for NEXT_PUBLIC_APP_DEFAULT_THEME.

Changes

App Theming System

Layer / File(s) Summary
Theme Registry & Types
packages/web/src/lib/app-themes.ts
New module defining AppThemeColorScheme, AppTheme interface, APP_THEMES registry (light, dark, system, blue), APP_THEME_IDS, DEFAULT_APP_THEME, and utilities getAppTheme() and resolveDefaultAppTheme() for lookup and validation.
CSS Design Tokens
packages/web/src/app/globals.css, packages/web/src/app/layout.tsx, packages/web/src/app/themes/blue.css
Added comments about branded theme files, imported ./themes/blue.css after globals.css, and added .blue theme CSS that overrides design-token custom properties when its class is active.
Site Configuration
packages/web/src/lib/site-config.ts
New APP_DEFAULT_THEME export derived from NEXT_PUBLIC_APP_DEFAULT_THEME via resolveDefaultAppTheme().
Provider Setup
packages/web/src/app/providers.tsx
ThemeProvider configured with explicit themes={APP_THEME_IDS} and defaultTheme={APP_DEFAULT_THEME} instead of hardcoded "system", keeping attribute="class" and enableSystem.
Settings UI
packages/web/src/components/settings/appearance-settings.tsx
New "App theme" section using useTheme() to read/set the app theme, populating a <select> from APP_THEMES and calling setTheme on change (fallback to DEFAULT_APP_THEME).
Syntax Highlighting Integration
packages/web/src/components/syntax-highlight-theme.tsx
When colorSchemeMode is "system", prefer getAppTheme(theme).colorScheme for light/dark decisions; add theme to effect dependencies so highlight.js stylesheet updates.
Component Color Updates
packages/web/src/components/sidebar/participants-section.tsx, packages/web/src/app/(app)/session/[id]/page.tsx, packages/web/src/components/settings/sandbox-settings.tsx
Swapped hard-coded classes (border-white, bg-white) for semantic tokens (border-background, bg-foreground) on avatars and toggle knobs.
Testing
packages/web/src/components/settings/appearance-settings.test.tsx
Vitest + RTL test suite mocking next-themes with controllable state, verifying option rendering, selected-value sync, setTheme calls, custom theme support, and absence of React uncontrolled→controlled warning on mount.
Deployment Configuration
terraform/environments/production/variables.tf, terraform/environments/production/terraform.tfvars.example, terraform/environments/production/web-cloudflare.tf, terraform/environments/production/web-vercel.tf
Added app_default_theme Terraform variable with validation (allowed: light,dark,system,blue), documented example in tfvars, and wired NEXT_PUBLIC_APP_DEFAULT_THEME = var.app_default_theme into Cloudflare and Vercel environments.
Documentation
docs/THEMING.md, docs/SETUP_GUIDE.md
New theming guide covering token architecture, runtime switching, theme registry, deploy-time defaults, adding branded themes, component authoring rules, an auditing command, and updated SETUP_GUIDE linking to THEMING.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • ColeMurray/background-agents#512: Introduces semantic CSS token system that this PR builds upon by implementing theme registry and switcher UI on top of the token infrastructure.

Poem

🐰 I hopped through tokens, blue and bright,
I twitched my nose at dark and light,
A picker clicked and classes turned,
Tokens hummed softly as styles returned,
Hooray — the app now shines in sight!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding an app theme toggle feature with support for multiple themes (default/dark/system/blue) and theme infrastructure, which aligns with the comprehensive changes across UI components, configuration, documentation, and deployment setup.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch upstream-app-theme-toggle

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/web/src/app/globals.css (1)

223-249: ⚡ Quick win

Consider adding a dark variant for the Blue theme.

The .blue class provides only a light palette. Users who prefer dark mode will see the standard dark theme when "Blue" is selected and their OS is in dark mode, which may be unexpected.

Consider adding a .blue.dark { ... } block with a dark-mode variant of the blue palette for consistency, or document that the Blue theme is light-only.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/app/globals.css` around lines 223 - 249, The Blue theme
(.blue) only defines a light palette so users in dark mode get the default dark
theme; add a dark-mode variant by creating a .blue.dark (or .dark .blue
depending on your theme class strategy) CSS block that overrides the CSS custom
properties (e.g., --background, --foreground, --card, --popover, --primary,
--accent, --muted, --border, --input, --ring) with darker values to provide a
consistent Blue dark theme, or alternatively add a comment in the .blue block
documenting that Blue is light-only so the behavior is explicit.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/web/src/app/globals.css`:
- Around line 223-249: The Blue theme (.blue) only defines a light palette so
users in dark mode get the default dark theme; add a dark-mode variant by
creating a .blue.dark (or .dark .blue depending on your theme class strategy)
CSS block that overrides the CSS custom properties (e.g., --background,
--foreground, --card, --popover, --primary, --accent, --muted, --border,
--input, --ring) with darker values to provide a consistent Blue dark theme, or
alternatively add a comment in the .blue block documenting that Blue is
light-only so the behavior is explicit.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7dc354b1-2f67-477e-b28b-3fdad82ae550

📥 Commits

Reviewing files that changed from the base of the PR and between 74a4fa1 and 63399f2.

📒 Files selected for processing (16)
  • docs/SETUP_GUIDE.md
  • docs/THEMING.md
  • packages/web/src/app/(app)/session/[id]/page.tsx
  • packages/web/src/app/globals.css
  • packages/web/src/app/providers.tsx
  • packages/web/src/components/settings/appearance-settings.test.tsx
  • packages/web/src/components/settings/appearance-settings.tsx
  • packages/web/src/components/settings/sandbox-settings.tsx
  • packages/web/src/components/sidebar/participants-section.tsx
  • packages/web/src/components/syntax-highlight-theme.tsx
  • packages/web/src/lib/app-themes.ts
  • packages/web/src/lib/site-config.ts
  • terraform/environments/production/terraform.tfvars.example
  • terraform/environments/production/variables.tf
  • terraform/environments/production/web-cloudflare.tf
  • terraform/environments/production/web-vercel.tf

donnfelker and others added 3 commits May 9, 2026 18:24
Adds JSDoc to the previously-undocumented exports in app-themes.ts so
docstring-coverage clears the 80% threshold, and rewrites the .blue
guidance in globals.css and THEMING.md to reflect that next-themes'
attribute="class" puts a single theme class on <html> at a time —
.blue.dark would never match, so a branded dark variant must be
registered as its own theme entry instead.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Pulls the .blue example out of globals.css into its own file imported
from app/layout.tsx after globals.css (so the cascade order makes
.blue's tokens override :root). Each branded theme is now one
self-contained file — adding or removing a brand is a localized change
across themes/<id>.css, the layout.tsx import, the APP_THEMES entry,
and the variables.tf validation list.

THEMING.md and the app-themes.ts docblock are updated to reflect the
new file pattern.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <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.

1 participant