fix(terminal): restore a dark-background contrast floor (#10108)
* fix(terminal): restore a dark-background contrast floor Fully disabling xterm minimumContrastRatio on dark backgrounds (#9599) left near-background body text unreadable — Antigravity paints #262b30 on #1e242a (~1.1:1). Keep light backgrounds at WCAG-AA 4.5 and use a milder dark floor (3) so dark-on-dark body text is lifted without the full light-bg correction strength. Fixes #10104 * fix(terminal): extend dark-bg contrast floor to preview + mobile terminals The dark-background minimumContrastRatio floor (#10104) is applied per `new Terminal()` construction site. Beyond the live pane, agent output also renders in the dashboard popout preview and the mobile WebView, which were still at the floor-1 default, so Antigravity output stayed unreadable there. - AgentTerminalPreview: gate via resolveTerminalMinimumContrastRatio - mobile WebView: port the gate as resolveTerminalContrastFloor (Chrome-74 JS) - tests: builtin-catalog guard + mobile vm-harness coverage Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
e8d5d50c35
commit
1648251fb8
|
|
@ -290,6 +290,7 @@ window.onerror = function(msg) {
|
|||
var defaultTheme = ${JSON.stringify(DEFAULT_TERMINAL_THEME)};
|
||||
var terminalThemeInput = null;
|
||||
var terminalTheme = defaultTheme;
|
||||
var terminalMinimumContrastRatio = 3;
|
||||
var webglAddon = null;
|
||||
var webglRecoveryTimer = null;
|
||||
var activeAltScreenSnapshot = false;
|
||||
|
|
@ -714,6 +715,7 @@ ${TERMINAL_WEBGL_RECOVERY_JS}
|
|||
cols: cols || 80,
|
||||
rows: rows || 24,
|
||||
theme: terminalTheme,
|
||||
minimumContrastRatio: terminalMinimumContrastRatio,
|
||||
fontFamily: terminalFontFamily,
|
||||
fontSize: fontPxForScale(currentTextScale),
|
||||
fontWeight: '300',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
import { Script } from 'node:vm'
|
||||
import { parse } from 'acorn'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { TERMINAL_WEBVIEW_THEME_JS } from './terminal-webview-theme-injected'
|
||||
|
||||
const DARK_FLOOR = 3
|
||||
const LIGHT_FLOOR = 4.5
|
||||
|
||||
// Eval the injected theme JS in a bare context so the declared helpers become
|
||||
// callable properties on it (mirrors terminal-webview-engine.test.ts).
|
||||
function loadThemeInjected(extra: Record<string, unknown> = {}): Record<string, unknown> {
|
||||
const context: Record<string, unknown> = {
|
||||
defaultTheme: { background: '#1a1b26', foreground: '#c0caf5' },
|
||||
...extra
|
||||
}
|
||||
new Script(TERMINAL_WEBVIEW_THEME_JS).runInNewContext(context)
|
||||
return context
|
||||
}
|
||||
|
||||
describe('mobile terminal-webview contrast floor gate', () => {
|
||||
it('parses at the Chrome 74 syntax floor', () => {
|
||||
expect(() => parse(TERMINAL_WEBVIEW_THEME_JS, { ecmaVersion: 2019 })).not.toThrow()
|
||||
})
|
||||
|
||||
it('picks the dark floor for dark composed backgrounds', () => {
|
||||
const { resolveTerminalContrastFloor } = loadThemeInjected() as {
|
||||
resolveTerminalContrastFloor: (bg: unknown) => number
|
||||
}
|
||||
for (const bg of ['#1a1b26', '#1e242a', '#282828', '#000000', 'black']) {
|
||||
expect(resolveTerminalContrastFloor(bg)).toBe(DARK_FLOOR)
|
||||
}
|
||||
})
|
||||
|
||||
it('picks the light floor for light composed backgrounds', () => {
|
||||
const { resolveTerminalContrastFloor } = loadThemeInjected() as {
|
||||
resolveTerminalContrastFloor: (bg: unknown) => number
|
||||
}
|
||||
for (const bg of ['#ffffff', '#fbf1c7', 'white', 'rgb(240 240 240)']) {
|
||||
expect(resolveTerminalContrastFloor(bg)).toBe(LIGHT_FLOOR)
|
||||
}
|
||||
})
|
||||
|
||||
it('composites transparency over the dark app surface before deciding', () => {
|
||||
const { resolveTerminalContrastFloor } = loadThemeInjected() as {
|
||||
resolveTerminalContrastFloor: (bg: unknown) => number
|
||||
}
|
||||
// Fully transparent → app surface (dark) → dark floor.
|
||||
expect(resolveTerminalContrastFloor('transparent')).toBe(DARK_FLOOR)
|
||||
// Faint white over the dark surface stays dark; opaque-enough white flips light.
|
||||
expect(resolveTerminalContrastFloor('rgba(255,255,255,0.15)')).toBe(DARK_FLOOR)
|
||||
expect(resolveTerminalContrastFloor('rgba(255,255,255,0.9)')).toBe(LIGHT_FLOOR)
|
||||
})
|
||||
|
||||
it('defaults unparseable backgrounds to the dark floor so output never stays invisible', () => {
|
||||
const { resolveTerminalContrastFloor } = loadThemeInjected() as {
|
||||
resolveTerminalContrastFloor: (bg: unknown) => number
|
||||
}
|
||||
for (const bg of [undefined, null, '', 'not-a-color', '#12', 42]) {
|
||||
expect(resolveTerminalContrastFloor(bg)).toBe(DARK_FLOOR)
|
||||
}
|
||||
})
|
||||
|
||||
it('writes the resolved floor onto a live terminal when the theme changes', () => {
|
||||
const term = { options: { theme: undefined as unknown, minimumContrastRatio: 1 } }
|
||||
const context = loadThemeInjected({
|
||||
term,
|
||||
document: {
|
||||
documentElement: { style: { background: '' } },
|
||||
body: { style: { background: '' } }
|
||||
}
|
||||
}) as Record<string, unknown> & { applyTerminalTheme: (input: unknown) => void }
|
||||
|
||||
context.applyTerminalTheme({ theme: { background: '#ffffff' } })
|
||||
expect(term.options.minimumContrastRatio).toBe(LIGHT_FLOOR)
|
||||
|
||||
context.applyTerminalTheme({ theme: { background: '#1e242a' } })
|
||||
expect(term.options.minimumContrastRatio).toBe(DARK_FLOOR)
|
||||
})
|
||||
})
|
||||
|
|
@ -1,7 +1,85 @@
|
|||
import { colors } from '../theme/mobile-theme'
|
||||
|
||||
// Theme normalization and page-surface painting injected into the WebView IIFE.
|
||||
// Mirrors the desktop minimumContrastRatio gate (src/renderer/src/lib/terminal-contrast-correction.ts,
|
||||
// #7934/#10104): a dark composed background gets a mild floor of 3 to rescue near-background body text
|
||||
// (e.g. Antigravity's #262b30 on #1e242a) without over-brightening vibrant ANSI colors; a light
|
||||
// background keeps the WCAG-AA 4.5 floor. Gate on the composed background luminance, not app mode,
|
||||
// because either theme slot can hold either kind of theme.
|
||||
export const TERMINAL_WEBVIEW_THEME_JS = `
|
||||
var DARK_BG_MIN_CONTRAST = 3;
|
||||
var LIGHT_BG_MIN_CONTRAST = 4.5;
|
||||
// Dark app surface a transparent terminal background composites over (matches desktop APP_SURFACE_COLORS.dark).
|
||||
var CONTRAST_APP_SURFACE = { r: 10, g: 10, b: 10 };
|
||||
|
||||
function parseTerminalBackgroundRgba(value) {
|
||||
if (typeof value !== 'string') return null;
|
||||
var v = value.trim().toLowerCase();
|
||||
if (!v) return null;
|
||||
if (v === 'black') return { r: 0, g: 0, b: 0, a: 1 };
|
||||
if (v === 'white') return { r: 255, g: 255, b: 255, a: 1 };
|
||||
if (v === 'transparent') return { r: 0, g: 0, b: 0, a: 0 };
|
||||
var hex = v.match(/^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/);
|
||||
if (hex) {
|
||||
var h = hex[1];
|
||||
var ch;
|
||||
if (h.length === 3 || h.length === 4) {
|
||||
ch = h.split('').map(function (p) { return parseInt(p + p, 16); });
|
||||
} else {
|
||||
ch = [];
|
||||
for (var i = 0; i < h.length; i += 2) ch.push(parseInt(h.slice(i, i + 2), 16));
|
||||
}
|
||||
return { r: ch[0], g: ch[1], b: ch[2], a: ch[3] === undefined ? 1 : ch[3] / 255 };
|
||||
}
|
||||
var rgb = v.match(/^rgba?\\(([^)]+)\\)$/);
|
||||
if (!rgb) return null;
|
||||
var parts = rgb[1].indexOf(',') >= 0 ? rgb[1].split(',') : rgb[1].split(/[\\s/]+/);
|
||||
parts = parts.map(function (p) { return p.trim(); }).filter(function (p) { return p.length > 0; });
|
||||
if (parts.length < 3) return null;
|
||||
var channel = function (p) {
|
||||
var n = p.charAt(p.length - 1) === '%' ? (parseFloat(p) / 100) * 255 : parseFloat(p);
|
||||
return isFinite(n) ? Math.min(255, Math.max(0, Math.round(n))) : null;
|
||||
};
|
||||
var r = channel(parts[0]), g = channel(parts[1]), b = channel(parts[2]);
|
||||
if (r === null || g === null || b === null) return null;
|
||||
var a = 1;
|
||||
if (parts[3] !== undefined) {
|
||||
var raw = parts[3].charAt(parts[3].length - 1) === '%' ? parseFloat(parts[3]) / 100 : parseFloat(parts[3]);
|
||||
a = isFinite(raw) ? Math.min(1, Math.max(0, raw)) : 1;
|
||||
}
|
||||
return { r: r, g: g, b: b, a: a };
|
||||
}
|
||||
|
||||
function terminalRelativeLuminance(rgb) {
|
||||
var lin = function (c) {
|
||||
var n = c / 255;
|
||||
return n <= 0.03928 ? n / 12.92 : Math.pow((n + 0.055) / 1.055, 2.4);
|
||||
};
|
||||
return 0.2126 * lin(rgb.r) + 0.7152 * lin(rgb.g) + 0.0722 * lin(rgb.b);
|
||||
}
|
||||
|
||||
function terminalContrastRatio(a, b) {
|
||||
var la = terminalRelativeLuminance(a), lb = terminalRelativeLuminance(b);
|
||||
return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
|
||||
}
|
||||
|
||||
// Pick the xterm minimumContrastRatio floor from the composed terminal background.
|
||||
// Unparseable input defaults to the dark floor so agent output never stays invisible.
|
||||
function resolveTerminalContrastFloor(background) {
|
||||
var color = parseTerminalBackgroundRgba(background);
|
||||
if (!color) return DARK_BG_MIN_CONTRAST;
|
||||
var composited = color.a < 1
|
||||
? {
|
||||
r: Math.round(color.r * color.a + CONTRAST_APP_SURFACE.r * (1 - color.a)),
|
||||
g: Math.round(color.g * color.a + CONTRAST_APP_SURFACE.g * (1 - color.a)),
|
||||
b: Math.round(color.b * color.a + CONTRAST_APP_SURFACE.b * (1 - color.a))
|
||||
}
|
||||
: color;
|
||||
var isLight = terminalContrastRatio({ r: 0, g: 0, b: 0 }, composited) >=
|
||||
terminalContrastRatio({ r: 255, g: 255, b: 255 }, composited);
|
||||
return isLight ? LIGHT_BG_MIN_CONTRAST : DARK_BG_MIN_CONTRAST;
|
||||
}
|
||||
|
||||
function normalizeTerminalTheme(input) {
|
||||
var source = input && typeof input === 'object' && input.theme && typeof input.theme === 'object'
|
||||
? input.theme
|
||||
|
|
@ -22,6 +100,10 @@ export const TERMINAL_WEBVIEW_THEME_JS = `
|
|||
var background = terminalTheme.background || '${colors.terminalBg}';
|
||||
document.documentElement.style.background = background;
|
||||
document.body.style.background = background;
|
||||
if (term) term.options.theme = terminalTheme;
|
||||
terminalMinimumContrastRatio = resolveTerminalContrastFloor(background);
|
||||
if (term) {
|
||||
term.options.theme = terminalTheme;
|
||||
term.options.minimumContrastRatio = terminalMinimumContrastRatio;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import {
|
|||
} from '@/components/terminal-pane/terminal-ime-native-text-forwarder'
|
||||
import { getMacNativeTextInputSourceTracker } from '@/components/terminal-pane/terminal-ime-input-source'
|
||||
import { composeActiveTerminalTheme } from '@/components/terminal-pane/terminal-appearance'
|
||||
import { resolveTerminalMinimumContrastRatio } from '@/lib/terminal-contrast-correction'
|
||||
import { useSystemPrefersDark } from '@/components/terminal-pane/use-system-prefers-dark'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { getBuiltinTheme, resolveEffectiveTerminalAppearance } from '@/lib/terminal-theme'
|
||||
|
|
@ -53,15 +54,16 @@ export function AgentTerminalPreview({ ptyId }: { ptyId: string }): React.JSX.El
|
|||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const settings = useAppStore((state) => state.settings)
|
||||
const systemPrefersDark = useSystemPrefersDark()
|
||||
const terminalTheme = useMemo(() => {
|
||||
const { terminalTheme, terminalMode } = useMemo(() => {
|
||||
if (!settings) {
|
||||
return null
|
||||
return { terminalTheme: null, terminalMode: 'dark' as const }
|
||||
}
|
||||
const appearance = resolveEffectiveTerminalAppearance(settings, systemPrefersDark)
|
||||
return composeActiveTerminalTheme(
|
||||
const theme = composeActiveTerminalTheme(
|
||||
appearance.theme ?? getBuiltinTheme(appearance.themeName),
|
||||
settings
|
||||
)
|
||||
return { terminalTheme: theme, terminalMode: appearance.mode }
|
||||
}, [settings, systemPrefersDark])
|
||||
// A null snapshot means no serializer knows this pty (it died or was never
|
||||
// spawned this session) — say so instead of painting a silent blank terminal.
|
||||
|
|
@ -271,6 +273,10 @@ export function AgentTerminalPreview({ ptyId }: { ptyId: string }): React.JSX.El
|
|||
cols: clamp(snap.cols ?? FALLBACK_COLS, 2, 500),
|
||||
rows: clamp(snap.rows ?? FALLBACK_ROWS, 2, 200),
|
||||
theme: terminalTheme ?? undefined,
|
||||
minimumContrastRatio: resolveTerminalMinimumContrastRatio(
|
||||
terminalTheme?.background,
|
||||
terminalMode
|
||||
),
|
||||
scrollback: 1000
|
||||
})
|
||||
try {
|
||||
|
|
@ -399,7 +405,7 @@ export function AgentTerminalPreview({ ptyId }: { ptyId: string }): React.JSX.El
|
|||
void window.api.terminalPreview.unsubscribe(ptyId)
|
||||
terminal?.dispose()
|
||||
}
|
||||
}, [ptyId, terminalTheme])
|
||||
}, [ptyId, terminalTheme, terminalMode])
|
||||
|
||||
return (
|
||||
// Why: a size FIXED by the viewport (not shrink-to-fit) + overflow-hidden
|
||||
|
|
|
|||
|
|
@ -417,13 +417,15 @@ describe('applyTerminalAppearance theme assignment', () => {
|
|||
expect(pane.terminal.options.minimumContrastRatio).toBe(4.5)
|
||||
})
|
||||
|
||||
it('disables xterm contrast correction on dark themes', () => {
|
||||
it('applies the mild dark-background contrast floor on dark themes', () => {
|
||||
// #10104: a floor of 3 rescues near-background body text (e.g. Antigravity's #262b30 on #1e242a)
|
||||
// without the 4.5-floor over-brightening of vibrant ANSI colors that #7934 fixed.
|
||||
const pane = makePane(1)
|
||||
const settings = getDefaultSettings('/tmp')
|
||||
|
||||
apply(pane, { ...settings, theme: 'dark' })
|
||||
|
||||
expect(pane.terminal.options.minimumContrastRatio).toBe(1)
|
||||
expect(pane.terminal.options.minimumContrastRatio).toBe(3)
|
||||
})
|
||||
|
||||
it('re-gates contrast correction when the theme flips live', () => {
|
||||
|
|
@ -434,17 +436,17 @@ describe('applyTerminalAppearance theme assignment', () => {
|
|||
expect(pane.terminal.options.minimumContrastRatio).toBe(4.5)
|
||||
|
||||
apply(pane, { ...settings, theme: 'dark' })
|
||||
expect(pane.terminal.options.minimumContrastRatio).toBe(1)
|
||||
expect(pane.terminal.options.minimumContrastRatio).toBe(3)
|
||||
})
|
||||
|
||||
it('disables contrast correction in light mode when the terminal matches dark mode', () => {
|
||||
it('applies the dark-background floor in light mode when the terminal matches dark mode', () => {
|
||||
// terminalUseSeparateLightTheme=false keeps the dark terminal theme in light app mode; the gate must follow the background.
|
||||
const pane = makePane(1)
|
||||
const settings = getDefaultSettings('/tmp')
|
||||
|
||||
apply(pane, { ...settings, theme: 'light', terminalUseSeparateLightTheme: false })
|
||||
|
||||
expect(pane.terminal.options.minimumContrastRatio).toBe(1)
|
||||
expect(pane.terminal.options.minimumContrastRatio).toBe(3)
|
||||
})
|
||||
|
||||
it('keeps contrast correction in dark mode when a light theme fills the dark slot', () => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,96 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
DARK_BG_MIN_CONTRAST,
|
||||
LIGHT_BG_MIN_CONTRAST,
|
||||
resolveTerminalMinimumContrastRatio
|
||||
} from './terminal-contrast-correction'
|
||||
import { TERMINAL_THEME_CATALOG } from './terminal-themes'
|
||||
|
||||
// WCAG relative-luminance contrast ratio, matching xterm's minimumContrastRatio gate.
|
||||
function contrastRatio(a: string, b: string): number {
|
||||
const lum = (hex: string): number => {
|
||||
const n = Number.parseInt(hex.replace('#', ''), 16)
|
||||
const toLinear = (channel: number): number => {
|
||||
const c = channel / 255
|
||||
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)
|
||||
}
|
||||
const r = toLinear((n >> 16) & 0xff)
|
||||
const g = toLinear((n >> 8) & 0xff)
|
||||
const bl = toLinear(n & 0xff)
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * bl
|
||||
}
|
||||
const la = lum(a)
|
||||
const lb = lum(b)
|
||||
return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05)
|
||||
}
|
||||
|
||||
describe('resolveTerminalMinimumContrastRatio', () => {
|
||||
it('returns the light-background floor for a light terminal background', () => {
|
||||
expect(resolveTerminalMinimumContrastRatio('#ffffff', 'light')).toBe(LIGHT_BG_MIN_CONTRAST)
|
||||
})
|
||||
|
||||
it('returns the dark-background floor for a dark terminal background', () => {
|
||||
expect(resolveTerminalMinimumContrastRatio('#1e242a', 'dark')).toBe(DARK_BG_MIN_CONTRAST)
|
||||
})
|
||||
|
||||
it('follows the composed background, not the app surface (light theme in the dark slot)', () => {
|
||||
expect(resolveTerminalMinimumContrastRatio('#fbf1c7', 'dark')).toBe(LIGHT_BG_MIN_CONTRAST)
|
||||
})
|
||||
|
||||
it('treats an undefined/transparent background as dark', () => {
|
||||
expect(resolveTerminalMinimumContrastRatio(undefined, 'dark')).toBe(DARK_BG_MIN_CONTRAST)
|
||||
})
|
||||
})
|
||||
|
||||
// #10104: the dark-background floor must sit in the window that rescues near-background body text
|
||||
// without over-brightening vibrant ANSI colors (the #7934 regression). Guarding both edges keeps a
|
||||
// future tweak from silently sliding out of that window.
|
||||
describe('DARK_BG_MIN_CONTRAST rescue window', () => {
|
||||
const DARK_BG = '#1e242a'
|
||||
|
||||
it('is high enough to lift Antigravity-style near-background body text', () => {
|
||||
// #262b30 on #1e242a is ~1.1:1 — invisible at floor 1. The floor must exceed it so xterm corrects it.
|
||||
expect(contrastRatio(DARK_BG, '#262b30')).toBeLessThan(DARK_BG_MIN_CONTRAST)
|
||||
})
|
||||
|
||||
it('stays below the contrast that saturated ANSI colors naturally reach on a dark background', () => {
|
||||
// Normal red/blue/magenta sit at ~3.0-3.4:1 here; the floor must not exceed them or xterm would
|
||||
// wash them toward white — exactly the over-brightening #7934 disabled the 4.5 floor to avoid.
|
||||
for (const ansi of ['#cd3131', '#2472c8', '#bc3fbc']) {
|
||||
expect(contrastRatio(DARK_BG, ansi)).toBeGreaterThanOrEqual(DARK_BG_MIN_CONTRAST)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// #10104: pin which real builtin dark themes have normal ANSI colors below the floor, so a new theme
|
||||
// or floor tweak forces an explicit decision instead of a silent #7934-style regression.
|
||||
describe('DARK_BG_MIN_CONTRAST vs the builtin theme catalog', () => {
|
||||
// Normal (non-bright) chromatic ANSI channels — the vibrant body-text colors #7934 protects.
|
||||
// Bright variants are excluded: several themes (e.g. Solarized) repurpose them as achromatic grays.
|
||||
const CHROMATIC_ANSI = ['red', 'green', 'yellow', 'blue', 'magenta', 'cyan'] as const
|
||||
|
||||
// Accepted below-floor cases: near-illegible primaries on very dark backgrounds where the mild
|
||||
// lift helps rather than harms. Keep in sync with the comment in terminal-contrast-correction.ts.
|
||||
const ACCEPTED_BELOW_FLOOR = ['Gruvbox Dark:red', 'Homebrew:blue', 'Homebrew:red']
|
||||
|
||||
it('leaves every dark-theme chromatic ANSI color at/above the floor, except the pinned exceptions', () => {
|
||||
const belowFloor: string[] = []
|
||||
for (const [name, theme] of Object.entries(TERMINAL_THEME_CATALOG)) {
|
||||
const background = theme.background
|
||||
// Only dark-slot themes get the dark floor; the resolver picks it exactly for those.
|
||||
if (
|
||||
!background ||
|
||||
resolveTerminalMinimumContrastRatio(background, 'dark') !== DARK_BG_MIN_CONTRAST
|
||||
) {
|
||||
continue
|
||||
}
|
||||
for (const channel of CHROMATIC_ANSI) {
|
||||
const color = theme[channel]
|
||||
if (color && contrastRatio(background, color) < DARK_BG_MIN_CONTRAST) {
|
||||
belowFloor.push(`${name}:${channel}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
expect(belowFloor.sort()).toEqual(ACCEPTED_BELOW_FLOOR)
|
||||
})
|
||||
})
|
||||
|
|
@ -1,10 +1,15 @@
|
|||
import { isTerminalBackgroundLight } from '@/lib/terminal-title-contrast'
|
||||
|
||||
// xterm minimumContrastRatio tuning (#7934). Light backgrounds keep WCAG-AA correction so invisible
|
||||
// white/bright-white ANSI body text stays readable; dark backgrounds disable it (ratio 1) because
|
||||
// correction over-brightens vibrant ANSI colors.
|
||||
// xterm minimumContrastRatio tuning (#7934, #9599, #10104). Light backgrounds keep WCAG-AA correction so
|
||||
// invisible white/bright-white ANSI body text stays readable. Dark backgrounds use a mild floor of 3
|
||||
// (WCAG-AA large-text): high enough to rescue near-background body text — e.g. Antigravity's #262b30
|
||||
// on #1e242a (~1.1:1) — while staying far milder than the light-background 4.5 that badly
|
||||
// over-brightened vibrant colors (#7934). On most dark themes saturated ANSI colors already clear 3:1
|
||||
// and are untouched; a few (e.g. Homebrew red/blue on pure black, Gruvbox Dark red) sit below 3:1 and
|
||||
// get mildly lifted — accepted because those were already near-illegible, so the nudge helps rather
|
||||
// than harms (see the builtin-catalog exceptions pinned in terminal-contrast-correction.test.ts).
|
||||
export const LIGHT_BG_MIN_CONTRAST = 4.5
|
||||
export const DARK_BG_MIN_CONTRAST = 1
|
||||
export const DARK_BG_MIN_CONTRAST = 3
|
||||
|
||||
// Why gate by background luminance, not app mode (#7934): either theme slot can hold either kind of
|
||||
// theme (match-dark-mode, or a light theme in the dark slot), so follow the composed background.
|
||||
|
|
|
|||
Loading…
Reference in New Issue