feat: add per-project font override settings

Add font selectors that allow per-project font customization for both
sans and mono fonts, independent of theme selection. Uses system fonts.

- Add fontFamilySans and fontFamilyMono to ProjectSettings and Project types
- Create ui-font-options.ts config with system font options
- Add store actions: setProjectFontSans, setProjectFontMono, getEffectiveFontSans, getEffectiveFontMono
- Apply font CSS variables in root component
- Add font selector UI in project-theme-section (Project Settings → Theme)
This commit is contained in:
Stefan de Vogelaere
2026-01-16 23:36:56 +01:00
parent aa35eb3d3a
commit 1322722db2
8 changed files with 263 additions and 1027 deletions

View File

@@ -0,0 +1,54 @@
/**
* Font options for per-project font customization
*
* These are system fonts (no bundled @fontsource packages required).
* Users must have the fonts installed on their system for them to work.
*/
export interface UIFontOption {
value: string; // CSS font-family value (empty string means "use default")
label: string; // Display label for the dropdown
}
/**
* Sans/UI fonts for headings, labels, and general text
*
* Empty value means "use the theme default" (Geist Sans for all themes)
*/
export const UI_SANS_FONT_OPTIONS: readonly UIFontOption[] = [
{ value: '', label: 'Default (Geist Sans)' },
{ value: "'Inter', system-ui, sans-serif", label: 'Inter' },
{ value: "'SF Pro', system-ui, sans-serif", label: 'SF Pro' },
{ value: "'Source Sans 3', system-ui, sans-serif", label: 'Source Sans' },
{ value: "'IBM Plex Sans', system-ui, sans-serif", label: 'IBM Plex Sans' },
{ value: "'Roboto', system-ui, sans-serif", label: 'Roboto' },
{ value: 'system-ui, sans-serif', label: 'System Default' },
] as const;
/**
* Mono/code fonts for code blocks, terminals, and monospaced text
*
* Empty value means "use the theme default" (Geist Mono for all themes)
*/
export const UI_MONO_FONT_OPTIONS: readonly UIFontOption[] = [
{ value: '', label: 'Default (Geist Mono)' },
{ value: "'JetBrains Mono', monospace", label: 'JetBrains Mono' },
{ value: "'Fira Code', monospace", label: 'Fira Code' },
{ value: "'SF Mono', Menlo, Monaco, monospace", label: 'SF Mono' },
{ value: "'Source Code Pro', monospace", label: 'Source Code Pro' },
{ value: "'IBM Plex Mono', monospace", label: 'IBM Plex Mono' },
{ value: "Menlo, Monaco, 'Courier New', monospace", label: 'Menlo / Monaco' },
{ value: "'Cascadia Code', monospace", label: 'Cascadia Code' },
] as const;
/**
* Get the display label for a font value
*/
export function getFontLabel(
fontValue: string | undefined,
options: readonly UIFontOption[]
): string {
if (!fontValue) return options[0].label;
const option = options.find((o) => o.value === fontValue);
return option?.label ?? fontValue;
}