6cf8a82889
- Settings → Appearance: mode Dark / Light / System (follows the OS live) and a named theme remembered per mode — dark: Default, Darcula, Apple Dark; light: Default, Apple Light, IntelliJ Light, Canvas. Each theme is a CSS-variable override block keyed by `data-theme` on <html>; the sidebar quick toggle gains System and swaps between the two remembered themes. - An inline boot script in index.html applies the stored mode and theme before first paint, so a reload no longer flashes the default colours. - Diff +/- rows are easier to tell apart on every theme: a /20 status tint, a 3px status bar and a bold coloured marker; the changed-span emphasis no longer vanishes on light themes. - #42: the Add-lane modal (Repo mode / "Current branch") asks POST /api/lanes/ensure with `exact: true`. Without it a folder nested inside an existing lane resolved to that parent lane with created:false, so the modal closed and no new card appeared. When the folder really already has a lane, the modal now says which one instead of closing. Closes #42
8.0 KiB
8.0 KiB
Appearance: mode + named themes, and readable diff rows
Requirement (from the user, 2026-09-23)
/settingsgets an Appearance section: mode Dark / Light / System, plus a named theme per mode.- Themes: dark = Default (current Radix slate), Darcula, Apple Dark; light = Default (current ivory), Apple Light, IntelliJ Light, Canvas (linen).
- A theme also recolours code syntax (
--syn-*). - The sidebar's dark/light control stays; each mode remembers its own theme (dark=Darcula, light=Canvas → toggling swaps between those two).
- The diff's +/- rows in Code changes (
LaneDiffView) get visibly more contrast, in every theme.
Approach
Themes are pure CSS-variable overrides — every colour in the app already reads --surface-*, --fg-*, --accent*, --status-*, --tint-base, --syn-*. No component learns theme names.
client/src/index.css- Light default block becomes
:root, [data-mode="light"](so a light preview can be scoped inside a dark page). - After the
.darkblock, one block per theme: dark ones.dark[data-theme="<id>"], light ones[data-theme="<id>"](placed after the defaults, same specificity as[data-mode=light], later wins). Each overrides only what differs;--tint-basealways = its--surface-3. - New
--diff-add/--diff-delNOT needed: rows use existingstatus-*tokens.
- Light default block becomes
client/src/hooks/useTheme.ts→ module-level store read viauseSyncExternalStore, so Settings and Sidebar never disagree.- Storage:
themekey now holdsdark|light|system(old values stay valid → no migration);agenthub.theme.dark/agenthub.theme.lighthold theme ids; unknown id ⇒default. systemresolves viamatchMedia("(prefers-color-scheme: dark)")and follows itschangeevent.- Applies
.darkclass +data-themeattribute on<html>(attribute removed fordefault). - Keeps today's API (
theme= resolveddark|light,setTheme,toggleTheme— toggle sets the explicit opposite mode) and addsmode,setMode,themeFor,setThemeFor(mode, id), plus exportedTHEMESregistry{ dark: [...ids], light: [...ids] }.
- Storage:
client/src/pages/Settings.tsx— new first sectionappearanceinSETTINGS_SECTIONS: a 3-button mode segment, then two grids (Dark themes / Light themes) of cards. Each card's swatch is a<div data-mode|class=dark data-theme=id>paintingbg-surface-0, abg-surface-3card, an accent dot and two syntax colours — the CSS vars re-scope inside it, so previews need zero duplicated hex. Selected card = accent ring +aria-pressed.client/src/components/Sidebar.tsx— expanded panel's theme grid gains System (3 buttons, Monitor icon); collapsed toggle unchanged.client/src/components/lanes/LaneDiffView.tsxrowTint→bg-status-*/20+ a 3px inset left bar in the status colour; the+/-marker colouredtext-status-*and bold.highlight.ts'sadd/delclasses use the same/20status tokens instead of fixedemerald/red-500/10.- i18n
en/vi:nav.themeNames.system,settings.appearance.*(title, mode, darkThemes, lightThemes, theme names).
Revisions after the plan challenge
- Store keeps NO cached module state:
getSnapshotreads localStorage +matchMediaand returns a primitive string;subscribelistens to a customagenthub-themeevent,storage(cross-tab) and the media-querychange. No DOM work at import. DOM applied synchronously in the setter and in auseLayoutEffect(no one-frame flash). window.matchMedia?.(...)guarded (jsdom lacks it); the system test installs its own listener-capable mock.<html>carriesdata-mode="dark|light"too; light themes scoped[data-mode="light"][data-theme=x], dark.dark[data-theme=x]— no source-order dependency.--tint-base: var(--surface-3)and--syn-plain: var(--fg-primary)declared once in each default block; theme blocks never set them.- Diff emphasis
LaneDiffView.tsx:48bg-white/15→bg-fg-primary/15(invisible on light otherwise). - Swatch root sets
text-fg-primary+bg-surface-0. - Palettes pinned below; contrast checked with a small node script (white-on-accent, fg-muted on surface-3, status on its /20 chip) — dark themes held to the SAME bar the current dark default meets (its #0090ff is 3.3:1 under white text), light themes to ≥4.5:1.
- Appearance section lives in its own component
client/src/components/settings/AppearanceSettings.tsx; Settings.tsx gains only the section entry + render. - Sidebar: check the expanded panel width with 3 theme buttons; collapsed toggle moves System → the explicit opposite mode (documented).
Palettes (hex; surface-0..5, border, border-light, accent, accent-hover, fg 1/2/muted, success/danger/warning; syntax)
- darcula: #2b2b2b #303234 #36383a #3c3f41 #45494a #4e5254 · #4b4f51 #5e6366 · #3592c4 #4aa3d6 · #dcdcdc #a9b7c6 #8c9296 · #6aab73 #ff6b68 #e0b25c · keyword #cc7832 string #6a8759 number #6897bb comment #808080 function #ffc66d property #9876aa tag #e8bf6a builtin #8888c6 operator/punct #a9b7c6 add #6aab73 del #ff6b68
- apple-dark: #161618 #1c1c1e #242426 #2c2c2e #363638 #3a3a3c · #38383a #48484a · #0a84ff #409cff · #f5f5f7 #aeaeb2 #8e8e93 · #30d158 #ff6961 #ffd60a · keyword #fc5fa3 string #fc6a5d number #d0bf69 comment #7f8c98 function #67b7a4 builtin #5dd8ff property #a167e6 tag #fc5fa3 operator/punct #a1a1a6 add #30d158 del #ff6961
- apple-light: #e9e9ee #efeff4 #f5f5f8 #fbfbfd #fdfdfe #ffffff · #d1d1d6 #e5e5ea · #0066cc #0055b3 · #1d1d1f #515154 #6e6e73 · #1f7a36 #c5000f #8a5a00 · keyword #9b2393 string #c41a16 number #1c00cf comment #5d6c79 function #326d74 builtin #0b4f79 property #3900a0 tag #9b2393 operator/punct #4b4b50 add #1f7a36 del #c5000f
- intellij-light: #eceef2 #f1f2f5 #f7f8fa #fdfdfe #ffffff #ffffff · #d3d5db #e4e6eb · #3574f0 #2e63cf · #1e1f22 #494b57 #6c707e · #1d7a35 #c01f2a #8f5f00 · keyword #0033b3 string #067d17 number #1750eb comment #707070 function #00627a builtin #000080 property #871094 tag #0033b3 operator/punct #4b4b4b add #067d17 del #c01f2a
- canvas: #e3dccd #e8e2d4 #ede8dc #f3efe6 #f7f4ed #faf8f3 · #cbbfa6 #dcd3c0 · #7a5230 #663f22 · #2b2620 #5e554a #6b6155 · #3f6a2a #9f2f23 #7d5700 · keyword #8a3b12 string #4d6b1f number #9a5b00 comment #7d7262 function #7a5a00 builtin #2f5e6e property #3b6660 tag #8e2f2f operator #8a3050 punct #6b6155 add #4d6b1f del #9f2f23 (Values that fail the contrast script are darkened/lightened until they pass; the final CSS is the source of truth.)
Out of scope
- Server-side persistence (theme stays a per-browser preference, like lane order).
A no-flash inline boot script— added after acceptance at the user's request (client/index.html).- User-defined/custom themes.
- Surfaces that already bypass the CSS variables (they clash with today's light default too): the xterm terminal (no
theme, TerminalView.tsx), Dashboard/workflow chart hardcoded hex (Dashboard.tsx, OrchestrationDAG.tsx, ErrorPropagationMap.tsx), ~415 raw Tailwind palette classes (toolStyle.ts, types.ts, …),index.html'stext-gray-200body.
Tests
hooks/__tests__/useTheme.test.ts: existing 4 stay green; add —systemfollows a mockedmatchMediaand its change event;setThemeForsetsdata-themeonly for the active mode and persists; unknown stored id ⇒ default (no attribute); two hook instances stay in sync.- Settings: a test that clicking a theme card sets
data-theme/ clicking System sets mode. - Snapshot diff for Settings/Sidebar reviewed, then regenerated deliberately.
npm run test:client,tscvia build,check-headers.sh. Manual: build + open/settings, screenshot each theme.
Acceptance
- Mode Dark/Light/System works and persists across reload; System follows the OS live.
- Each of the 7 themes selectable; per-mode memory; sidebar toggle swaps between the two remembered themes.
- Code syntax recolours per theme.
- Diff +/- rows clearly distinguishable in every theme.
- Docs (client README / CLAUDE.md marker) updated.