feat: add new themes, Zed fonts, and sort theme/font lists

New themes added:
- Dark: Ayu Dark, Ayu Mirage, Ember, Matcha
- Light: Ayu Light, One Light, Bluloco, Feather

Other changes:
- Bundle Zed Sans and Zed Mono fonts from zed-industries/zed-fonts
- Sort font options alphabetically (default first)
- Sort theme options alphabetically (Dark/Light first)
- Improve Ayu Dark text contrast for better readability
- Fix Matcha theme to have green undertone instead of blue
This commit is contained in:
Stefan de Vogelaere
2026-01-17 01:58:29 +01:00
parent f3b00d0f78
commit 1a7bf27ead
33 changed files with 1904 additions and 224 deletions

View File

@@ -4,6 +4,11 @@ import type { Project, TrashedProject } from '@/lib/electron';
import { getElectronAPI } from '@/lib/electron';
import { createLogger } from '@automaker/utils/logger';
import { setItem, getItem } from '@/lib/storage';
import {
UI_SANS_FONT_OPTIONS,
UI_MONO_FONT_OPTIONS,
DEFAULT_FONT_VALUE,
} from '@/config/ui-font-options';
import type {
Feature as BaseFeature,
FeatureImagePath,
@@ -65,9 +70,10 @@ export type ViewMode =
| 'ideation';
export type ThemeMode =
| 'light'
| 'dark'
// Special modes
| 'system'
// Dark themes
| 'dark'
| 'retro'
| 'dracula'
| 'nord'
@@ -79,12 +85,40 @@ export type ThemeMode =
| 'onedark'
| 'synthwave'
| 'red'
| 'cream'
| 'sunset'
| 'gray';
| 'gray'
| 'forest'
| 'ocean'
| 'ember'
| 'ayu-dark'
| 'ayu-mirage'
| 'matcha'
// Light themes
| 'light'
| 'cream'
| 'solarizedlight'
| 'github'
| 'paper'
| 'rose'
| 'mint'
| 'lavender'
| 'sand'
| 'sky'
| 'peach'
| 'snow'
| 'sepia'
| 'gruvboxlight'
| 'nordlight'
| 'blossom'
| 'ayu-light'
| 'onelight'
| 'bluloco'
| 'feather';
// LocalStorage key for theme persistence (fallback when server settings aren't available)
// LocalStorage keys for persistence (fallback when server settings aren't available)
export const THEME_STORAGE_KEY = 'automaker:theme';
export const FONT_SANS_STORAGE_KEY = 'automaker:font-sans';
export const FONT_MONO_STORAGE_KEY = 'automaker:font-mono';
// Maximum number of output lines to keep in init script state (prevents unbounded memory growth)
export const MAX_INIT_OUTPUT_LINES = 500;
@@ -123,6 +157,40 @@ function saveThemeToStorage(theme: ThemeMode): void {
setItem(THEME_STORAGE_KEY, theme);
}
/**
* Get fonts from localStorage as a fallback
* Used before server settings are loaded (e.g., on login/setup pages)
*/
export function getStoredFontSans(): string | null {
return getItem(FONT_SANS_STORAGE_KEY);
}
export function getStoredFontMono(): string | null {
return getItem(FONT_MONO_STORAGE_KEY);
}
/**
* Save fonts to localStorage for immediate persistence
* This is used as a fallback when server settings can't be loaded
*/
function saveFontSansToStorage(fontFamily: string | null): void {
if (fontFamily) {
setItem(FONT_SANS_STORAGE_KEY, fontFamily);
} else {
// Remove from storage if null (using default)
localStorage.removeItem(FONT_SANS_STORAGE_KEY);
}
}
function saveFontMonoToStorage(fontFamily: string | null): void {
if (fontFamily) {
setItem(FONT_MONO_STORAGE_KEY, fontFamily);
} else {
// Remove from storage if null (using default)
localStorage.removeItem(FONT_MONO_STORAGE_KEY);
}
}
function persistEffectiveThemeForProject(project: Project | null, fallbackTheme: ThemeMode): void {
const projectTheme = project?.theme as ThemeMode | undefined;
const themeToStore = projectTheme ?? fallbackTheme;
@@ -1270,8 +1338,8 @@ const initialState: AppState = {
mobileSidebarHidden: false, // Sidebar visible by default on mobile
lastSelectedSessionByProject: {},
theme: getStoredTheme() || 'dark', // Use localStorage theme as initial value, fallback to 'dark'
fontFamilySans: null, // Global UI font (null = use default Geist Sans)
fontFamilyMono: null, // Global code font (null = use default Geist Mono)
fontFamilySans: getStoredFontSans(), // Use localStorage font as initial value (null = use default Geist Sans)
fontFamilyMono: getStoredFontMono(), // Use localStorage font as initial value (null = use default Geist Mono)
features: [],
appSpec: '',
ipcConnected: false,
@@ -1748,12 +1816,21 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
setPreviewTheme: (theme) => set({ previewTheme: theme }),
// Font actions (global + per-project override)
setFontSans: (fontFamily) => set({ fontFamilySans: fontFamily }),
setFontSans: (fontFamily) => {
// Save to localStorage for fallback when server settings aren't available
saveFontSansToStorage(fontFamily);
set({ fontFamilySans: fontFamily });
},
setFontMono: (fontFamily) => set({ fontFamilyMono: fontFamily }),
setFontMono: (fontFamily) => {
// Save to localStorage for fallback when server settings aren't available
saveFontMonoToStorage(fontFamily);
set({ fontFamilyMono: fontFamily });
},
setProjectFontSans: (projectId, fontFamily) => {
// Update the project's fontFamilySans property
// null means "clear to use global", any string (including 'default') means explicit override
const projects = get().projects.map((p) =>
p.id === projectId
? { ...p, fontFamilySans: fontFamily === null ? undefined : fontFamily }
@@ -1775,6 +1852,7 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
setProjectFontMono: (projectId, fontFamily) => {
// Update the project's fontFamilyMono property
// null means "clear to use global", any string (including 'default') means explicit override
const projects = get().projects.map((p) =>
p.id === projectId
? { ...p, fontFamilyMono: fontFamily === null ? undefined : fontFamily }
@@ -1797,19 +1875,41 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
getEffectiveFontSans: () => {
const currentProject = get().currentProject;
// Return project override if set, otherwise global, otherwise null for default
// 'default' value means explicitly using default font, so return null for CSS
// Also validate that the font is in the available options list
const isValidFont = (font: string | null | undefined): boolean => {
if (!font || font === DEFAULT_FONT_VALUE) return true;
return UI_SANS_FONT_OPTIONS.some((opt) => opt.value === font);
};
if (currentProject?.fontFamilySans) {
return currentProject.fontFamilySans;
const font = currentProject.fontFamilySans;
if (!isValidFont(font)) return null; // Fallback to default if font not in list
return font === DEFAULT_FONT_VALUE ? null : font;
}
return get().fontFamilySans;
const globalFont = get().fontFamilySans;
if (!isValidFont(globalFont)) return null; // Fallback to default if font not in list
return globalFont === DEFAULT_FONT_VALUE ? null : globalFont;
},
getEffectiveFontMono: () => {
const currentProject = get().currentProject;
// Return project override if set, otherwise global, otherwise null for default
// 'default' value means explicitly using default font, so return null for CSS
// Also validate that the font is in the available options list
const isValidFont = (font: string | null | undefined): boolean => {
if (!font || font === DEFAULT_FONT_VALUE) return true;
return UI_MONO_FONT_OPTIONS.some((opt) => opt.value === font);
};
if (currentProject?.fontFamilyMono) {
return currentProject.fontFamilyMono;
const font = currentProject.fontFamilyMono;
if (!isValidFont(font)) return null; // Fallback to default if font not in list
return font === DEFAULT_FONT_VALUE ? null : font;
}
return get().fontFamilyMono;
const globalFont = get().fontFamilyMono;
if (!isValidFont(globalFont)) return null; // Fallback to default if font not in list
return globalFont === DEFAULT_FONT_VALUE ? null : globalFont;
},
// Feature actions