fix(theme): 浅色主题 ANSI 调色板重做到 WCAG AA + MCR/bold 动态开关 + antd 算法随亮度切换;github-light 底色柔化 #f6f8fa

This commit is contained in:
Bill committed 2026-10-16 20:23:33 +08:00
1 parent 2136221f2c
commit 79bea68c13
6 files changed
+114 -57

No files matched your search

+1 -1
View File
@@ -13,7 +13,7 @@
"preview": "electron-vite preview",
"typecheck": "tsc --noEmit -p tsconfig.node.json && tsc --noEmit -p tsconfig.web.json",
"pretest": "npm run typecheck",
"test": "node tests/build-bundles.cjs && node tests/ssh-loopback.mjs && node tests/commands-store.mjs && node tests/connections-store.mjs && node tests/settings-store.mjs && node tests/local-path-grants.mjs && node tests/lock-store.mjs && node tests/lock-controller.mjs && node tests/lock-shortcuts.mjs && node tests/reserved-accelerators.mjs && node tests/.terminal-title.cjs && node tests/ipc-guard.mjs && node tests/updater-fallback.mjs && node tests/log-sanitizer.mjs && node tests/app-log.mjs && node tests/sftp-timeout.mjs && node tests/.hl-split-smoke.cjs && node tests/.hl-rules.cjs && node tests/zmodem-e2e.mjs && node tests/ssh-session-e2e.mjs && node tests/sysinfo-e2e.mjs && node tests/terminal-cwd.mjs && node tests/terminal-links.mjs && node tests/broadcast-store.mjs && node tests/write-batcher.mjs && node tests/refit-throttle.mjs && node tests/semantic-theme.mjs",
"test": "node tests/build-bundles.cjs && node tests/ssh-loopback.mjs && node tests/commands-store.mjs && node tests/connections-store.mjs && node tests/settings-store.mjs && node tests/local-path-grants.mjs && node tests/lock-store.mjs && node tests/lock-controller.mjs && node tests/lock-shortcuts.mjs && node tests/reserved-accelerators.mjs && node tests/.terminal-title.cjs && node tests/ipc-guard.mjs && node tests/updater-fallback.mjs && node tests/log-sanitizer.mjs && node tests/app-log.mjs && node tests/sftp-timeout.mjs && node tests/.hl-split-smoke.cjs && node tests/.hl-rules.cjs && node tests/zmodem-e2e.mjs && node tests/ssh-session-e2e.mjs && node tests/sysinfo-e2e.mjs && node tests/terminal-cwd.mjs && node tests/terminal-links.mjs && node tests/broadcast-store.mjs && node tests/write-batcher.mjs && node tests/refit-throttle.mjs && node tests/semantic-theme.mjs && node tests/theme-contrast.mjs",
"predist": "node scripts/verify-deps.cjs && npm test && npm install --package-lock-only",
"dist": "electron-vite build && electron-builder --win nsis",
"dist:dir": "electron-vite build && electron-builder --win --dir"
+2 -2
View File
@@ -56,7 +56,7 @@ class ErrorBoundaryClass extends Component<ErrorBoundaryClassProps, ErrorBoundar
alignItems: 'center',
justifyContent: 'center',
gap: 12,
color: '#cccccc'
color: 'var(--ot-text, #cccccc)'
}}
>
<div>{tFor(this.props.language, 'workspace.error.title')}</div>
@@ -66,7 +66,7 @@ class ErrorBoundaryClass extends Component<ErrorBoundaryClassProps, ErrorBoundar
maxHeight: 240,
overflow: 'auto',
whiteSpace: 'pre-wrap',
color: '#ff6b6b',
color: 'var(--ot-danger-fg, #ff6b6b)',
fontSize: 12
}}
>
+6 -3
View File
@@ -10,6 +10,7 @@ import { ErrorBoundary } from './ErrorBoundary'
import ZmodemOffers from './workspace/ZmodemOffers'
import { useSettingsStore } from './settings/store'
import { mix } from './theme/chrome'
import { isLightTheme } from './theme/semantic'
import './global.css'
// Diagnostics: report renderer main-thread freezes after recovery. console.error
@@ -221,14 +222,16 @@ function ThemedConfigProvider({ children }: { children: ReactNode }): React.JSX.
const themeId = useSettingsStore((s) => s.settings.terminal.themeId)
const customThemes = useSettingsStore((s) => s.settings.customThemes)
const colors = getThemeById(themeId, customThemes).colors
const light = isLightTheme(colors.background)
return (
<ConfigProvider
theme={{
algorithm: antdTheme.darkAlgorithm,
// antd has no lightAlgorithm — light is the default algorithm.
algorithm: light ? antdTheme.defaultAlgorithm : antdTheme.darkAlgorithm,
token: {
colorBgBase: colors.background,
colorBgContainer: mix(colors.background, colors.foreground, 0.06) ?? '#1f1f1f',
colorBgElevated: mix(colors.background, colors.foreground, 0.1) ?? '#252525',
colorBgContainer: mix(colors.background, colors.foreground, 0.06) ?? (light ? '#f5f5f5' : '#1f1f1f'),
colorBgElevated: mix(colors.background, colors.foreground, 0.1) ?? (light ? '#f5f5f5' : '#252525'),
colorTextBase: colors.foreground,
borderRadius: 6
}
+17 -9
View File
@@ -9,6 +9,7 @@ import { ClearOutlined, CopyOutlined, ExportOutlined, FolderOpenOutlined, PauseO
import { Checkbox, Modal } from 'antd'
import { DEFAULT_LANGUAGE, tFor, type Language } from '@shared/i18n'
import { getThemeById } from '@shared/theme'
import { isLightTheme } from '@renderer/theme/semantic'
import type { CommandItem } from '@shared/commands'
import { highlightModeOf, type TerminalSettings } from '@shared/settings'
import { rulesForProfile } from '@shared/highlightProfiles'
@@ -846,6 +847,8 @@ export function TerminalView({
// ---- terminal instance: built once per session ----
useLayoutEffect(() => {
const themeColors = getThemeById(themeOpts.themeId, themeOpts.customThemes).colors
const lightTheme = isLightTheme(themeColors.background)
const term = new Terminal({
fontSize: fontOpts.fontSize,
fontFamily: fontOpts.fontFamily,
@@ -857,16 +860,21 @@ export function TerminalView({
cursorBlink: fontOpts.cursorBlink,
cursorStyle: fontOpts.cursorStyle,
cursorInactiveStyle: fontOpts.cursorInactiveStyle,
theme: withChromeColors(getThemeById(themeOpts.themeId, themeOpts.customThemes).colors as ITheme, bgOpts.image !== ''),
theme: withChromeColors(themeColors as ITheme, bgOpts.image !== ''),
allowProposedApi: true,
allowTransparency: true,
overviewRuler: { width: 9, showTopBorder: false, showBottomBorder: false },
drawBoldTextInBrightColors: true,
// Light themes ship WCAG-compliant palettes, but apps still emit
// dark-tuned bright/bold colors; bold must NOT jump to the bright set.
drawBoldTextInBrightColors: !lightTheme,
// In image mode the xterm background is transparent (#00000000), which
// xterm's contrast cache treats as black; a raised contrast floor lifts
// dark foreground colors (light themes) enough to stay readable over
// the wallpaper. Without an image keep 1 (off) to honor theme colors.
minimumContrastRatio: bgOpts.image ? 4.5 : 1,
// the wallpaper. Without an image light themes keep the 4.5 floor to
// rescue low-contrast app output; dark themes keep 1 (off) to honor
// theme colors. (Spelled minimumContrastRatio — minContrastRatio is
// silently ignored.)
minimumContrastRatio: bgOpts.image || lightTheme ? 4.5 : 1,
automaticFontFallback: true,
convertEol: false,
windowsPty: { backend: 'conpty', buildNumber: 0 }
@@ -1261,11 +1269,11 @@ export function TerminalView({
useEffect(() => {
const term = termRef.current
if (!term) return
term.options.theme = withChromeColors(
getThemeById(themeOpts.themeId, themeOpts.customThemes).colors as ITheme,
bgOpts.image !== ''
)
term.options.minimumContrastRatio = bgOpts.image ? 4.5 : 1
const colors = getThemeById(themeOpts.themeId, themeOpts.customThemes).colors
const lightTheme = isLightTheme(colors.background)
term.options.theme = withChromeColors(colors as ITheme, bgOpts.image !== '')
term.options.drawBoldTextInBrightColors = !lightTheme
term.options.minimumContrastRatio = bgOpts.image || lightTheme ? 4.5 : 1
}, [themeOpts, bgOpts])
// ---- renderer mode changes: keep the instance, swap renderer, refit ----
+42 -42
View File
@@ -133,21 +133,21 @@ export const BUILTIN_THEMES: TerminalTheme[] = [
cursorAccent: '#fafafa',
selectionBackground: '#d2d6db',
black: '#383a42',
red: '#e45649',
green: '#50a14f',
yellow: '#c18401',
blue: '#0184bc',
red: '#c2493e',
green: '#40813f',
yellow: '#9a6a01',
blue: '#0177a9',
magenta: '#a626a4',
cyan: '#0997b3',
white: '#fafafa',
cyan: '#07798f',
white: '#717171',
brightBlack: '#4f525e',
brightRed: '#e06c75',
brightGreen: '#98c379',
brightYellow: '#e5c07b',
brightBlue: '#61afef',
brightMagenta: '#c678dd',
brightCyan: '#56b6c2',
brightWhite: '#ffffff'
brightRed: '#b3565e',
brightGreen: '#5b7549',
brightYellow: '#7e6a44',
brightBlue: '#3f729b',
brightMagenta: '#955aa6',
brightCyan: '#38767e',
brightWhite: '#737373'
}
},
{
@@ -184,26 +184,26 @@ export const BUILTIN_THEMES: TerminalTheme[] = [
builtin: true,
colors: {
background: '#fdf6e3',
foreground: '#657b83',
foreground: '#586e75',
cursor: '#586e75',
cursorAccent: '#fdf6e3',
selectionBackground: '#eee8d5',
black: '#073642',
red: '#dc322f',
green: '#859900',
yellow: '#b58900',
blue: '#268bd2',
magenta: '#d33682',
cyan: '#2aa198',
white: '#eee8d5',
brightBlack: '#93a1a1',
brightRed: '#cb4b16',
red: '#d1302d',
green: '#647300',
yellow: '#886700',
blue: '#2076b3',
magenta: '#c8337c',
cyan: '#207972',
white: '#6b6860',
brightBlack: '#677171',
brightRed: '#c14715',
brightGreen: '#586e75',
brightYellow: '#657b83',
brightBlue: '#839496',
brightMagenta: '#6c71c4',
brightCyan: '#93a1a1',
brightWhite: '#fdf6e3'
brightYellow: '#5b6f76',
brightBlue: '#626f71',
brightMagenta: '#6166b0',
brightCyan: '#677171',
brightWhite: '#726f66'
}
},
{
@@ -295,27 +295,27 @@ export const BUILTIN_THEMES: TerminalTheme[] = [
name: 'GitHub Light',
builtin: true,
colors: {
background: '#ffffff',
background: '#f6f8fa',
foreground: '#4d4d4c',
cursor: '#4d4d4c',
cursorAccent: '#ffffff',
cursorAccent: '#f6f8fa',
selectionBackground: '#dcdcdc',
black: '#3e3e3e',
red: '#c91b00',
green: '#00c200',
yellow: '#c7c400',
green: '#007e00',
yellow: '#777600',
blue: '#0225c7',
magenta: '#c930c7',
cyan: '#00c5c7',
white: '#c7c7c7',
magenta: '#bf2ebd',
cyan: '#007677',
white: '#6d6d6d',
brightBlack: '#676767',
brightRed: '#ff6d67',
brightGreen: '#5ff967',
brightYellow: '#ffff6c',
brightBlue: '#6871ff',
brightMagenta: '#ff76ff',
brightCyan: '#5ffdff',
brightWhite: '#ffffff'
brightRed: '#b34c48',
brightGreen: '#307d34',
brightYellow: '#737331',
brightBlue: '#5860d9',
brightMagenta: '#a64da6',
brightCyan: '#2b7273',
brightWhite: '#666666'
}
},
{
+46
View File
@@ -0,0 +1,46 @@
/**
* Builtin theme palette contrast gate. Light themes: foreground + all 16 ANSI
* colors must reach 4.5:1 against the theme background (WCAG AA body text);
* cursor must reach 3:1. Dark themes: only the foreground sanity check (>=4.5)
* — dark palettes are locked aesthetics, not regression targets here.
* Imports ../src/shared/theme.ts directly (zero-import module, Node >= 22.18).
* Run: node tests/theme-contrast.mjs (must exit 0)
*/
import { BUILTIN_THEMES } from '../src/shared/theme.ts'
let failed = 0
const ok = (cond, msg) => {
console.log(` ${cond ? 'ok' : 'FAIL'}: ${msg}`)
if (!cond) failed += 1
}
const L = (h) => {
const c = [1, 3, 5].map((i) => parseInt(h.slice(i, i + 2), 16) / 255)
.map((s) => (s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4))
return 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2]
}
const cr = (a, b) => {
const x = L(a), y = L(b)
return (Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05)
}
const ANSI = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white',
'brightBlack', 'brightRed', 'brightGreen', 'brightYellow', 'brightBlue',
'brightMagenta', 'brightCyan', 'brightWhite']
const isLight = (bg) => L(bg) > 0.5
for (const t of BUILTIN_THEMES) {
const c = t.colors
const light = isLight(c.background)
console.log(`${t.id} (${light ? 'light' : 'dark'}, bg ${c.background}):`)
ok(cr(c.foreground, c.background) >= 4.5, `foreground ${c.foreground} ${cr(c.foreground, c.background).toFixed(2)}`)
ok(cr(c.cursor, c.background) >= 3, `cursor ${c.cursor} ${cr(c.cursor, c.background).toFixed(2)}`)
if (!light) continue
for (const k of ANSI) {
ok(cr(c[k], c.background) >= 4.5, `${k} ${c[k]} ${cr(c[k], c.background).toFixed(2)}`)
}
}
if (failed > 0) {
console.error(`${failed} check(s) failed`)
process.exit(1)
}
console.log('theme-contrast: all ok')