mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
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:
@@ -1,8 +1,16 @@
|
||||
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 } from '@/config/ui-font-options';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import type { Project } from '@/lib/electron';
|
||||
@@ -12,8 +20,21 @@ interface ProjectThemeSectionProps {
|
||||
}
|
||||
|
||||
export function ProjectThemeSection({ project }: ProjectThemeSectionProps) {
|
||||
const { theme: globalTheme, setProjectTheme } = useAppStore();
|
||||
const {
|
||||
theme: globalTheme,
|
||||
setProjectTheme,
|
||||
setProjectFontSans,
|
||||
setProjectFontMono,
|
||||
} = useAppStore();
|
||||
const [activeTab, setActiveTab] = useState<'dark' | 'light'>('dark');
|
||||
const [fontSans, setFontSansLocal] = useState<string>(project.fontFamilySans || '');
|
||||
const [fontMono, setFontMonoLocal] = useState<string>(project.fontFamilyMono || '');
|
||||
|
||||
// Sync font state when project changes
|
||||
useEffect(() => {
|
||||
setFontSansLocal(project.fontFamilySans || '');
|
||||
setFontMonoLocal(project.fontFamilyMono || '');
|
||||
}, [project]);
|
||||
|
||||
const projectTheme = project.theme as Theme | undefined;
|
||||
const hasCustomTheme = projectTheme !== undefined;
|
||||
@@ -35,6 +56,18 @@ export function ProjectThemeSection({ project }: ProjectThemeSectionProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleFontSansChange = (value: string) => {
|
||||
setFontSansLocal(value);
|
||||
// Empty string means default, so we pass null to clear the override
|
||||
setProjectFontSans(project.id, value || null);
|
||||
};
|
||||
|
||||
const handleFontMonoChange = (value: string) => {
|
||||
setFontMonoLocal(value);
|
||||
// Empty string means default, so we pass null to clear the override
|
||||
setProjectFontMono(project.id, value || null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -158,6 +191,63 @@ 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>
|
||||
<p className="text-xs text-muted-foreground -mt-2 mb-4">
|
||||
Override the default fonts for this project. Fonts must be installed on your system.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* UI Font Selector */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="ui-font-select" className="text-sm">
|
||||
UI Font
|
||||
</Label>
|
||||
<Select value={fontSans} 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 || 'default-sans'} value={option.value}>
|
||||
<span style={{ fontFamily: option.value || undefined }}>{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="code-font-select" className="text-sm">
|
||||
Code Font
|
||||
</Label>
|
||||
<Select value={fontMono} 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 || 'default-mono'} value={option.value}>
|
||||
<span style={{ fontFamily: option.value || undefined }}>{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>
|
||||
);
|
||||
|
||||
@@ -26,6 +26,8 @@ export interface Project {
|
||||
name: string;
|
||||
path: string;
|
||||
theme?: string;
|
||||
fontFamilySans?: string;
|
||||
fontFamilyMono?: string;
|
||||
icon?: string;
|
||||
customIconPath?: string;
|
||||
}
|
||||
|
||||
54
apps/ui/src/config/ui-font-options.ts
Normal file
54
apps/ui/src/config/ui-font-options.ts
Normal 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;
|
||||
}
|
||||
@@ -3287,6 +3287,8 @@ export interface Project {
|
||||
path: string;
|
||||
lastOpened?: string;
|
||||
theme?: string; // Per-project theme override (uses ThemeMode from app-store)
|
||||
fontFamilySans?: string; // Per-project UI/sans font override
|
||||
fontFamilyMono?: string; // Per-project code/mono font override
|
||||
isFavorite?: boolean; // Pin project to top of dashboard
|
||||
icon?: string; // Lucide icon name for project identification
|
||||
customIconPath?: string; // Path to custom uploaded icon image in .automaker/images/
|
||||
|
||||
@@ -158,6 +158,8 @@ function RootLayoutContent() {
|
||||
projectHistory,
|
||||
upsertAndSetCurrentProject,
|
||||
getEffectiveTheme,
|
||||
getEffectiveFontSans,
|
||||
getEffectiveFontMono,
|
||||
skipSandboxWarning,
|
||||
setSkipSandboxWarning,
|
||||
fetchCodexModels,
|
||||
@@ -248,6 +250,10 @@ function RootLayoutContent() {
|
||||
// Defer the theme value to keep UI responsive during rapid hover changes
|
||||
const deferredTheme = useDeferredValue(effectiveTheme);
|
||||
|
||||
// Get effective fonts for the current project
|
||||
const effectiveFontSans = getEffectiveFontSans();
|
||||
const effectiveFontMono = getEffectiveFontMono();
|
||||
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
}, []);
|
||||
@@ -727,6 +733,23 @@ function RootLayoutContent() {
|
||||
}
|
||||
}, [deferredTheme]);
|
||||
|
||||
// Apply font CSS variables for project-specific font overrides
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
|
||||
if (effectiveFontSans) {
|
||||
root.style.setProperty('--font-sans', effectiveFontSans);
|
||||
} else {
|
||||
root.style.removeProperty('--font-sans');
|
||||
}
|
||||
|
||||
if (effectiveFontMono) {
|
||||
root.style.setProperty('--font-mono', effectiveFontMono);
|
||||
} else {
|
||||
root.style.removeProperty('--font-mono');
|
||||
}
|
||||
}, [effectiveFontSans, effectiveFontMono]);
|
||||
|
||||
// Show sandbox rejection screen if user denied the risk warning
|
||||
if (sandboxStatus === 'denied') {
|
||||
return <SandboxRejectionScreen />;
|
||||
|
||||
@@ -920,6 +920,12 @@ export interface AppActions {
|
||||
getEffectiveTheme: () => ThemeMode; // Get the effective theme (project, global, or preview if set)
|
||||
setPreviewTheme: (theme: ThemeMode | null) => void; // Set preview theme for hover preview (null to clear)
|
||||
|
||||
// Font actions (per-project)
|
||||
setProjectFontSans: (projectId: string, fontFamily: string | null) => void; // Set per-project UI/sans font (null to clear)
|
||||
setProjectFontMono: (projectId: string, fontFamily: string | null) => void; // Set per-project code/mono font (null to clear)
|
||||
getEffectiveFontSans: () => string | null; // Get effective UI font (project override or null for default)
|
||||
getEffectiveFontMono: () => string | null; // Get effective code font (project override or null for default)
|
||||
|
||||
// Feature actions
|
||||
setFeatures: (features: Feature[]) => void;
|
||||
updateFeature: (id: string, updates: Partial<Feature>) => void;
|
||||
@@ -1733,6 +1739,67 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
|
||||
|
||||
setPreviewTheme: (theme) => set({ previewTheme: theme }),
|
||||
|
||||
// Font actions (per-project)
|
||||
setProjectFontSans: (projectId, fontFamily) => {
|
||||
// Update the project's fontFamilySans property
|
||||
const projects = get().projects.map((p) =>
|
||||
p.id === projectId
|
||||
? { ...p, fontFamilySans: fontFamily === null ? undefined : fontFamily }
|
||||
: p
|
||||
);
|
||||
set({ projects });
|
||||
|
||||
// Also update currentProject if it's the same project
|
||||
const currentProject = get().currentProject;
|
||||
if (currentProject?.id === projectId) {
|
||||
set({
|
||||
currentProject: {
|
||||
...currentProject,
|
||||
fontFamilySans: fontFamily === null ? undefined : fontFamily,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
setProjectFontMono: (projectId, fontFamily) => {
|
||||
// Update the project's fontFamilyMono property
|
||||
const projects = get().projects.map((p) =>
|
||||
p.id === projectId
|
||||
? { ...p, fontFamilyMono: fontFamily === null ? undefined : fontFamily }
|
||||
: p
|
||||
);
|
||||
set({ projects });
|
||||
|
||||
// Also update currentProject if it's the same project
|
||||
const currentProject = get().currentProject;
|
||||
if (currentProject?.id === projectId) {
|
||||
set({
|
||||
currentProject: {
|
||||
...currentProject,
|
||||
fontFamilyMono: fontFamily === null ? undefined : fontFamily,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
getEffectiveFontSans: () => {
|
||||
const currentProject = get().currentProject;
|
||||
// Return the project's font override, or null for default
|
||||
if (currentProject?.fontFamilySans) {
|
||||
return currentProject.fontFamilySans;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
getEffectiveFontMono: () => {
|
||||
const currentProject = get().currentProject;
|
||||
// Return the project's font override, or null for default
|
||||
if (currentProject?.fontFamilyMono) {
|
||||
return currentProject.fontFamilyMono;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
// Feature actions
|
||||
setFeatures: (features) => set({ features }),
|
||||
|
||||
|
||||
@@ -718,6 +718,12 @@ export interface ProjectSettings {
|
||||
/** Project theme (undefined = use global setting) */
|
||||
theme?: ThemeMode;
|
||||
|
||||
// Font Configuration (project-specific override)
|
||||
/** UI/Sans font family override (undefined = use default Geist Sans) */
|
||||
fontFamilySans?: string;
|
||||
/** Code/Mono font family override (undefined = use default Geist Mono) */
|
||||
fontFamilyMono?: string;
|
||||
|
||||
// Worktree Management
|
||||
/** Project-specific worktree preference override */
|
||||
useWorktrees?: boolean;
|
||||
|
||||
1040
package-lock.json
generated
1040
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user