From 689bae8ff805b0feaf115fc39f9e5a1b502f2d53 Mon Sep 17 00:00:00 2001 From: wsp Date: Wed, 23 Sep 2026 12:44:12 +0800 Subject: [PATCH] fix(web-ui): align Chinese code and terminal fonts Use HarmonyOS Sans SC for Chinese glyphs in the bundled mono stack. Resolve active font tokens for interactive terminals and command output, and include the font family in output row-height cache matching. Cover runtime font overrides and canonical fallback in tests. --- .../BackgroundTerminalProjection.tsx | 4 ++-- .../src/font-profiles/harmony-bundled.css | 2 ++ .../LazyTerminalOutputRenderer.test.tsx | 18 ++++++++++++++++++ .../src/tools/terminal/components/Terminal.tsx | 4 ++-- .../components/TerminalOutputRenderer.tsx | 4 ++-- .../components/terminalOutputPresentation.ts | 13 +++++++++---- 6 files changed, 35 insertions(+), 10 deletions(-) diff --git a/src/web-ui/src/flow_chat/components/background-command/BackgroundTerminalProjection.tsx b/src/web-ui/src/flow_chat/components/background-command/BackgroundTerminalProjection.tsx index 5419c0df3a..14f6a2c40d 100644 --- a/src/web-ui/src/flow_chat/components/background-command/BackgroundTerminalProjection.tsx +++ b/src/web-ui/src/flow_chat/components/background-command/BackgroundTerminalProjection.tsx @@ -4,7 +4,7 @@ import { FitAddon } from '@xterm/addon-fit'; import { xtermAppearanceAdapter } from '@/infrastructure/appearance/adapters/XtermAppearanceAdapter'; import { registerTerminalActions, unregisterTerminalActions } from '@/tools/terminal/services/TerminalActionManager'; import { - TERMINAL_OUTPUT_FONT_FAMILY, TERMINAL_OUTPUT_FONT_SIZE, TERMINAL_OUTPUT_FONT_WEIGHT, + readTerminalOutputFontFamily, TERMINAL_OUTPUT_FONT_SIZE, TERMINAL_OUTPUT_FONT_WEIGHT, TERMINAL_OUTPUT_FONT_WEIGHT_BOLD, TERMINAL_OUTPUT_LINE_HEIGHT, } from '@/tools/terminal/components/terminalOutputPresentation'; import type { TerminalProjection } from './backgroundTerminalReplay'; @@ -26,7 +26,7 @@ export default function BackgroundTerminalProjection({ projection }: { const element = host.current!; const terminal = new Terminal({ disableStdin: true, cursorBlink: false, cursorInactiveStyle: 'none', - fontFamily: TERMINAL_OUTPUT_FONT_FAMILY, fontSize: TERMINAL_OUTPUT_FONT_SIZE, + fontFamily: readTerminalOutputFontFamily(), fontSize: TERMINAL_OUTPUT_FONT_SIZE, fontWeight: TERMINAL_OUTPUT_FONT_WEIGHT, fontWeightBold: TERMINAL_OUTPUT_FONT_WEIGHT_BOLD, lineHeight: TERMINAL_OUTPUT_LINE_HEIGHT, scrollback: 5000, convertEol: true, theme: xtermAppearanceAdapter.getColors('output'), diff --git a/src/web-ui/src/font-profiles/harmony-bundled.css b/src/web-ui/src/font-profiles/harmony-bundled.css index 06d392c145..9274e36f1c 100644 --- a/src/web-ui/src/font-profiles/harmony-bundled.css +++ b/src/web-ui/src/font-profiles/harmony-bundled.css @@ -85,6 +85,8 @@ } :where([data-openbitfun-design-system-root]):lang(zh-CN) { + /* Keep Latin code monospaced and use the same Chinese face as body text. */ + --openbitfun-font-family-mono: "JetBrains Mono", "Fira Code", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, "Cascadia Mono", "Cascadia Code", Consolas, "Liberation Mono", "Courier New", "OpenBitFun HarmonyOS Sans SC", "OpenBitFun HarmonyOS Sans", "Microsoft YaHei UI", monospace; --openbitfun-font-family-sans: "OpenBitFun HarmonyOS Sans SC", "OpenBitFun HarmonyOS Sans", system-ui, "Microsoft YaHei UI", sans-serif; --openbitfun-font-family-control: "OpenBitFun HarmonyOS Sans SC", "OpenBitFun HarmonyOS Sans", system-ui, "Microsoft YaHei UI", sans-serif; } diff --git a/src/web-ui/src/tools/terminal/components/LazyTerminalOutputRenderer.test.tsx b/src/web-ui/src/tools/terminal/components/LazyTerminalOutputRenderer.test.tsx index 5112e5a67c..a172790841 100644 --- a/src/web-ui/src/tools/terminal/components/LazyTerminalOutputRenderer.test.tsx +++ b/src/web-ui/src/tools/terminal/components/LazyTerminalOutputRenderer.test.tsx @@ -5,6 +5,24 @@ import { createRoot, type Root } from 'react-dom/client'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { TerminalOutputFallback } from './LazyTerminalOutputRenderer'; +import { readTerminalOutputFontFamily } from './terminalOutputPresentation'; +import { getTypographyTokenValue } from '@/infrastructure/design-system/typographyRuntime'; + +it('resolves the active output font at use time rather than module import time', () => { + const style = document.documentElement.style; + const property = '--openbitfun-font-family-mono'; + const previous = style.getPropertyValue(property); + try { + const family = '"Fira Code", "OpenBitFun HarmonyOS Sans SC", monospace'; + style.setProperty(property, family); + expect(readTerminalOutputFontFamily()).toBe(family); + style.removeProperty(property); + expect(readTerminalOutputFontFamily()).toBe(getTypographyTokenValue('font.family.mono')); + } finally { + if (previous) style.setProperty(property, previous); + else style.removeProperty(property); + } +}); describe('TerminalOutputFallback', () => { let container: HTMLDivElement; diff --git a/src/web-ui/src/tools/terminal/components/Terminal.tsx b/src/web-ui/src/tools/terminal/components/Terminal.tsx index 27f2976d2c..4910bc8eec 100644 --- a/src/web-ui/src/tools/terminal/components/Terminal.tsx +++ b/src/web-ui/src/tools/terminal/components/Terminal.tsx @@ -20,7 +20,7 @@ import { sendDebugProbe } from '@/shared/utils/debugProbe'; import { nowMs } from '@/shared/utils/timing'; import { getTypographyTokenNumber, - getTypographyTokenValue, + readActiveTypographyTokenValue, readActiveTypographyTokenPx, } from '@/infrastructure/design-system/typographyRuntime'; import { fontPreferenceService } from '@/infrastructure/font-preference'; @@ -188,7 +188,6 @@ function readTerminalFontSize(): number { } const DEFAULT_OPTIONS: TerminalOptions = { - fontFamily: getTypographyTokenValue('font.family.mono'), lineHeight: getTypographyTokenNumber('lineHeight.tight'), minimumContrastRatio: DEFAULT_XTERM_MINIMUM_CONTRAST_RATIO, cursorStyle: 'block', @@ -251,6 +250,7 @@ const Terminal = forwardRef(({ // the black-background flash that occurs when a light theme is active. const mergedOptions = { ...DEFAULT_OPTIONS, + fontFamily: readActiveTypographyTokenValue('font.family.mono'), fontSize, ...options, theme: getInitialXtermColors(), diff --git a/src/web-ui/src/tools/terminal/components/TerminalOutputRenderer.tsx b/src/web-ui/src/tools/terminal/components/TerminalOutputRenderer.tsx index a23cc98c9f..1b002e5a2d 100644 --- a/src/web-ui/src/tools/terminal/components/TerminalOutputRenderer.tsx +++ b/src/web-ui/src/tools/terminal/components/TerminalOutputRenderer.tsx @@ -14,7 +14,7 @@ import { calculateTerminalOutputHeight, getEstimatedTerminalOutputRowHeight, prepareReadOnlyTerminalOutput, - TERMINAL_OUTPUT_FONT_FAMILY, + readTerminalOutputFontFamily, TERMINAL_OUTPUT_FONT_SIZE, TERMINAL_OUTPUT_FONT_WEIGHT, TERMINAL_OUTPUT_FONT_WEIGHT_BOLD, @@ -122,7 +122,7 @@ const TerminalOutputRendererComponent = forwardRef 0 && cachedDevicePixelRatio === devicePixelRatio) { + const fontFamily = readTerminalOutputFontFamily(); + if (cachedRowHeight > 0 && cachedDevicePixelRatio === devicePixelRatio && cachedFontFamily === fontFamily) { return cachedRowHeight; } @@ -37,7 +41,7 @@ export function getEstimatedTerminalOutputRowHeight(): number { return DEFAULT_OUTPUT_ROW_HEIGHT; } - context.font = `${TERMINAL_OUTPUT_FONT_SIZE}px ${TERMINAL_OUTPUT_FONT_FAMILY}`; + context.font = `${TERMINAL_OUTPUT_FONT_SIZE}px ${fontFamily}`; const metrics = context.measureText('W'); const fontHeight = metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent; if (!Number.isFinite(fontHeight) || fontHeight <= 0) { @@ -47,6 +51,7 @@ export function getEstimatedTerminalOutputRowHeight(): number { const deviceCharHeight = Math.ceil(fontHeight * devicePixelRatio); const deviceCellHeight = Math.floor(deviceCharHeight * TERMINAL_OUTPUT_LINE_HEIGHT); cachedDevicePixelRatio = devicePixelRatio; + cachedFontFamily = fontFamily; cachedRowHeight = deviceCellHeight / devicePixelRatio; return cachedRowHeight; } catch {