From 799998c5f8afb49c0a45485487b42055f10e2cc6 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Sun, 26 Apr 2026 18:48:43 +0300 Subject: [PATCH] feat: add selectable interface and code fonts (10 for each) Adds separate UI and code font choices in settings Applies font changes immediately Lazy-loads selected remote fonts --- packages/ui/src/App.tsx | 3 + .../openchamber/OpenChamberVisualSettings.tsx | 71 +++++++++ packages/ui/src/hooks/useFontPreferences.ts | 8 +- packages/ui/src/lib/api/types.ts | 2 + packages/ui/src/lib/appearanceAutoSave.ts | 13 ++ packages/ui/src/lib/desktop.ts | 2 + packages/ui/src/lib/fontLoader.ts | 59 ++++++++ packages/ui/src/lib/fontOptions.ts | 142 +++++++++++++++++- .../ui/src/lib/i18n/messages/en.settings.ts | 6 + .../ui/src/lib/i18n/messages/es.settings.ts | 6 + .../src/lib/i18n/messages/pt-BR.settings.ts | 6 + .../ui/src/lib/i18n/messages/uk.settings.ts | 6 + .../src/lib/i18n/messages/zh-CN.settings.ts | 6 + packages/ui/src/lib/persistence.ts | 13 ++ packages/ui/src/stores/useUIStore.ts | 17 +++ 15 files changed, 356 insertions(+), 4 deletions(-) create mode 100644 packages/ui/src/lib/fontLoader.ts diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index 967223a6..250a3a8d 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -43,6 +43,7 @@ import { useSync } from '@/sync/use-sync'; import { setOptimisticRefs } from '@/sync/session-actions'; import { useFontPreferences } from '@/hooks/useFontPreferences'; import { CODE_FONT_OPTION_MAP, DEFAULT_MONO_FONT, DEFAULT_UI_FONT, UI_FONT_OPTION_MAP } from '@/lib/fontOptions'; +import { loadMonoFont, loadUiFont } from '@/lib/fontLoader'; import { ConfigUpdateOverlay } from '@/components/ui/ConfigUpdateOverlay'; import { AboutDialog } from '@/components/ui/AboutDialog'; import { RuntimeAPIProvider } from '@/contexts/RuntimeAPIProvider'; @@ -269,6 +270,8 @@ function App({ apis }: AppProps) { const root = document.documentElement; const uiStack = UI_FONT_OPTION_MAP[uiFont]?.stack ?? UI_FONT_OPTION_MAP[DEFAULT_UI_FONT].stack; const monoStack = CODE_FONT_OPTION_MAP[monoFont]?.stack ?? CODE_FONT_OPTION_MAP[DEFAULT_MONO_FONT].stack; + void loadUiFont(uiFont); + void loadMonoFont(monoFont); root.style.setProperty('--font-sans', uiStack); root.style.setProperty('--font-heading', uiStack); diff --git a/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx b/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx index 50869b5d..c313c414 100644 --- a/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx +++ b/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx @@ -23,6 +23,7 @@ import { isDesktopShell, isVSCodeRuntime, isWebRuntime } from '@/lib/desktop'; import { useDeviceInfo } from '@/lib/device'; import { usePwaDetection } from '@/hooks/usePwaDetection'; import { updateDesktopSettings } from '@/lib/persistence'; +import { CODE_FONT_OPTIONS, DEFAULT_MONO_FONT, DEFAULT_UI_FONT, UI_FONT_OPTIONS, type MonoFontOption, type UiFontOption } from '@/lib/fontOptions'; import { useI18n, type Locale } from '@/lib/i18n'; import { useConfigStore } from '@/stores/useConfigStore'; import { @@ -249,6 +250,10 @@ export const OpenChamberVisualSettings: React.FC const setFontSize = useUIStore(state => state.setFontSize); const terminalFontSize = useUIStore(state => state.terminalFontSize); const setTerminalFontSize = useUIStore(state => state.setTerminalFontSize); + const uiFont = useUIStore(state => state.uiFont); + const setUiFont = useUIStore(state => state.setUiFont); + const monoFont = useUIStore(state => state.monoFont); + const setMonoFont = useUIStore(state => state.setMonoFont); const padding = useUIStore(state => state.padding); const setPadding = useUIStore(state => state.setPadding); const inputBarOffset = useUIStore(state => state.inputBarOffset); @@ -847,6 +852,72 @@ export const OpenChamberVisualSettings: React.FC

{t('settings.openchamber.visual.section.spacingAndLayout')}

+ {shouldShow('fontSize') && !isMobile && ( +
+
+ {t('settings.openchamber.visual.field.interfaceFont')} +
+
+ + +
+
+ )} + + {shouldShow('terminalFontSize') && ( +
+
+ {t('settings.openchamber.visual.field.codeFont')} +
+
+ + +
+
+ )} + {shouldShow('fontSize') && !isMobile && (
diff --git a/packages/ui/src/hooks/useFontPreferences.ts b/packages/ui/src/hooks/useFontPreferences.ts index 0f3e9778..e6955d64 100644 --- a/packages/ui/src/hooks/useFontPreferences.ts +++ b/packages/ui/src/hooks/useFontPreferences.ts @@ -1,4 +1,5 @@ import type { MonoFontOption, UiFontOption } from '@/lib/fontOptions'; +import { useUIStore } from '@/stores/useUIStore'; interface FontPreferences { uiFont: UiFontOption; @@ -6,8 +7,11 @@ interface FontPreferences { } export const useFontPreferences = (): FontPreferences => { + const uiFont = useUIStore(state => state.uiFont); + const monoFont = useUIStore(state => state.monoFont); + return { - uiFont: 'ibm-plex-sans', - monoFont: 'ibm-plex-mono', + uiFont, + monoFont, }; }; diff --git a/packages/ui/src/lib/api/types.ts b/packages/ui/src/lib/api/types.ts index 631f4950..411ad716 100644 --- a/packages/ui/src/lib/api/types.ts +++ b/packages/ui/src/lib/api/types.ts @@ -574,6 +574,8 @@ export interface SettingsPayload { mermaidRenderingMode?: 'svg' | 'ascii'; fontSize?: number; terminalFontSize?: number; + uiFont?: string; + monoFont?: string; padding?: number; cornerRadius?: number; inputBarOffset?: number; diff --git a/packages/ui/src/lib/appearanceAutoSave.ts b/packages/ui/src/lib/appearanceAutoSave.ts index 7250c389..0099890e 100644 --- a/packages/ui/src/lib/appearanceAutoSave.ts +++ b/packages/ui/src/lib/appearanceAutoSave.ts @@ -1,6 +1,7 @@ import { useUIStore } from '@/stores/useUIStore'; import { updateDesktopSettings } from '@/lib/persistence'; import type { DesktopSettings } from '@/lib/desktop'; +import type { MonoFontOption, UiFontOption } from '@/lib/fontOptions'; type AppearanceSlice = { showReasoningTraces: boolean; @@ -26,6 +27,8 @@ type AppearanceSlice = { sessionRetentionAction: 'archive' | 'delete'; fontSize: number; terminalFontSize: number; + uiFont: UiFontOption; + monoFont: MonoFontOption; padding: number; cornerRadius: number; inputBarOffset: number; @@ -62,6 +65,8 @@ export const startAppearanceAutoSave = (): void => { sessionRetentionAction: useUIStore.getState().sessionRetentionAction, fontSize: useUIStore.getState().fontSize, terminalFontSize: useUIStore.getState().terminalFontSize, + uiFont: useUIStore.getState().uiFont, + monoFont: useUIStore.getState().monoFont, padding: useUIStore.getState().padding, cornerRadius: useUIStore.getState().cornerRadius, inputBarOffset: useUIStore.getState().inputBarOffset, @@ -110,6 +115,8 @@ export const startAppearanceAutoSave = (): void => { sessionRetentionAction: state.sessionRetentionAction, fontSize: state.fontSize, terminalFontSize: state.terminalFontSize, + uiFont: state.uiFont, + monoFont: state.monoFont, padding: state.padding, cornerRadius: state.cornerRadius, inputBarOffset: state.inputBarOffset, @@ -174,6 +181,12 @@ export const startAppearanceAutoSave = (): void => { if (current.terminalFontSize !== previous.terminalFontSize) { diff.terminalFontSize = current.terminalFontSize; } + if (current.uiFont !== previous.uiFont) { + diff.uiFont = current.uiFont; + } + if (current.monoFont !== previous.monoFont) { + diff.monoFont = current.monoFont; + } if (current.padding !== previous.padding) { diff.padding = current.padding; } diff --git a/packages/ui/src/lib/desktop.ts b/packages/ui/src/lib/desktop.ts index fa231ee4..a04f1d57 100644 --- a/packages/ui/src/lib/desktop.ts +++ b/packages/ui/src/lib/desktop.ts @@ -135,6 +135,8 @@ export type DesktopSettings = { stickyUserHeader?: boolean; fontSize?: number; terminalFontSize?: number; + uiFont?: string; + monoFont?: string; padding?: number; cornerRadius?: number; inputBarOffset?: number; diff --git a/packages/ui/src/lib/fontLoader.ts b/packages/ui/src/lib/fontLoader.ts new file mode 100644 index 00000000..9df09c25 --- /dev/null +++ b/packages/ui/src/lib/fontLoader.ts @@ -0,0 +1,59 @@ +import { CODE_FONT_OPTION_MAP, UI_FONT_OPTION_MAP, type FontFaceSource, type MonoFontOption, type UiFontOption } from '@/lib/fontOptions'; + +const loadedFaces = new Set(); +const pendingFaces = new Map>(); + +const buildFontUrl = (source: FontFaceSource, weight: number) => { + const packageName = encodeURIComponent(source.packageName).replace('%40', '@').replace('%2F', '/'); + return `https://cdn.jsdelivr.net/npm/${packageName}/files/${source.filePrefix}-latin-${weight}-normal.woff2`; +}; + +const loadFace = (source: FontFaceSource, weight: number) => { + const key = `${source.family}:${weight}`; + if (loadedFaces.has(key)) { + return Promise.resolve(); + } + + const pending = pendingFaces.get(key); + if (pending) { + return pending; + } + + if (typeof document === 'undefined' || typeof FontFace === 'undefined' || !document.fonts) { + return Promise.resolve(); + } + + const face = new FontFace(source.family, `url(${buildFontUrl(source, weight)}) format('woff2')`, { + style: 'normal', + weight: String(weight), + display: 'swap', + }); + + document.fonts.add(face); + const promise = face.load() + .then(() => { + loadedFaces.add(key); + }) + .catch((error) => { + document.fonts.delete(face); + console.warn(`Failed to load font: ${source.family} ${weight}`, error); + }) + .finally(() => { + pendingFaces.delete(key); + }); + + pendingFaces.set(key, promise); + return promise; +}; + +const loadSource = (source: FontFaceSource | undefined) => { + if (!source) { + return Promise.resolve(); + } + + return Promise.all(source.weights.map((weight) => loadFace(source, weight))).then(() => undefined); +}; + +export const loadUiFont = (font: UiFontOption) => loadSource(UI_FONT_OPTION_MAP[font]?.source); + +export const loadMonoFont = (font: MonoFontOption) => loadSource(CODE_FONT_OPTION_MAP[font]?.source); diff --git a/packages/ui/src/lib/fontOptions.ts b/packages/ui/src/lib/fontOptions.ts index ab7f97e1..42bb7651 100644 --- a/packages/ui/src/lib/fontOptions.ts +++ b/packages/ui/src/lib/fontOptions.ts @@ -1,6 +1,13 @@ -export type UiFontOption = 'ibm-plex-sans'; +export type UiFontOption = 'ibm-plex-sans' | 'inter' | 'geist-sans' | 'atkinson-hyperlegible' | 'source-sans-3' | 'roboto' | 'noto-sans' | 'dm-sans' | 'manrope' | 'system'; -export type MonoFontOption = 'ibm-plex-mono'; +export type MonoFontOption = 'ibm-plex-mono' | 'jetbrains-mono' | 'fira-code' | 'geist-mono' | 'commit-mono' | 'source-code-pro' | 'cascadia-code' | 'roboto-mono' | 'iosevka' | 'system-mono'; + +export interface FontFaceSource { + family: string; + packageName: string; + filePrefix: string; + weights: number[]; +} export interface FontOptionDefinition { id: T; @@ -8,6 +15,7 @@ export interface FontOptionDefinition { description: string; stack: string; notes?: string; + source?: FontFaceSource; } export const UI_FONT_OPTIONS: FontOptionDefinition[] = [ @@ -16,6 +24,68 @@ export const UI_FONT_OPTIONS: FontOptionDefinition[] = [ label: 'IBM Plex Sans', description: 'Humanist sans-serif for optimal readability in the interface.', stack: '"IBM Plex Sans", "SF Pro Text", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif' + }, + { + id: 'inter', + label: 'Inter', + description: 'Modern UI sans with excellent readability at small sizes.', + stack: '"Inter", "SF Pro Text", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', + source: { family: 'Inter', packageName: '@fontsource/inter', filePrefix: 'inter', weights: [400, 500, 600] } + }, + { + id: 'geist-sans', + label: 'Geist Sans', + description: 'Crisp sans-serif with a technical interface feel.', + stack: '"Geist Sans", "SF Pro Text", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', + source: { family: 'Geist Sans', packageName: '@fontsource/geist-sans', filePrefix: 'geist-sans', weights: [400, 500, 600] } + }, + { + id: 'atkinson-hyperlegible', + label: 'Atkinson Hyperlegible', + description: 'Accessibility-focused sans-serif optimized for character distinction.', + stack: '"Atkinson Hyperlegible", "SF Pro Text", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', + source: { family: 'Atkinson Hyperlegible', packageName: '@fontsource/atkinson-hyperlegible', filePrefix: 'atkinson-hyperlegible', weights: [400, 700] } + }, + { + id: 'source-sans-3', + label: 'Source Sans 3', + description: 'Adobe sans-serif tuned for clean, readable interfaces.', + stack: '"Source Sans 3", "SF Pro Text", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', + source: { family: 'Source Sans 3', packageName: '@fontsource/source-sans-3', filePrefix: 'source-sans-3', weights: [400, 500, 600] } + }, + { + id: 'roboto', + label: 'Roboto', + description: 'Familiar Material-style sans-serif with broad UI usage.', + stack: '"Roboto", "SF Pro Text", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', + source: { family: 'Roboto', packageName: '@fontsource/roboto', filePrefix: 'roboto', weights: [400, 500, 600] } + }, + { + id: 'noto-sans', + label: 'Noto Sans', + description: 'Readable sans-serif with strong international coverage.', + stack: '"Noto Sans", "SF Pro Text", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', + source: { family: 'Noto Sans', packageName: '@fontsource/noto-sans', filePrefix: 'noto-sans', weights: [400, 500, 600] } + }, + { + id: 'dm-sans', + label: 'DM Sans', + description: 'Modern product UI sans-serif with friendly proportions.', + stack: '"DM Sans", "SF Pro Text", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', + source: { family: 'DM Sans', packageName: '@fontsource/dm-sans', filePrefix: 'dm-sans', weights: [400, 500, 600] } + }, + { + id: 'manrope', + label: 'Manrope', + description: 'Polished geometric sans-serif for modern app interfaces.', + stack: '"Manrope", "SF Pro Text", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', + source: { family: 'Manrope', packageName: '@fontsource/manrope', filePrefix: 'manrope', weights: [400, 500, 600] } + }, + { + id: 'system', + label: 'System', + description: 'Native operating system interface font.', + stack: '"SF Pro Text", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif' } ]; @@ -25,6 +95,68 @@ export const CODE_FONT_OPTIONS: FontOptionDefinition[] = [ label: 'IBM Plex Mono', description: 'Balanced monospace for code blocks and technical content.', stack: '"IBM Plex Mono", "SFMono-Regular", "Menlo", monospace' + }, + { + id: 'jetbrains-mono', + label: 'JetBrains Mono', + description: 'Developer-focused monospace with strong punctuation clarity.', + stack: '"JetBrains Mono", "SFMono-Regular", "Menlo", monospace', + source: { family: 'JetBrains Mono', packageName: '@fontsource/jetbrains-mono', filePrefix: 'jetbrains-mono', weights: [400, 500, 600] } + }, + { + id: 'fira-code', + label: 'Fira Code', + description: 'Readable coding font with ligature support.', + stack: '"Fira Code", "SFMono-Regular", "Menlo", monospace', + source: { family: 'Fira Code', packageName: '@fontsource/fira-code', filePrefix: 'fira-code', weights: [400, 500, 600] } + }, + { + id: 'geist-mono', + label: 'Geist Mono', + description: 'Sharp monospace pair for Geist Sans.', + stack: '"Geist Mono", "SFMono-Regular", "Menlo", monospace', + source: { family: 'Geist Mono', packageName: '@fontsource/geist-mono', filePrefix: 'geist-mono', weights: [400, 500, 600] } + }, + { + id: 'commit-mono', + label: 'Commit Mono', + description: 'Code-oriented monospace with polished editor ergonomics.', + stack: '"Commit Mono", "SFMono-Regular", "Menlo", monospace', + source: { family: 'Commit Mono', packageName: '@fontsource/commit-mono', filePrefix: 'commit-mono', weights: [400, 500, 600] } + }, + { + id: 'source-code-pro', + label: 'Source Code Pro', + description: 'Adobe monospace designed for source code readability.', + stack: '"Source Code Pro", "SFMono-Regular", "Menlo", monospace', + source: { family: 'Source Code Pro', packageName: '@fontsource/source-code-pro', filePrefix: 'source-code-pro', weights: [400, 500, 600] } + }, + { + id: 'cascadia-code', + label: 'Cascadia Code', + description: 'Microsoft coding font popular in terminals and editors.', + stack: '"Cascadia Code", "SFMono-Regular", "Menlo", monospace', + source: { family: 'Cascadia Code', packageName: '@fontsource/cascadia-code', filePrefix: 'cascadia-code', weights: [400, 500, 600] } + }, + { + id: 'roboto-mono', + label: 'Roboto Mono', + description: 'Neutral monospace companion to Roboto.', + stack: '"Roboto Mono", "SFMono-Regular", "Menlo", monospace', + source: { family: 'Roboto Mono', packageName: '@fontsource/roboto-mono', filePrefix: 'roboto-mono', weights: [400, 500, 600] } + }, + { + id: 'iosevka', + label: 'Iosevka', + description: 'Compact monospace for dense code and terminal layouts.', + stack: '"Iosevka", "SFMono-Regular", "Menlo", monospace', + source: { family: 'Iosevka', packageName: '@fontsource/iosevka', filePrefix: 'iosevka', weights: [400, 500, 600] } + }, + { + id: 'system-mono', + label: 'System Mono', + description: 'Native operating system monospace font.', + stack: 'ui-monospace, "SFMono-Regular", "Menlo", "Cascadia Mono", "Segoe UI Mono", monospace' } ]; @@ -36,3 +168,9 @@ export const CODE_FONT_OPTION_MAP = buildFontMap(CODE_FONT_OPTIONS); export const DEFAULT_UI_FONT: UiFontOption = 'ibm-plex-sans'; export const DEFAULT_MONO_FONT: MonoFontOption = 'ibm-plex-mono'; + +export const isUiFontOption = (value: unknown): value is UiFontOption => + typeof value === 'string' && value in UI_FONT_OPTION_MAP; + +export const isMonoFontOption = (value: unknown): value is MonoFontOption => + typeof value === 'string' && value in CODE_FONT_OPTION_MAP; diff --git a/packages/ui/src/lib/i18n/messages/en.settings.ts b/packages/ui/src/lib/i18n/messages/en.settings.ts index e28e5daa..f13a4130 100644 --- a/packages/ui/src/lib/i18n/messages/en.settings.ts +++ b/packages/ui/src/lib/i18n/messages/en.settings.ts @@ -1363,9 +1363,15 @@ export const settingsDict = { 'settings.openchamber.visual.field.selectOrientationPlaceholder': 'Select orientation', 'settings.openchamber.visual.actions.resetInstallOrientationAria': 'Reset install orientation', 'settings.openchamber.visual.field.interfaceFontSize': 'Interface Font Size', + 'settings.openchamber.visual.field.interfaceFont': 'Interface Font', + 'settings.openchamber.visual.field.selectInterfaceFontAria': 'Select interface font', + 'settings.openchamber.visual.actions.resetInterfaceFontAria': 'Reset interface font', 'settings.openchamber.visual.field.fontSizePercentageAria': 'Font size percentage', 'settings.openchamber.visual.actions.resetFontSizeAria': 'Reset font size', 'settings.openchamber.visual.field.terminalFontSize': 'Terminal Font Size', + 'settings.openchamber.visual.field.codeFont': 'Code Font', + 'settings.openchamber.visual.field.selectCodeFontAria': 'Select code font', + 'settings.openchamber.visual.actions.resetCodeFontAria': 'Reset code font', 'settings.openchamber.visual.actions.resetTerminalFontSizeAria': 'Reset terminal font size', 'settings.openchamber.visual.field.spacingDensity': 'Spacing Density', 'settings.openchamber.visual.actions.resetSpacingAria': 'Reset spacing', diff --git a/packages/ui/src/lib/i18n/messages/es.settings.ts b/packages/ui/src/lib/i18n/messages/es.settings.ts index 403b98f5..f9a40944 100644 --- a/packages/ui/src/lib/i18n/messages/es.settings.ts +++ b/packages/ui/src/lib/i18n/messages/es.settings.ts @@ -1363,9 +1363,15 @@ export const settingsDict = { "settings.openchamber.visual.field.selectOrientationPlaceholder": "Seleccionar orientación", "settings.openchamber.visual.actions.resetInstallOrientationAria": "Restablecer orientación de instalación", "settings.openchamber.visual.field.interfaceFontSize": "Tamaño de fuente de la interfaz", + "settings.openchamber.visual.field.interfaceFont": "Fuente de la interfaz", + "settings.openchamber.visual.field.selectInterfaceFontAria": "Seleccionar fuente de interfaz", + "settings.openchamber.visual.actions.resetInterfaceFontAria": "Restablecer fuente de interfaz", "settings.openchamber.visual.field.fontSizePercentageAria": "Porcentaje de tamaño de fuente", "settings.openchamber.visual.actions.resetFontSizeAria": "Restablecer tamaño de fuente", "settings.openchamber.visual.field.terminalFontSize": "Tamaño de fuente del terminal", + "settings.openchamber.visual.field.codeFont": "Fuente de código", + "settings.openchamber.visual.field.selectCodeFontAria": "Seleccionar fuente de código", + "settings.openchamber.visual.actions.resetCodeFontAria": "Restablecer fuente de código", "settings.openchamber.visual.actions.resetTerminalFontSizeAria": "Restablecer tamaño de fuente del terminal", "settings.openchamber.visual.field.spacingDensity": "Densidad de espaciado", "settings.openchamber.visual.actions.resetSpacingAria": "Restablecer espaciado", diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts b/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts index 91fb58ae..7f06c474 100644 --- a/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts +++ b/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts @@ -1363,9 +1363,15 @@ export const settingsDict = { "settings.openchamber.visual.field.selectOrientationPlaceholder": "Selecionar orientação", "settings.openchamber.visual.actions.resetInstallOrientationAria": "Redefinir orientação de instalação", "settings.openchamber.visual.field.interfaceFontSize": "Tamanho base da interface", + "settings.openchamber.visual.field.interfaceFont": "Fonte da interface", + "settings.openchamber.visual.field.selectInterfaceFontAria": "Selecionar fonte da interface", + "settings.openchamber.visual.actions.resetInterfaceFontAria": "Redefinir fonte da interface", "settings.openchamber.visual.field.fontSizePercentageAria": "Porcentagem do tamanho base", "settings.openchamber.visual.actions.resetFontSizeAria": "Redefinir tamanho base", "settings.openchamber.visual.field.terminalFontSize": "Tamanho base do terminal", + "settings.openchamber.visual.field.codeFont": "Fonte do código", + "settings.openchamber.visual.field.selectCodeFontAria": "Selecionar fonte do código", + "settings.openchamber.visual.actions.resetCodeFontAria": "Redefinir fonte do código", "settings.openchamber.visual.actions.resetTerminalFontSizeAria": "Redefinir tamanho base do terminal", "settings.openchamber.visual.field.spacingDensity": "Densidade de espaçamento", "settings.openchamber.visual.actions.resetSpacingAria": "Redefinir espaçamento", diff --git a/packages/ui/src/lib/i18n/messages/uk.settings.ts b/packages/ui/src/lib/i18n/messages/uk.settings.ts index 5f657219..9a482f9f 100644 --- a/packages/ui/src/lib/i18n/messages/uk.settings.ts +++ b/packages/ui/src/lib/i18n/messages/uk.settings.ts @@ -1363,9 +1363,15 @@ export const settingsDict = { "settings.openchamber.visual.field.selectOrientationPlaceholder": "Вибрати орієнтацію", "settings.openchamber.visual.actions.resetInstallOrientationAria": "Скинути орієнтацію встановлення", "settings.openchamber.visual.field.interfaceFontSize": "Розмір шрифту інтерфейсу", + "settings.openchamber.visual.field.interfaceFont": "Шрифт інтерфейсу", + "settings.openchamber.visual.field.selectInterfaceFontAria": "Вибрати шрифт інтерфейсу", + "settings.openchamber.visual.actions.resetInterfaceFontAria": "Скинути шрифт інтерфейсу", "settings.openchamber.visual.field.fontSizePercentageAria": "Розмір шрифту у відсотках", "settings.openchamber.visual.actions.resetFontSizeAria": "Скинути розмір шрифту", "settings.openchamber.visual.field.terminalFontSize": "Розмір шрифту терміналу", + "settings.openchamber.visual.field.codeFont": "Шрифт коду", + "settings.openchamber.visual.field.selectCodeFontAria": "Вибрати шрифт коду", + "settings.openchamber.visual.actions.resetCodeFontAria": "Скинути шрифт коду", "settings.openchamber.visual.actions.resetTerminalFontSizeAria": "Скинути розмір шрифту терміналу", "settings.openchamber.visual.field.spacingDensity": "Щільність інтервалу", "settings.openchamber.visual.actions.resetSpacingAria": "Скинути інтервал", diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts b/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts index 2a26c225..f0c9fdb1 100644 --- a/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts +++ b/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts @@ -1363,9 +1363,15 @@ export const settingsDict = { 'settings.openchamber.visual.field.selectOrientationPlaceholder': '选择方向', 'settings.openchamber.visual.actions.resetInstallOrientationAria': '重置安装方向', 'settings.openchamber.visual.field.interfaceFontSize': '界面字体大小', + 'settings.openchamber.visual.field.interfaceFont': '界面字体', + 'settings.openchamber.visual.field.selectInterfaceFontAria': '选择界面字体', + 'settings.openchamber.visual.actions.resetInterfaceFontAria': '重置界面字体', 'settings.openchamber.visual.field.fontSizePercentageAria': '字体大小百分比', 'settings.openchamber.visual.actions.resetFontSizeAria': '重置字体大小', 'settings.openchamber.visual.field.terminalFontSize': '终端字体大小', + 'settings.openchamber.visual.field.codeFont': '代码字体', + 'settings.openchamber.visual.field.selectCodeFontAria': '选择代码字体', + 'settings.openchamber.visual.actions.resetCodeFontAria': '重置代码字体', 'settings.openchamber.visual.actions.resetTerminalFontSizeAria': '重置终端字体大小', 'settings.openchamber.visual.field.spacingDensity': '间距密度', 'settings.openchamber.visual.actions.resetSpacingAria': '重置间距', diff --git a/packages/ui/src/lib/persistence.ts b/packages/ui/src/lib/persistence.ts index 0e9ee7c1..3f28c0af 100644 --- a/packages/ui/src/lib/persistence.ts +++ b/packages/ui/src/lib/persistence.ts @@ -1,6 +1,7 @@ import type { DesktopSettings } from '@/lib/desktop'; import { createProjectIdFromPath } from '@/lib/projectId'; import { useUIStore } from '@/stores/useUIStore'; +import { isMonoFontOption, isUiFontOption } from '@/lib/fontOptions'; import { useMessageQueueStore } from '@/stores/messageQueueStore'; import { setDirectoryShowHidden } from '@/lib/directoryShowHidden'; import { setFilesViewShowGitignored } from '@/lib/filesViewShowGitignored'; @@ -439,6 +440,12 @@ const applyDesktopUiPreferences = (settings: DesktopSettings) => { if (typeof settings.terminalFontSize === 'number' && Number.isFinite(settings.terminalFontSize) && settings.terminalFontSize !== store.terminalFontSize) { store.setTerminalFontSize(settings.terminalFontSize); } + if (isUiFontOption(settings.uiFont) && settings.uiFont !== store.uiFont) { + store.setUiFont(settings.uiFont); + } + if (isMonoFontOption(settings.monoFont) && settings.monoFont !== store.monoFont) { + store.setMonoFont(settings.monoFont); + } if (typeof settings.padding === 'number' && Number.isFinite(settings.padding) && settings.padding !== store.padding) { store.setPadding(settings.padding); } @@ -852,6 +859,12 @@ const sanitizeWebSettings = (payload: unknown): DesktopSettings | null => { if (typeof candidate.terminalFontSize === 'number' && Number.isFinite(candidate.terminalFontSize)) { result.terminalFontSize = candidate.terminalFontSize; } + if (isUiFontOption(candidate.uiFont)) { + result.uiFont = candidate.uiFont; + } + if (isMonoFontOption(candidate.monoFont)) { + result.monoFont = candidate.monoFont; + } if (typeof candidate.padding === 'number' && Number.isFinite(candidate.padding)) { result.padding = candidate.padding; } diff --git a/packages/ui/src/stores/useUIStore.ts b/packages/ui/src/stores/useUIStore.ts index 829cae20..76f74e4d 100644 --- a/packages/ui/src/stores/useUIStore.ts +++ b/packages/ui/src/stores/useUIStore.ts @@ -4,6 +4,7 @@ import type { SidebarSection } from '@/constants/sidebar'; import { getSafeStorage } from './utils/safeStorage'; import { SEMANTIC_TYPOGRAPHY, getTypographyVariable, type SemanticTypographyKey } from '@/lib/typography'; import type { ShortcutCombo } from '@/lib/shortcuts'; +import { DEFAULT_MONO_FONT, DEFAULT_UI_FONT, type MonoFontOption, type UiFontOption } from '@/lib/fontOptions'; export type MainTab = 'chat' | 'plan' | 'git' | 'diff' | 'terminal' | 'files'; export type RightSidebarTab = 'git' | 'files' | 'context'; @@ -512,6 +513,8 @@ interface UIStore { messageLimit: number; fontSize: number; terminalFontSize: number; + uiFont: UiFontOption; + monoFont: MonoFontOption; padding: number; cornerRadius: number; inputBarOffset: number; @@ -633,6 +636,8 @@ interface UIStore { setMessageLimit: (value: number) => void; setFontSize: (size: number) => void; setTerminalFontSize: (size: number) => void; + setUiFont: (font: UiFontOption) => void; + setMonoFont: (font: MonoFontOption) => void; setPadding: (size: number) => void; setCornerRadius: (radius: number) => void; setInputBarOffset: (offset: number) => void; @@ -750,6 +755,8 @@ export const useUIStore = create()( messageLimit: 200, fontSize: 100, terminalFontSize: 13, + uiFont: DEFAULT_UI_FONT, + monoFont: DEFAULT_MONO_FONT, padding: 100, cornerRadius: 18, inputBarOffset: 0, @@ -1370,6 +1377,14 @@ export const useUIStore = create()( set({ terminalFontSize: clamped }); }, + setUiFont: (font) => { + set({ uiFont: font }); + }, + + setMonoFont: (font) => { + set({ monoFont: font }); + }, + setPadding: (size) => { // Clamp between 50% and 200% const clampedSize = Math.max(50, Math.min(200, size)); @@ -1892,6 +1907,8 @@ export const useUIStore = create()( messageLimit: state.messageLimit, fontSize: state.fontSize, terminalFontSize: state.terminalFontSize, + uiFont: state.uiFont, + monoFont: state.monoFont, padding: state.padding, cornerRadius: state.cornerRadius, favoriteModels: state.favoriteModels,