feat(ui): implement standardized selection background with interpolation

This commit is contained in:
Keith Guerin
2026-02-28 23:11:50 -08:00
parent 4e812232a8
commit e90f2c1339
18 changed files with 541 additions and 303 deletions
@@ -25,6 +25,7 @@ describe('ColorsDisplay', () => {
primary: '#000000', primary: '#000000',
message: '#111111', message: '#111111',
input: '#222222', input: '#222222',
selection: '#333333',
diff: { diff: {
added: '#003300', added: '#003300',
removed: '#330000', removed: '#330000',
@@ -32,6 +32,30 @@ type ColorRow = StandardColorRow | GradientColorRow | BackgroundColorRow;
const VALUE_COLUMN_WIDTH = 10; const VALUE_COLUMN_WIDTH = 10;
const COLOR_DESCRIPTIONS: Record<string, string> = {
'text.primary': 'Primary text color (uses terminal default if blank)',
'text.secondary': 'Secondary/dimmed text color',
'text.link': 'Hyperlink and highlighting color',
'text.accent': 'Accent color for emphasis',
'text.response':
'Color for model response text (uses terminal default if blank)',
'background.primary': 'Main terminal background color',
'background.message': 'Subtle background for message blocks',
'background.input': 'Background for the input prompt',
'background.selection': 'Background highlight for selected/focused items',
'background.diff.added': 'Background for added lines in diffs',
'background.diff.removed': 'Background for removed lines in diffs',
'border.default': 'Standard border color',
'border.focused': 'Border color when an element is focused',
'ui.comment': 'Color for code comments and metadata',
'ui.symbol': 'Color for technical symbols and UI icons',
'ui.dark': 'Deeply dimmed color for subtle UI elements',
'ui.focus': 'Color for focused or selected UI elements (e.g. menu items)',
'status.error': 'Color for error messages and critical status',
'status.success': 'Color for success messages and positive status',
'status.warning': 'Color for warnings and cautionary status',
};
interface ColorsDisplayProps { interface ColorsDisplayProps {
activeTheme: Theme; activeTheme: Theme;
} }
@@ -179,20 +203,28 @@ export const ColorsDisplay: React.FC<ColorsDisplayProps> = ({
function renderStandardRow({ name, value }: StandardColorRow) { function renderStandardRow({ name, value }: StandardColorRow) {
const isHex = value.startsWith('#'); const isHex = value.startsWith('#');
const displayColor = isHex ? value : theme.text.primary; const displayColor = isHex ? value : theme.text.primary;
const description = COLOR_DESCRIPTIONS[name] || '';
return ( return (
<Box key={name} flexDirection="row" paddingX={1}> <Box key={name} flexDirection="row" paddingX={1}>
<Box width={VALUE_COLUMN_WIDTH}> <Box width={VALUE_COLUMN_WIDTH}>
<Text color={displayColor}>{value || '(blank)'}</Text> <Text color={displayColor}>{value || '(blank)'}</Text>
</Box> </Box>
<Box flexGrow={1}> <Box flexGrow={1} flexDirection="row">
<Text color={displayColor}>{name}</Text> <Box width="30%">
<Text color={displayColor}>{name}</Text>
</Box>
<Box flexGrow={1} paddingLeft={1}>
<Text color={theme.text.secondary}>{description}</Text>
</Box>
</Box> </Box>
</Box> </Box>
); );
} }
function renderGradientRow({ name, value }: GradientColorRow) { function renderGradientRow({ name, value }: GradientColorRow) {
const description = COLOR_DESCRIPTIONS[name] || '';
return ( return (
<Box key={name} flexDirection="row" paddingX={1}> <Box key={name} flexDirection="row" paddingX={1}>
<Box width={VALUE_COLUMN_WIDTH} flexDirection="column"> <Box width={VALUE_COLUMN_WIDTH} flexDirection="column">
@@ -202,16 +234,23 @@ function renderGradientRow({ name, value }: GradientColorRow) {
</Text> </Text>
))} ))}
</Box> </Box>
<Box flexGrow={1}> <Box flexGrow={1} flexDirection="row">
<Gradient colors={value}> <Box width="30%">
<Text>{name}</Text> <Gradient colors={value}>
</Gradient> <Text>{name}</Text>
</Gradient>
</Box>
<Box flexGrow={1} paddingLeft={1}>
<Text color={theme.text.secondary}>{description}</Text>
</Box>
</Box> </Box>
</Box> </Box>
); );
} }
function renderBackgroundRow({ name, value }: BackgroundColorRow) { function renderBackgroundRow({ name, value }: BackgroundColorRow) {
const description = COLOR_DESCRIPTIONS[name] || '';
return ( return (
<Box key={name} flexDirection="row" paddingX={1}> <Box key={name} flexDirection="row" paddingX={1}>
<Box <Box
@@ -224,8 +263,13 @@ function renderBackgroundRow({ name, value }: BackgroundColorRow) {
{value || 'default'} {value || 'default'}
</Text> </Text>
</Box> </Box>
<Box flexGrow={1} paddingLeft={1}> <Box flexGrow={1} flexDirection="row" paddingLeft={1}>
<Text color={theme.text.primary}>{name}</Text> <Box width="30%">
<Text color={theme.text.primary}>{name}</Text>
</Box>
<Box flexGrow={1} paddingLeft={1}>
<Text color={theme.text.secondary}>{description}</Text>
</Box>
</Box> </Box>
</Box> </Box>
); );
@@ -98,6 +98,7 @@ describe('<Header />', () => {
primary: '', primary: '',
message: '', message: '',
input: '', input: '',
selection: '',
diff: { added: '', removed: '' }, diff: { added: '', removed: '' },
}, },
border: { border: {
@@ -484,7 +484,10 @@ const SessionItem = ({
)); ));
return ( return (
<Box flexDirection="row"> <Box
flexDirection="row"
backgroundColor={isActive ? theme.background.selection : undefined}
>
<Text color={textColor()} dimColor={isDisabled}> <Text color={textColor()} dimColor={isDisabled}>
{prefix} {prefix}
</Text> </Text>
@@ -97,7 +97,11 @@ export function SuggestionsDisplay({
); );
return ( return (
<Box key={`${suggestion.value}-${originalIndex}`} flexDirection="row"> <Box
key={`${suggestion.value}-${originalIndex}`}
flexDirection="row"
backgroundColor={isActive ? theme.background.selection : undefined}
>
<Box <Box
{...(mode === 'slash' {...(mode === 'slash'
? { width: commandColumnWidth, flexShrink: 0 as const } ? { width: commandColumnWidth, flexShrink: 0 as const }
@@ -18,43 +18,68 @@ exports[`Initial Theme Selection > should default to a dark theme when terminal
│ 11. Ayu Light │ │ │ │ 11. Ayu Light │ │ │
│ 12. Default Light └─────────────────────────────────────────────────┘ │ │ 12. Default Light └─────────────────────────────────────────────────┘ │
│ ▼ │ │ ▼ │
│ ╭────────────────────────────────────────────────╮ │ ╭────────────────────────────────────────────────╮ │
│ │ DEVELOPER TOOLS (Not visible to users) │ │ │ DEVELOPER TOOLS (Not visible to users) │ │
│ │ │ │ │ │ │
│ │ How do colors get applied? │ │ │ How do colors get applied? │ │
│ │ • Hex: Rendered exactly by modern terminals. │ │ │ • Hex: Rendered exactly by modern terminals. │ │
│ │ Not overridden by app themes. │ │ │ Not overridden by app themes. │ │
│ │ • Blank: Uses your terminal's default │ │ │ • Blank: Uses your terminal's default │ │
│ │ foreground/background. │ │ │ foreground/background. │ │
│ │ • Compatibility: On older terminals, hex is │ │ │ • Compatibility: On older terminals, hex is │ │
│ │ approximated to the nearest ANSI color. │ │ │ approximated to the nearest ANSI color. │ │
│ │ • ANSI Names: 'red', 'green', etc. are │ │ • ANSI Names: 'red', 'green', etc. are mapped │ │
│ │ mapped to your terminal app's palette. │ │ │ to your terminal app's palette. │ │
│ │ │ │ │ │ │
│ │ Value Name │ │ │ Value Name │ │
│ │ #1E1E2E background.primary │ │ #1E1E backgroun Main terminal background │ │
│ │ #2a2b3c background.message │ │ d.primary color │ │
│ │ #313243 background.input │ │ #2a2… backgroun Subtle background for │ │
│ │ #28350B background.diff.added │ │ d.message message blocks │ │
│ │ #430000 background.diff.removed │ │ #313… backgroun Background for the input │ │
│ │ (blank) text.primary │ │ d.input prompt │ │
│ │ #6C7086 text.secondary │ │ #39… background. Background highlight for │ │
│ │ #89B4FA text.link │ │ selection selected/focused items │ │
│ │ #CBA6F7 text.accent │ │ #283… backgrou Background for added lines │ │
│ │ (blank) text.response │ │ nd.diff. in diffs │ │
│ │ #3d3f51 border.default │ │ added │ │
│ │ #89B4FA border.focused │ │ #430… backgroun Background for removed │ │
│ │ #6C7086 ui.comment │ │ d.diff.re lines in diffs │ │
│ │ #89DCEB ui.symbol │ │ moved │ │
│ │ #3d3f51 ui.dark │ │ (blank text.prim Primary text color (uses │ │
│ │ #89B4FA ui.focus │ │ ) ary terminal default if blank) │ │
│ │ #F38BA8 status.error │ │ #6C7086 text.secon Secondary/dimmed text │ │
│ │ #A6E3A1 status.success │ │ dary color │ │
│ │ #F9E2AF status.warning │ │ #89B4FA text.link Hyperlink and highlighting │ │
│ │ #4796E4 ui.gradient │ │ color │ │
│ │ #847ACE │ │ #CBA6F7 text.accen Accent color for │ │
│ │ #C3677F │ │ t emphasis │ │
╰────────────────────────────────────────────────╯ │ (blank) text.res Color for model response │
│ │ ponse text (uses terminal default │ │
│ │ if blank) │ │
│ │ #3d3f51 border.def Standard border color │ │
│ │ ault │ │
│ │ #89B4FAborder.fo Border color when an │ │
│ │ cused element is focused │ │
│ │ #6C7086ui.comme Color for code comments and │ │
│ │ nt metadata │ │
│ │ #89DCE ui.symbol Color for technical symbols │ │
│ │ B and UI icons │ │
│ │ #3d3f5 ui.dark Deeply dimmed color for │ │
│ │ 1 subtle UI elements │ │
│ │ #89B4F ui.focus Color for focused or │ │
│ │ A selected UI elements (e.g. │ │
│ │ menu items) │ │
│ │ #F38BA8status.err Color for error messages │ │
│ │ or and critical status │ │
│ │ #A6E3A1status.suc Color for success messages │ │
│ │ cess and positive status │ │
│ │ #F9E2A status.wa Color for warnings and │ │
│ │ F rning cautionary status │ │
│ │ #4796E4 ui.gradien │ │
│ │ #847ACE t │ │
│ │ #C3677F │ │
│ ╰─────────────────────────────────────────────────╯ │
│ │ │ │
│ (Use Enter to select, Tab to configure scope, Esc to close) │ │ (Use Enter to select, Tab to configure scope, Esc to close) │
│ │ │ │
@@ -80,43 +105,68 @@ exports[`Initial Theme Selection > should default to a light theme when terminal
│ 11. Default Dark (Incompatible) │ │ │ │ 11. Default Dark (Incompatible) │ │ │
│ 12. Dracula Dark (Incompatible) └─────────────────────────────────────────────────┘ │ │ 12. Dracula Dark (Incompatible) └─────────────────────────────────────────────────┘ │
│ ▼ │ │ ▼ │
│ ╭────────────────────────────────────────────────╮ │ ╭────────────────────────────────────────────────╮ │
│ │ DEVELOPER TOOLS (Not visible to users) │ │ │ DEVELOPER TOOLS (Not visible to users) │ │
│ │ │ │ │ │ │
│ │ How do colors get applied? │ │ │ How do colors get applied? │ │
│ │ • Hex: Rendered exactly by modern terminals. │ │ │ • Hex: Rendered exactly by modern terminals. │ │
│ │ Not overridden by app themes. │ │ │ Not overridden by app themes. │ │
│ │ • Blank: Uses your terminal's default │ │ │ • Blank: Uses your terminal's default │ │
│ │ foreground/background. │ │ │ foreground/background. │ │
│ │ • Compatibility: On older terminals, hex is │ │ │ • Compatibility: On older terminals, hex is │ │
│ │ approximated to the nearest ANSI color. │ │ │ approximated to the nearest ANSI color. │ │
│ │ • ANSI Names: 'red', 'green', etc. are │ │ • ANSI Names: 'red', 'green', etc. are mapped │ │
│ │ mapped to your terminal app's palette. │ │ │ to your terminal app's palette. │ │
│ │ │ │ │ │ │
│ │ Value Name │ │ │ Value Name │ │
│ │ #FAFAFA background.primary │ │ #FAFA backgroun Main terminal background │ │
│ │ #eaecee background.message │ │ d.primary color │ │
│ │ #e2e4e8 background.input │ │ #eae… backgroun Subtle background for │ │
│ │ #C6EAD8 background.diff.added │ │ d.message message blocks │ │
│ │ #FFCCCC background.diff.removed │ │ #e2e… backgroun Background for the input │ │
│ │ (blank) text.primary │ │ d.input prompt │ │
│ │ #97a0b0 text.secondary │ │ #d4… background. Background highlight for │ │
│ │ #3B82F6 text.link │ │ selection selected/focused items │ │
│ │ #8B5CF6 text.accent │ │ #C6E… backgrou Background for added lines │ │
│ │ (blank) text.response │ │ nd.diff. in diffs │ │
│ │ #d2d6dc border.default │ │ added │ │
│ │ #3B82F6 border.focused │ │ #FFC… backgroun Background for removed │ │
│ │ #97a0b0 ui.comment │ │ d.diff.re lines in diffs │ │
│ │ #06B6D4 ui.symbol │ │ moved │ │
│ │ #d2d6dc ui.dark │ │ (blank text.prim Primary text color (uses │ │
│ │ #3B82F6 ui.focus │ │ ) ary terminal default if blank) │ │
│ │ #DD4C4C status.error │ │ #97a0b0 text.secon Secondary/dimmed text │ │
│ │ #3CA84B status.success │ │ dary color │ │
│ │ #D5A40A status.warning │ │ #3B82F6 text.link Hyperlink and highlighting │ │
│ │ #4796E4 ui.gradient │ │ color │ │
│ │ #847ACE │ │ #8B5CF6 text.accen Accent color for │ │
│ │ #C3677F │ │ t emphasis │ │
╰────────────────────────────────────────────────╯ │ (blank) text.res Color for model response │
│ │ ponse text (uses terminal default │ │
│ │ if blank) │ │
│ │ #d2d6dc border.def Standard border color │ │
│ │ ault │ │
│ │ #3B82F6border.fo Border color when an │ │
│ │ cused element is focused │ │
│ │ #97a0b0ui.comme Color for code comments and │ │
│ │ nt metadata │ │
│ │ #06B6D ui.symbol Color for technical symbols │ │
│ │ 4 and UI icons │ │
│ │ #d2d6d ui.dark Deeply dimmed color for │ │
│ │ c subtle UI elements │ │
│ │ #3B82F ui.focus Color for focused or │ │
│ │ 6 selected UI elements (e.g. │ │
│ │ menu items) │ │
│ │ #DD4C4Cstatus.err Color for error messages │ │
│ │ or and critical status │ │
│ │ #3CA84Bstatus.suc Color for success messages │ │
│ │ cess and positive status │ │
│ │ #D5A40 status.wa Color for warnings and │ │
│ │ A rning cautionary status │ │
│ │ #4796E4 ui.gradien │ │
│ │ #847ACE t │ │
│ │ #C3677F │ │
│ ╰─────────────────────────────────────────────────╯ │
│ │ │ │
│ (Use Enter to select, Tab to configure scope, Esc to close) │ │ (Use Enter to select, Tab to configure scope, Esc to close) │
│ │ │ │
@@ -142,43 +192,68 @@ exports[`Initial Theme Selection > should use the theme from settings even if te
│ 11. Ayu Light │ │ │ │ 11. Ayu Light │ │ │
│ 12. Default Light └─────────────────────────────────────────────────┘ │ │ 12. Default Light └─────────────────────────────────────────────────┘ │
│ ▼ │ │ ▼ │
│ ╭────────────────────────────────────────────────╮ │ ╭────────────────────────────────────────────────╮ │
│ │ DEVELOPER TOOLS (Not visible to users) │ │ │ DEVELOPER TOOLS (Not visible to users) │ │
│ │ │ │ │ │ │
│ │ How do colors get applied? │ │ │ How do colors get applied? │ │
│ │ • Hex: Rendered exactly by modern terminals. │ │ │ • Hex: Rendered exactly by modern terminals. │ │
│ │ Not overridden by app themes. │ │ │ Not overridden by app themes. │ │
│ │ • Blank: Uses your terminal's default │ │ │ • Blank: Uses your terminal's default │ │
│ │ foreground/background. │ │ │ foreground/background. │ │
│ │ • Compatibility: On older terminals, hex is │ │ │ • Compatibility: On older terminals, hex is │ │
│ │ approximated to the nearest ANSI color. │ │ │ approximated to the nearest ANSI color. │ │
│ │ • ANSI Names: 'red', 'green', etc. are │ │ • ANSI Names: 'red', 'green', etc. are mapped │ │
│ │ mapped to your terminal app's palette. │ │ │ to your terminal app's palette. │ │
│ │ │ │ │ │ │
│ │ Value Name │ │ │ Value Name │ │
│ │ #1E1E2E background.primary │ │ #1E1E backgroun Main terminal background │ │
│ │ #2a2b3c background.message │ │ d.primary color │ │
│ │ #313243 background.input │ │ #2a2… backgroun Subtle background for │ │
│ │ #28350B background.diff.added │ │ d.message message blocks │ │
│ │ #430000 background.diff.removed │ │ #313… backgroun Background for the input │ │
│ │ (blank) text.primary │ │ d.input prompt │ │
│ │ #6C7086 text.secondary │ │ #39… background. Background highlight for │ │
│ │ #89B4FA text.link │ │ selection selected/focused items │ │
│ │ #CBA6F7 text.accent │ │ #283… backgrou Background for added lines │ │
│ │ (blank) text.response │ │ nd.diff. in diffs │ │
│ │ #3d3f51 border.default │ │ added │ │
│ │ #89B4FA border.focused │ │ #430… backgroun Background for removed │ │
│ │ #6C7086 ui.comment │ │ d.diff.re lines in diffs │ │
│ │ #89DCEB ui.symbol │ │ moved │ │
│ │ #3d3f51 ui.dark │ │ (blank text.prim Primary text color (uses │ │
│ │ #89B4FA ui.focus │ │ ) ary terminal default if blank) │ │
│ │ #F38BA8 status.error │ │ #6C7086 text.secon Secondary/dimmed text │ │
│ │ #A6E3A1 status.success │ │ dary color │ │
│ │ #F9E2AF status.warning │ │ #89B4FA text.link Hyperlink and highlighting │ │
│ │ #4796E4 ui.gradient │ │ color │ │
│ │ #847ACE │ │ #CBA6F7 text.accen Accent color for │ │
│ │ #C3677F │ │ t emphasis │ │
╰────────────────────────────────────────────────╯ │ (blank) text.res Color for model response │
│ │ ponse text (uses terminal default │ │
│ │ if blank) │ │
│ │ #3d3f51 border.def Standard border color │ │
│ │ ault │ │
│ │ #89B4FAborder.fo Border color when an │ │
│ │ cused element is focused │ │
│ │ #6C7086ui.comme Color for code comments and │ │
│ │ nt metadata │ │
│ │ #89DCE ui.symbol Color for technical symbols │ │
│ │ B and UI icons │ │
│ │ #3d3f5 ui.dark Deeply dimmed color for │ │
│ │ 1 subtle UI elements │ │
│ │ #89B4F ui.focus Color for focused or │ │
│ │ A selected UI elements (e.g. │ │
│ │ menu items) │ │
│ │ #F38BA8status.err Color for error messages │ │
│ │ or and critical status │ │
│ │ #A6E3A1status.suc Color for success messages │ │
│ │ cess and positive status │ │
│ │ #F9E2A status.wa Color for warnings and │ │
│ │ F rning cautionary status │ │
│ │ #4796E4 ui.gradien │ │
│ │ #847ACE t │ │
│ │ #C3677F │ │
│ ╰─────────────────────────────────────────────────╯ │
│ │ │ │
│ (Use Enter to select, Tab to configure scope, Esc to close) │ │ (Use Enter to select, Tab to configure scope, Esc to close) │
│ │ │ │
@@ -218,43 +293,66 @@ exports[`ThemeDialog Snapshots > should render correctly in theme selection mode
│ 11. Ayu Light │ │ │ │ 11. Ayu Light │ │ │
│ 12. Default Light └─────────────────────────────────────────────────┘ │ │ 12. Default Light └─────────────────────────────────────────────────┘ │
│ ▼ │ │ ▼ │
│ ╭────────────────────────────────────────────────╮ │ ╭────────────────────────────────────────────────╮ │
│ │ DEVELOPER TOOLS (Not visible to users) │ │ │ DEVELOPER TOOLS (Not visible to users) │ │
│ │ │ │ │ │ │
│ │ How do colors get applied? │ │ │ How do colors get applied? │ │
│ │ • Hex: Rendered exactly by modern terminals. │ │ │ • Hex: Rendered exactly by modern terminals. │ │
│ │ Not overridden by app themes. │ │ │ Not overridden by app themes. │ │
│ │ • Blank: Uses your terminal's default │ │ │ • Blank: Uses your terminal's default │ │
│ │ foreground/background. │ │ │ foreground/background. │ │
│ │ • Compatibility: On older terminals, hex is │ │ │ • Compatibility: On older terminals, hex is │ │
│ │ approximated to the nearest ANSI color. │ │ │ approximated to the nearest ANSI color. │ │
│ │ • ANSI Names: 'red', 'green', etc. are │ │ • ANSI Names: 'red', 'green', etc. are mapped │ │
│ │ mapped to your terminal app's palette. │ │ │ to your terminal app's palette. │ │
│ │ │ │ │ │ │
│ │ Value Name │ │ │ Value Name │ │
│ │ #1E1E2E background.primary │ │ #1E1E backgroun Main terminal background │ │
│ │ #2a2b3c background.message │ │ d.primary color │ │
│ │ #313243 background.input │ │ #2a2… backgroun Subtle background for │ │
│ │ #28350B background.diff.added │ │ d.message message blocks │ │
│ │ #430000 background.diff.removed │ │ #313… backgroun Background for the input │ │
│ │ (blank) text.primary │ │ d.input prompt │ │
│ │ #6C7086 text.secondary │ │ #283… backgrou Background for added lines │ │
│ │ #89B4FA text.link │ │ nd.diff. in diffs │ │
│ │ #CBA6F7 text.accent │ │ added │ │
│ │ (blank) text.response │ │ │ #430… backgroun Background for removed │
│ │ #3d3f51 border.default │ │ d.diff.re lines in diffs │ │
│ │ #89B4FA border.focused │ │ │ moved │ │
│ │ #6C7086 ui.comment │ │ (blank text.prim Primary text color (uses │ │
│ │ #6C7086 ui.symbol │ │ ) ary terminal default if blank) │ │
│ │ #3d3f51 ui.dark │ │ #6C7086 text.secon Secondary/dimmed text │ │
│ │ #89B4FA ui.focus │ │ dary color │ │
│ │ #F38BA8 status.error │ │ #89B4FA text.link Hyperlink and highlighting │ │
│ │ #A6E3A1 status.success │ │ color │ │
│ │ #F9E2AF status.warning │ │ #CBA6F7 text.accen Accent color for │ │
│ │ #4796E4 ui.gradient │ │ t emphasis │ │
│ │ #847ACE │ │ (blank) text.res Color for model response │ │
│ │ #C3677F │ │ ponse text (uses terminal default │ │
╰────────────────────────────────────────────────╯ │ if blank) │
│ │ #3d3f51 border.def Standard border color │ │
│ │ ault │ │
│ │ #89B4FAborder.fo Border color when an │ │
│ │ cused element is focused │ │
│ │ #6C7086ui.comme Color for code comments and │ │
│ │ nt metadata │ │
│ │ #6C708 ui.symbol Color for technical symbols │ │
│ │ 6 and UI icons │ │
│ │ #3d3f5 ui.dark Deeply dimmed color for │ │
│ │ 1 subtle UI elements │ │
│ │ #89B4F ui.focus Color for focused or │ │
│ │ A selected UI elements (e.g. │ │
│ │ menu items) │ │
│ │ #F38BA8status.err Color for error messages │ │
│ │ or and critical status │ │
│ │ #A6E3A1status.suc Color for success messages │ │
│ │ cess and positive status │ │
│ │ #F9E2A status.wa Color for warnings and │ │
│ │ F rning cautionary status │ │
│ │ #4796E4 ui.gradien │ │
│ │ #847ACE t │ │
│ │ #C3677F │ │
│ ╰─────────────────────────────────────────────────╯ │
│ │ │ │
│ (Use Enter to select, Tab to configure scope, Esc to close) │ │ (Use Enter to select, Tab to configure scope, Esc to close) │
│ │ │ │
@@ -137,7 +137,13 @@ export function BaseSelectionList<
)}.`; )}.`;
return ( return (
<Box key={item.key} alignItems="flex-start"> <Box
key={item.key}
alignItems="flex-start"
backgroundColor={
isSelected ? theme.background.selection : undefined
}
>
{/* Radio button indicator */} {/* Radio button indicator */}
<Box minWidth={2} flexShrink={0}> <Box minWidth={2} flexShrink={0}>
<Text <Text
@@ -514,7 +514,14 @@ export function BaseSettingsDialog({
return ( return (
<React.Fragment key={item.key}> <React.Fragment key={item.key}>
<Box marginX={1} flexDirection="row" alignItems="flex-start"> <Box
marginX={1}
flexDirection="row"
alignItems="flex-start"
backgroundColor={
isActive ? theme.background.selection : undefined
}
>
<Box minWidth={2} flexShrink={0}> <Box minWidth={2} flexShrink={0}>
<Text <Text
color={isActive ? theme.ui.focus : theme.text.secondary} color={isActive ? theme.ui.focus : theme.text.secondary}
+1
View File
@@ -37,6 +37,7 @@ export const EXPAND_HINT_DURATION_MS = 5000;
export const DEFAULT_BACKGROUND_OPACITY = 0.16; export const DEFAULT_BACKGROUND_OPACITY = 0.16;
export const DEFAULT_INPUT_BACKGROUND_OPACITY = 0.24; export const DEFAULT_INPUT_BACKGROUND_OPACITY = 0.24;
export const DEFAULT_SELECTION_OPACITY = 0.2;
export const DEFAULT_BORDER_OPACITY = 0.4; export const DEFAULT_BORDER_OPACITY = 0.4;
export const KEYBOARD_SHORTCUTS_URL = export const KEYBOARD_SHORTCUTS_URL =
+1
View File
@@ -23,6 +23,7 @@ const ansiColors: ColorsTheme = {
Comment: 'gray', Comment: 'gray',
Gray: 'gray', Gray: 'gray',
DarkGray: 'gray', DarkGray: 'gray',
SelectionBackground: 'black',
GradientColors: ['cyan', 'green'], GradientColors: ['cyan', 'green'],
}; };
+18 -137
View File
@@ -4,38 +4,25 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import { debugLogger } from '@google/gemini-cli-core'; import {
import tinygradient from 'tinygradient'; resolveColor,
import tinycolor from 'tinycolor2'; interpolateColor,
getThemeTypeFromBackgroundColor,
INK_SUPPORTED_NAMES,
INK_NAME_TO_HEX_MAP,
getLuminance,
CSS_NAME_TO_HEX_MAP,
} from './theme.js';
// Define the set of Ink's named colors for quick lookup export {
export const INK_SUPPORTED_NAMES = new Set([ resolveColor,
'black', interpolateColor,
'red', getThemeTypeFromBackgroundColor,
'green', INK_SUPPORTED_NAMES,
'yellow', INK_NAME_TO_HEX_MAP,
'blue', getLuminance,
'cyan', CSS_NAME_TO_HEX_MAP,
'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}`]),
);
/** /**
* Checks if a color string is valid (hex, Ink-supported color name, or CSS color name). * Checks if a color string is valid (hex, Ink-supported color name, or CSS color name).
@@ -66,45 +53,6 @@ export function isValidColor(color: string): boolean {
return false; return false;
} }
/**
* 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
if (CSS_NAME_TO_HEX_MAP[lowerColor]) {
return CSS_NAME_TO_HEX_MAP[lowerColor]; // Use mapped hex
}
// 4. Could not resolve
debugLogger.warn(
`[ColorUtils] Could not resolve color "${colorValue}" to an Ink-compatible format.`,
);
return undefined;
}
/** /**
* Returns a "safe" background color to use in low-color terminals if the * Returns a "safe" background color to use in low-color terminals if the
* terminal background is a standard black or white. * terminal background is a standard black or white.
@@ -132,73 +80,6 @@ export function getSafeLowColorBackground(
return undefined; 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 '';
}
const gradient = tinygradient(color1, color2);
const color = gradient.rgbAt(factor);
return color.toHexString();
}
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';
}
// 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;
}
// Hysteresis thresholds to prevent flickering when the background color // Hysteresis thresholds to prevent flickering when the background color
// is ambiguous (near the midpoint). // is ambiguous (near the midpoint).
export const LIGHT_THEME_LUMINANCE_THRESHOLD = 140; export const LIGHT_THEME_LUMINANCE_THRESHOLD = 140;
+2
View File
@@ -26,6 +26,7 @@ const noColorColorsTheme: ColorsTheme = {
DarkGray: '', DarkGray: '',
InputBackground: '', InputBackground: '',
MessageBackground: '', MessageBackground: '',
SelectionBackground: '',
}; };
const noColorSemanticColors: SemanticColors = { const noColorSemanticColors: SemanticColors = {
@@ -40,6 +41,7 @@ const noColorSemanticColors: SemanticColors = {
primary: '', primary: '',
message: '', message: '',
input: '', input: '',
selection: '',
diff: { diff: {
added: '', added: '',
removed: '', removed: '',
@@ -18,6 +18,7 @@ export interface SemanticColors {
primary: string; primary: string;
message: string; message: string;
input: string; input: string;
selection: string;
diff: { diff: {
added: string; added: string;
removed: string; removed: string;
+7 -1
View File
@@ -4,8 +4,9 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import { type ColorsTheme, Theme } from './theme.js'; import { type ColorsTheme, Theme, interpolateColor } from './theme.js';
import { type SemanticColors } from './semantic-tokens.js'; import { type SemanticColors } from './semantic-tokens.js';
import { DEFAULT_SELECTION_OPACITY } from '../constants.js';
const solarizedDarkColors: ColorsTheme = { const solarizedDarkColors: ColorsTheme = {
type: 'dark', type: 'dark',
@@ -38,6 +39,11 @@ const semanticColors: SemanticColors = {
primary: '#002b36', primary: '#002b36',
message: '#073642', message: '#073642',
input: '#073642', input: '#073642',
selection: interpolateColor(
'#002b36',
'#859900',
DEFAULT_SELECTION_OPACITY,
),
diff: { diff: {
added: '#00382f', added: '#00382f',
removed: '#3d0115', removed: '#3d0115',
@@ -4,8 +4,9 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import { type ColorsTheme, Theme } from './theme.js'; import { type ColorsTheme, Theme, interpolateColor } from './theme.js';
import { type SemanticColors } from './semantic-tokens.js'; import { type SemanticColors } from './semantic-tokens.js';
import { DEFAULT_SELECTION_OPACITY } from '../constants.js';
const solarizedLightColors: ColorsTheme = { const solarizedLightColors: ColorsTheme = {
type: 'light', type: 'light',
@@ -38,6 +39,11 @@ const semanticColors: SemanticColors = {
primary: '#fdf6e3', primary: '#fdf6e3',
message: '#eee8d5', message: '#eee8d5',
input: '#eee8d5', input: '#eee8d5',
selection: interpolateColor(
'#fdf6e3',
'#859900',
DEFAULT_SELECTION_OPACITY,
),
diff: { diff: {
added: '#d7f2d7', added: '#d7f2d7',
removed: '#f2d7d7', removed: '#f2d7d7',
+10 -4
View File
@@ -22,16 +22,16 @@ import * as fs from 'node:fs';
import * as path from 'node:path'; import * as path from 'node:path';
import type { Theme, ThemeType, ColorsTheme } from './theme.js'; import type { Theme, ThemeType, ColorsTheme } from './theme.js';
import type { CustomTheme } from '@google/gemini-cli-core'; import type { CustomTheme } from '@google/gemini-cli-core';
import { createCustomTheme, validateCustomTheme } from './theme.js'; import { createCustomTheme, validateCustomTheme ,
import type { SemanticColors } from './semantic-tokens.js';
import {
interpolateColor, interpolateColor,
getThemeTypeFromBackgroundColor, getThemeTypeFromBackgroundColor,
resolveColor, resolveColor,
} from './color-utils.js'; } from './theme.js';
import type { SemanticColors } from './semantic-tokens.js';
import { import {
DEFAULT_BACKGROUND_OPACITY, DEFAULT_BACKGROUND_OPACITY,
DEFAULT_INPUT_BACKGROUND_OPACITY, DEFAULT_INPUT_BACKGROUND_OPACITY,
DEFAULT_SELECTION_OPACITY,
DEFAULT_BORDER_OPACITY, DEFAULT_BORDER_OPACITY,
} from '../constants.js'; } from '../constants.js';
import { ANSI } from './ansi.js'; import { ANSI } from './ansi.js';
@@ -369,6 +369,11 @@ class ThemeManager {
colors.Gray, colors.Gray,
DEFAULT_BACKGROUND_OPACITY, DEFAULT_BACKGROUND_OPACITY,
), ),
SelectionBackground: interpolateColor(
this.terminalBackground,
colors.AccentGreen,
DEFAULT_SELECTION_OPACITY,
),
}; };
} else { } else {
this.cachedColors = colors; this.cachedColors = colors;
@@ -402,6 +407,7 @@ class ThemeManager {
primary: this.terminalBackground, primary: this.terminalBackground,
message: colors.MessageBackground!, message: colors.MessageBackground!,
input: colors.InputBackground!, input: colors.InputBackground!,
selection: colors.SelectionBackground!,
}, },
border: { border: {
...semanticColors.border, ...semanticColors.border,
+3
View File
@@ -32,6 +32,7 @@ describe('createCustomTheme', () => {
DiffRemoved: '#FF0000', DiffRemoved: '#FF0000',
Comment: '#808080', Comment: '#808080',
Gray: '#cccccc', Gray: '#cccccc',
SelectionBackground: '#004000',
// DarkGray intentionally omitted to test fallback // DarkGray intentionally omitted to test fallback
}; };
@@ -103,6 +104,7 @@ describe('validateCustomTheme', () => {
DiffRemoved: '#FF0000', DiffRemoved: '#FF0000',
Comment: '#808080', Comment: '#808080',
Gray: '#808080', Gray: '#808080',
SelectionBackground: '#004000',
}; };
it('should return isValid: true for a valid theme', () => { it('should return isValid: true for a valid theme', () => {
@@ -153,6 +155,7 @@ describe('themeManager.loadCustomThemes', () => {
AccentRed: '#F00', AccentRed: '#F00',
Comment: '#888', Comment: '#888',
Gray: '#888', Gray: '#888',
SelectionBackground: '#040',
}; };
it('should use values from DEFAULT_THEME when DiffAdded and DiffRemoved are not provided', () => { it('should use values from DEFAULT_THEME when DiffAdded and DiffRemoved are not provided', () => {
+167
View File
@@ -18,8 +18,150 @@ import type { CustomTheme } from '@google/gemini-cli-core';
import { import {
DEFAULT_BACKGROUND_OPACITY, DEFAULT_BACKGROUND_OPACITY,
DEFAULT_INPUT_BACKGROUND_OPACITY, DEFAULT_INPUT_BACKGROUND_OPACITY,
DEFAULT_SELECTION_OPACITY,
DEFAULT_BORDER_OPACITY, DEFAULT_BORDER_OPACITY,
} from '../constants.js'; } 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 }; export type { CustomTheme };
@@ -43,6 +185,7 @@ export interface ColorsTheme {
DarkGray: string; DarkGray: string;
InputBackground?: string; InputBackground?: string;
MessageBackground?: string; MessageBackground?: string;
SelectionBackground?: string;
GradientColors?: string[]; GradientColors?: string[];
} }
@@ -72,6 +215,11 @@ export const lightTheme: ColorsTheme = {
'#97a0b0', '#97a0b0',
DEFAULT_BACKGROUND_OPACITY, DEFAULT_BACKGROUND_OPACITY,
), ),
SelectionBackground: interpolateColor(
'#FAFAFA',
'#3CA84B',
DEFAULT_SELECTION_OPACITY,
),
GradientColors: ['#4796E4', '#847ACE', '#C3677F'], GradientColors: ['#4796E4', '#847ACE', '#C3677F'],
}; };
@@ -101,6 +249,11 @@ export const darkTheme: ColorsTheme = {
'#6C7086', '#6C7086',
DEFAULT_BACKGROUND_OPACITY, DEFAULT_BACKGROUND_OPACITY,
), ),
SelectionBackground: interpolateColor(
'#1E1E2E',
'#A6E3A1',
DEFAULT_SELECTION_OPACITY,
),
GradientColors: ['#4796E4', '#847ACE', '#C3677F'], GradientColors: ['#4796E4', '#847ACE', '#C3677F'],
}; };
@@ -122,6 +275,7 @@ export const ansiTheme: ColorsTheme = {
DarkGray: 'gray', DarkGray: 'gray',
InputBackground: 'black', InputBackground: 'black',
MessageBackground: 'black', MessageBackground: 'black',
SelectionBackground: 'black',
}; };
export class Theme { export class Theme {
@@ -173,6 +327,13 @@ export class Theme {
this.colors.Gray, this.colors.Gray,
DEFAULT_INPUT_BACKGROUND_OPACITY, DEFAULT_INPUT_BACKGROUND_OPACITY,
), ),
selection:
this.colors.SelectionBackground ??
interpolateColor(
this.colors.Background,
this.colors.AccentGreen,
DEFAULT_SELECTION_OPACITY,
),
diff: { diff: {
added: this.colors.DiffAdded, added: this.colors.DiffAdded,
removed: this.colors.DiffRemoved, removed: this.colors.DiffRemoved,
@@ -295,6 +456,11 @@ export function createCustomTheme(customTheme: CustomTheme): Theme {
customTheme.text?.secondary ?? customTheme.Gray ?? '', customTheme.text?.secondary ?? customTheme.Gray ?? '',
DEFAULT_BACKGROUND_OPACITY, DEFAULT_BACKGROUND_OPACITY,
), ),
SelectionBackground: interpolateColor(
customTheme.background?.primary ?? customTheme.Background ?? '',
customTheme.status?.success ?? customTheme.AccentGreen ?? '#3CA84B', // Fallback to a default green if not found
DEFAULT_SELECTION_OPACITY,
),
GradientColors: customTheme.ui?.gradient ?? customTheme.GradientColors, GradientColors: customTheme.ui?.gradient ?? customTheme.GradientColors,
}; };
@@ -451,6 +617,7 @@ export function createCustomTheme(customTheme: CustomTheme): Theme {
primary: customTheme.background?.primary ?? colors.Background, primary: customTheme.background?.primary ?? colors.Background,
message: colors.MessageBackground!, message: colors.MessageBackground!,
input: colors.InputBackground!, input: colors.InputBackground!,
selection: colors.SelectionBackground!,
diff: { diff: {
added: customTheme.background?.diff?.added ?? colors.DiffAdded, added: customTheme.background?.diff?.added ?? colors.DiffAdded,
removed: customTheme.background?.diff?.removed ?? colors.DiffRemoved, removed: customTheme.background?.diff?.removed ?? colors.DiffRemoved,