mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-08-29 18:31:33 +00:00
Add accessible interface font choices (#1306)
This commit is contained in:
+6
-2
@@ -9,9 +9,9 @@ import { isElectron } from "@/lib/electron";
|
|||||||
import { Toaster } from "@/components/sonner";
|
import { Toaster } from "@/components/sonner";
|
||||||
import { Auth, getStoredAuth, clearStoredAuth } from "@/auth/Auth";
|
import { Auth, getStoredAuth, clearStoredAuth } from "@/auth/Auth";
|
||||||
import { getUserInfo, getCurrentToken, appReadyPromise } from "@/main-axios";
|
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 { 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 { useServiceWorker } from "@/hooks/use-service-worker";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { UiPreferencesProvider } from "@/contexts/UiPreferencesContext";
|
import { UiPreferencesProvider } from "@/contexts/UiPreferencesContext";
|
||||||
@@ -202,6 +202,10 @@ function App() {
|
|||||||
"termix-font-size",
|
"termix-font-size",
|
||||||
) as FontSizeId | null;
|
) as FontSizeId | null;
|
||||||
applyFontSize(savedSize ?? "md");
|
applyFontSize(savedSize ?? "md");
|
||||||
|
applyUiFont(
|
||||||
|
(localStorage.getItem("termix-ui-font") as UiFontId | null) ??
|
||||||
|
"jetbrains-mono",
|
||||||
|
);
|
||||||
return () => {
|
return () => {
|
||||||
if (timerRef.current) clearTimeout(timerRef.current);
|
if (timerRef.current) clearTimeout(timerRef.current);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -357,6 +357,12 @@ export type ThemeId =
|
|||||||
| "one-dark"
|
| "one-dark"
|
||||||
| "gruvbox";
|
| "gruvbox";
|
||||||
export type FontSizeId = "xs" | "sm" | "md" | "lg" | "xl";
|
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 =
|
export type ToolsTab =
|
||||||
"ssh-tools" | "snippets" | "macros" | "history" | "split-screen";
|
"ssh-tools" | "snippets" | "macros" | "history" | "split-screen";
|
||||||
|
|||||||
@@ -861,6 +861,7 @@ export function AppShell({
|
|||||||
const SNAPSHOT_KEYS = [
|
const SNAPSHOT_KEYS = [
|
||||||
"termix-accent",
|
"termix-accent",
|
||||||
"termix-font-size",
|
"termix-font-size",
|
||||||
|
"termix-ui-font",
|
||||||
"i18nextLng",
|
"i18nextLng",
|
||||||
"commandAutocomplete",
|
"commandAutocomplete",
|
||||||
"commandPaletteShortcutEnabled",
|
"commandPaletteShortcutEnabled",
|
||||||
|
|||||||
+1
-1
@@ -55,7 +55,7 @@
|
|||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--font-heading: var(--font-mono);
|
--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-accent-brand: var(--accent-brand);
|
||||||
--color-sidebar-ring: var(--sidebar-ring);
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
--color-sidebar-border: var(--sidebar-border);
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type {
|
import type {
|
||||||
DashboardCardConfig,
|
DashboardCardConfig,
|
||||||
FontSizeId,
|
FontSizeId,
|
||||||
|
UiFontId,
|
||||||
SplitMode,
|
SplitMode,
|
||||||
} from "@/types/ui-types";
|
} from "@/types/ui-types";
|
||||||
|
|
||||||
@@ -89,6 +90,40 @@ export function applyFontSize(id: FontSizeId) {
|
|||||||
localStorage.setItem("termix-font-size", id);
|
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 = [
|
export const FOLDER_COLORS = [
|
||||||
"#ef4444",
|
"#ef4444",
|
||||||
"#f97316",
|
"#f97316",
|
||||||
|
|||||||
@@ -4150,6 +4150,8 @@
|
|||||||
"languageLabel": "Language",
|
"languageLabel": "Language",
|
||||||
"themeLabel": "Theme",
|
"themeLabel": "Theme",
|
||||||
"fontSizeLabel": "Font Size",
|
"fontSizeLabel": "Font Size",
|
||||||
|
"interfaceFontLabel": "Interface Font",
|
||||||
|
"interfaceFontDescription": "Changes the application interface font. Terminal profiles keep their own font settings.",
|
||||||
"accentColorLabel": "Accent Color",
|
"accentColorLabel": "Accent Color",
|
||||||
"settingsTerminal": "Terminal",
|
"settingsTerminal": "Terminal",
|
||||||
"commandAutocomplete": "Command Autocomplete",
|
"commandAutocomplete": "Command Autocomplete",
|
||||||
|
|||||||
@@ -77,11 +77,13 @@ import {
|
|||||||
ACCENT_PRESET_COLORS,
|
ACCENT_PRESET_COLORS,
|
||||||
applyAccentColor,
|
applyAccentColor,
|
||||||
applyFontSize,
|
applyFontSize,
|
||||||
|
applyUiFont,
|
||||||
FONT_SIZES,
|
FONT_SIZES,
|
||||||
|
UI_FONTS,
|
||||||
} from "@/lib/theme";
|
} from "@/lib/theme";
|
||||||
import type { ApiKey } from "@/main-axios";
|
import type { ApiKey } from "@/main-axios";
|
||||||
import { useTheme } from "@/components/theme-provider";
|
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 { toast } from "sonner";
|
||||||
import { changeAppLanguage, normalizeLanguageCode } from "@/i18n/i18n";
|
import { changeAppLanguage, normalizeLanguageCode } from "@/i18n/i18n";
|
||||||
import { clearLocalAdaptivePreferences } from "@/lib/local-adaptive-preferences";
|
import { clearLocalAdaptivePreferences } from "@/lib/local-adaptive-preferences";
|
||||||
@@ -597,6 +599,10 @@ export function UserProfilePanel({
|
|||||||
const [fontSize, setFontSize] = useState<FontSizeId>(
|
const [fontSize, setFontSize] = useState<FontSizeId>(
|
||||||
() => (localStorage.getItem("termix-font-size") as FontSizeId) ?? "md",
|
() => (localStorage.getItem("termix-font-size") as FontSizeId) ?? "md",
|
||||||
);
|
);
|
||||||
|
const [uiFont, setUiFont] = useState<UiFontId>(
|
||||||
|
() =>
|
||||||
|
(localStorage.getItem("termix-ui-font") as UiFontId) ?? "jetbrains-mono",
|
||||||
|
);
|
||||||
const [language, setLanguage] = useState(() =>
|
const [language, setLanguage] = useState(() =>
|
||||||
normalizeLanguageCode(localStorage.getItem("i18nextLng")),
|
normalizeLanguageCode(localStorage.getItem("i18nextLng")),
|
||||||
);
|
);
|
||||||
@@ -831,6 +837,7 @@ export function UserProfilePanel({
|
|||||||
const SNAPSHOT_KEYS = [
|
const SNAPSHOT_KEYS = [
|
||||||
"termix-accent",
|
"termix-accent",
|
||||||
"termix-font-size",
|
"termix-font-size",
|
||||||
|
"termix-ui-font",
|
||||||
"i18nextLng",
|
"i18nextLng",
|
||||||
"commandAutocomplete",
|
"commandAutocomplete",
|
||||||
"commandPaletteShortcutEnabled",
|
"commandPaletteShortcutEnabled",
|
||||||
@@ -961,6 +968,8 @@ export function UserProfilePanel({
|
|||||||
setTheme("system");
|
setTheme("system");
|
||||||
setFontSize("md");
|
setFontSize("md");
|
||||||
applyFontSize("md");
|
applyFontSize("md");
|
||||||
|
setUiFont("jetbrains-mono");
|
||||||
|
applyUiFont("jetbrains-mono");
|
||||||
setAccentColor(DEFAULT_ACCENT);
|
setAccentColor(DEFAULT_ACCENT);
|
||||||
setCustomColorInput(DEFAULT_ACCENT);
|
setCustomColorInput(DEFAULT_ACCENT);
|
||||||
localStorage.setItem("termix-accent", DEFAULT_ACCENT);
|
localStorage.setItem("termix-accent", DEFAULT_ACCENT);
|
||||||
@@ -1047,6 +1056,12 @@ export function UserProfilePanel({
|
|||||||
setFontSize(restoredFontSize);
|
setFontSize(restoredFontSize);
|
||||||
applyFontSize(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";
|
const restoredAccent = restore("termix-accent", "#f59145") ?? "#f59145";
|
||||||
setAccentColor(restoredAccent);
|
setAccentColor(restoredAccent);
|
||||||
setCustomColorInput(restoredAccent);
|
setCustomColorInput(restoredAccent);
|
||||||
@@ -1156,6 +1171,11 @@ export function UserProfilePanel({
|
|||||||
if (storageMode === "cloud") saveToCloud({ fontSize: id });
|
if (storageMode === "cloud") saveToCloud({ fontSize: id });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleUiFontChange(id: UiFontId) {
|
||||||
|
setUiFont(id);
|
||||||
|
applyUiFont(id);
|
||||||
|
}
|
||||||
|
|
||||||
function handleLanguageChange(code: string) {
|
function handleLanguageChange(code: string) {
|
||||||
void changeAppLanguage(code)
|
void changeAppLanguage(code)
|
||||||
.then((language) => {
|
.then((language) => {
|
||||||
@@ -1723,6 +1743,29 @@ export function UserProfilePanel({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground flex items-center gap-1.5">
|
||||||
|
<Type className="size-3" />
|
||||||
|
{t("newUi.sidebar.userProfile.interfaceFontLabel")}
|
||||||
|
</span>
|
||||||
|
<select
|
||||||
|
value={uiFont}
|
||||||
|
onChange={(event) =>
|
||||||
|
handleUiFontChange(event.target.value as UiFontId)
|
||||||
|
}
|
||||||
|
className="px-2.5 py-1.5 text-xs bg-background border border-border text-foreground outline-none focus:ring-1 focus:ring-ring w-full"
|
||||||
|
>
|
||||||
|
{UI_FONTS.map((font) => (
|
||||||
|
<option key={font.id} value={font.id}>
|
||||||
|
{font.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<span className="text-[10px] text-muted-foreground">
|
||||||
|
{t("newUi.sidebar.userProfile.interfaceFontDescription")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground flex items-center gap-1.5">
|
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground flex items-center gap-1.5">
|
||||||
<Type className="size-3" />
|
<Type className="size-3" />
|
||||||
|
|||||||
@@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user