Skip to content

feat(web): add color theme presets#964

Open
NightWatcher314 wants to merge 4 commits into
tiann:mainfrom
NightWatcher314:feature/color-theme-presets
Open

feat(web): add color theme presets#964
NightWatcher314 wants to merge 4 commits into
tiann:mainfrom
NightWatcher314:feature/color-theme-presets

Conversation

@NightWatcher314

Copy link
Copy Markdown
Contributor

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

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Findings

  • [Major] Color theme presets are stripped by the custom-color sync — selecting a preset calls applyColorTheme(), then this new hapi-color-theme-change listener calls applyThemeColors(). With no custom colors saved, applyThemeColors() removes every target in THEME_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 because initializeTheme() applies the stored preset before initializeThemeColors() 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 useThemeColors test where hapi-color-theme = 'one', applyColorTheme('one', 'light'), then applyThemeColors() leaves --app-bg as #fbfbff when 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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[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]!)
        }
    }
}

@NightWatcher314

Copy link
Copy Markdown
Contributor Author

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

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Findings

  • [Minor] Color-theme changes do not sync to already-open app tabs unless Settings is mounted — the only storage listener for hapi-color-theme lives inside useColorTheme(), so a tab on /sessions never receives another tab's preset change; the dispatched CustomEvent is 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-theme changes 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(() => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[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()
})

@NightWatcher314

Copy link
Copy Markdown
Contributor Author

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

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Findings

  • [Minor] Boot-time inline html background is never cleared — web/index.html:91 writes document.documentElement.style.backgroundColor, and later theme changes only update data-theme, CSS variables, and the theme-color meta tag. Because inline style wins over html[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.backgroundColor is cleared or updated.

HAPI Bot

Comment thread web/index.html
: themeBackgrounds[colorTheme][paletteScheme]
document.documentElement.setAttribute('data-theme', scheme)
document.documentElement.setAttribute('data-color-theme', colorTheme)
document.documentElement.style.backgroundColor = color

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[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)
}

@NightWatcher314

Copy link
Copy Markdown
Contributor Author

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

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.backgroundColor during 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): bun is not installed in this runner (bun: command not found).

HAPI Bot

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