feat(web): add color theme presets#964
Conversation
There was a problem hiding this comment.
Findings
- [Major] Color theme presets are stripped by the custom-color sync — selecting a preset calls
applyColorTheme(), then this newhapi-color-theme-changelistener callsapplyThemeColors(). With no custom colors saved,applyThemeColors()removes every target inTHEME_COLOR_KEYS, including--app-bg,--app-fg,--app-link,--app-secondary-bg, and--app-border, so most of the selected preset immediately falls back to the base light/dark/OLED CSS. The same layering problem also happens on startup becauseinitializeTheme()applies the stored preset beforeinitializeThemeColors()runs. Evidence:web/src/hooks/useThemeColors.ts:285.
Suggested fix:import { applyColorTheme, getStoredColorTheme, type ColorScheme } from './useColorTheme' export function applyThemeColors(): void { if (!isBrowser()) return const scheme = getThemeScheme() applyColorTheme(getStoredColorTheme(), scheme as ColorScheme) const overrides = getStoredThemeColors()[scheme] ?? {} const rootStyle = document.documentElement.style for (const key of THEME_COLOR_KEYS) { const override = overrides[key.id] const hex = override && isHexColor(override) ? override : null if (!hex) continue for (const cssVar of key.targets) rootStyle.setProperty(cssVar, hex) if (key.derivedTargets && key.derive) { const derived = key.derive(hex, scheme) for (const cssVar of key.derivedTargets) rootStyle.setProperty(cssVar, derived[cssVar]!) } } }
Summary
- Review mode: initial
- Found one preset/custom-color layering regression. Residual risk: I did not run the contributor-reported web typecheck/test/build commands in this automation review.
Testing
- Not run (automation). Suggested coverage: add a
useThemeColorstest wherehapi-color-theme = 'one',applyColorTheme('one', 'light'), thenapplyThemeColors()leaves--app-bgas#fbfbffwhen no custom colors are stored.
HAPI Bot
| window.addEventListener('storage', (event: StorageEvent) => { | ||
| if (event.key === STORAGE_KEY) applyThemeColors() | ||
| }) | ||
| window.addEventListener('hapi-color-theme-change', applyThemeColors) |
There was a problem hiding this comment.
[MAJOR] This listener makes a newly selected preset get applied and then mostly removed again. setColorTheme() calls applyColorTheme(), dispatches hapi-color-theme-change, and this handler calls applyThemeColors(). When no custom colors are saved, applyThemeColors() removes the shared variables (--app-bg, --app-fg, --app-link, --app-secondary-bg, --app-border, etc.), so the preset falls back to the base CSS instead of staying active. Re-apply the preset as the base layer inside applyThemeColors(), then only write actual custom overrides.
Suggested fix:
import { applyColorTheme, getStoredColorTheme, type ColorScheme } from './useColorTheme'
export function applyThemeColors(): void {
if (!isBrowser()) return
const scheme = getThemeScheme()
applyColorTheme(getStoredColorTheme(), scheme as ColorScheme)
const overrides = getStoredThemeColors()[scheme] ?? {}
const rootStyle = document.documentElement.style
for (const key of THEME_COLOR_KEYS) {
const override = overrides[key.id]
const hex = override && isHexColor(override) ? override : null
if (!hex) continue
for (const cssVar of key.targets) rootStyle.setProperty(cssVar, hex)
if (key.derivedTargets && key.derive) {
const derived = key.derive(hex, scheme)
for (const cssVar of key.derivedTargets) rootStyle.setProperty(cssVar, derived[cssVar]!)
}
}
}|
Thanks, this was a real layering bug. Fixed by making custom color sync re-apply the selected preset first, then only layer saved custom overrides on top. Added coverage for the no-custom-colors case and custom-over-preset case.\n\nRe-ran:\n- bun run typecheck:web\n- bun run --cwd web test -- src/hooks/useColorTheme.test.ts src/hooks/useTheme.test.ts src/hooks/useThemeColors.test.ts src/hooks/useChatSurfaceColors.test.ts src/routes/settings/index.test.tsx\n- bun run build:web |
There was a problem hiding this comment.
Findings
- [Minor] Color-theme changes do not sync to already-open app tabs unless Settings is mounted — the only
storagelistener forhapi-color-themelives insideuseColorTheme(), so a tab on/sessionsnever receives another tab's preset change; the dispatchedCustomEventis same-tab only. Evidence:web/src/hooks/useColorTheme.ts:295.
Suggested fix:// useTheme.ts window.addEventListener('storage', (event: StorageEvent) => { if (event.key === APPEARANCE_KEY) updateScheme() if (event.key === getColorThemeStorageKey()) updateScheme(true) }) // useThemeColors.ts window.addEventListener('storage', (event: StorageEvent) => { if (event.key === STORAGE_KEY || event.key === getColorThemeStorageKey()) applyThemeColors() })
Summary
- Review mode: follow-up after new commits
- Prior bot finding was addressed. One remaining cross-tab sync gap found for the new color theme preference.
Testing
- Not run (automation). Suggested coverage: add a storage-event test where
hapi-color-themechanges after app initialization while no Settings hook is mounted, and assert preset variables plus custom overrides refresh.
HAPI Bot
| export function useColorTheme(): { colorTheme: ColorThemePreset; setColorTheme: (theme: ColorThemePreset) => void } { | ||
| const [colorTheme, setColorThemeState] = useState<ColorThemePreset>(getStoredColorTheme) | ||
|
|
||
| useEffect(() => { |
There was a problem hiding this comment.
[MINOR] This storage listener only runs while useColorTheme() is mounted, which currently means the Settings page. If a user has another HAPI tab open on /sessions, changing the color theme in this tab only updates localStorage; the other tab gets a browser storage event, but no app-level listener handles hapi-color-theme, and the CustomEvent dispatched below is same-tab only. Appearance and custom key colors already have global startup listeners, so presets should use the same path.
Suggested fix:
// useTheme.ts
window.addEventListener('storage', (event: StorageEvent) => {
if (event.key === APPEARANCE_KEY) updateScheme()
if (event.key === getColorThemeStorageKey()) updateScheme(true)
})
// useThemeColors.ts
window.addEventListener('storage', (event: StorageEvent) => {
if (event.key === STORAGE_KEY || event.key === getColorThemeStorageKey()) applyThemeColors()
})|
Fixed the follow-up cross-tab sync gap too. App-level storage listeners now handle hapi-color-theme changes even when Settings is not mounted, and custom-color reapplication listens for the same key. Added tests for both app initialization/theme-color update and cross-tab preset refresh.\n\nRe-ran:\n- bun run typecheck:web\n- bun run --cwd web test -- src/hooks/useColorTheme.test.ts src/hooks/useTheme.test.ts src/hooks/useThemeColors.test.ts src/hooks/useChatSurfaceColors.test.ts src/routes/settings/index.test.tsx\n- bun run build:web |
There was a problem hiding this comment.
Findings
- [Minor] Boot-time inline
htmlbackground is never cleared —web/index.html:91writesdocument.documentElement.style.backgroundColor, and later theme changes only updatedata-theme, CSS variables, and the theme-color meta tag. Because inline style wins overhtml[data-theme]CSS, the document canvas can keep the launch preset/appearance color after the user switches appearance or color theme.
Suggested fix:function applyTheme(scheme: ColorScheme): void { document.documentElement.style.removeProperty("background-color") document.documentElement.setAttribute("data-theme", scheme) applyColorTheme(getStoredColorTheme(), scheme) applyBrowserThemeColor(scheme) }
Summary
- Review mode: follow-up after new commits
- Prior bot finding appears addressed. One remaining issue found in the full latest diff: the early flash-prevention inline background persists after app startup/theme changes.
Testing
- Not run (automation). Suggested coverage: initialize from one preset, switch appearance or color theme, and assert
document.documentElement.style.backgroundColoris cleared or updated.
HAPI Bot
| : themeBackgrounds[colorTheme][paletteScheme] | ||
| document.documentElement.setAttribute('data-theme', scheme) | ||
| document.documentElement.setAttribute('data-color-theme', colorTheme) | ||
| document.documentElement.style.backgroundColor = color |
There was a problem hiding this comment.
[MINOR] This inline backgroundColor is set during boot, but the runtime theme path never clears or updates document.documentElement.style.backgroundColor; applyTheme() only updates data-theme, preset CSS variables, and the meta tag. Since inline style outranks the later html[data-theme] rules, the document canvas can retain the launch appearance/preset color after a user changes appearance or color theme.
Suggested fix:
function applyTheme(scheme: ColorScheme): void {
document.documentElement.style.removeProperty("background-color")
document.documentElement.setAttribute("data-theme", scheme)
applyColorTheme(getStoredColorTheme(), scheme)
applyBrowserThemeColor(scheme)
}|
Fixed the boot-time inline background issue as well. Runtime theme application now clears the flash-prevention inline background before applying data-theme/preset variables, with a regression test covering initialization from a preset.\n\nRe-ran:\n- bun run typecheck:web\n- bun run --cwd web test -- src/hooks/useColorTheme.test.ts src/hooks/useTheme.test.ts src/hooks/useThemeColors.test.ts src/hooks/useChatSurfaceColors.test.ts src/routes/settings/index.test.tsx\n- bun run build:web |
There was a problem hiding this comment.
Findings
- No issues found with confidence >= 80% in the latest diff.
Summary
- Review mode: follow-up after new commits
- Prior bot finding about the boot-time inline HTML background appears addressed by clearing
document.documentElement.style.backgroundColorduring runtime theme initialization. - Residual risk: visual/theme regressions can still slip through unit tests because the changed surface is mostly CSS variable composition and browser rendering behavior.
Testing
- Not run (automation):
bunis not installed in this runner (bun: command not found).
HAPI Bot
Summary\n- add a Color theme preset picker in Settings with Codex App-style Aa swatches\n- persist the selected preset in localStorage and apply semantic CSS variables for light/dark/OLED appearances\n- apply the preset during early page boot to reduce theme flash, while keeping existing custom color and chat surface overrides layered on top\n\nCloses #963\n\n## Test plan\n- bun run typecheck:web\n- bun run --cwd web test -- src/hooks/useColorTheme.test.ts src/hooks/useTheme.test.ts src/hooks/useThemeColors.test.ts src/hooks/useChatSurfaceColors.test.ts src/routes/settings/index.test.tsx\n- bun run build:web