diff --git a/src/main.tsx b/src/main.tsx index aa0d961a8..fd4a61680 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -9,9 +9,9 @@ import { isElectron } from "@/lib/electron"; import { Toaster } from "@/components/sonner"; import { Auth, getStoredAuth, clearStoredAuth } from "@/auth/Auth"; import { getUserInfo, getCurrentToken, appReadyPromise } from "@/main-axios"; -import { applyAccentColor, applyFontSize } from "@/lib/theme"; +import { applyAccentColor, applyFontSize, applyUiFont } from "@/lib/theme"; import { installElectronWheelZoomGuard } from "@/lib/electron-wheel-zoom"; -import type { FontSizeId } from "@/types/ui-types"; +import type { FontSizeId, UiFontId } from "@/types/ui-types"; import { useServiceWorker } from "@/hooks/use-service-worker"; import { useTranslation } from "react-i18next"; import { UiPreferencesProvider } from "@/contexts/UiPreferencesContext"; @@ -202,6 +202,10 @@ function App() { "termix-font-size", ) as FontSizeId | null; applyFontSize(savedSize ?? "md"); + applyUiFont( + (localStorage.getItem("termix-ui-font") as UiFontId | null) ?? + "jetbrains-mono", + ); return () => { if (timerRef.current) clearTimeout(timerRef.current); }; diff --git a/src/types/ui-types.ts b/src/types/ui-types.ts index 79708ca3f..d26561d61 100644 --- a/src/types/ui-types.ts +++ b/src/types/ui-types.ts @@ -357,6 +357,12 @@ export type ThemeId = | "one-dark" | "gruvbox"; export type FontSizeId = "xs" | "sm" | "md" | "lg" | "xl"; +export type UiFontId = + | "jetbrains-mono" + | "system-sans" + | "fira-code" + | "source-code-pro" + | "caskaydia-cove"; export type ToolsTab = "ssh-tools" | "snippets" | "macros" | "history" | "split-screen"; diff --git a/src/ui/AppShell.tsx b/src/ui/AppShell.tsx index ca74e3c12..bccf81892 100644 --- a/src/ui/AppShell.tsx +++ b/src/ui/AppShell.tsx @@ -859,6 +859,7 @@ export function AppShell({ const SNAPSHOT_KEYS = [ "termix-accent", "termix-font-size", + "termix-ui-font", "i18nextLng", "commandAutocomplete", "commandPaletteShortcutEnabled", diff --git a/src/ui/index.css b/src/ui/index.css index 016763eb1..96703ae10 100644 --- a/src/ui/index.css +++ b/src/ui/index.css @@ -55,7 +55,7 @@ @theme inline { --font-heading: var(--font-mono); - --font-mono: "JetBrains Mono Variable", monospace; + --font-mono: var(--font-ui, "JetBrains Mono Variable", monospace); --color-accent-brand: var(--accent-brand); --color-sidebar-ring: var(--sidebar-ring); --color-sidebar-border: var(--sidebar-border); diff --git a/src/ui/lib/theme.ts b/src/ui/lib/theme.ts index 2bfa53dbe..01298694d 100644 --- a/src/ui/lib/theme.ts +++ b/src/ui/lib/theme.ts @@ -1,6 +1,7 @@ import type { DashboardCardConfig, FontSizeId, + UiFontId, SplitMode, } from "@/types/ui-types"; @@ -89,6 +90,40 @@ export function applyFontSize(id: FontSizeId) { localStorage.setItem("termix-font-size", id); } +export const UI_FONTS: { id: UiFontId; label: string; family: string }[] = [ + { + id: "jetbrains-mono", + label: "JetBrains Mono", + family: '"JetBrains Mono Variable", monospace', + }, + { + id: "system-sans", + label: "System Sans", + family: "ui-sans-serif, system-ui, sans-serif", + }, + { + id: "fira-code", + label: "Fira Code", + family: '"Fira Code", monospace', + }, + { + id: "source-code-pro", + label: "Source Code Pro", + family: '"Source Code Pro", monospace', + }, + { + id: "caskaydia-cove", + label: "Caskaydia Cove", + family: '"Caskaydia Cove Nerd Font Mono", monospace', + }, +]; + +export function applyUiFont(id: UiFontId) { + const font = UI_FONTS.find((candidate) => candidate.id === id) ?? UI_FONTS[0]; + document.documentElement.style.setProperty("--font-ui", font.family); + localStorage.setItem("termix-ui-font", font.id); +} + export const FOLDER_COLORS = [ "#ef4444", "#f97316", diff --git a/src/ui/locales/en.json b/src/ui/locales/en.json index 62ba72d55..0546d37b3 100644 --- a/src/ui/locales/en.json +++ b/src/ui/locales/en.json @@ -4150,6 +4150,8 @@ "languageLabel": "Language", "themeLabel": "Theme", "fontSizeLabel": "Font Size", + "interfaceFontLabel": "Interface Font", + "interfaceFontDescription": "Changes the application interface font. Terminal profiles keep their own font settings.", "accentColorLabel": "Accent Color", "settingsTerminal": "Terminal", "commandAutocomplete": "Command Autocomplete", diff --git a/src/ui/sidebar/UserProfilePanel.tsx b/src/ui/sidebar/UserProfilePanel.tsx index 917b7d8a6..5c2782fe7 100644 --- a/src/ui/sidebar/UserProfilePanel.tsx +++ b/src/ui/sidebar/UserProfilePanel.tsx @@ -77,11 +77,13 @@ import { ACCENT_PRESET_COLORS, applyAccentColor, applyFontSize, + applyUiFont, FONT_SIZES, + UI_FONTS, } from "@/lib/theme"; import type { ApiKey } from "@/main-axios"; import { useTheme } from "@/components/theme-provider"; -import type { FontSizeId, ThemeId } from "@/types/ui-types"; +import type { FontSizeId, ThemeId, UiFontId } from "@/types/ui-types"; import { toast } from "sonner"; import { changeAppLanguage, normalizeLanguageCode } from "@/i18n/i18n"; import { clearLocalAdaptivePreferences } from "@/lib/local-adaptive-preferences"; @@ -597,6 +599,10 @@ export function UserProfilePanel({ const [fontSize, setFontSize] = useState( () => (localStorage.getItem("termix-font-size") as FontSizeId) ?? "md", ); + const [uiFont, setUiFont] = useState( + () => + (localStorage.getItem("termix-ui-font") as UiFontId) ?? "jetbrains-mono", + ); const [language, setLanguage] = useState(() => normalizeLanguageCode(localStorage.getItem("i18nextLng")), ); @@ -831,6 +837,7 @@ export function UserProfilePanel({ const SNAPSHOT_KEYS = [ "termix-accent", "termix-font-size", + "termix-ui-font", "i18nextLng", "commandAutocomplete", "commandPaletteShortcutEnabled", @@ -961,6 +968,8 @@ export function UserProfilePanel({ setTheme("system"); setFontSize("md"); applyFontSize("md"); + setUiFont("jetbrains-mono"); + applyUiFont("jetbrains-mono"); setAccentColor(DEFAULT_ACCENT); setCustomColorInput(DEFAULT_ACCENT); localStorage.setItem("termix-accent", DEFAULT_ACCENT); @@ -1047,6 +1056,12 @@ export function UserProfilePanel({ setFontSize(restoredFontSize); applyFontSize(restoredFontSize); + const restoredUiFont = + (restore("termix-ui-font", "jetbrains-mono") as UiFontId) ?? + "jetbrains-mono"; + setUiFont(restoredUiFont); + applyUiFont(restoredUiFont); + const restoredAccent = restore("termix-accent", "#f59145") ?? "#f59145"; setAccentColor(restoredAccent); setCustomColorInput(restoredAccent); @@ -1156,6 +1171,11 @@ export function UserProfilePanel({ if (storageMode === "cloud") saveToCloud({ fontSize: id }); } + function handleUiFontChange(id: UiFontId) { + setUiFont(id); + applyUiFont(id); + } + function handleLanguageChange(code: string) { void changeAppLanguage(code) .then((language) => { @@ -1723,6 +1743,29 @@ export function UserProfilePanel({ +
+ + + {t("newUi.sidebar.userProfile.interfaceFontLabel")} + + + + {t("newUi.sidebar.userProfile.interfaceFontDescription")} + +
+
diff --git a/src/ui/tests/lib/ui-font.test.ts b/src/ui/tests/lib/ui-font.test.ts new file mode 100644 index 000000000..74aa0179b --- /dev/null +++ b/src/ui/tests/lib/ui-font.test.ts @@ -0,0 +1,24 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { applyUiFont } from "../../lib/theme"; + +describe("applyUiFont", () => { + beforeEach(() => { + localStorage.clear(); + document.documentElement.style.removeProperty("--font-ui"); + }); + + it("applies and persists a readable system font", () => { + applyUiFont("system-sans"); + + expect(document.documentElement.style.getPropertyValue("--font-ui")).toBe( + "ui-sans-serif, system-ui, sans-serif", + ); + expect(localStorage.getItem("termix-ui-font")).toBe("system-sans"); + }); + + it("falls back to JetBrains Mono for an unknown stored value", () => { + applyUiFont("unknown" as never); + + expect(localStorage.getItem("termix-ui-font")).toBe("jetbrains-mono"); + }); +});