Default desktop startup to pure white

New desktop installs and settings payloads without an explicit theme now hydrate to the pure white workspace. The General settings selector keeps the other two themes available, but presents pure white first so the default and visible ordering agree.

Constraint: Existing persisted theme choices must continue to win over the default.
Rejected: Remove the warm classic theme | users still need the alternate light appearance.
Confidence: high
Scope-risk: narrow
Directive: Keep the localStorage and user-settings theme fallbacks aligned when changing desktop theme defaults.
Tested: cd desktop && bun run test src/stores/uiStore.test.ts src/stores/settingsStore.test.ts src/__tests__/generalSettings.test.tsx
Tested: cd desktop && bun run lint
Tested: bun run check:desktop
Tested: git diff --check
Not-tested: Full root bun run verify.
This commit is contained in:
程序员阿江(Relakkes) 2026-05-16 00:18:21 +08:00
parent c75ff0bf2d
commit 5ed1081252
8 changed files with 73 additions and 11 deletions

View File

@ -1,5 +1,5 @@
<!doctype html>
<html lang="zh-CN">
<html lang="zh-CN" data-theme="white">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />

View File

@ -240,6 +240,12 @@ describe('Settings > General tab', () => {
render(<Settings />)
fireEvent.click(screen.getByText('General'))
const pureWhite = screen.getByRole('button', { name: 'Pure White' })
const warmClassic = screen.getByRole('button', { name: 'Warm Classic' })
const dark = screen.getByRole('button', { name: 'Dark' })
expect((pureWhite.compareDocumentPosition(warmClassic) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0).toBe(true)
expect((warmClassic.compareDocumentPosition(dark) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0).toBe(true)
fireEvent.click(screen.getByRole('button', { name: 'Pure White' }))
expect(useSettingsStore.getState().setTheme).toHaveBeenCalledWith('white')

View File

@ -1459,9 +1459,9 @@ function GeneralSettings() {
RESPONSE_LANGUAGES.find(({ value }) => value === responseLanguage)?.label ?? RESPONSE_LANGUAGES[0]!.label
const THEMES: Array<{ value: ThemeMode; label: string }> = [
{ value: 'white', label: t('settings.general.appearance.white') },
{ value: 'light', label: t('settings.general.appearance.light') },
{ value: 'dark', label: t('settings.general.appearance.dark') },
{ value: 'white', label: t('settings.general.appearance.white') },
]
const WEB_SEARCH_MODES: Array<{ value: WebSearchMode; label: string }> = [

View File

@ -314,6 +314,53 @@ describe('settingsStore theme persistence', () => {
document.documentElement.style.colorScheme = ''
})
it('falls back to the pure white theme when user settings have no theme', async () => {
vi.doMock('../api/settings', () => ({
settingsApi: {
getUser: vi.fn().mockResolvedValue({}),
updateUser: vi.fn(),
getPermissionMode: vi.fn().mockResolvedValue({ mode: 'default' }),
setPermissionMode: vi.fn(),
getCliLauncherStatus: vi.fn(),
},
}))
vi.doMock('../api/models', () => ({
modelsApi: {
list: vi.fn().mockResolvedValue({ models: [] }),
getCurrent: vi.fn().mockResolvedValue({ model: null }),
setCurrent: vi.fn(),
getEffort: vi.fn().mockResolvedValue({ level: 'medium' }),
setEffort: vi.fn(),
},
}))
vi.doMock('../api/h5Access', () => ({
h5AccessApi: {
get: vi.fn().mockResolvedValue({
settings: {
enabled: false,
tokenPreview: null,
allowedOrigins: [],
publicBaseUrl: null,
},
}),
enable: vi.fn(),
disable: vi.fn(),
regenerate: vi.fn(),
update: vi.fn(),
},
}))
const { useSettingsStore } = await import('./settingsStore')
const { useUIStore } = await import('./uiStore')
await useSettingsStore.getState().fetchAll()
expect(useSettingsStore.getState().theme).toBe('white')
expect(useUIStore.getState().theme).toBe('white')
expect(document.documentElement.getAttribute('data-theme')).toBe('white')
expect(document.documentElement.style.colorScheme).toBe('light')
})
it('hydrates the pure white theme from user settings', async () => {
vi.doMock('../api/settings', () => ({
settingsApi: {

View File

@ -116,7 +116,7 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
settingsApi.getUser(),
loadH5AccessSettings(previousH5Access),
])
const theme = isThemeMode(userSettings.theme) ? userSettings.theme : 'light'
const theme = isThemeMode(userSettings.theme) ? userSettings.theme : 'white'
useUIStore.getState().setTheme(theme)
set({
permissionMode: mode,

View File

@ -8,6 +8,15 @@ describe('uiStore theme handling', () => {
document.documentElement.style.colorScheme = ''
})
it('defaults new installs to the pure white theme', async () => {
const { initializeTheme, useUIStore } = await import('./uiStore')
expect(useUIStore.getState().theme).toBe('white')
initializeTheme()
expect(document.documentElement.getAttribute('data-theme')).toBe('white')
expect(document.documentElement.style.colorScheme).toBe('light')
})
it('hydrates and applies the pure white theme as a light color scheme', async () => {
window.localStorage.setItem('cc-haha-theme', 'white')
@ -19,9 +28,13 @@ describe('uiStore theme handling', () => {
expect(document.documentElement.style.colorScheme).toBe('light')
})
it('cycles through light, dark, and pure white themes', async () => {
it('cycles through pure white, warm classic, and dark themes', async () => {
const { useUIStore } = await import('./uiStore')
useUIStore.getState().toggleTheme()
expect(useUIStore.getState().theme).toBe('light')
expect(document.documentElement.style.colorScheme).toBe('light')
useUIStore.getState().toggleTheme()
expect(useUIStore.getState().theme).toBe('dark')
expect(document.documentElement.style.colorScheme).toBe('dark')
@ -29,9 +42,5 @@ describe('uiStore theme handling', () => {
useUIStore.getState().toggleTheme()
expect(useUIStore.getState().theme).toBe('white')
expect(document.documentElement.style.colorScheme).toBe('light')
useUIStore.getState().toggleTheme()
expect(useUIStore.getState().theme).toBe('light')
expect(document.documentElement.style.colorScheme).toBe('light')
})
})

View File

@ -8,7 +8,7 @@ function getStoredTheme(): ThemeMode {
const stored = localStorage.getItem(THEME_STORAGE_KEY)
if (isThemeMode(stored)) return stored
} catch { /* localStorage unavailable */ }
return 'light'
return 'white'
}
export function applyTheme(theme: ThemeMode) {
@ -89,7 +89,7 @@ export const useUIStore = create<UIStore>((set) => ({
toggleTheme: () => {
set((state) => {
const currentIndex = THEME_MODES.indexOf(state.theme)
const next = THEME_MODES[(currentIndex + 1) % THEME_MODES.length] ?? 'light'
const next = THEME_MODES[(currentIndex + 1) % THEME_MODES.length] ?? 'white'
applyTheme(next)
try { localStorage.setItem(THEME_STORAGE_KEY, next) } catch { /* noop */ }
return { theme: next }

View File

@ -3,7 +3,7 @@
export type PermissionMode = 'default' | 'acceptEdits' | 'plan' | 'bypassPermissions' | 'dontAsk'
export type EffortLevel = 'low' | 'medium' | 'high' | 'max'
export const THEME_MODES = ['light', 'dark', 'white'] as const
export const THEME_MODES = ['white', 'light', 'dark'] as const
export type ThemeMode = (typeof THEME_MODES)[number]
export function isThemeMode(value: unknown): value is ThemeMode {