feat: user-selectable color themes (theme registry)

Author: nateQQCreated Aug 5, 2026Updated Sep 16, 2026
Labelsenhancement

Problem

Right now the color palette is hardcoded in src/index.css under a single :root block (light) and .dark block (dark). If you want a different color scheme, the only option today is to fork and maintain a full CSS diff, then re-merge it by hand on every upstream release.

I've been running a Gruvbox-based fork for a few months exactly this way: a full override of every CSS variable in index.css, rebased on every update. Not sustainable for a personal fork, and not something the project should have to carry either.

Evidence there's demand

  • #1079 (open) asks for the theme to follow OS appearance - easier to reason about once themes are externalized from index.css in the first place.
  • #907 and #787 (closed) were bugs about the code editor / dark mode not syncing - symptoms of theme state living in more than one place.

Proposed design

Goal: don't break anything currently running, don't change the default look.

a. Extract color variables out of index.css Move the existing :root / .dark block into a default theme file, e.g. src/themes/default.css, scoped under [data-theme="default"] and [data-theme="default"].dark. Add src/themes/gruvbox.css with the same variable set. No component changes needed - everything already consumes hsl(var(--background)) etc.

b. Extend ThemeContext.jsx Currently it only exposes { isDarkMode, toggleDarkMode }. Add:

javascript
{ isDarkMode, toggleDarkMode, theme, setTheme, availableThemes }

setTheme writes document.documentElement.dataset.theme and persists to localStorage (color-theme). isDarkMode stays as-is - light/dark remains its own axis, independent from the color palette.

c. Theme registry src/themes/index.ts exporting [{ id, name, previewColors }]. Adding a theme becomes: one CSS file + one entry in this array.

d. Settings UI A dropdown in Settings with a small color swatch preview per option.

e. Sync meta[name="theme-color"] ThemeContext.jsx currently hardcodes the iOS status bar color per mode:

javascript
themeColorMeta.setAttribute('content', '#141414'); // dark
themeColorMeta.setAttribute('content', '#f6f4ef'); // light

Reading the active --background value instead is a small fix that falls naturally out of this work.

f. Scope of the first PR Just default + one more theme, to keep the diff reviewable. Additional themes would be follow-up PRs.

Screenshot

Attaching below - my daily driver running the Gruvbox palette purely via CSS variable overrides, no component code touched. That's roughly the shape of what a second theme would look like.

Image

Question for maintainers

If this direction looks reasonable, I'm happy to build it. One thing I'd like your take on before starting: should theming live entirely in core (as above), or would you rather core just expose the registry/hook and themes ship as plugins? Core-first seems like the safer incremental step either way, since plugins currently only have a tab slot (server/utils/plugin-loader.js) with no way to touch global styles - but wanted to check your preference before writing code.