mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-14 12:00:18 -07:00
feat(ui): standardize semantic focus colors and enhance history visibility (#20745)
Co-authored-by: jacob314 <jacob314@gmail.com>
This commit is contained in:
@@ -8,18 +8,153 @@ import type { CSSProperties } from 'react';
|
||||
|
||||
import type { SemanticColors } from './semantic-tokens.js';
|
||||
|
||||
import {
|
||||
resolveColor,
|
||||
interpolateColor,
|
||||
getThemeTypeFromBackgroundColor,
|
||||
} from './color-utils.js';
|
||||
|
||||
import type { CustomTheme } from '@google/gemini-cli-core';
|
||||
import {
|
||||
DEFAULT_BACKGROUND_OPACITY,
|
||||
DEFAULT_INPUT_BACKGROUND_OPACITY,
|
||||
DEFAULT_SELECTION_OPACITY,
|
||||
DEFAULT_BORDER_OPACITY,
|
||||
} from '../constants.js';
|
||||
import tinygradient from 'tinygradient';
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
// Define the set of Ink's named colors for quick lookup
|
||||
export const INK_SUPPORTED_NAMES = new Set([
|
||||
'black',
|
||||
'red',
|
||||
'green',
|
||||
'yellow',
|
||||
'blue',
|
||||
'cyan',
|
||||
'magenta',
|
||||
'white',
|
||||
'gray',
|
||||
'grey',
|
||||
'blackbright',
|
||||
'redbright',
|
||||
'greenbright',
|
||||
'yellowbright',
|
||||
'bluebright',
|
||||
'cyanbright',
|
||||
'magentabright',
|
||||
'whitebright',
|
||||
]);
|
||||
|
||||
// Use tinycolor's built-in names map for CSS colors, excluding ones Ink supports
|
||||
export const CSS_NAME_TO_HEX_MAP = Object.fromEntries(
|
||||
Object.entries(tinycolor.names)
|
||||
.filter(([name]) => !INK_SUPPORTED_NAMES.has(name))
|
||||
.map(([name, hex]) => [name, `#${hex}`]),
|
||||
);
|
||||
|
||||
// Mapping for ANSI bright colors that are not in tinycolor's standard CSS names
|
||||
export const INK_NAME_TO_HEX_MAP: Readonly<Record<string, string>> = {
|
||||
blackbright: '#555555',
|
||||
redbright: '#ff5555',
|
||||
greenbright: '#55ff55',
|
||||
yellowbright: '#ffff55',
|
||||
bluebright: '#5555ff',
|
||||
magentabright: '#ff55ff',
|
||||
cyanbright: '#55ffff',
|
||||
whitebright: '#ffffff',
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates the relative luminance of a color.
|
||||
* See https://www.w3.org/TR/WCAG20/#relativeluminancedef
|
||||
*
|
||||
* @param color Color string (hex or Ink-supported name)
|
||||
* @returns Luminance value (0-255)
|
||||
*/
|
||||
export function getLuminance(color: string): number {
|
||||
const resolved = color.toLowerCase();
|
||||
const hex = INK_NAME_TO_HEX_MAP[resolved] || resolved;
|
||||
|
||||
const colorObj = tinycolor(hex);
|
||||
if (!colorObj.isValid()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// tinycolor returns 0-1, we need 0-255
|
||||
return colorObj.getLuminance() * 255;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a CSS color value (name or hex) into an Ink-compatible color string.
|
||||
* @param colorValue The raw color string (e.g., 'blue', '#ff0000', 'darkkhaki').
|
||||
* @returns An Ink-compatible color string (hex or name), or undefined if not resolvable.
|
||||
*/
|
||||
export function resolveColor(colorValue: string): string | undefined {
|
||||
const lowerColor = colorValue.toLowerCase();
|
||||
|
||||
// 1. Check if it's already a hex code and valid
|
||||
if (lowerColor.startsWith('#')) {
|
||||
if (/^#[0-9A-Fa-f]{3}([0-9A-Fa-f]{3})?$/.test(colorValue)) {
|
||||
return lowerColor;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle hex codes without #
|
||||
if (/^[0-9A-Fa-f]{3}([0-9A-Fa-f]{3})?$/.test(colorValue)) {
|
||||
return `#${lowerColor}`;
|
||||
}
|
||||
|
||||
// 2. Check if it's an Ink supported name (lowercase)
|
||||
if (INK_SUPPORTED_NAMES.has(lowerColor)) {
|
||||
return lowerColor; // Use Ink name directly
|
||||
}
|
||||
|
||||
// 3. Check if it's a known CSS name we can map to hex
|
||||
// We can't import CSS_NAME_TO_HEX_MAP here due to circular deps,
|
||||
// but we can use tinycolor directly for named colors.
|
||||
const colorObj = tinycolor(lowerColor);
|
||||
if (colorObj.isValid()) {
|
||||
return colorObj.toHexString();
|
||||
}
|
||||
|
||||
// 4. Could not resolve
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function interpolateColor(
|
||||
color1: string,
|
||||
color2: string,
|
||||
factor: number,
|
||||
) {
|
||||
if (factor <= 0 && color1) {
|
||||
return color1;
|
||||
}
|
||||
if (factor >= 1 && color2) {
|
||||
return color2;
|
||||
}
|
||||
if (!color1 || !color2) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
const gradient = tinygradient(color1, color2);
|
||||
const color = gradient.rgbAt(factor);
|
||||
return color.toHexString();
|
||||
} catch (_e) {
|
||||
return color1;
|
||||
}
|
||||
}
|
||||
|
||||
export function getThemeTypeFromBackgroundColor(
|
||||
backgroundColor: string | undefined,
|
||||
): 'light' | 'dark' | undefined {
|
||||
if (!backgroundColor) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const resolvedColor = resolveColor(backgroundColor);
|
||||
if (!resolvedColor) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const luminance = getLuminance(resolvedColor);
|
||||
return luminance > 128 ? 'light' : 'dark';
|
||||
}
|
||||
|
||||
export type { CustomTheme };
|
||||
|
||||
@@ -43,6 +178,8 @@ export interface ColorsTheme {
|
||||
DarkGray: string;
|
||||
InputBackground?: string;
|
||||
MessageBackground?: string;
|
||||
FocusBackground?: string;
|
||||
FocusColor?: string;
|
||||
GradientColors?: string[];
|
||||
}
|
||||
|
||||
@@ -70,7 +207,12 @@ export const lightTheme: ColorsTheme = {
|
||||
MessageBackground: interpolateColor(
|
||||
'#FAFAFA',
|
||||
'#97a0b0',
|
||||
DEFAULT_BACKGROUND_OPACITY,
|
||||
DEFAULT_INPUT_BACKGROUND_OPACITY,
|
||||
),
|
||||
FocusBackground: interpolateColor(
|
||||
'#FAFAFA',
|
||||
'#3CA84B',
|
||||
DEFAULT_SELECTION_OPACITY,
|
||||
),
|
||||
GradientColors: ['#4796E4', '#847ACE', '#C3677F'],
|
||||
};
|
||||
@@ -99,7 +241,12 @@ export const darkTheme: ColorsTheme = {
|
||||
MessageBackground: interpolateColor(
|
||||
'#1E1E2E',
|
||||
'#6C7086',
|
||||
DEFAULT_BACKGROUND_OPACITY,
|
||||
DEFAULT_INPUT_BACKGROUND_OPACITY,
|
||||
),
|
||||
FocusBackground: interpolateColor(
|
||||
'#1E1E2E',
|
||||
'#A6E3A1',
|
||||
DEFAULT_SELECTION_OPACITY,
|
||||
),
|
||||
GradientColors: ['#4796E4', '#847ACE', '#C3677F'],
|
||||
};
|
||||
@@ -122,6 +269,7 @@ export const ansiTheme: ColorsTheme = {
|
||||
DarkGray: 'gray',
|
||||
InputBackground: 'black',
|
||||
MessageBackground: 'black',
|
||||
FocusBackground: 'black',
|
||||
};
|
||||
|
||||
export class Theme {
|
||||
@@ -164,7 +312,7 @@ export class Theme {
|
||||
interpolateColor(
|
||||
this.colors.Background,
|
||||
this.colors.Gray,
|
||||
DEFAULT_BACKGROUND_OPACITY,
|
||||
DEFAULT_INPUT_BACKGROUND_OPACITY,
|
||||
),
|
||||
input:
|
||||
this.colors.InputBackground ??
|
||||
@@ -173,6 +321,13 @@ export class Theme {
|
||||
this.colors.Gray,
|
||||
DEFAULT_INPUT_BACKGROUND_OPACITY,
|
||||
),
|
||||
focus:
|
||||
this.colors.FocusBackground ??
|
||||
interpolateColor(
|
||||
this.colors.Background,
|
||||
this.colors.FocusColor ?? this.colors.AccentGreen,
|
||||
DEFAULT_SELECTION_OPACITY,
|
||||
),
|
||||
diff: {
|
||||
added: this.colors.DiffAdded,
|
||||
removed: this.colors.DiffRemoved,
|
||||
@@ -180,12 +335,13 @@ export class Theme {
|
||||
},
|
||||
border: {
|
||||
default: this.colors.DarkGray,
|
||||
focused: this.colors.AccentBlue,
|
||||
},
|
||||
ui: {
|
||||
comment: this.colors.Gray,
|
||||
symbol: this.colors.AccentCyan,
|
||||
active: this.colors.AccentBlue,
|
||||
dark: this.colors.DarkGray,
|
||||
focus: this.colors.FocusColor ?? this.colors.AccentGreen,
|
||||
gradient: this.colors.GradientColors,
|
||||
},
|
||||
status: {
|
||||
@@ -292,8 +448,14 @@ export function createCustomTheme(customTheme: CustomTheme): Theme {
|
||||
MessageBackground: interpolateColor(
|
||||
customTheme.background?.primary ?? customTheme.Background ?? '',
|
||||
customTheme.text?.secondary ?? customTheme.Gray ?? '',
|
||||
DEFAULT_BACKGROUND_OPACITY,
|
||||
DEFAULT_INPUT_BACKGROUND_OPACITY,
|
||||
),
|
||||
FocusBackground: interpolateColor(
|
||||
customTheme.background?.primary ?? customTheme.Background ?? '',
|
||||
customTheme.status?.success ?? customTheme.AccentGreen ?? '#3CA84B', // Fallback to a default green if not found
|
||||
DEFAULT_SELECTION_OPACITY,
|
||||
),
|
||||
FocusColor: customTheme.ui?.focus ?? customTheme.AccentGreen,
|
||||
GradientColors: customTheme.ui?.gradient ?? customTheme.GradientColors,
|
||||
};
|
||||
|
||||
@@ -450,6 +612,7 @@ export function createCustomTheme(customTheme: CustomTheme): Theme {
|
||||
primary: customTheme.background?.primary ?? colors.Background,
|
||||
message: colors.MessageBackground!,
|
||||
input: colors.InputBackground!,
|
||||
focus: colors.FocusBackground!,
|
||||
diff: {
|
||||
added: customTheme.background?.diff?.added ?? colors.DiffAdded,
|
||||
removed: customTheme.background?.diff?.removed ?? colors.DiffRemoved,
|
||||
@@ -457,12 +620,13 @@ export function createCustomTheme(customTheme: CustomTheme): Theme {
|
||||
},
|
||||
border: {
|
||||
default: colors.DarkGray,
|
||||
focused: customTheme.border?.focused ?? colors.AccentBlue,
|
||||
},
|
||||
ui: {
|
||||
comment: customTheme.ui?.comment ?? colors.Comment,
|
||||
symbol: customTheme.ui?.symbol ?? colors.Gray,
|
||||
active: customTheme.ui?.active ?? colors.AccentBlue,
|
||||
dark: colors.DarkGray,
|
||||
focus: colors.FocusColor ?? colors.AccentGreen,
|
||||
gradient: customTheme.ui?.gradient ?? colors.GradientColors,
|
||||
},
|
||||
status: {
|
||||
|
||||
Reference in New Issue
Block a user