diff --git a/desktop/src/__tests__/generalSettings.test.tsx b/desktop/src/__tests__/generalSettings.test.tsx index 604a1a37..cf9cecfe 100644 --- a/desktop/src/__tests__/generalSettings.test.tsx +++ b/desktop/src/__tests__/generalSettings.test.tsx @@ -144,6 +144,7 @@ describe('Settings > General tab', () => { skipWebFetchPreflight: true, desktopNotificationsEnabled: true, responseLanguage: '', + uiZoom: 1, webSearch: { mode: 'auto', tavilyApiKey: '', braveApiKey: '' }, h5Access: { enabled: false, @@ -167,6 +168,9 @@ describe('Settings > General tab', () => { setResponseLanguage: vi.fn().mockImplementation(async (language: string) => { useSettingsStore.setState({ responseLanguage: language }) }), + setUiZoom: vi.fn().mockImplementation((uiZoom: number) => { + useSettingsStore.setState({ uiZoom }) + }), setWebSearch: vi.fn().mockImplementation(async (webSearch) => { useSettingsStore.setState({ webSearch }) }), @@ -251,6 +255,26 @@ describe('Settings > General tab', () => { expect(screen.getByRole('button', { name: 'Warm Classic' })).toHaveAttribute('aria-pressed', 'false') }) + it('updates the shared UI zoom setting from the General display control', async () => { + render() + + fireEvent.click(screen.getByText('General')) + await act(async () => { + fireEvent.change(screen.getByLabelText('UI Zoom'), { + target: { value: '1.25', valueAsNumber: 1.25 }, + }) + }) + + expect(useSettingsStore.getState().setUiZoom).toHaveBeenCalledWith(1.25) + expect(screen.getByText('125%')).toBeInTheDocument() + + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: 'Reset UI zoom to 100%' })) + }) + + expect(useSettingsStore.getState().setUiZoom).toHaveBeenLastCalledWith(1) + }) + it('opens the Token usage tab from Settings navigation above Diagnostics', () => { render() diff --git a/desktop/src/components/layout/AppShell.tsx b/desktop/src/components/layout/AppShell.tsx index af09547b..c9e578ab 100644 --- a/desktop/src/components/layout/AppShell.tsx +++ b/desktop/src/components/layout/AppShell.tsx @@ -28,7 +28,6 @@ function isChatTab(tab: Tab | undefined) { export function AppShell() { const fetchSettings = useSettingsStore((s) => s.fetchAll) - const uiZoom = useSettingsStore((s) => s.uiZoom) const sidebarOpen = useUIStore((s) => s.sidebarOpen) const toggleSidebar = useUIStore((s) => s.toggleSidebar) const setSidebarOpen = useUIStore((s) => s.setSidebarOpen) @@ -195,7 +194,7 @@ export function AppShell() { } return ( -
+
{isMobileShell && effectiveSidebarOpen ? (
+ {/* UI Zoom */} +
+
+
+

{t('settings.general.uiZoom')}

+

{t('settings.general.uiZoomDescription')}

+
+
+ + {Math.round(uiZoom * 100)}% + + +
+
+
+ {Math.round(UI_ZOOM_MIN * 100)}% + setUiZoom(e.currentTarget.valueAsNumber)} + className="flex-1" + /> + {Math.round(UI_ZOOM_MAX * 100)}% +
+
+ {/* Language selector */}

{t('settings.general.languageTitle')}

{t('settings.general.languageDescription')}

@@ -1777,33 +1816,6 @@ function GeneralSettings() {
- {/* UI Zoom */} -
-

{t('settings.general.uiZoom')}

-

{t('settings.general.uiZoomDescription')}

-
-
- - {Math.round(uiZoom * 100)}% - -
-
- {Math.round(UI_ZOOM_MIN * 100)}% - setUiZoom(parseFloat(e.target.value))} - onMouseUp={(e) => e.currentTarget.blur()} - className="flex-1" - /> - {Math.round(UI_ZOOM_MAX * 100)}% -
-
-
- ) } diff --git a/desktop/src/stores/settingsStore.test.ts b/desktop/src/stores/settingsStore.test.ts index 77e7d8d4..f632dc79 100644 --- a/desktop/src/stores/settingsStore.test.ts +++ b/desktop/src/stores/settingsStore.test.ts @@ -22,6 +22,47 @@ describe('settingsStore locale defaults', () => { }) }) +describe('settingsStore UI zoom', () => { + beforeEach(() => { + vi.resetModules() + window.localStorage.clear() + document.documentElement.removeAttribute('data-app-zoom-percent') + document.documentElement.style.removeProperty('--app-zoom') + document.body.style.removeProperty('zoom') + }) + + it('hydrates from the app zoom storage key', async () => { + window.localStorage.setItem('cc-haha-app-zoom', '1.25') + + const { useSettingsStore } = await import('./settingsStore') + + expect(useSettingsStore.getState().uiZoom).toBe(1.25) + }) + + it('applies and persists UI zoom changes through the shared app zoom controller', async () => { + const { useSettingsStore } = await import('./settingsStore') + + useSettingsStore.getState().setUiZoom(1.25) + + await vi.waitFor(() => { + expect(window.localStorage.getItem('cc-haha-app-zoom')).toBe('1.25') + }) + expect(useSettingsStore.getState().uiZoom).toBe(1.25) + expect(document.documentElement.getAttribute('data-app-zoom-percent')).toBe('125') + }) + + it('clamps UI zoom changes to the supported range', async () => { + const { useSettingsStore } = await import('./settingsStore') + + useSettingsStore.getState().setUiZoom(9) + + await vi.waitFor(() => { + expect(window.localStorage.getItem('cc-haha-app-zoom')).toBe('2') + }) + expect(useSettingsStore.getState().uiZoom).toBe(2) + }) +}) + describe('settingsStore desktop notification persistence', () => { beforeEach(() => { vi.resetModules() diff --git a/desktop/src/stores/settingsStore.ts b/desktop/src/stores/settingsStore.ts index 81b7e9ee..088319cb 100644 --- a/desktop/src/stores/settingsStore.ts +++ b/desktop/src/stores/settingsStore.ts @@ -5,13 +5,22 @@ import { modelsApi } from '../api/models' import { h5AccessApi } from '../api/h5Access' import { isThemeMode, type H5AccessSettings, type PermissionMode, type EffortLevel, type ModelInfo, type ThemeMode, type WebSearchSettings } from '../types/settings' import type { Locale } from '../i18n' +import { + APP_ZOOM_CONTROL_STEP, + DEFAULT_APP_ZOOM, + MAX_APP_ZOOM, + MIN_APP_ZOOM, + applyAppZoomLevel, + normalizeAppZoomLevel, + readStoredAppZoomLevel, +} from '../lib/appZoom' import { useUIStore } from './uiStore' const LOCALE_STORAGE_KEY = 'cc-haha-locale' -const UI_ZOOM_STORAGE_KEY = 'cc-haha-ui-zoom' -export const UI_ZOOM_MIN = 0.5 -export const UI_ZOOM_MAX = 2.0 -export const UI_ZOOM_STEP = 0.05 +export const UI_ZOOM_MIN = MIN_APP_ZOOM +export const UI_ZOOM_MAX = MAX_APP_ZOOM +export const UI_ZOOM_STEP = APP_ZOOM_CONTROL_STEP +export const UI_ZOOM_DEFAULT = DEFAULT_APP_ZOOM let desktopNotificationsSaveQueue: Promise = Promise.resolve() function getStoredLocale(): Locale { @@ -22,17 +31,6 @@ function getStoredLocale(): Locale { return 'zh' } -function getStoredUiZoom(): number { - try { - const stored = localStorage.getItem(UI_ZOOM_STORAGE_KEY) - if (stored === null) return 1.0 - const parsed = parseFloat(stored) - if (isNaN(parsed)) return 1.0 - return Math.min(UI_ZOOM_MAX, Math.max(UI_ZOOM_MIN, parsed)) - } catch { /* localStorage unavailable */ } - return 1.0 -} - type SettingsStore = { permissionMode: PermissionMode currentModel: ModelInfo | null @@ -96,14 +94,14 @@ export const useSettingsStore = create((set, get) => ({ h5Access: DEFAULT_H5_ACCESS_SETTINGS, h5AccessError: null, responseLanguage: '', - uiZoom: getStoredUiZoom(), + uiZoom: readStoredAppZoomLevel(), isLoading: false, error: null, setUiZoom: (zoom: number) => { - const clamped = Math.min(UI_ZOOM_MAX, Math.max(UI_ZOOM_MIN, zoom)) - set({ uiZoom: clamped }) - try { localStorage.setItem(UI_ZOOM_STORAGE_KEY, String(clamped)) } catch { /* noop */ } + const level = normalizeAppZoomLevel(zoom) + set({ uiZoom: level }) + void applyAppZoomLevel(level) }, fetchAll: async () => { diff --git a/desktop/src/types/settings.ts b/desktop/src/types/settings.ts index 7a2e5393..4a5b247a 100644 --- a/desktop/src/types/settings.ts +++ b/desktop/src/types/settings.ts @@ -43,6 +43,5 @@ export type UserSettings = { desktopNotificationsEnabled?: boolean webSearch?: WebSearchSettings language?: string - uiZoom?: number [key: string]: unknown }