+
+
+ {catLabel}
+
+
+ {fonts.map((f) => {
+ const isActive = f.id === fontId;
+ return (
+
setFont(f.id)}
+ role="option"
+ >
+
+
+ {/* Preview the font in its own stack. */}
+
+ {f.label}
+
+
+
+
+ );
+ })}
+
+ );
+ })}
+ >
+ );
+}
+
function ThemeSwatch({ theme }: { theme: DashboardTheme }) {
// Inverted themes (Nous Blue / future lens themes) author their palette
// pre-inversion — `#FFAC02` reads as `#0053FD` blue once the foreground-
@@ -247,6 +356,12 @@ interface ThemeSwitcherOptionsProps {
themeName: string;
}
+interface FontSectionProps {
+ fontChoices: FontChoice[];
+ fontId: string;
+ setFont: (id: string) => void;
+}
+
interface ThemeSwitcherProps {
collapsed?: boolean;
dropUp?: boolean;
diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts
index 9cde64dec65..8203c25bd52 100644
--- a/web/src/i18n/en.ts
+++ b/web/src/i18n/en.ts
@@ -518,6 +518,12 @@ export const en: Translations = {
theme: {
title: "Theme",
switchTheme: "Switch theme",
+ fontTitle: "Font",
+ fontDefault: "Theme default",
+ fontDefaultHint: "Use the active theme's font",
+ fontSans: "Sans",
+ fontSerif: "Serif",
+ fontMono: "Mono",
},
achievements: {
diff --git a/web/src/i18n/types.ts b/web/src/i18n/types.ts
index 6d745ba763f..14bc41f2d08 100644
--- a/web/src/i18n/types.ts
+++ b/web/src/i18n/types.ts
@@ -539,6 +539,13 @@ export interface Translations {
theme: {
title: string;
switchTheme: string;
+ /** Font-override section (optional — locales fall back to English). */
+ fontTitle?: string;
+ fontDefault?: string;
+ fontDefaultHint?: string;
+ fontSans?: string;
+ fontSerif?: string;
+ fontMono?: string;
};
// ── Achievements plugin (plugins/hermes-achievements) ──
diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts
index cbcb7fe1440..980faf3d11f 100644
--- a/web/src/lib/api.ts
+++ b/web/src/lib/api.ts
@@ -741,6 +741,14 @@ export const api = {
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name }),
}),
+ getFontPref: () =>
+ fetchJSON