Skip to content

feat(web): add modular theme library - #5226

Open
StiensWout wants to merge 108 commits into
pingdotgg:mainfrom
StiensWout:t3code/modular-theme-system
Open

feat(web): add modular theme library#5226
StiensWout wants to merge 108 commits into
pingdotgg:mainfrom
StiensWout:t3code/modular-theme-system

Conversation

@StiensWout

@StiensWout StiensWout commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a token-based theme system to the web client and desktop app: 57 semantic color roles, applied before React mounts so there is no flash of the wrong theme.

  • Color scheme first — the Appearance section leads with three wide tiles (System, Light, Dark), each a miniature of the app painted in the currently picked palettes; the System tile splits to show both halves.
  • Mix light and dark from different themes — every theme card shows its light and dark balls; clicking one assigns that theme to that half of the mix (marked with a ring and a sun/moon badge), and System flips between the two picks with the OS. Picks persist, apply live, and resolve in the boot splash — parity between the inline boot script and the runtime is CI-enforced by tests that derive from the real palette definitions.
  • Six built-in themes — T3 Code (default), T3 Chat (anchored to the official t3.chat palette), Grove, Ocean, Ember, and Iris. Each maintainer theme pairs its accent with a companion action color that drives every solid control, while the accent handles surfaces, selection, and focus.
  • Personal themes — guided creation from two colors (an OKLCH generator derives a readable palette from the exact seeds), an advanced editor for every role, and JSON import/export. The editor opens as a draggable panel above the router that applies its draft to the running app, and starts from the theme you already use.
  • VS Code themes convert on import — the drop box takes T3 Code and VS Code theme files (several at once), maps the workbench colors it finds onto a contrast-solved palette, flattens alpha overlays and wide-gamut color() values, and pairs a family's light and dark files into one dual-mode theme. Re-importing an installed theme asks whether to update it or keep both.
  • Full-surface coverage — sidebar, composer, chat timeline (including code blocks in replies), diffs and their gutters, terminals, file previews, the typography previews from feat(web): configurable fonts and sizes under Settings → Appearance #5103, toolbars, status colors, and the browser chrome color.

Closes #418
Closes #233
Closes #4302

Supersedes #5258

Screenshots

Light half active Dark half active
Appearance with the scheme picker, T3 Chat picked for light Appearance with the scheme picker, Grove picked for dark

Both shots show the same mix — T3 Chat in light, Grove in dark — from either side of the System toggle.

Theme editor Importing themes
The floating theme editor in advanced mode, filtering color roles The import dialog: a drop row above the JSON editor

The editor floats above the app and paints its draft on the live interface, so a color is judged by browsing the app rather than a miniature; it drags anywhere and survives navigation. Advanced mode lists one role per row behind a filter.

Themed typography previews
Typography previews following the active theme

Built with GPT-5.6-Luna and Claude Fable 5 in T3 Code.


Note

Medium Risk
Large cross-cutting UI and localStorage theme state with boot/runtime parity requirements; desktop IPC reads user-selected JSON files but caps size—behavioral changes to setTheme (clears mix, boolean return) affect restore and editor flows.

Overview
Introduces a modular theme system that replaces the simple light/dark/system select in Appearance with a ThemeLibrary: built-in palettes, custom themes, light/dark half-mixing, guided and advanced editors (ThemeColorPicker, ThemeEditorHost), and import/export—including VS Code theme JSON on desktop via new pickThemeFiles IPC (ElectronDialog.pickFiles, size-capped reads, default ~/.vscode/extensions).

index.html boot script now mirrors runtime theme resolution (legacy IDs, custom themes from localStorage, mix halves) and paints the splash with --boot-* variables before React mounts.

Semantic tokens (text-icon-muted, text-secondary-label, text-placeholder, bg-message, bg-message-action, --code-background, --terminal-*, error/warning/update) replace ad-hoc opacity classes across sidebar, composer, timeline, and settings; diffs and file preview share DIFF_SURFACE_THEME_UNSAFE_CSS and code-surface variables; terminals read --terminal-* from the document theme.

Smaller UX fixes: workspace titlebar controls use a consistent inset; sidebar headers sit above stage backdrop bleed; composer send drops stage backdrop art for message-action styling; restore-defaults rolls back theme/mix/follow-system with explicit failure handling.

Reviewed by Cursor Bugbot for commit 2033017. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add modular theme library with custom theme creation, import, and per-appearance mixing

  • Introduces a full theme management UI in the Appearance settings panel, replacing the simple system/light/dark select with a ThemeLibrary component supporting custom themes, per-appearance "halves" mixing, import/export, creation, and deletion.
  • Adds themePalette.ts as the core module defining typed theme roles, storage keys, built-in theme IDs, and APIs to parse, resolve, and manage theme definitions at runtime.
  • Adds a ThemeEditorPanel for interactively creating or editing themes with live preview, and a ThemeImportDialog supporting VS Code theme file import with color space conversion and overlay flattening.
  • Extends useTheme to return setTheme (now returns a success boolean), plus new APIs: setAppearanceMode, setFollowSystem, setThemeHalf, clearThemeHalves, and refreshTheme.
  • Updates index.html boot script to resolve built-in and custom themes (including halves mixing) before React loads, applying splash colors and dark class from stored preferences.
  • Adds a desktop IPC channel (desktop:pick-theme-files) so the Electron app can open a file picker and return JSON theme file contents to the renderer.
  • Migrates UI color tokens across chat, sidebar, diff, terminal, and alert components from ad-hoc muted-foreground shades to semantic tokens (text-secondary-label, text-icon-muted, text-placeholder, text-message-foreground, text-error, text-warning-foreground, etc.).
  • Risk: setTheme behavior change — it now clears any stored theme halves when called, and returns false instead of throwing on storage failure.

