fix: use sentinel value for default font selection

Radix UI Select doesn't allow empty string values, so use 'default'
as a sentinel value instead.
This commit is contained in:
Stefan de Vogelaere
2026-01-16 23:45:05 +01:00
parent 1322722db2
commit c747baaee2
3 changed files with 48 additions and 23 deletions

View File

@@ -10,7 +10,11 @@ import {
} from '@/components/ui/select'; } from '@/components/ui/select';
import { Palette, Moon, Sun, Type } from 'lucide-react'; import { Palette, Moon, Sun, Type } from 'lucide-react';
import { darkThemes, lightThemes, type Theme } from '@/config/theme-options'; import { darkThemes, lightThemes, type Theme } from '@/config/theme-options';
import { UI_SANS_FONT_OPTIONS, UI_MONO_FONT_OPTIONS } from '@/config/ui-font-options'; import {
UI_SANS_FONT_OPTIONS,
UI_MONO_FONT_OPTIONS,
DEFAULT_FONT_VALUE,
} from '@/config/ui-font-options';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useAppStore } from '@/store/app-store'; import { useAppStore } from '@/store/app-store';
import type { Project } from '@/lib/electron'; import type { Project } from '@/lib/electron';
@@ -27,13 +31,17 @@ export function ProjectThemeSection({ project }: ProjectThemeSectionProps) {
setProjectFontMono, setProjectFontMono,
} = useAppStore(); } = useAppStore();
const [activeTab, setActiveTab] = useState<'dark' | 'light'>('dark'); const [activeTab, setActiveTab] = useState<'dark' | 'light'>('dark');
const [fontSans, setFontSansLocal] = useState<string>(project.fontFamilySans || ''); const [fontSans, setFontSansLocal] = useState<string>(
const [fontMono, setFontMonoLocal] = useState<string>(project.fontFamilyMono || ''); project.fontFamilySans || DEFAULT_FONT_VALUE
);
const [fontMono, setFontMonoLocal] = useState<string>(
project.fontFamilyMono || DEFAULT_FONT_VALUE
);
// Sync font state when project changes // Sync font state when project changes
useEffect(() => { useEffect(() => {
setFontSansLocal(project.fontFamilySans || ''); setFontSansLocal(project.fontFamilySans || DEFAULT_FONT_VALUE);
setFontMonoLocal(project.fontFamilyMono || ''); setFontMonoLocal(project.fontFamilyMono || DEFAULT_FONT_VALUE);
}, [project]); }, [project]);
const projectTheme = project.theme as Theme | undefined; const projectTheme = project.theme as Theme | undefined;
@@ -58,14 +66,14 @@ export function ProjectThemeSection({ project }: ProjectThemeSectionProps) {
const handleFontSansChange = (value: string) => { const handleFontSansChange = (value: string) => {
setFontSansLocal(value); setFontSansLocal(value);
// Empty string means default, so we pass null to clear the override // 'default' means use theme default, so we pass null to clear the override
setProjectFontSans(project.id, value || null); setProjectFontSans(project.id, value === DEFAULT_FONT_VALUE ? null : value);
}; };
const handleFontMonoChange = (value: string) => { const handleFontMonoChange = (value: string) => {
setFontMonoLocal(value); setFontMonoLocal(value);
// Empty string means default, so we pass null to clear the override // 'default' means use theme default, so we pass null to clear the override
setProjectFontMono(project.id, value || null); setProjectFontMono(project.id, value === DEFAULT_FONT_VALUE ? null : value);
}; };
return ( return (
@@ -214,8 +222,15 @@ export function ProjectThemeSection({ project }: ProjectThemeSectionProps) {
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{UI_SANS_FONT_OPTIONS.map((option) => ( {UI_SANS_FONT_OPTIONS.map((option) => (
<SelectItem key={option.value || 'default-sans'} value={option.value}> <SelectItem key={option.value} value={option.value}>
<span style={{ fontFamily: option.value || undefined }}>{option.label}</span> <span
style={{
fontFamily:
option.value === DEFAULT_FONT_VALUE ? undefined : option.value,
}}
>
{option.label}
</span>
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
@@ -236,8 +251,15 @@ export function ProjectThemeSection({ project }: ProjectThemeSectionProps) {
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{UI_MONO_FONT_OPTIONS.map((option) => ( {UI_MONO_FONT_OPTIONS.map((option) => (
<SelectItem key={option.value || 'default-mono'} value={option.value}> <SelectItem key={option.value} value={option.value}>
<span style={{ fontFamily: option.value || undefined }}>{option.label}</span> <span
style={{
fontFamily:
option.value === DEFAULT_FONT_VALUE ? undefined : option.value,
}}
>
{option.label}
</span>
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>

View File

@@ -5,18 +5,21 @@
* Users must have the fonts installed on their system for them to work. * Users must have the fonts installed on their system for them to work.
*/ */
// Sentinel value for "use default font" - Radix Select doesn't allow empty strings
export const DEFAULT_FONT_VALUE = 'default';
export interface UIFontOption { export interface UIFontOption {
value: string; // CSS font-family value (empty string means "use default") value: string; // CSS font-family value ('default' means "use default")
label: string; // Display label for the dropdown label: string; // Display label for the dropdown
} }
/** /**
* Sans/UI fonts for headings, labels, and general text * Sans/UI fonts for headings, labels, and general text
* *
* Empty value means "use the theme default" (Geist Sans for all themes) * 'default' value means "use the theme default" (Geist Sans for all themes)
*/ */
export const UI_SANS_FONT_OPTIONS: readonly UIFontOption[] = [ export const UI_SANS_FONT_OPTIONS: readonly UIFontOption[] = [
{ value: '', label: 'Default (Geist Sans)' }, { value: DEFAULT_FONT_VALUE, label: 'Default (Geist Sans)' },
{ value: "'Inter', system-ui, sans-serif", label: 'Inter' }, { value: "'Inter', system-ui, sans-serif", label: 'Inter' },
{ value: "'SF Pro', system-ui, sans-serif", label: 'SF Pro' }, { value: "'SF Pro', system-ui, sans-serif", label: 'SF Pro' },
{ value: "'Source Sans 3', system-ui, sans-serif", label: 'Source Sans' }, { value: "'Source Sans 3', system-ui, sans-serif", label: 'Source Sans' },
@@ -28,10 +31,10 @@ export const UI_SANS_FONT_OPTIONS: readonly UIFontOption[] = [
/** /**
* Mono/code fonts for code blocks, terminals, and monospaced text * Mono/code fonts for code blocks, terminals, and monospaced text
* *
* Empty value means "use the theme default" (Geist Mono for all themes) * 'default' value means "use the theme default" (Geist Mono for all themes)
*/ */
export const UI_MONO_FONT_OPTIONS: readonly UIFontOption[] = [ export const UI_MONO_FONT_OPTIONS: readonly UIFontOption[] = [
{ value: '', label: 'Default (Geist Mono)' }, { value: DEFAULT_FONT_VALUE, label: 'Default (Geist Mono)' },
{ value: "'JetBrains Mono', monospace", label: 'JetBrains Mono' }, { value: "'JetBrains Mono', monospace", label: 'JetBrains Mono' },
{ value: "'Fira Code', monospace", label: 'Fira Code' }, { value: "'Fira Code', monospace", label: 'Fira Code' },
{ value: "'SF Mono', Menlo, Monaco, monospace", label: 'SF Mono' }, { value: "'SF Mono', Menlo, Monaco, monospace", label: 'SF Mono' },
@@ -48,7 +51,7 @@ export function getFontLabel(
fontValue: string | undefined, fontValue: string | undefined,
options: readonly UIFontOption[] options: readonly UIFontOption[]
): string { ): string {
if (!fontValue) return options[0].label; if (!fontValue || fontValue === DEFAULT_FONT_VALUE) return options[0].label;
const option = options.find((o) => o.value === fontValue); const option = options.find((o) => o.value === fontValue);
return option?.label ?? fontValue; return option?.label ?? fontValue;
} }

8
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "automaker", "name": "automaker",
"version": "1.0.0", "version": "0.12.0rc",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "automaker", "name": "automaker",
"version": "1.0.0", "version": "0.12.0rc",
"hasInstallScript": true, "hasInstallScript": true,
"workspaces": [ "workspaces": [
"apps/*", "apps/*",
@@ -29,7 +29,7 @@
}, },
"apps/server": { "apps/server": {
"name": "@automaker/server", "name": "@automaker/server",
"version": "0.11.0", "version": "0.12.0",
"license": "SEE LICENSE IN LICENSE", "license": "SEE LICENSE IN LICENSE",
"dependencies": { "dependencies": {
"@anthropic-ai/claude-agent-sdk": "0.1.76", "@anthropic-ai/claude-agent-sdk": "0.1.76",
@@ -80,7 +80,7 @@
}, },
"apps/ui": { "apps/ui": {
"name": "@automaker/ui", "name": "@automaker/ui",
"version": "0.11.0", "version": "0.12.0",
"hasInstallScript": true, "hasInstallScript": true,
"license": "SEE LICENSE IN LICENSE", "license": "SEE LICENSE IN LICENSE",
"dependencies": { "dependencies": {