diff --git a/scripts/check_light_theme_contrast.ts b/scripts/check_light_theme_contrast.ts new file mode 100644 index 0000000000..a26a71530b --- /dev/null +++ b/scripts/check_light_theme_contrast.ts @@ -0,0 +1,85 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import tinycolor from 'tinycolor2'; +import { + lightTheme, + darkTheme, + type ColorsTheme, +} from '../packages/cli/src/ui/themes/theme.js'; + +function hexToAnsi(fgHex: string, bgHex: string, text: string): string { + const fg = tinycolor(fgHex).toRgb(); + const bg = tinycolor(bgHex).toRgb(); + return `\x1b[38;2;${fg.r};${fg.g};${fg.b}m\x1b[48;2;${bg.r};${bg.g};${bg.b}m ${text} \x1b[0m`; +} + +function checkTheme(theme: ColorsTheme, themeName: string) { + const background = + theme.Background || (theme.type === 'light' ? '#FAFAFA' : '#1E1E2E'); + const defaultForeground = + theme.Foreground || (theme.type === 'light' ? '#000000' : '#CDD6F4'); + + console.log(`Checking contrast ratios for ${themeName} colors...\n`); + console.log( + `${'Name'.padEnd(20)} | ${'FG Hex'.padEnd(10)} | ${'Sample'.padEnd( + 10, + )} | ${'Contrast'.padEnd(8)} | ${'Passes'}`, + ); + console.log('-'.repeat(75)); + + const results = []; + + for (const [name, color] of Object.entries(theme)) { + if (name === 'Background' || name === 'type' || name === 'GradientColors') + continue; + + // Skip non-color strings (except Foreground which we handle with a default) + if ( + name !== 'Foreground' && + (typeof color !== 'string' || !color.startsWith('#')) + ) + continue; + + const isBgLike = + name.toLowerCase().includes('background') || + name.toLowerCase().includes('diff'); + const fg = isBgLike ? defaultForeground : color || defaultForeground; + const bg = isBgLike ? color || background : background; + + const contrast = tinycolor.readability(bg, fg); + const passes = contrast >= 4.5; + const sample = hexToAnsi(fg, bg, 'TEXT'); + + console.log( + `${name.padEnd(20)} | ${fg.padEnd(10)} | ${sample} | ${contrast + .toFixed(2) + .padEnd(8)} | ${passes ? '✅ PASS' : '❌ FAIL'}`, + ); + + results.push({ name, color, contrast, passes }); + } + + const failures = results.filter((r) => !r.passes); + if (failures.length > 0) { + console.log(`\nFailures detected in ${themeName}:`); + failures.forEach((f) => { + console.log(`- ${f.name} (${f.color}): ${f.contrast.toFixed(2)}`); + }); + return false; + } else { + console.log(`\nAll ${themeName} colors pass the 4.5:1 contrast ratio!`); + return true; + } +} + +const lightPass = checkTheme(lightTheme, 'lightTheme'); +console.log('\n' + '='.repeat(75) + '\n'); +const darkPass = checkTheme(darkTheme, 'darkTheme'); + +if (!lightPass || !darkPass) { + process.exit(1); +}