Macroscope summarized 2033017.

@coderabbitai

coderabbitai Bot commented Aug 2, 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 UI

Review profile: CHILL

Plan: Pro Plus

Run ID: bae1ee05-4be6-4c78-9105-e51ec1a373dc

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

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.

@github-actions github-actions Bot added size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Aug 2, 2026
Comment thread apps/web/src/components/settings/SettingsPanels.tsx Outdated
Comment thread apps/web/src/hooks/useTheme.ts
Comment thread apps/web/src/themePalette.ts Outdated
Comment thread apps/web/src/components/chat/MessagesTimeline.tsx Outdated
Comment thread apps/web/src/components/settings/SettingsPanels.tsx Outdated
@StiensWout
StiensWout force-pushed the t3code/modular-theme-system branch from e0f79e4 to 607dd6b Compare August 2, 2026 11:52
Comment thread apps/web/src/hooks/useTheme.ts Outdated
Comment thread apps/web/src/hooks/useTheme.ts Outdated
Comment thread apps/web/src/components/settings/SettingsPanels.tsx Outdated
Comment thread apps/web/src/components/settings/SettingsPanels.tsx Outdated
Comment thread apps/web/src/themePalette.ts
Comment thread apps/web/src/themePalette.ts Outdated
Comment thread apps/web/src/index.css Outdated
@StiensWout
StiensWout marked this pull request as ready for review August 2, 2026 12:27
Comment thread apps/web/index.html
Comment thread apps/web/index.html Outdated
Comment thread apps/web/index.html
@macroscopeapp

macroscopeapp Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

Diff is too large for automated approval analysis. A human reviewer should evaluate this PR.

You can customize Macroscope's approvability policy. Learn more.

Comment thread apps/web/src/components/settings/SettingsPanels.tsx Outdated
Comment thread apps/web/src/components/settings/SettingsPanels.tsx Outdated
Comment thread apps/web/src/components/settings/SettingsPanels.tsx Outdated
Comment thread apps/web/src/components/settings/SettingsPanels.tsx Outdated
Comment thread apps/web/src/components/settings/SettingsPanels.tsx Outdated
Comment thread apps/web/src/components/settings/SettingsPanels.tsx Outdated
Comment thread apps/web/src/components/settings/SettingsPanels.tsx Outdated
Comment thread apps/web/src/components/settings/SettingsPanels.tsx Outdated
Comment thread apps/web/src/components/settings/SettingsPanels.tsx Outdated
Comment thread apps/web/src/themePalette.ts
Comment thread apps/web/src/themePalette.ts
Comment thread apps/web/src/components/settings/SettingsPanels.tsx Outdated
Comment thread apps/web/src/components/settings/SettingsPanels.tsx Outdated
Comment thread apps/web/index.html Outdated
Comment thread apps/web/index.html
Comment thread apps/web/index.html Outdated
Comment thread apps/web/src/components/settings/ThemeSettings.tsx
Comment thread apps/web/src/components/settings/ThemeSettings.tsx Outdated
Comment thread apps/web/index.html Outdated
@13zebras

13zebras commented Aug 4, 2026

Copy link
Copy Markdown

I did not have time to go through this PR thoroughly. But the general direction taken is exactly what I had in mind when I started #418. From what I see, it should be easy to create a custom theme based on the tokens created.

Adds semantic theme roles, persisted light and dark variants, personal theme import and creation, contrast-safe surfaces, and themed message actions. Includes the T3 Chat palette, follow-system behavior, splash handling, and the theme library UI.
…hemes

The panel header becomes a drag handle (clamped to the viewport, and
re-clamped on resize), replacing the left/right dock toggle. Imported
VS Code themes get a "VS Code · " name prefix so their origin shows in
the library.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread apps/web/src/vscodeThemeImport.ts Outdated
Clicking a one-appearance theme's card now picks its only half instead
of making it the base for both appearances (which forced dark onto
light mode), and the first-pick mirror skips appearances the theme
cannot render. Imported VS Code themes drop the name prefix and read
slug names as words; the Personal tag on cards is gone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread apps/web/src/vscodeThemeImport.ts
Comment thread apps/web/src/vscodeThemeImport.ts Outdated
The import dialog becomes Upload files / Paste JSON tabs. Several files
import as a batch: a light and dark VS Code theme whose names differ
only by the appearance word merge into one dual-mode theme, ambiguous
families and loners stay individual, and batches install without
activating. The settings button reads Import theme.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread apps/web/src/vscodeThemeImport.ts
Comment thread apps/web/src/components/settings/ThemeSettings.tsx
…ports

