Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 34 additions & 58 deletions packages/cli/src/ui/themes/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,69 +185,45 @@ export interface ColorsTheme {

export const lightTheme: ColorsTheme = {
type: 'light',
Background: '#FAFAFA',
Foreground: '',
LightBlue: '#89BDCD',
AccentBlue: '#3B82F6',
AccentPurple: '#8B5CF6',
AccentCyan: '#06B6D4',
AccentGreen: '#3CA84B',
AccentYellow: '#D5A40A',
AccentRed: '#DD4C4C',
DiffAdded: '#C6EAD8',
DiffRemoved: '#FFCCCC',
Comment: '#008000',
Gray: '#97a0b0',
DarkGray: interpolateColor('#FAFAFA', '#97a0b0', DEFAULT_BORDER_OPACITY),
InputBackground: interpolateColor(
'#FAFAFA',
'#97a0b0',
DEFAULT_INPUT_BACKGROUND_OPACITY,
),
MessageBackground: interpolateColor(
'#FAFAFA',
'#97a0b0',
DEFAULT_INPUT_BACKGROUND_OPACITY,
),
FocusBackground: interpolateColor(
'#FAFAFA',
'#3CA84B',
DEFAULT_SELECTION_OPACITY,
),
Background: '#FFFFFF',
Foreground: '#000000',
LightBlue: '#005FAF',
AccentBlue: '#005FAF',
AccentPurple: '#5F00FF',
AccentCyan: '#005F87',
AccentGreen: '#005F00',
AccentYellow: '#875F00',
AccentRed: '#AF0000',
DiffAdded: '#D7FFD7',
DiffRemoved: '#FFD7D7',
Comment: '#008700',
Gray: '#5F5F5F',
DarkGray: '#5F5F5F',
InputBackground: '#E4E4E4',
MessageBackground: '#FAFAFA',
FocusBackground: '#D7FFD7',
GradientColors: ['#4796E4', '#847ACE', '#C3677F'],
};

export const darkTheme: ColorsTheme = {
type: 'dark',
Background: '#1E1E2E',
Foreground: '',
LightBlue: '#ADD8E6',
AccentBlue: '#89B4FA',
AccentPurple: '#CBA6F7',
AccentCyan: '#89DCEB',
AccentGreen: '#A6E3A1',
AccentYellow: '#F9E2AF',
AccentRed: '#F38BA8',
DiffAdded: '#28350B',
DiffRemoved: '#430000',
Comment: '#6C7086',
Gray: '#6C7086',
DarkGray: interpolateColor('#1E1E2E', '#6C7086', DEFAULT_BORDER_OPACITY),
InputBackground: interpolateColor(
'#1E1E2E',
'#6C7086',
DEFAULT_INPUT_BACKGROUND_OPACITY,
),
MessageBackground: interpolateColor(
'#1E1E2E',
'#6C7086',
DEFAULT_INPUT_BACKGROUND_OPACITY,
),
FocusBackground: interpolateColor(
'#1E1E2E',
'#A6E3A1',
DEFAULT_SELECTION_OPACITY,
),
Background: '#000000',
Foreground: '#FFFFFF',
LightBlue: '#AFD7D7',
AccentBlue: '#87AFFF',
AccentPurple: '#D7AFFF',
AccentCyan: '#87D7D7',
AccentGreen: '#D7FFD7',
AccentYellow: '#FFFFAF',
AccentRed: '#FF87AF',
DiffAdded: '#005F00',
DiffRemoved: '#5F0000',
Comment: '#AFAFAF',
Gray: '#AFAFAF',
DarkGray: '#878787',
InputBackground: '#5F5F5F',
MessageBackground: '#5F5F5F',
FocusBackground: '#005F00',
GradientColors: ['#4796E4', '#847ACE', '#C3677F'],
};

Expand Down
198 changes: 198 additions & 0 deletions scripts/preview-dark-theme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import chalk from 'chalk';
import tinycolor from 'tinycolor2';
import tinygradient from 'tinygradient';
import {
darkTheme,
lightTheme,
type ColorsTheme,
} from '../packages/cli/src/ui/themes/theme.js';

const BLACK = '#000000';
const WHITE = '#FFFFFF';
const TABLE_WIDTH = 85;
const backgroundColors = new Set([
'Background',
'DiffAdded',
'DiffRemoved',
'InputBackground',
'MessageBackground',
'FocusBackground',
]);

// xterm-256 levels as specified
const CUBE_LEVELS = [0, 95, 135, 175, 215, 255];
const GRAY_LEVELS = Array.from({ length: 24 }, (_, i) => 8 + i * 10);

function toHex(n: number) {
return n.toString(16).padStart(2, '0');
}

function getClosestXterm(hex: string): { code: number; hex: string } {
if (!hex || hex === '') return { code: 0, hex: '#000000' };
const color = tinycolor(hex).toRgb();

// 1. Check Grayscale ramp (232-255)
let bestGray = -1;
let minGrayDist = Infinity;
// Simple grayscale conversion
const avg = (color.r + color.g + color.b) / 3;
for (let i = 0; i < GRAY_LEVELS.length; i++) {
const dist = Math.abs(GRAY_LEVELS[i] - avg);
if (dist < minGrayDist) {
minGrayDist = dist;
bestGray = i;
}
}

// 2. Check Color Cube (16-231)
const findClosestLevel = (val: number) => {
let bestL = 0;
let minDist = Infinity;
for (let i = 0; i < CUBE_LEVELS.length; i++) {
const dist = Math.abs(CUBE_LEVELS[i] - val);
if (dist < minDist) {
minDist = dist;
bestL = i;
}
}
return bestL;
};

const rIdx = findClosestLevel(color.r);
const gIdx = findClosestLevel(color.g);
const bIdx = findClosestLevel(color.b);

const cubeDist = Math.sqrt(
Math.pow(CUBE_LEVELS[rIdx] - color.r, 2) +
Math.pow(CUBE_LEVELS[gIdx] - color.g, 2) +
Math.pow(CUBE_LEVELS[bIdx] - color.b, 2),
);

// Compare grayscale vs cube distance (cube typically wins if it's colorful)
// We'll also check if the input is very desaturated
const isDesaturated =
Math.max(color.r, color.g, color.b) - Math.min(color.r, color.g, color.b) <
10;

if (isDesaturated && minGrayDist < cubeDist) {
const code = 232 + bestGray;
const v = GRAY_LEVELS[bestGray];
return { code, hex: `#${toHex(v)}${toHex(v)}${toHex(v)}` };
} else {
const code = 16 + 36 * rIdx + 6 * gIdx + bIdx;
return {
code,
hex: `#${toHex(CUBE_LEVELS[rIdx])}${toHex(CUBE_LEVELS[gIdx])}${toHex(CUBE_LEVELS[bIdx])}`,
};
}
}

function getRating(contrast: number): string {
const contrastStr = `${contrast.toFixed(2)}:1`;
if (contrast >= 7) return chalk.green(`(AAA) ${contrastStr.padEnd(7)}`);
if (contrast >= 4.5) return chalk.cyan(`(AA) ${contrastStr.padEnd(7)}`);
if (contrast >= 3) return chalk.yellow(`(LG) ${contrastStr.padEnd(7)}`);
return chalk.red(`(FAIL) ${contrastStr.padEnd(7)}`);
}

function previewTheme(themeName: string, theme: ColorsTheme) {
const isDark = theme.type === 'dark';
const baseBackground = isDark ? BLACK : WHITE;
const defaultForeground = isDark ? WHITE : BLACK;
const foreground =
theme.Foreground && theme.Foreground !== ''
? theme.Foreground
: defaultForeground;

console.log(
'\n' +
chalk.bold.underline(
`${themeName}: Hex vs Corrected xterm-256 Comparison`,
),
);
console.log(chalk.dim('xterm cube levels: 00, 5f, 87, af, d7, ff'));
console.log('');

const headers = [
'Name'.padEnd(18),
'Original (Hex)'.padEnd(25),
'Contrast Comparison',
];
console.log(headers.join(' | '));
console.log('-'.repeat(TABLE_WIDTH));

for (const [name, hexValue] of Object.entries(theme)) {
if (Array.isArray(hexValue) || !hexValue || name === 'type') continue;
const hex = hexValue as string;

// 1. Correct xterm Mapping
const xterm = getClosestXterm(hex);
const xtermFg = getClosestXterm(foreground);
const xtermGreen = getClosestXterm(theme.AccentGreen);
const xtermBaseBg = getClosestXterm(baseBackground);

// 2. Previews
let originalPreview: string;
let label: string;

if (name === 'FocusBackground') {
originalPreview = chalk.bgHex(hex).hex(theme.AccentGreen)(' Text ');
label = 'vs Green';
} else if (backgroundColors.has(name)) {
originalPreview = chalk.bgHex(hex).hex(foreground)(' Text ');
label = 'vs Fore';
} else {
originalPreview = chalk.bgHex(baseBackground).hex(hex)(' Text ');
label = isDark ? 'vs Black' : 'vs White';
}

// 3. Contrast Ratios
const xtermFgHex =
name === 'FocusBackground'
? xtermGreen.hex
: backgroundColors.has(name)
? xtermFg.hex
: xterm.hex;
const xtermBgHex = backgroundColors.has(name) ? xterm.hex : xtermBaseBg.hex;

const originalContrast = tinycolor.readability(
backgroundColors.has(name)
? name === 'FocusBackground'
? theme.AccentGreen
: foreground
: hex,
backgroundColors.has(name) ? hex : baseBackground,
);
const xtermContrast = tinycolor.readability(xtermFgHex, xtermBgHex);

const originalCol = `${hex.toUpperCase().padEnd(10)} ${originalPreview}`;
const contrastCol = `${getRating(originalContrast)} -> ${getRating(xtermContrast)} ${chalk.dim(label)}`;

console.log(
`${name.padEnd(18)} | ${originalCol.padEnd(25)} | ${contrastCol}`,
);
}

console.log('-'.repeat(TABLE_WIDTH));
if (theme.GradientColors && theme.GradientColors.length > 0) {
const NAME_COL_WIDTH = 18;
const SEP_WIDTH = 3; // " | "
const gradientWidth = TABLE_WIDTH - NAME_COL_WIDTH - SEP_WIDTH;
const gradient = tinygradient(theme.GradientColors);
const gradientStr = Array.from({ length: gradientWidth }, (_, i) => {
const color = gradient.rgbAt(i / (gradientWidth - 1));
return chalk.hex(color.toHexString())('█');
}).join('');
console.log('Gradient Test'.padEnd(NAME_COL_WIDTH) + ' | ' + gradientStr);
console.log('-'.repeat(TABLE_WIDTH));
}
}

previewTheme('Dark Theme', darkTheme);
previewTheme('Light Theme', lightTheme);
console.log('');
Loading