Merge pull request #535 from stefandevo/v0.12.0rc

feat: add font customization and 8 new themes
This commit is contained in:
Shirone
2026-01-17 18:06:04 +00:00
committed by GitHub
38 changed files with 2556 additions and 1293 deletions

View File

@@ -1,8 +1,20 @@
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { Label } from '@/components/ui/label';
import { Checkbox } from '@/components/ui/checkbox';
import { Palette, Moon, Sun } from 'lucide-react';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Palette, Moon, Sun, Type } from 'lucide-react';
import { darkThemes, lightThemes, type Theme } from '@/config/theme-options';
import {
UI_SANS_FONT_OPTIONS,
UI_MONO_FONT_OPTIONS,
DEFAULT_FONT_VALUE,
} from '@/config/ui-font-options';
import { cn } from '@/lib/utils';
import { useAppStore } from '@/store/app-store';
import type { Project } from '@/lib/electron';
@@ -12,29 +24,125 @@ interface ProjectThemeSectionProps {
}
export function ProjectThemeSection({ project }: ProjectThemeSectionProps) {
const { theme: globalTheme, setProjectTheme } = useAppStore();
const [activeTab, setActiveTab] = useState<'dark' | 'light'>('dark');
const {
theme: globalTheme,
fontFamilySans: globalFontSans,
fontFamilyMono: globalFontMono,
setProjectTheme,
setProjectFontSans,
setProjectFontMono,
} = useAppStore();
// Theme state
const projectTheme = project.theme as Theme | undefined;
const hasCustomTheme = projectTheme !== undefined;
const effectiveTheme = projectTheme || globalTheme;
// Determine if current theme is light or dark
const isLightTheme = lightThemes.some((t) => t.value === effectiveTheme);
const [activeTab, setActiveTab] = useState<'dark' | 'light'>(isLightTheme ? 'light' : 'dark');
// Helper to validate fonts against available options
const isValidSansFont = (font?: string): boolean =>
!!font && UI_SANS_FONT_OPTIONS.some((opt) => opt.value === font);
const isValidMonoFont = (font?: string): boolean =>
!!font && UI_MONO_FONT_OPTIONS.some((opt) => opt.value === font);
// Helper to get initial font value with validation
const getInitialFontValue = (font: string | undefined, validator: (f?: string) => boolean) =>
font && validator(font) ? font : DEFAULT_FONT_VALUE;
// Font local state - tracks what's selected when using custom fonts
// Falls back to default if stored font is not in available options
const [fontSansLocal, setFontSansLocal] = useState<string>(() =>
getInitialFontValue(project.fontFamilySans, isValidSansFont)
);
const [fontMonoLocal, setFontMonoLocal] = useState<string>(() =>
getInitialFontValue(project.fontFamilyMono, isValidMonoFont)
);
// Sync state when project changes
useEffect(() => {
setFontSansLocal(getInitialFontValue(project.fontFamilySans, isValidSansFont));
setFontMonoLocal(getInitialFontValue(project.fontFamilyMono, isValidMonoFont));
// Also sync the active tab based on current theme
const currentIsLight = lightThemes.some((t) => t.value === (project.theme || globalTheme));
setActiveTab(currentIsLight ? 'light' : 'dark');
}, [project, globalTheme]);
// Font state - check if project has custom fonts set
const hasCustomFontSans = project.fontFamilySans !== undefined;
const hasCustomFontMono = project.fontFamilyMono !== undefined;
const themesToShow = activeTab === 'dark' ? darkThemes : lightThemes;
// Theme handlers
const handleThemeChange = (theme: Theme) => {
setProjectTheme(project.id, theme);
};
const handleUseGlobalTheme = (checked: boolean) => {
if (checked) {
// Clear project theme to use global
setProjectTheme(project.id, null);
} else {
// Set project theme to current global theme
setProjectTheme(project.id, globalTheme);
}
};
// Font handlers
const handleUseGlobalFontSans = (checked: boolean) => {
if (checked) {
// Clear project font to use global
setProjectFontSans(project.id, null);
setFontSansLocal(DEFAULT_FONT_VALUE);
} else {
// Set explicit project override - use 'default' value to indicate explicit default choice
const fontToSet = globalFontSans || DEFAULT_FONT_VALUE;
setFontSansLocal(fontToSet);
// Store the actual value (including 'default') so hasCustomFontSans stays true
setProjectFontSans(project.id, fontToSet);
}
};
const handleUseGlobalFontMono = (checked: boolean) => {
if (checked) {
// Clear project font to use global
setProjectFontMono(project.id, null);
setFontMonoLocal(DEFAULT_FONT_VALUE);
} else {
// Set explicit project override - use 'default' value to indicate explicit default choice
const fontToSet = globalFontMono || DEFAULT_FONT_VALUE;
setFontMonoLocal(fontToSet);
// Store the actual value (including 'default') so hasCustomFontMono stays true
setProjectFontMono(project.id, fontToSet);
}
};
const handleFontSansChange = (value: string) => {
setFontSansLocal(value);
// Store the actual value (including 'default') - only null clears to use global
setProjectFontSans(project.id, value);
};
const handleFontMonoChange = (value: string) => {
setFontMonoLocal(value);
// Store the actual value (including 'default') - only null clears to use global
setProjectFontMono(project.id, value);
};
// Get display label for global font
const getGlobalFontSansLabel = () => {
if (!globalFontSans) return 'Default (Geist Sans)';
const option = UI_SANS_FONT_OPTIONS.find((o) => o.value === globalFontSans);
return option?.label || globalFontSans;
};
const getGlobalFontMonoLabel = () => {
if (!globalFontMono) return 'Default (Geist Mono)';
const option = UI_MONO_FONT_OPTIONS.find((o) => o.value === globalFontMono);
return option?.label || globalFontMono;
};
return (
<div
className={cn(
@@ -49,10 +157,10 @@ export function ProjectThemeSection({ project }: ProjectThemeSectionProps) {
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
<Palette className="w-5 h-5 text-brand-500" />
</div>
<h2 className="text-lg font-semibold text-foreground tracking-tight">Theme</h2>
<h2 className="text-lg font-semibold text-foreground tracking-tight">Theme & Fonts</h2>
</div>
<p className="text-sm text-muted-foreground/80 ml-12">
Customize the theme for this project.
Customize the appearance for this project.
</p>
</div>
<div className="p-6 space-y-6">
@@ -158,6 +266,122 @@ export function ProjectThemeSection({ project }: ProjectThemeSectionProps) {
</p>
</div>
)}
{/* Fonts Section */}
<div className="space-y-4 pt-6 border-t border-border/50">
<div className="flex items-center gap-2 mb-4">
<Type className="w-4 h-4 text-muted-foreground" />
<Label className="text-foreground font-medium">Fonts</Label>
</div>
<div className="space-y-4">
{/* UI Font */}
<div className="space-y-3">
<div className="group flex items-start space-x-3 p-3 rounded-xl hover:bg-accent/30 transition-colors duration-200 -mx-3">
<Checkbox
id="use-global-font-sans"
checked={!hasCustomFontSans}
onCheckedChange={handleUseGlobalFontSans}
className="mt-1"
/>
<div className="flex-1 space-y-1.5">
<Label
htmlFor="use-global-font-sans"
className="text-foreground cursor-pointer font-medium"
>
Use Global UI Font
</Label>
{!hasCustomFontSans && (
<p className="text-xs text-muted-foreground">
Currently using:{' '}
<span className="font-medium">{getGlobalFontSansLabel()}</span>
</p>
)}
</div>
</div>
{hasCustomFontSans && (
<div className="ml-6 space-y-2">
<Label htmlFor="ui-font-select" className="text-sm">
Project UI Font
</Label>
<Select value={fontSansLocal} onValueChange={handleFontSansChange}>
<SelectTrigger id="ui-font-select" className="w-full">
<SelectValue placeholder="Default (Geist Sans)" />
</SelectTrigger>
<SelectContent>
{UI_SANS_FONT_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
<span
style={{
fontFamily:
option.value === DEFAULT_FONT_VALUE ? undefined : option.value,
}}
>
{option.label}
</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
</div>
{/* Code Font */}
<div className="space-y-3">
<div className="group flex items-start space-x-3 p-3 rounded-xl hover:bg-accent/30 transition-colors duration-200 -mx-3">
<Checkbox
id="use-global-font-mono"
checked={!hasCustomFontMono}
onCheckedChange={handleUseGlobalFontMono}
className="mt-1"
/>
<div className="flex-1 space-y-1.5">
<Label
htmlFor="use-global-font-mono"
className="text-foreground cursor-pointer font-medium"
>
Use Global Code Font
</Label>
{!hasCustomFontMono && (
<p className="text-xs text-muted-foreground">
Currently using:{' '}
<span className="font-medium">{getGlobalFontMonoLabel()}</span>
</p>
)}
</div>
</div>
{hasCustomFontMono && (
<div className="ml-6 space-y-2">
<Label htmlFor="code-font-select" className="text-sm">
Project Code Font
</Label>
<Select value={fontMonoLocal} onValueChange={handleFontMonoChange}>
<SelectTrigger id="code-font-select" className="w-full">
<SelectValue placeholder="Default (Geist Mono)" />
</SelectTrigger>
<SelectContent>
{UI_MONO_FONT_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
<span
style={{
fontFamily:
option.value === DEFAULT_FONT_VALUE ? undefined : option.value,
}}
>
{option.label}
</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
</div>
</div>
</div>
</div>
</div>
);

View File

@@ -1,8 +1,21 @@
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { Label } from '@/components/ui/label';
import { Palette, Moon, Sun } from 'lucide-react';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Palette, Moon, Sun, Type } from 'lucide-react';
import { darkThemes, lightThemes } from '@/config/theme-options';
import {
UI_SANS_FONT_OPTIONS,
UI_MONO_FONT_OPTIONS,
DEFAULT_FONT_VALUE,
} from '@/config/ui-font-options';
import { cn } from '@/lib/utils';
import { useAppStore } from '@/store/app-store';
import type { Theme } from '../shared/types';
interface AppearanceSectionProps {
@@ -11,10 +24,43 @@ interface AppearanceSectionProps {
}
export function AppearanceSection({ effectiveTheme, onThemeChange }: AppearanceSectionProps) {
const [activeTab, setActiveTab] = useState<'dark' | 'light'>('dark');
const { fontFamilySans, fontFamilyMono, setFontSans, setFontMono } = useAppStore();
// Determine if current theme is light or dark
const isLightTheme = lightThemes.some((t) => t.value === effectiveTheme);
const [activeTab, setActiveTab] = useState<'dark' | 'light'>(isLightTheme ? 'light' : 'dark');
// Sync active tab when theme changes
useEffect(() => {
const currentIsLight = lightThemes.some((t) => t.value === effectiveTheme);
setActiveTab(currentIsLight ? 'light' : 'dark');
}, [effectiveTheme]);
const themesToShow = activeTab === 'dark' ? darkThemes : lightThemes;
// Convert null to 'default' for Select component
// Also fallback to default if the stored font is not in the available options
const isValidSansFont = (font: string | null): boolean => {
if (!font) return false;
return UI_SANS_FONT_OPTIONS.some((opt) => opt.value === font);
};
const isValidMonoFont = (font: string | null): boolean => {
if (!font) return false;
return UI_MONO_FONT_OPTIONS.some((opt) => opt.value === font);
};
const fontSansValue =
fontFamilySans && isValidSansFont(fontFamilySans) ? fontFamilySans : DEFAULT_FONT_VALUE;
const fontMonoValue =
fontFamilyMono && isValidMonoFont(fontFamilyMono) ? fontFamilyMono : DEFAULT_FONT_VALUE;
const handleFontSansChange = (value: string) => {
setFontSans(value === DEFAULT_FONT_VALUE ? null : value);
};
const handleFontMonoChange = (value: string) => {
setFontMono(value === DEFAULT_FONT_VALUE ? null : value);
};
return (
<div
className={cn(
@@ -102,6 +148,77 @@ export function AppearanceSection({ effectiveTheme, onThemeChange }: AppearanceS
})}
</div>
</div>
{/* Fonts Section */}
<div className="space-y-4 pt-6 border-t border-border/50">
<div className="flex items-center gap-2 mb-4">
<Type className="w-4 h-4 text-muted-foreground" />
<Label className="text-foreground font-medium">Fonts</Label>
</div>
<p className="text-xs text-muted-foreground -mt-2 mb-4">
Set default fonts for all projects. Individual projects can override these settings.
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* UI Font Selector */}
<div className="space-y-2">
<Label htmlFor="global-ui-font-select" className="text-sm">
UI Font
</Label>
<Select value={fontSansValue} onValueChange={handleFontSansChange}>
<SelectTrigger id="global-ui-font-select" className="w-full">
<SelectValue placeholder="Default (Geist Sans)" />
</SelectTrigger>
<SelectContent>
{UI_SANS_FONT_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
<span
style={{
fontFamily:
option.value === DEFAULT_FONT_VALUE ? undefined : option.value,
}}
>
{option.label}
</span>
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
Used for headings, labels, and UI text
</p>
</div>
{/* Code Font Selector */}
<div className="space-y-2">
<Label htmlFor="global-code-font-select" className="text-sm">
Code Font
</Label>
<Select value={fontMonoValue} onValueChange={handleFontMonoChange}>
<SelectTrigger id="global-code-font-select" className="w-full">
<SelectValue placeholder="Default (Geist Mono)" />
</SelectTrigger>
<SelectContent>
{UI_MONO_FONT_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
<span
style={{
fontFamily:
option.value === DEFAULT_FONT_VALUE ? undefined : option.value,
}}
>
{option.label}
</span>
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
Used for code blocks and monospaced text
</p>
</div>
</div>
</div>
</div>
</div>
);

View File

@@ -26,6 +26,8 @@ export interface Project {
name: string;
path: string;
theme?: string;
fontFamilySans?: string;
fontFamilyMono?: string;
icon?: string;
customIconPath?: string;
}

View File

@@ -2,11 +2,19 @@ import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
import { Slider } from '@/components/ui/slider';
import { Input } from '@/components/ui/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { SquareTerminal } from 'lucide-react';
import { cn } from '@/lib/utils';
import { useAppStore } from '@/store/app-store';
import { toast } from 'sonner';
import { TERMINAL_FONT_OPTIONS } from '@/config/terminal-themes';
import { DEFAULT_FONT_VALUE } from '@/config/ui-font-options';
export function TerminalSection() {
const {
@@ -53,27 +61,32 @@ export function TerminalSection() {
{/* Font Family */}
<div className="space-y-3">
<Label className="text-foreground font-medium">Font Family</Label>
<select
value={fontFamily}
onChange={(e) => {
setTerminalFontFamily(e.target.value);
<Select
value={fontFamily || DEFAULT_FONT_VALUE}
onValueChange={(value) => {
setTerminalFontFamily(value);
toast.info('Font family changed', {
description: 'Restart terminal for changes to take effect',
});
}}
className={cn(
'w-full px-3 py-2 rounded-lg',
'bg-accent/30 border border-border/50',
'text-foreground text-sm',
'focus:outline-none focus:ring-2 focus:ring-green-500/30'
)}
>
{TERMINAL_FONT_OPTIONS.map((font) => (
<option key={font.value} value={font.value}>
{font.label}
</option>
))}
</select>
<SelectTrigger className="w-full">
<SelectValue placeholder="Default (Menlo / Monaco)" />
</SelectTrigger>
<SelectContent>
{TERMINAL_FONT_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
<span
style={{
fontFamily: option.value === DEFAULT_FONT_VALUE ? undefined : option.value,
}}
>
{option.label}
</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Default Font Size */}

View File

@@ -25,8 +25,17 @@ import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Slider } from '@/components/ui/slider';
import { Switch } from '@/components/ui/switch';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { TERMINAL_FONT_OPTIONS } from '@/config/terminal-themes';
import { DEFAULT_FONT_VALUE } from '@/config/ui-font-options';
import { toast } from 'sonner';
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
import { TerminalPanel } from './terminal-view/terminal-panel';
@@ -232,6 +241,8 @@ export function TerminalView() {
setTerminalDefaultRunScript,
setTerminalFontFamily,
setTerminalLineHeight,
setTerminalScrollbackLines,
setTerminalScreenReaderMode,
updateTerminalPanelSizes,
} = useAppStore();
@@ -1457,9 +1468,9 @@ export function TerminalView() {
<Settings className="h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-80" align="end">
<PopoverContent className="w-72" align="end">
<div className="space-y-4">
<div className="space-y-2">
<div className="space-y-1">
<h4 className="font-medium text-sm">Terminal Settings</h4>
<p className="text-xs text-muted-foreground">
Configure global terminal appearance
@@ -1469,15 +1480,15 @@ export function TerminalView() {
{/* Default Font Size */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-sm">Default Font Size</Label>
<span className="text-sm text-muted-foreground">
<Label className="text-xs font-medium">Default Font Size</Label>
<span className="text-xs text-muted-foreground">
{terminalState.defaultFontSize}px
</span>
</div>
<Slider
value={[terminalState.defaultFontSize]}
min={8}
max={24}
max={32}
step={1}
onValueChange={([value]) => setTerminalDefaultFontSize(value)}
onValueCommit={() => {
@@ -1488,37 +1499,79 @@ export function TerminalView() {
/>
</div>
{/* Default Run Script */}
<div className="space-y-2">
<Label className="text-xs font-medium">Run on New Terminal</Label>
<Input
value={terminalState.defaultRunScript}
onChange={(e) => setTerminalDefaultRunScript(e.target.value)}
placeholder="e.g., claude"
className="h-7 text-xs"
/>
<p className="text-[10px] text-muted-foreground">
Command to run when creating a new terminal
</p>
</div>
{/* Font Family */}
<div className="space-y-2">
<Label className="text-sm">Font Family</Label>
<select
value={terminalState.fontFamily}
onChange={(e) => {
setTerminalFontFamily(e.target.value);
<Label className="text-xs font-medium">Font Family</Label>
<Select
value={terminalState.fontFamily || DEFAULT_FONT_VALUE}
onValueChange={(value) => {
setTerminalFontFamily(value);
toast.info('Font family changed', {
description: 'Restart terminal for changes to take effect',
});
}}
className={cn(
'w-full px-2 py-1.5 rounded-md text-sm',
'bg-accent/50 border border-border',
'text-foreground',
'focus:outline-none focus:ring-2 focus:ring-ring'
)}
>
{TERMINAL_FONT_OPTIONS.map((font) => (
<option key={font.value} value={font.value}>
{font.label}
</option>
))}
</select>
<SelectTrigger className="w-full h-8 text-xs">
<SelectValue placeholder="Default (Menlo / Monaco)" />
</SelectTrigger>
<SelectContent>
{TERMINAL_FONT_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
<span
style={{
fontFamily:
option.value === DEFAULT_FONT_VALUE ? undefined : option.value,
}}
>
{option.label}
</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Scrollback */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-xs font-medium">Scrollback</Label>
<span className="text-xs text-muted-foreground">
{(terminalState.scrollbackLines / 1000).toFixed(0)}k lines
</span>
</div>
<Slider
value={[terminalState.scrollbackLines]}
min={1000}
max={100000}
step={1000}
onValueChange={([value]) => setTerminalScrollbackLines(value)}
onValueCommit={() => {
toast.info('Scrollback changed', {
description: 'Restart terminal for changes to take effect',
});
}}
/>
</div>
{/* Line Height */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-sm">Line Height</Label>
<span className="text-sm text-muted-foreground">
<Label className="text-xs font-medium">Line Height</Label>
<span className="text-xs text-muted-foreground">
{terminalState.lineHeight.toFixed(1)}
</span>
</div>
@@ -1536,18 +1589,21 @@ export function TerminalView() {
/>
</div>
{/* Default Run Script */}
<div className="space-y-2">
<Label className="text-sm">Default Run Script</Label>
<Input
value={terminalState.defaultRunScript}
onChange={(e) => setTerminalDefaultRunScript(e.target.value)}
placeholder="e.g., claude, npm run dev"
className="h-8 text-sm"
{/* Screen Reader */}
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label className="text-xs font-medium">Screen Reader</Label>
<p className="text-[10px] text-muted-foreground">Enable accessibility mode</p>
</div>
<Switch
checked={terminalState.screenReaderMode}
onCheckedChange={(checked) => {
setTerminalScreenReaderMode(checked);
toast.info(checked ? 'Screen reader enabled' : 'Screen reader disabled', {
description: 'Restart terminal for changes to take effect',
});
}}
/>
<p className="text-xs text-muted-foreground">
Command to run when opening new terminals
</p>
</div>
</div>
</PopoverContent>

View File

@@ -30,6 +30,13 @@ import { Slider } from '@/components/ui/slider';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { Switch } from '@/components/ui/switch';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { useDraggable, useDroppable } from '@dnd-kit/core';
import { useAppStore, DEFAULT_KEYBOARD_SHORTCUTS, type KeyboardShortcuts } from '@/store/app-store';
import { useShallow } from 'zustand/react/shallow';
@@ -38,7 +45,9 @@ import {
getTerminalTheme,
TERMINAL_FONT_OPTIONS,
DEFAULT_TERMINAL_FONT,
getTerminalFontFamily,
} from '@/config/terminal-themes';
import { DEFAULT_FONT_VALUE } from '@/config/ui-font-options';
import { toast } from 'sonner';
import { getElectronAPI } from '@/lib/electron';
import { getApiKey, getSessionToken, getServerUrlSync } from '@/lib/http-api-client';
@@ -567,7 +576,7 @@ export function TerminalPanel({
// Get settings from store (read at initialization time)
const terminalSettings = useAppStore.getState().terminalState;
const screenReaderEnabled = terminalSettings.screenReaderMode;
const terminalFontFamily = terminalSettings.fontFamily || DEFAULT_TERMINAL_FONT;
const terminalFontFamily = getTerminalFontFamily(terminalSettings.fontFamily);
const terminalScrollback = terminalSettings.scrollbackLines || 5000;
const terminalLineHeight = terminalSettings.lineHeight || 1.0;
@@ -1269,7 +1278,7 @@ export function TerminalPanel({
useEffect(() => {
if (xtermRef.current && isTerminalReady) {
xtermRef.current.options.fontFamily = fontFamily;
xtermRef.current.options.fontFamily = getTerminalFontFamily(fontFamily);
fitAddonRef.current?.fit();
}
}, [fontFamily, isTerminalReady]);
@@ -1902,22 +1911,33 @@ export function TerminalPanel({
<div className="space-y-2">
<Label className="text-xs font-medium">Font Family</Label>
<select
value={fontFamily}
onChange={(e) => {
setTerminalFontFamily(e.target.value);
<Select
value={fontFamily || DEFAULT_FONT_VALUE}
onValueChange={(value) => {
setTerminalFontFamily(value);
toast.info('Font family changed', {
description: 'Restart terminal for changes to take effect',
});
}}
className="w-full h-7 text-xs bg-background border border-input rounded-md px-2"
>
{TERMINAL_FONT_OPTIONS.map((font) => (
<option key={font.value} value={font.value}>
{font.label}
</option>
))}
</select>
<SelectTrigger className="w-full h-8 text-xs">
<SelectValue placeholder="Default (Menlo / Monaco)" />
</SelectTrigger>
<SelectContent>
{TERMINAL_FONT_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
<span
style={{
fontFamily:
option.value === DEFAULT_FONT_VALUE ? undefined : option.value,
}}
>
{option.label}
</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">