Importing a theme that is already installed now asks whether to update
it in place or keep both (the copy takes the source file's name when
that differs, else a v2 suffix); updating a theme that is showing
re-applies it. Batches where several files share one internal name --
Dracula ships dracula.json and dracula-soft.json that both say
"Dracula" -- are told apart by their file names.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread apps/web/src/components/settings/ThemeImportDialog.tsx
Comment thread apps/web/src/components/settings/ThemeImportDialog.tsx
A pickThemeFiles bridge method drives a native multi-select JSON picker
that starts in ~/.vscode/extensions when it exists (the same dotfolder
on Windows, macOS, and Linux) and reads the files in the main process,
size-capped. The web import dialog uses it when the bridge offers it
and falls back to the plain file input elsewhere.

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

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

One finding: the new pickFiles operation reuses the folder-picker error tag, so the failure is no longer structurally identified by its tag or message. See the inline comment.

Posted via Macroscope — Effect Service Conventions

Comment thread apps/desktop/src/electron/ElectronDialog.ts
Comment thread apps/web/src/components/settings/ThemeImportDialog.tsx
Comment thread apps/desktop/src/ipc/methods/window.ts
StiensWout and others added 5 commits August 6, 2026 08:34
… accent

The diff panel's code-token surface mapping moves to a shared constant
that the file preview now applies too, so its gutter, rows, and body
follow the theme instead of the renderer's bundled palette. Imported
VS Code themes derive their fallback roles from a muted accent: the
vivid ramp carries the accent hue into every surface, which painted a
neutral gray theme's code pane and text blue.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… picker

A resolved mix half counts as a selection in the boot splash even when
the base theme is gone; theme names whose displayName humanizes to
nothing fall through to the name; a light/dark pair whose stripped name
collides with a reserved id stays two single themes instead of failing
the batch; importing a single-appearance theme activates its half like
a card click; and the desktop file picker gets its own error tag.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The file row and the JSON editor share one view again, with the row as
the drop target; the tab switcher and the dialog subtitle are gone.
Multi-file drops and VS Code conversion are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated and imported themes inherited the built-in error and warning
roles, so the destructive button stayed brand pink on every palette --
the remove-theme dialog being the visible case. Both engines now solve a
red and an amber against their own canvas: the solid tone stays dark
enough for the white label those buttons use, and the surface and
foreground pair keeps alert contrast.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread apps/web/src/themePalette.ts Outdated
StiensWout and others added 2 commits August 6, 2026 10:54
Solving red and amber against the canvas alone landed every theme on the
same tone. Each family now leans up to 14 degrees toward the accent and
picks up its chroma, so Grove reads warm, Ocean and Iris crimson, Ember
orange-red -- still unmistakably a destructive red.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated and imported palettes inherited their status roles from the
flagship theme, whose dark error is a brand pink -- that is what turned
every destructive button pink. They now fall back to the red and amber
the app shows unthemed, with surfaces laid over the theme's own canvas.
A theme file that names its own error or warning still wins.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread apps/web/src/themePalette.ts Outdated
StiensWout and others added 2 commits August 6, 2026 11:14
The standard pair was chosen by the slot while the rest of the palette
follows the canvas, so an inverted seed (dark canvas saved as a light
theme) got the light pair on a dark alert surface. Both engines now pick
by canvas luminance, and the standard foreground is nudged in lightness
when a tinted surface would leave it under 4.5.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Merge light and dark palettes into existing themes
- Add explanatory tooltips and palette collision feedback
- Use numbered names for imported theme copies
Comment thread apps/web/src/components/settings/ThemeEditorPanel.tsx
- Add a draggable corner grip with viewport-clamped dimensions
- Preserve minimized layout while retaining the chosen panel width
Comment thread apps/web/src/components/settings/ThemeEditorPanel.tsx
StiensWout and others added 3 commits August 6, 2026 19:46
The splash paints the theme's flat canvas again; the accent glow overlay
is gone. The boot script still publishes --boot-accent for the parity
tests and future use.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The grip capped width and height against the whole viewport, so a panel
parked away from the top-left could push its far edges -- and the grip
itself -- off screen. The room to grow is now measured from the corner
the resize holds still.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e-system

# Conflicts:
#	apps/web/src/components/SidebarV2.tsx

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 2033017. Configure here.

settings.sidebarThreadPreviewCount,
settings.timestampFormat,
settings.wordWrap,
followSystem,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Restore misses theme mix dirty

Medium Severity

changedSettingLabels includes a “Theme mix” entry from themeHalves, but themeHalves is omitted from the useMemo dependency list. Mixing light/dark themes without changing the base preference or follow-system flag can leave Restore defaults disabled and omit the mix from the confirm list.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2033017. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Improve theme comfort and cohesion (light and dark) Themes and UI cusomization Unmatched Contrast Levels

3 participants