diff --git a/src/components/settings/hooks/useSettingsController.ts b/src/components/settings/hooks/useSettingsController.ts index cc57d37732..a493501293 100644 --- a/src/components/settings/hooks/useSettingsController.ts +++ b/src/components/settings/hooks/useSettingsController.ts @@ -22,6 +22,8 @@ import type { type ThemeContextValue = { isDarkMode: boolean; toggleDarkMode: () => void; + useSystemFont: boolean; + setUseSystemFont: (value: boolean) => void; }; type UseSettingsControllerArgs = { @@ -138,7 +140,12 @@ const normalizeNotificationPreferences = ( }; export function useSettingsController({ isOpen, initialTab }: UseSettingsControllerArgs) { - const { isDarkMode, toggleDarkMode } = useTheme() as ThemeContextValue; + const { + isDarkMode, + toggleDarkMode, + useSystemFont, + setUseSystemFont, + } = useTheme() as ThemeContextValue; const closeTimerRef = useRef(null); const [activeTab, setActiveTab] = useState(() => normalizeMainTab(initialTab)); @@ -381,6 +388,8 @@ export function useSettingsController({ isOpen, initialTab }: UseSettingsControl setActiveTab, isDarkMode, toggleDarkMode, + useSystemFont, + setUseSystemFont, saveStatus, projectSortOrder, setProjectSortOrder, diff --git a/src/components/settings/view/Settings.tsx b/src/components/settings/view/Settings.tsx index 37f40bc96d..0ebf25d1fd 100644 --- a/src/components/settings/view/Settings.tsx +++ b/src/components/settings/view/Settings.tsx @@ -39,6 +39,8 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set activeTab, setActiveTab, saveStatus, + useSystemFont, + setUseSystemFont, projectSortOrder, setProjectSortOrder, codeEditorSettings, @@ -163,6 +165,8 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set
{activeTab === 'appearance' && ( void; projectSortOrder: ProjectSortOrder; onProjectSortOrderChange: (value: ProjectSortOrder) => void; codeEditorSettings: CodeEditorSettingsState; @@ -18,6 +20,8 @@ type AppearanceSettingsTabProps = { }; export default function AppearanceSettingsTab({ + useSystemFont, + onUseSystemFontChange, projectSortOrder, onProjectSortOrderChange, codeEditorSettings, @@ -65,6 +69,21 @@ export default function AppearanceSettingsTab({ + + + + + + + + { // Check for saved theme preference or default to system preference const [isDarkMode, setIsDarkMode] = useState(() => { // Check localStorage first - const savedTheme = localStorage.getItem('theme'); + const savedTheme = getLocalStorageItem('theme'); if (savedTheme) { return savedTheme === 'dark'; } @@ -27,11 +29,15 @@ export const ThemeProvider = ({ children }) => { return false; }); + const [useSystemFont, setUseSystemFont] = useState( + () => getLocalStorageItem('useSystemFont') === 'true', + ); + // Update document class and localStorage when theme changes useEffect(() => { if (isDarkMode) { document.documentElement.classList.add('dark'); - localStorage.setItem('theme', 'dark'); + setLocalStorageItem('theme', 'dark'); // Update iOS status bar style and theme color for dark mode const statusBarMeta = document.querySelector('meta[name="apple-mobile-web-app-status-bar-style"]'); @@ -45,7 +51,7 @@ export const ThemeProvider = ({ children }) => { } } else { document.documentElement.classList.remove('dark'); - localStorage.setItem('theme', 'light'); + setLocalStorageItem('theme', 'light'); // Update iOS status bar style and theme color for light mode const statusBarMeta = document.querySelector('meta[name="apple-mobile-web-app-status-bar-style"]'); @@ -60,6 +66,11 @@ export const ThemeProvider = ({ children }) => { } }, [isDarkMode]); + useEffect(() => { + document.documentElement.classList.toggle('system-font', useSystemFont); + setLocalStorageItem('useSystemFont', String(useSystemFont)); + }, [useSystemFont]); + // Listen for system theme changes useEffect(() => { if (!window.matchMedia) return; @@ -67,7 +78,7 @@ export const ThemeProvider = ({ children }) => { const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); const handleChange = (e) => { // Only update if user hasn't manually set a preference - const savedTheme = localStorage.getItem('theme'); + const savedTheme = getLocalStorageItem('theme'); if (!savedTheme) { setIsDarkMode(e.matches); } @@ -77,14 +88,19 @@ export const ThemeProvider = ({ children }) => { return () => mediaQuery.removeEventListener('change', handleChange); }, []); - const toggleDarkMode = () => { + const toggleDarkMode = useCallback(() => { setIsDarkMode(prev => !prev); - }; + }, []); - const value = { - isDarkMode, - toggleDarkMode, - }; + const value = useMemo( + () => ({ + isDarkMode, + toggleDarkMode, + useSystemFont, + setUseSystemFont, + }), + [isDarkMode, toggleDarkMode, useSystemFont], + ); return ( diff --git a/src/i18n/locales/en/settings.json b/src/i18n/locales/en/settings.json index 305ce574b2..b661226023 100644 --- a/src/i18n/locales/en/settings.json +++ b/src/i18n/locales/en/settings.json @@ -157,6 +157,13 @@ "alphabetical": "Alphabetical", "recentActivity": "Recent Activity" }, + "typography": { + "title": "Typography", + "systemFont": { + "label": "Use system font", + "description": "Use your device's default font throughout the interface" + } + }, "codeEditor": { "title": "Code Editor", "theme": { diff --git a/src/index.css b/src/index.css index adbcb0dead..cbd069dccc 100644 --- a/src/index.css +++ b/src/index.css @@ -43,6 +43,8 @@ --input: 44 14% 87%; --ring: 221.2 83.2% 53.3%; --radius: 0.5rem; + --font-sans: "Encode Sans", -apple-system, "BlinkMacSystemFont", "Segoe UI", "Roboto", "Helvetica Neue", "Arial", sans-serif; + --font-serif: "Merriweather", "Georgia", "Cambria", "Times New Roman", serif; /* Nav design tokens */ --nav-glass-bg: 44 22% 96% / 0.7; @@ -73,6 +75,11 @@ --header-base-padding: 8px; --header-total-padding: calc(var(--header-safe-area-top) + var(--header-base-padding)); } + + :root.system-font { + --font-sans: -apple-system, "BlinkMacSystemFont", "Segoe UI", "Roboto", "Helvetica Neue", "Arial", sans-serif; + --font-serif: -apple-system, "BlinkMacSystemFont", "Segoe UI", "Roboto", "Helvetica Neue", "Arial", sans-serif; + } /* Fallback for older iOS versions */ @supports (padding-top: constant(safe-area-inset-top)) { @@ -129,7 +136,7 @@ body { @apply bg-background text-foreground; - font-family: "Encode Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + font-family: var(--font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; margin: 0; diff --git a/src/main.jsx b/src/main.jsx index 0c88aea637..aa03783123 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,12 +1,22 @@ import React from 'react' import ReactDOM from 'react-dom/client' + import App from './App.tsx' +import { getLocalStorageItem } from './utils/localStorage.js' import './index.css' import 'katex/dist/katex.min.css' // Initialize i18n import './i18n/config.js' +// Apply the saved font preference before React renders to prevent a font flash. +const useSystemFont = getLocalStorageItem('useSystemFont') === 'true' + +document.documentElement.classList.toggle( + 'system-font', + useSystemFont, +) + // Register service worker for PWA + Web Push support if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js').catch(err => { diff --git a/src/utils/localStorage.js b/src/utils/localStorage.js new file mode 100644 index 0000000000..8a45fe65bb --- /dev/null +++ b/src/utils/localStorage.js @@ -0,0 +1,15 @@ +export const getLocalStorageItem = (key) => { + try { + return localStorage.getItem(key); + } catch { + return null; + } +}; + +export const setLocalStorageItem = (key, value) => { + try { + localStorage.setItem(key, value); + } catch { + // Preferences remain in memory when browser storage is unavailable. + } +}; diff --git a/tailwind.config.js b/tailwind.config.js index ccb9a311b4..503382a4be 100755 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -15,8 +15,8 @@ export default { }, extend: { fontFamily: { - sans: ['"Encode Sans"', '-apple-system', 'BlinkMacSystemFont', '"Segoe UI"', 'Roboto', '"Helvetica Neue"', 'Arial', 'sans-serif'], - serif: ['Merriweather', 'Georgia', 'Cambria', '"Times New Roman"', 'serif'], + sans: ['var(--font-sans)'], + serif: ['var(--font-serif)'], }, colors: { border: "hsl(var(--border))",