From b10ecd096c9023d2e8abe4b7d7b1b5a15f1bf64d Mon Sep 17 00:00:00 2001 From: lxk955 <164191365+lxk955@users.noreply.github.com> Date: Sun, 4 Oct 2026 09:55:44 +0800 Subject: [PATCH] feat(editor): add CJK fallback fonts to monospace stacks --- .../src/lib/app/__tests__/appFonts.spec.ts | 72 +++++++++++++++++++ apps/desktop/src/lib/app/appFonts.ts | 33 ++++++--- apps/desktop/src/lib/app/fontFamilyOptions.ts | 12 +++- .../stores/__tests__/settingsStore.spec.ts | 12 ++++ apps/desktop/src/stores/settingsStore.ts | 9 ++- apps/desktop/src/styles/tokens.css | 2 +- plugins/sdk/dev-host/ui/App.vue | 4 +- 7 files changed, 126 insertions(+), 18 deletions(-) create mode 100644 apps/desktop/src/lib/app/__tests__/appFonts.spec.ts diff --git a/apps/desktop/src/lib/app/__tests__/appFonts.spec.ts b/apps/desktop/src/lib/app/__tests__/appFonts.spec.ts new file mode 100644 index 0000000000..f4266de9d7 --- /dev/null +++ b/apps/desktop/src/lib/app/__tests__/appFonts.spec.ts @@ -0,0 +1,72 @@ +import { describe, expect, it } from "vitest"; +import { cssFontFamilyForName, DEFAULT_MONO_FALLBACK_FONTS, DEFAULT_MONO_FONT_FAMILY, FONT_FAMILIES, LEGACY_PRESET_FONT_MAP, normalizeCustomFontFamilyInput, readableFontFamily } from "../appFonts"; +import { displayFontFamily, isPresetFontFamily } from "../fontFamilyOptions"; + +describe("appFonts", () => { + it("includes CJK fallbacks in DEFAULT_MONO_FALLBACK_FONTS", () => { + expect(DEFAULT_MONO_FALLBACK_FONTS).toContain("PingFang SC"); + expect(DEFAULT_MONO_FALLBACK_FONTS).toContain("Hiragino Sans GB"); + expect(DEFAULT_MONO_FALLBACK_FONTS).toContain("Microsoft YaHei"); + expect(DEFAULT_MONO_FALLBACK_FONTS).toMatch(/monospace$/); + }); + + it("appends CJK fallbacks to DEFAULT_MONO_FONT_FAMILY", () => { + expect(DEFAULT_MONO_FONT_FAMILY).toContain("'Fira Code'"); + expect(DEFAULT_MONO_FONT_FAMILY).toContain(DEFAULT_MONO_FALLBACK_FONTS); + }); + + it("appends CJK fallbacks to every preset font family", () => { + for (const preset of FONT_FAMILIES) { + expect(preset.value).toContain(DEFAULT_MONO_FALLBACK_FONTS); + } + }); + + it("maps legacy preset values to updated stacks with CJK fallbacks", () => { + expect(LEGACY_PRESET_FONT_MAP["'Fira Code', 'Cascadia Code', 'Cascadia Mono', 'JetBrains Mono', monospace"]).toBe(DEFAULT_MONO_FONT_FAMILY); + expect(LEGACY_PRESET_FONT_MAP["'JetBrains Mono', 'Fira Code', monospace"]).toContain(DEFAULT_MONO_FALLBACK_FONTS); + expect(LEGACY_PRESET_FONT_MAP["'Consolas', 'Courier New', monospace"]).toContain(DEFAULT_MONO_FALLBACK_FONTS); + expect(LEGACY_PRESET_FONT_MAP["monospace"]).toBe(DEFAULT_MONO_FALLBACK_FONTS); + }); + + it("constructs CSS font family for system font name with CJK fallbacks", () => { + expect(cssFontFamilyForName("Ubuntu Mono")).toBe(`'Ubuntu Mono', ${DEFAULT_MONO_FALLBACK_FONTS}`); + expect(cssFontFamilyForName("JetBrains Mono")).toBe(`'JetBrains Mono', ${DEFAULT_MONO_FALLBACK_FONTS}`); + expect(cssFontFamilyForName("Font's Name")).toBe(`'Font\\'s Name', ${DEFAULT_MONO_FALLBACK_FONTS}`); + }); + + it("extracts readable font family name from font stacks", () => { + expect(readableFontFamily(DEFAULT_MONO_FONT_FAMILY)).toBe("Fira Code"); + expect(readableFontFamily(`'JetBrains Mono', ${DEFAULT_MONO_FALLBACK_FONTS}`)).toBe("JetBrains Mono"); + expect(readableFontFamily("Consolas")).toBe("Consolas"); + }); + + it("normalizes custom font family input", () => { + expect(normalizeCustomFontFamilyInput("")).toBe(""); + expect(normalizeCustomFontFamilyInput(" ")).toBe(""); + expect(normalizeCustomFontFamilyInput("Hack")).toBe(`'Hack', ${DEFAULT_MONO_FALLBACK_FONTS}`); + // Custom font stacks with quotes or commas are preserved verbatim + expect(normalizeCustomFontFamilyInput("'My Custom Font', monospace")).toBe("'My Custom Font', monospace"); + expect(normalizeCustomFontFamilyInput("Fira Code, PingFang SC, monospace")).toBe("Fira Code, PingFang SC, monospace"); + }); + + it("recognizes preset font labels for both current and legacy preset values", () => { + expect(displayFontFamily(DEFAULT_MONO_FONT_FAMILY)).toBe("Fira Code"); + expect(displayFontFamily(DEFAULT_MONO_FALLBACK_FONTS)).toBe("System Monospace"); + + // Legacy preset values + expect(displayFontFamily("'Fira Code', 'Cascadia Code', 'Cascadia Mono', 'JetBrains Mono', monospace")).toBe("Fira Code"); + expect(displayFontFamily("'JetBrains Mono', 'Fira Code', monospace")).toBe("JetBrains Mono"); + expect(displayFontFamily("monospace")).toBe("System Monospace"); + + // Custom font fallback + expect(displayFontFamily("'My Font', monospace")).toBe("My Font"); + }); + + it("identifies preset font values for both current and legacy presets", () => { + expect(isPresetFontFamily(DEFAULT_MONO_FONT_FAMILY)).toBe(true); + expect(isPresetFontFamily(DEFAULT_MONO_FALLBACK_FONTS)).toBe(true); + expect(isPresetFontFamily("'Fira Code', 'Cascadia Code', 'Cascadia Mono', 'JetBrains Mono', monospace")).toBe(true); + expect(isPresetFontFamily("monospace")).toBe(true); + expect(isPresetFontFamily("'Unknown Font', monospace")).toBe(false); + }); +}); diff --git a/apps/desktop/src/lib/app/appFonts.ts b/apps/desktop/src/lib/app/appFonts.ts index 54d5e8e44b..97cddac739 100644 --- a/apps/desktop/src/lib/app/appFonts.ts +++ b/apps/desktop/src/lib/app/appFonts.ts @@ -3,26 +3,41 @@ export const FONT_MONO_CSS_VAR = "--font-mono"; export const DATA_GRID_FONT_FAMILY_CSS_VAR = "--dbx-data-grid-font-family"; export const DEFAULT_UI_FONT_FAMILY = `"Geist Variable", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Segoe UI", system-ui, sans-serif`; +// CJK and system fallback fonts appended to monospace font stacks so Chinese/CJK +// glyphs fall back smoothly to sans-serif CJK fonts (e.g. PingFang SC on macOS, +// Microsoft YaHei on Windows) instead of ugly SimSun/NSimSun or bitmap fonts (dbx#674, dbx#1510). +export const DEFAULT_MONO_FALLBACK_FONTS = `"PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", monospace`; + // Keep in sync with the editor font default in settingsStore; also the baseline // `--font-mono` value in tokens.css, which plugin sandboxes consume via the bridge. -export const DEFAULT_MONO_FONT_FAMILY = `'Fira Code', 'Cascadia Code', 'Cascadia Mono', 'JetBrains Mono', monospace`; +export const DEFAULT_MONO_FONT_FAMILY = `'Fira Code', 'Cascadia Code', 'Cascadia Mono', 'JetBrains Mono', ${DEFAULT_MONO_FALLBACK_FONTS}`; export const DEFAULT_DATA_GRID_FONT_FAMILY = `"Geist Variable Tabular", "Geist Variable", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif`; // Native-feeling UI option without DBX's bundled/brand font at the front of the stack. export const SYSTEM_UI_FONT_FAMILY = `system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif`; export const FONT_FAMILIES: { value: string; label: string }[] = [ - { value: "'Fira Code', 'Cascadia Code', 'Cascadia Mono', 'JetBrains Mono', monospace", label: "Fira Code" }, - { value: "'JetBrains Mono', 'Fira Code', monospace", label: "JetBrains Mono" }, - { value: "'Cascadia Code', 'Cascadia Mono', monospace", label: "Cascadia Code" }, - { value: "'Source Code Pro', monospace", label: "Source Code Pro" }, - { value: "'SF Mono', 'Menlo', monospace", label: "SF Mono / Menlo" }, - { value: "'Consolas', 'Courier New', monospace", label: "Consolas" }, - { value: "monospace", label: "System Monospace" }, + { value: `'Fira Code', 'Cascadia Code', 'Cascadia Mono', 'JetBrains Mono', ${DEFAULT_MONO_FALLBACK_FONTS}`, label: "Fira Code" }, + { value: `'JetBrains Mono', 'Fira Code', ${DEFAULT_MONO_FALLBACK_FONTS}`, label: "JetBrains Mono" }, + { value: `'Cascadia Code', 'Cascadia Mono', ${DEFAULT_MONO_FALLBACK_FONTS}`, label: "Cascadia Code" }, + { value: `'Source Code Pro', ${DEFAULT_MONO_FALLBACK_FONTS}`, label: "Source Code Pro" }, + { value: `'SF Mono', 'Menlo', ${DEFAULT_MONO_FALLBACK_FONTS}`, label: "SF Mono / Menlo" }, + { value: `'Consolas', 'Courier New', ${DEFAULT_MONO_FALLBACK_FONTS}`, label: "Consolas" }, + { value: DEFAULT_MONO_FALLBACK_FONTS, label: "System Monospace" }, ]; +export const LEGACY_PRESET_FONT_MAP: Record = { + "'Fira Code', 'Cascadia Code', 'Cascadia Mono', 'JetBrains Mono', monospace": DEFAULT_MONO_FONT_FAMILY, + "'JetBrains Mono', 'Fira Code', monospace": `'JetBrains Mono', 'Fira Code', ${DEFAULT_MONO_FALLBACK_FONTS}`, + "'Cascadia Code', 'Cascadia Mono', monospace": `'Cascadia Code', 'Cascadia Mono', ${DEFAULT_MONO_FALLBACK_FONTS}`, + "'Source Code Pro', monospace": `'Source Code Pro', ${DEFAULT_MONO_FALLBACK_FONTS}`, + "'SF Mono', 'Menlo', monospace": `'SF Mono', 'Menlo', ${DEFAULT_MONO_FALLBACK_FONTS}`, + "'Consolas', 'Courier New', monospace": `'Consolas', 'Courier New', ${DEFAULT_MONO_FALLBACK_FONTS}`, + monospace: DEFAULT_MONO_FALLBACK_FONTS, +}; + export function cssFontFamilyForName(name: string): string { - return `'${name.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}', monospace`; + return `'${name.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}', ${DEFAULT_MONO_FALLBACK_FONTS}`; } export function readableFontFamily(value: string): string { diff --git a/apps/desktop/src/lib/app/fontFamilyOptions.ts b/apps/desktop/src/lib/app/fontFamilyOptions.ts index 51723efe60..843877847f 100644 --- a/apps/desktop/src/lib/app/fontFamilyOptions.ts +++ b/apps/desktop/src/lib/app/fontFamilyOptions.ts @@ -1,11 +1,17 @@ -import { cssFontFamilyForName, FONT_FAMILIES, readableFontFamily } from "@/lib/app/appFonts"; +import { cssFontFamilyForName, FONT_FAMILIES, LEGACY_PRESET_FONT_MAP, readableFontFamily } from "@/lib/app/appFonts"; import { listSystemFonts } from "@/lib/backend/api"; let cachedSystemFontNames: string[] | null = null; let pendingSystemFontNames: Promise | null = null; -const presetFontLabels = new Map(FONT_FAMILIES.map((font) => [font.value, font.label])); -const presetFontValues = new Set(FONT_FAMILIES.map((font) => font.value)); +const presetFontLabels = new Map([ + ...FONT_FAMILIES.map((font) => [font.value, font.label] as const), + ...Object.entries(LEGACY_PRESET_FONT_MAP).map(([legacyVal, newVal]) => { + const label = FONT_FAMILIES.find((f) => f.value === newVal)?.label ?? "System Monospace"; + return [legacyVal, label] as const; + }), +]); +const presetFontValues = new Set([...FONT_FAMILIES.map((font) => font.value), ...Object.keys(LEGACY_PRESET_FONT_MAP)]); export function buildFontFamilyOptions(systemFontNames: readonly string[], selectedValues: readonly string[] = [], leadingValues: readonly string[] = []): string[] { return [...new Set([...leadingValues, ...FONT_FAMILIES.map((font) => font.value), ...systemFontNames.map(cssFontFamilyForName), ...selectedValues.filter(Boolean)])]; diff --git a/apps/desktop/src/stores/__tests__/settingsStore.spec.ts b/apps/desktop/src/stores/__tests__/settingsStore.spec.ts index 19b0d8eeeb..d845748a9c 100644 --- a/apps/desktop/src/stores/__tests__/settingsStore.spec.ts +++ b/apps/desktop/src/stores/__tests__/settingsStore.spec.ts @@ -21,6 +21,7 @@ import { } from "@/stores/settingsStore"; import type { AiConfigItem } from "@/types/ai"; import { DATA_GRID_EXTRACTOR_OPTIONS_MIGRATION_VERSION } from "@/lib/dataGrid/dataGridCopyExtractor"; +import { DEFAULT_MONO_FONT_FAMILY } from "@/lib/app/appFonts"; describe("normalizeEditorSettings", () => { it("preserves comment-first naming for existing settings and permits opting out", () => { @@ -647,6 +648,17 @@ describe("normalizeEditorSettings", () => { expect(normalizeEditorSettings({ tableFontFamily: " " }).tableFontFamily).toBe(defaultFontFamily); }); + it("defaults the editor font with CJK fallbacks and migrates legacy default presets", () => { + expect(normalizeEditorSettings({}).fontFamily).toBe(DEFAULT_MONO_FONT_FAMILY); + expect( + normalizeEditorSettings({ + fontFamily: "'Fira Code', 'Cascadia Code', 'Cascadia Mono', 'JetBrains Mono', monospace", + }).fontFamily, + ).toBe(DEFAULT_MONO_FONT_FAMILY); + expect(normalizeEditorSettings({ fontFamily: "'Custom Mono', monospace" }).fontFamily).toBe("'Custom Mono', monospace"); + expect(normalizeEditorSettings({ fontFamily: " " }).fontFamily).toBe(DEFAULT_MONO_FONT_FAMILY); + }); + it("shows cell detail metadata by default and preserves collapsed state", () => { expect(normalizeEditorSettings({}).cellDetailMetadataCollapsed).toBe(false); expect(normalizeEditorSettings({ cellDetailMetadataCollapsed: true }).cellDetailMetadataCollapsed).toBe(true); diff --git a/apps/desktop/src/stores/settingsStore.ts b/apps/desktop/src/stores/settingsStore.ts index fcc52059b5..6f6b655d4b 100644 --- a/apps/desktop/src/stores/settingsStore.ts +++ b/apps/desktop/src/stores/settingsStore.ts @@ -3,7 +3,7 @@ import { defineStore } from "pinia"; import { computed, ref } from "vue"; import { aiConfigToItem, generateId, getConfigKey } from "@/lib/ai/aiConfigList"; import { AI_CONVERSATION_FONT_FAMILY_DEFAULT, AI_CONVERSATION_FONT_SIZE_DEFAULT, normalizeAiConversationFontFamily, normalizeAiConversationFontSize } from "@/lib/ai/aiTypography"; -import { DEFAULT_DATA_GRID_FONT_FAMILY, DEFAULT_UI_FONT_FAMILY } from "@/lib/app/appFonts"; +import { DEFAULT_DATA_GRID_FONT_FAMILY, DEFAULT_MONO_FONT_FAMILY, DEFAULT_UI_FONT_FAMILY } from "@/lib/app/appFonts"; import { emitAlwaysOnTopToolbarVisibilityChanged } from "@/lib/app/windowAlwaysOnTop"; import { defaultBackgroundImageSettings, normalizeBackgroundImageSettings, type BackgroundImageSettings } from "@/lib/app/appBackgroundImage"; import * as api from "@/lib/backend/api"; @@ -1176,7 +1176,7 @@ export const EXECUTE_MODE_CURRENT_DEFAULT_VERSION = 1; export const SIDEBAR_BROWSE_OBJECTS_MIGRATION_VERSION = 1; export const DEFAULT_EDITOR_SETTINGS: EditorSettings = { - fontFamily: "'Fira Code', 'Cascadia Code', 'Cascadia Mono', 'JetBrains Mono', monospace", + fontFamily: DEFAULT_MONO_FONT_FAMILY, fontSize: 13, uiFontFamily: DEFAULT_UI_FONT_FAMILY, aiFontFamily: AI_CONVERSATION_FONT_FAMILY_DEFAULT, @@ -1392,10 +1392,13 @@ function normalizeUiScale(value: unknown): number { return Math.min(MAX_UI_SCALE, Math.max(MIN_UI_SCALE, Math.round(value * 100) / 100)); } +const LEGACY_DEFAULT_MONO_FONT_FAMILY = "'Fira Code', 'Cascadia Code', 'Cascadia Mono', 'JetBrains Mono', monospace"; + function normalizeFontFamily(value: unknown, fallback: string): string { if (typeof value !== "string") return fallback; const trimmed = value.trim(); - return trimmed || fallback; + if (!trimmed || trimmed === LEGACY_DEFAULT_MONO_FONT_FAMILY) return fallback; + return trimmed; } function normalizeDrawerWidth(value: unknown, min: number, fallback: number): number { diff --git a/apps/desktop/src/styles/tokens.css b/apps/desktop/src/styles/tokens.css index 378a30d0fe..0e5ea20952 100644 --- a/apps/desktop/src/styles/tokens.css +++ b/apps/desktop/src/styles/tokens.css @@ -42,7 +42,7 @@ --font-heading: var(--font-sans); /* Baseline mono token; App.vue overrides it inline with the editor font setting, and plugin sandboxes receive the resolved value via the bridge. */ - --font-mono: 'Fira Code', 'Cascadia Code', 'Cascadia Mono', 'JetBrains Mono', monospace; + --font-mono: 'Fira Code', 'Cascadia Code', 'Cascadia Mono', 'JetBrains Mono', "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", monospace; --color-sidebar-ring: var(--sidebar-ring); --color-sidebar-border: var(--sidebar-border); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); diff --git a/plugins/sdk/dev-host/ui/App.vue b/plugins/sdk/dev-host/ui/App.vue index 629d819b0d..1b6ccba414 100644 --- a/plugins/sdk/dev-host/ui/App.vue +++ b/plugins/sdk/dev-host/ui/App.vue @@ -105,7 +105,7 @@ const themes = () => ({ "--color-destructive": "#f3625f", "--color-destructive-foreground": "#18181b", "--font-sans": '-apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif', - "--font-mono": "'Fira Code', 'Cascadia Code', 'Cascadia Mono', 'JetBrains Mono', monospace", + "--font-mono": "'Fira Code', 'Cascadia Code', 'Cascadia Mono', 'JetBrains Mono', \"PingFang SC\", \"Hiragino Sans GB\", \"Microsoft YaHei\", monospace", "--radius-md": "6px", "--radius-lg": "8px", } @@ -124,7 +124,7 @@ const themes = () => ({ "--color-destructive": "#e7000b", "--color-destructive-foreground": "#ffffff", "--font-sans": '-apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif', - "--font-mono": "'Fira Code', 'Cascadia Code', 'Cascadia Mono', 'JetBrains Mono', monospace", + "--font-mono": "'Fira Code', 'Cascadia Code', 'Cascadia Mono', 'JetBrains Mono', \"PingFang SC\", \"Hiragino Sans GB\", \"Microsoft YaHei\", monospace", "--radius-md": "6px", "--radius-lg": "8px", },