From 6951251b3308bd94e1c604280b9ea4c3d260883b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enes=20Cing=C3=B6z?= <60349121+enescingoz@users.noreply.github.com> Date: Tue, 23 Dec 2025 11:30:32 +0000 Subject: [PATCH] feat: Add UI scale feature with 75-200% range (#125) * feat: add UI scale feature * refactor: extract UI scale bounds to shared constants * fix: duplicated import --- apps/frontend/package.json | 2 +- apps/frontend/src/renderer/App.tsx | 10 +- .../components/settings/AppSettings.tsx | 9 +- .../components/settings/DisplaySettings.tsx | 163 ++++++++++++++++++ .../components/settings/hooks/useSettings.ts | 25 ++- apps/frontend/src/renderer/styles/globals.css | 38 ++++ apps/frontend/src/shared/constants/config.ts | 13 +- apps/frontend/src/shared/types/settings.ts | 2 + 8 files changed, 249 insertions(+), 13 deletions(-) create mode 100644 apps/frontend/src/renderer/components/settings/DisplaySettings.tsx diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 9c0d22c1..77eb83d0 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -98,7 +98,7 @@ "@types/uuid": "^10.0.0", "@vitejs/plugin-react": "^5.1.2", "autoprefixer": "^10.4.22", - "electron": "^39.2.6", + "electron": "^39.2.7", "electron-builder": "^26.0.12", "electron-vite": "^5.0.0", "eslint": "^9.39.1", diff --git a/apps/frontend/src/renderer/App.tsx b/apps/frontend/src/renderer/App.tsx index 60968df9..1201ab75 100644 --- a/apps/frontend/src/renderer/App.tsx +++ b/apps/frontend/src/renderer/App.tsx @@ -55,7 +55,7 @@ import { useTaskStore, loadTasks } from './stores/task-store'; import { useSettingsStore, loadSettings } from './stores/settings-store'; import { useTerminalStore, restoreTerminalSessions } from './stores/terminal-store'; import { useIpcListeners } from './hooks/useIpc'; -import { COLOR_THEMES } from '../shared/constants'; +import { COLOR_THEMES, UI_SCALE_MIN, UI_SCALE_MAX, UI_SCALE_DEFAULT } from '../shared/constants'; import type { Task, Project, ColorTheme } from '../shared/types'; import { ProjectTabBar } from './components/ProjectTabBar'; @@ -357,6 +357,14 @@ export function App() { }; }, [settings.theme, settings.colorTheme]); + // Apply UI scale + useEffect(() => { + const root = document.documentElement; + const scale = settings.uiScale ?? UI_SCALE_DEFAULT; + const clampedScale = Math.max(UI_SCALE_MIN, Math.min(UI_SCALE_MAX, scale)); + root.setAttribute('data-ui-scale', clampedScale.toString()); + }, [settings.uiScale]); + // Update selected task when tasks change (for real-time updates) useEffect(() => { if (selectedTask) { diff --git a/apps/frontend/src/renderer/components/settings/AppSettings.tsx b/apps/frontend/src/renderer/components/settings/AppSettings.tsx index 8c9c2832..ab778396 100644 --- a/apps/frontend/src/renderer/components/settings/AppSettings.tsx +++ b/apps/frontend/src/renderer/components/settings/AppSettings.tsx @@ -13,7 +13,8 @@ import { Zap, Github, Database, - Sparkles + Sparkles, + Monitor } from 'lucide-react'; import { FullScreenDialog, @@ -29,6 +30,7 @@ import { ScrollArea } from '../ui/scroll-area'; import { cn } from '../../lib/utils'; import { useSettings } from './hooks/useSettings'; import { ThemeSettings } from './ThemeSettings'; +import { DisplaySettings } from './DisplaySettings'; import { GeneralSettings } from './GeneralSettings'; import { IntegrationSettings } from './IntegrationSettings'; import { AdvancedSettings } from './AdvancedSettings'; @@ -46,7 +48,7 @@ interface AppSettingsDialogProps { } // App-level settings sections -export type AppSection = 'appearance' | 'agent' | 'paths' | 'integrations' | 'updates' | 'notifications'; +export type AppSection = 'appearance' | 'display' | 'agent' | 'paths' | 'integrations' | 'updates' | 'notifications'; interface NavItem { id: T; @@ -57,6 +59,7 @@ interface NavItem { const appNavItems: NavItem[] = [ { id: 'appearance', label: 'Appearance', icon: Palette, description: 'Theme and visual preferences' }, + { id: 'display', label: 'Display', icon: Monitor, description: 'UI scale and zoom' }, { id: 'agent', label: 'Agent Settings', icon: Bot, description: 'Default model and framework' }, { id: 'paths', label: 'Paths', icon: FolderOpen, description: 'Python and framework paths' }, { id: 'integrations', label: 'Integrations', icon: Key, description: 'API keys & Claude accounts' }, @@ -157,6 +160,8 @@ export function AppSettingsDialog({ open, onOpenChange, initialSection, initialP switch (appSection) { case 'appearance': return ; + case 'display': + return ; case 'agent': return ; case 'paths': diff --git a/apps/frontend/src/renderer/components/settings/DisplaySettings.tsx b/apps/frontend/src/renderer/components/settings/DisplaySettings.tsx new file mode 100644 index 00000000..0d626371 --- /dev/null +++ b/apps/frontend/src/renderer/components/settings/DisplaySettings.tsx @@ -0,0 +1,163 @@ +import { Monitor, ZoomIn, ZoomOut, RotateCcw } from 'lucide-react'; +import { cn } from '../../lib/utils'; +import { Label } from '../ui/label'; +import { SettingsSection } from './SettingsSection'; +import { useSettingsStore } from '../../stores/settings-store'; +import { UI_SCALE_MIN, UI_SCALE_MAX, UI_SCALE_DEFAULT, UI_SCALE_STEP } from '../../../shared/constants'; +import type { AppSettings } from '../../../shared/types'; + +interface DisplaySettingsProps { + settings: AppSettings; + onSettingsChange: (settings: AppSettings) => void; +} + +// Preset scale values (100%, 125%, 150%) +const SCALE_PRESETS = [ + { value: UI_SCALE_DEFAULT, label: '100%', description: 'Default' }, + { value: 125, label: '125%', description: 'Comfortable' }, + { value: 150, label: '150%', description: 'Large' } +] as const; + +/** + * Display settings section for UI scale/zoom control + * Provides preset buttons (100%, 125%, 150%) and a fine-tune slider (75-200%) + * Changes apply immediately for live preview (like theme), saved on "Save Settings" + */ +export function DisplaySettings({ settings, onSettingsChange }: DisplaySettingsProps) { + const updateStoreSettings = useSettingsStore((state) => state.updateSettings); + + const currentScale = settings.uiScale ?? UI_SCALE_DEFAULT; + + const handleScaleChange = (newScale: number) => { + // Clamp to valid range + const clampedScale = Math.max(UI_SCALE_MIN, Math.min(UI_SCALE_MAX, newScale)); + + // Update local draft state + onSettingsChange({ ...settings, uiScale: clampedScale }); + + // Apply immediately to store for live preview (triggers App.tsx useEffect) + updateStoreSettings({ uiScale: clampedScale }); + }; + + const handleReset = () => { + handleScaleChange(UI_SCALE_DEFAULT); + }; + + return ( + +
+ {/* Preset Buttons */} +
+ +

+ Quick scale options for common preferences +

+
+ {SCALE_PRESETS.map((preset) => { + const isSelected = currentScale === preset.value; + return ( + + ); + })} +
+
+ + {/* Fine-tune Slider */} +
+
+ +
+ + {currentScale}% + + {currentScale !== UI_SCALE_DEFAULT && ( + + )} +
+
+

+ Adjust from {UI_SCALE_MIN}% to {UI_SCALE_MAX}% in {UI_SCALE_STEP}% increments +

+ + {/* Slider with icons */} +
+ + handleScaleChange(parseInt(e.target.value, 10))} + className={cn( + 'flex-1 h-2 bg-muted rounded-lg appearance-none cursor-pointer', + 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2', + // Webkit (Chrome, Safari, Edge) + '[&::-webkit-slider-thumb]:appearance-none', + '[&::-webkit-slider-thumb]:w-4', + '[&::-webkit-slider-thumb]:h-4', + '[&::-webkit-slider-thumb]:rounded-full', + '[&::-webkit-slider-thumb]:bg-primary', + '[&::-webkit-slider-thumb]:cursor-pointer', + '[&::-webkit-slider-thumb]:transition-all', + '[&::-webkit-slider-thumb]:hover:scale-110', + // Firefox + '[&::-moz-range-thumb]:w-4', + '[&::-moz-range-thumb]:h-4', + '[&::-moz-range-thumb]:rounded-full', + '[&::-moz-range-thumb]:bg-primary', + '[&::-moz-range-thumb]:border-0', + '[&::-moz-range-thumb]:cursor-pointer', + '[&::-moz-range-thumb]:transition-all', + '[&::-moz-range-thumb]:hover:scale-110' + )} + /> + +
+ + {/* Scale markers */} +
+ {UI_SCALE_MIN}% + {UI_SCALE_MAX}% +
+
+ + {/* Preview hint */} +
+

+ Changes preview immediately. Click Save Settings to persist your preferences. +

+
+
+
+ ); +} diff --git a/apps/frontend/src/renderer/components/settings/hooks/useSettings.ts b/apps/frontend/src/renderer/components/settings/hooks/useSettings.ts index d9419e7d..24f65352 100644 --- a/apps/frontend/src/renderer/components/settings/hooks/useSettings.ts +++ b/apps/frontend/src/renderer/components/settings/hooks/useSettings.ts @@ -1,13 +1,14 @@ import { useState, useEffect, useRef, useCallback } from 'react'; import { useSettingsStore, saveSettings as saveSettingsToStore, loadSettings as loadSettingsFromStore } from '../../../stores/settings-store'; import type { AppSettings } from '../../../../shared/types'; +import { UI_SCALE_DEFAULT } from '../../../../shared/constants'; /** * Custom hook for managing application settings * Provides state management and save/load functionality * - * Theme changes are applied immediately for live preview. If the user cancels - * without saving, call revertTheme() to restore the original theme. + * Theme and UI scale changes are applied immediately for live preview. If the user + * cancels without saving, call revertTheme() to restore the original values. */ export function useSettings() { const currentSettings = useSettingsStore((state) => state.settings); @@ -18,9 +19,14 @@ export function useSettings() { // Store the original theme settings when the hook mounts (dialog opens) // This allows us to revert if the user cancels - const originalThemeRef = useRef<{ theme: AppSettings['theme']; colorTheme: AppSettings['colorTheme'] }>({ + const originalThemeRef = useRef<{ + theme: AppSettings['theme']; + colorTheme: AppSettings['colorTheme']; + uiScale: number; + }>({ theme: currentSettings.theme, - colorTheme: currentSettings.colorTheme + colorTheme: currentSettings.colorTheme, + uiScale: currentSettings.uiScale ?? UI_SCALE_DEFAULT }); // Sync with store @@ -34,7 +40,8 @@ export function useSettings() { // Update the original theme ref when settings load originalThemeRef.current = { theme: currentSettings.theme, - colorTheme: currentSettings.colorTheme + colorTheme: currentSettings.colorTheme, + uiScale: currentSettings.uiScale ?? UI_SCALE_DEFAULT }; }, []); @@ -87,7 +94,8 @@ export function useSettings() { const original = originalThemeRef.current; updateStoreSettings({ theme: original.theme, - colorTheme: original.colorTheme + colorTheme: original.colorTheme, + uiScale: original.uiScale }); }, [updateStoreSettings]); @@ -98,9 +106,10 @@ export function useSettings() { const commitTheme = useCallback(() => { originalThemeRef.current = { theme: settings.theme, - colorTheme: settings.colorTheme + colorTheme: settings.colorTheme, + uiScale: settings.uiScale ?? UI_SCALE_DEFAULT }; - }, [settings.theme, settings.colorTheme]); + }, [settings.theme, settings.colorTheme, settings.uiScale]); return { settings, diff --git a/apps/frontend/src/renderer/styles/globals.css b/apps/frontend/src/renderer/styles/globals.css index 4f709139..dc208009 100644 --- a/apps/frontend/src/renderer/styles/globals.css +++ b/apps/frontend/src/renderer/styles/globals.css @@ -1651,3 +1651,41 @@ body { [data-state="closed"].animate-out.zoom-out-98 { animation: zoom-out-98 0.2s ease-in; } + +/* ============================================ + UI Scale System (75% - 200%) + ============================================ */ + +/* Explicit base font size */ +:root { + font-size: 16px; + transition: font-size 0.2s cubic-bezier(0.4, 0, 0.2, 1); +} + +/* Scale levels via data-ui-scale attribute */ +[data-ui-scale="75"] { font-size: 12px; } +[data-ui-scale="80"] { font-size: 12.8px; } +[data-ui-scale="85"] { font-size: 13.6px; } +[data-ui-scale="90"] { font-size: 14.4px; } +[data-ui-scale="95"] { font-size: 15.2px; } +[data-ui-scale="100"] { font-size: 16px; } +[data-ui-scale="105"] { font-size: 16.8px; } +[data-ui-scale="110"] { font-size: 17.6px; } +[data-ui-scale="115"] { font-size: 18.4px; } +[data-ui-scale="120"] { font-size: 19.2px; } +[data-ui-scale="125"] { font-size: 20px; } +[data-ui-scale="130"] { font-size: 20.8px; } +[data-ui-scale="135"] { font-size: 21.6px; } +[data-ui-scale="140"] { font-size: 22.4px; } +[data-ui-scale="145"] { font-size: 23.2px; } +[data-ui-scale="150"] { font-size: 24px; } +[data-ui-scale="155"] { font-size: 24.8px; } +[data-ui-scale="160"] { font-size: 25.6px; } +[data-ui-scale="165"] { font-size: 26.4px; } +[data-ui-scale="170"] { font-size: 27.2px; } +[data-ui-scale="175"] { font-size: 28px; } +[data-ui-scale="180"] { font-size: 28.8px; } +[data-ui-scale="185"] { font-size: 29.6px; } +[data-ui-scale="190"] { font-size: 30.4px; } +[data-ui-scale="195"] { font-size: 31.2px; } +[data-ui-scale="200"] { font-size: 32px; } diff --git a/apps/frontend/src/shared/constants/config.ts b/apps/frontend/src/shared/constants/config.ts index 48a06596..9db58298 100644 --- a/apps/frontend/src/shared/constants/config.ts +++ b/apps/frontend/src/shared/constants/config.ts @@ -3,6 +3,15 @@ * Default settings, file paths, and project structure */ +// ============================================ +// UI Scale Constants +// ============================================ + +export const UI_SCALE_MIN = 75; +export const UI_SCALE_MAX = 200; +export const UI_SCALE_DEFAULT = 100; +export const UI_SCALE_STEP = 5; + // ============================================ // Default App Settings // ============================================ @@ -31,7 +40,9 @@ export const DEFAULT_APP_SETTINGS = { // Changelog preferences (persisted between sessions) changelogFormat: 'keep-a-changelog' as const, changelogAudience: 'user-facing' as const, - changelogEmojiLevel: 'none' as const + changelogEmojiLevel: 'none' as const, + // UI Scale (default 100% - standard size) + uiScale: UI_SCALE_DEFAULT }; // ============================================ diff --git a/apps/frontend/src/shared/types/settings.ts b/apps/frontend/src/shared/types/settings.ts index e60d80b2..af291093 100644 --- a/apps/frontend/src/shared/types/settings.ts +++ b/apps/frontend/src/shared/types/settings.ts @@ -106,6 +106,8 @@ export interface AppSettings { changelogFormat?: ChangelogFormat; changelogAudience?: ChangelogAudience; changelogEmojiLevel?: ChangelogEmojiLevel; + // UI Scale setting (75-200%, default 100) + uiScale?: number; // Migration flags (internal use) _migratedAgentProfileToAuto?: boolean; }