fix(theme): 浅色主题 ANSI 调色板重做到 WCAG AA + MCR/bold 动态开关 + antd 算法随亮度切换;github-light 底色柔化 #f6f8fa
This commit is contained in:
1 parent
2136221f2c
commit
79bea68c13
6 files changed
+114
-57
No files matched your search
+1
-1
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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')
|
||||
Reference in new issue
Block a user