mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-05 09:33:07 +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 { Label } from '@/components/ui/label';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
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 { 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 { 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';
|
||||||
@@ -12,8 +20,21 @@ interface ProjectThemeSectionProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ProjectThemeSection({ project }: 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 [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 projectTheme = project.theme as Theme | undefined;
|
||||||
const hasCustomTheme = projectTheme !== 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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -158,6 +191,63 @@ export function ProjectThemeSection({ project }: ProjectThemeSectionProps) {
|
|||||||
</p>
|
</p>
|
||||||
</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">
|
||||||
|
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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export interface Project {
|
|||||||
name: string;
|
name: string;
|
||||||
path: string;
|
path: string;
|
||||||
theme?: string;
|
theme?: string;
|
||||||
|
fontFamilySans?: string;
|
||||||
|
fontFamilyMono?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
customIconPath?: 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;
|
path: string;
|
||||||
lastOpened?: string;
|
lastOpened?: string;
|
||||||
theme?: string; // Per-project theme override (uses ThemeMode from app-store)
|
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
|
isFavorite?: boolean; // Pin project to top of dashboard
|
||||||
icon?: string; // Lucide icon name for project identification
|
icon?: string; // Lucide icon name for project identification
|
||||||
customIconPath?: string; // Path to custom uploaded icon image in .automaker/images/
|
customIconPath?: string; // Path to custom uploaded icon image in .automaker/images/
|
||||||
|
|||||||
@@ -158,6 +158,8 @@ function RootLayoutContent() {
|
|||||||
projectHistory,
|
projectHistory,
|
||||||
upsertAndSetCurrentProject,
|
upsertAndSetCurrentProject,
|
||||||
getEffectiveTheme,
|
getEffectiveTheme,
|
||||||
|
getEffectiveFontSans,
|
||||||
|
getEffectiveFontMono,
|
||||||
skipSandboxWarning,
|
skipSandboxWarning,
|
||||||
setSkipSandboxWarning,
|
setSkipSandboxWarning,
|
||||||
fetchCodexModels,
|
fetchCodexModels,
|
||||||
@@ -248,6 +250,10 @@ function RootLayoutContent() {
|
|||||||
// Defer the theme value to keep UI responsive during rapid hover changes
|
// Defer the theme value to keep UI responsive during rapid hover changes
|
||||||
const deferredTheme = useDeferredValue(effectiveTheme);
|
const deferredTheme = useDeferredValue(effectiveTheme);
|
||||||
|
|
||||||
|
// Get effective fonts for the current project
|
||||||
|
const effectiveFontSans = getEffectiveFontSans();
|
||||||
|
const effectiveFontMono = getEffectiveFontMono();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsMounted(true);
|
setIsMounted(true);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -727,6 +733,23 @@ function RootLayoutContent() {
|
|||||||
}
|
}
|
||||||
}, [deferredTheme]);
|
}, [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
|
// Show sandbox rejection screen if user denied the risk warning
|
||||||
if (sandboxStatus === 'denied') {
|
if (sandboxStatus === 'denied') {
|
||||||
return <SandboxRejectionScreen />;
|
return <SandboxRejectionScreen />;
|
||||||
|
|||||||
@@ -920,6 +920,12 @@ export interface AppActions {
|
|||||||
getEffectiveTheme: () => ThemeMode; // Get the effective theme (project, global, or preview if set)
|
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)
|
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
|
// Feature actions
|
||||||
setFeatures: (features: Feature[]) => void;
|
setFeatures: (features: Feature[]) => void;
|
||||||
updateFeature: (id: string, updates: Partial<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 }),
|
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
|
// Feature actions
|
||||||
setFeatures: (features) => set({ features }),
|
setFeatures: (features) => set({ features }),
|
||||||
|
|
||||||
|
|||||||
@@ -718,6 +718,12 @@ export interface ProjectSettings {
|
|||||||
/** Project theme (undefined = use global setting) */
|
/** Project theme (undefined = use global setting) */
|
||||||
theme?: ThemeMode;
|
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
|
// Worktree Management
|
||||||
/** Project-specific worktree preference override */
|
/** Project-specific worktree preference override */
|
||||||
useWorktrees?: boolean;
|
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