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
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<T extends string> {
|
||||
id: T;
|
||||
@@ -57,6 +59,7 @@ interface NavItem<T extends string> {
|
||||
|
||||
const appNavItems: NavItem<AppSection>[] = [
|
||||
{ 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 <ThemeSettings settings={settings} onSettingsChange={setSettings} />;
|
||||
case 'display':
|
||||
return <DisplaySettings settings={settings} onSettingsChange={setSettings} />;
|
||||
case 'agent':
|
||||
return <GeneralSettings settings={settings} onSettingsChange={setSettings} section="agent" />;
|
||||
case 'paths':
|
||||
|
||||
@@ -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 (
|
||||
<SettingsSection
|
||||
title="Display"
|
||||
description="Adjust the size of UI elements"
|
||||
>
|
||||
<div className="space-y-6">
|
||||
{/* Preset Buttons */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-medium text-foreground">Scale Presets</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Quick scale options for common preferences
|
||||
</p>
|
||||
<div className="grid grid-cols-3 gap-3 max-w-md pt-1">
|
||||
{SCALE_PRESETS.map((preset) => {
|
||||
const isSelected = currentScale === preset.value;
|
||||
return (
|
||||
<button
|
||||
key={preset.value}
|
||||
onClick={() => handleScaleChange(preset.value)}
|
||||
className={cn(
|
||||
'flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-all',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
||||
isSelected
|
||||
? 'border-primary bg-primary/5'
|
||||
: 'border-border hover:border-primary/50 hover:bg-accent/50'
|
||||
)}
|
||||
>
|
||||
<Monitor className="h-4 w-4" />
|
||||
<div className="text-center">
|
||||
<div className="text-sm font-medium">{preset.label}</div>
|
||||
<div className="text-xs text-muted-foreground">{preset.description}</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Fine-tune Slider */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-sm font-medium text-foreground">Fine-tune Scale</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-mono text-muted-foreground">
|
||||
{currentScale}%
|
||||
</span>
|
||||
{currentScale !== UI_SCALE_DEFAULT && (
|
||||
<button
|
||||
onClick={handleReset}
|
||||
className={cn(
|
||||
'p-1.5 rounded-md transition-colors',
|
||||
'hover:bg-accent text-muted-foreground hover:text-foreground',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring'
|
||||
)}
|
||||
title="Reset to default (100%)"
|
||||
>
|
||||
<RotateCcw className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Adjust from {UI_SCALE_MIN}% to {UI_SCALE_MAX}% in {UI_SCALE_STEP}% increments
|
||||
</p>
|
||||
|
||||
{/* Slider with icons */}
|
||||
<div className="flex items-center gap-3 pt-1">
|
||||
<ZoomOut className="h-4 w-4 text-muted-foreground shrink-0" />
|
||||
<input
|
||||
type="range"
|
||||
min={UI_SCALE_MIN}
|
||||
max={UI_SCALE_MAX}
|
||||
step={UI_SCALE_STEP}
|
||||
value={currentScale}
|
||||
onChange={(e) => 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'
|
||||
)}
|
||||
/>
|
||||
<ZoomIn className="h-4 w-4 text-muted-foreground shrink-0" />
|
||||
</div>
|
||||
|
||||
{/* Scale markers */}
|
||||
<div className="flex justify-between text-xs text-muted-foreground">
|
||||
<span>{UI_SCALE_MIN}%</span>
|
||||
<span>{UI_SCALE_MAX}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Preview hint */}
|
||||
<div className="rounded-lg bg-muted/50 border border-border p-4 text-sm">
|
||||
<p className="text-muted-foreground">
|
||||
Changes preview immediately. Click <strong className="text-foreground">Save Settings</strong> to persist your preferences.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
// ============================================
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user