UI Polish for theme selector (#294)

This commit is contained in:
Miguel Solorio
2025-05-08 16:00:55 -07:00
committed by GitHub
parent 6b0ac084b8
commit a685597b70
16 changed files with 171 additions and 81 deletions

View File

@@ -5,6 +5,7 @@
*/
import { Box, Text } from 'ink';
import { Colors } from '../colors.js';
export interface Suggestion {
label: string;
value: string;
@@ -48,7 +49,7 @@ export function SuggestionsDisplay({
return (
<Box borderStyle="round" flexDirection="column" paddingX={1} width={width}>
{scrollOffset > 0 && <Text color="gray"></Text>}
{scrollOffset > 0 && <Text color={Colors.Foreground}></Text>}
{visibleSuggestions.map((suggestion, index) => {
const originalIndex = startIndex + index;
@@ -56,8 +57,8 @@ export function SuggestionsDisplay({
return (
<Text
key={`${suggestion}-${originalIndex}`}
color={isActive ? 'black' : 'white'}
backgroundColor={isActive ? 'blue' : undefined}
color={isActive ? Colors.Background : Colors.Foreground}
backgroundColor={isActive ? Colors.AccentBlue : undefined}
>
{suggestion.label}
</Text>

View File

@@ -32,16 +32,22 @@ export function ThemeDialog({
SettingScope.User,
);
const themeItems = themeManager.getAvailableThemes().map((theme) => ({
label: theme.active ? `${theme.name} (Active)` : theme.name,
value: theme.name,
}));
// Generate theme items
const themeItems = themeManager.getAvailableThemes().map((theme) => {
const typeString = theme.type.charAt(0).toUpperCase() + theme.type.slice(1);
return {
label: theme.name,
value: theme.name,
themeNameDisplay: theme.name,
themeTypeDisplay: typeString,
};
});
const [selectInputKey, setSelectInputKey] = useState(Date.now());
// Determine which radio button should be initially selected in the theme list
// This should reflect the theme *saved* for the selected scope, or the default
const initialThemeIndex = themeItems.findIndex(
(item) =>
item.value ===
(settings.forScope(selectedScope).settings.theme || DEFAULT_THEME.name),
(item) => item.value === (settings.merged.theme || DEFAULT_THEME.name),
);
const scopeItems = [
@@ -88,45 +94,49 @@ export function ThemeDialog({
return (
<Box
borderStyle="round"
borderColor={Colors.AccentCyan}
flexDirection="column"
borderColor={Colors.AccentPurple}
flexDirection="row"
padding={1}
width="50%"
width="100%"
>
<Text bold={focusedSection === 'theme'}>
{focusedSection === 'theme' ? '> ' : ' '}Select Theme{' '}
<Text color={Colors.SubtleComment}>{otherScopeModifiedMessage}</Text>
</Text>
<RadioButtonSelect
key={selectInputKey}
items={themeItems}
initialIndex={initialThemeIndex}
onSelect={handleThemeSelect} // Use the wrapper handler
onHighlight={onHighlight}
isFocused={focusedSection === 'theme'}
/>
{/* Scope Selection */}
<Box marginTop={1} flexDirection="column">
<Text bold={focusedSection === 'scope'}>
{focusedSection === 'scope' ? '> ' : ' '}Apply To
{/* Left Column: Selection */}
<Box flexDirection="column" width="50%" paddingRight={2}>
<Text bold={focusedSection === 'theme'}>
{focusedSection === 'theme' ? '> ' : ' '}Select Theme{' '}
<Text color={Colors.SubtleComment}>{otherScopeModifiedMessage}</Text>
</Text>
<RadioButtonSelect
items={scopeItems}
initialIndex={0} // Default to User Settings
onSelect={handleScopeSelect}
onHighlight={handleScopeHighlight}
isFocused={focusedSection === 'scope'}
key={selectInputKey}
items={themeItems}
initialIndex={initialThemeIndex}
onSelect={handleThemeSelect}
onHighlight={onHighlight}
isFocused={focusedSection === 'theme'}
/>
{/* Scope Selection */}
<Box marginTop={1} flexDirection="column">
<Text bold={focusedSection === 'scope'}>
{focusedSection === 'scope' ? '> ' : ' '}Apply To
</Text>
<RadioButtonSelect
items={scopeItems}
initialIndex={0} // Default to User Settings
onSelect={handleScopeSelect}
onHighlight={handleScopeHighlight}
isFocused={focusedSection === 'scope'}
/>
</Box>
<Box marginTop={1}>
<Text color={Colors.SubtleComment}>
(Use / arrows and Enter to select, Tab to change focus)
</Text>
</Box>
</Box>
<Box marginTop={1}>
<Text color={Colors.SubtleComment}>
(Use / arrows and Enter to select, Tab to change focus)
</Text>
</Box>
<Box marginTop={1} flexDirection="column">
{/* Right Column: Preview */}
<Box flexDirection="column" width="50%" paddingLeft={3}>
<Text bold>Preview</Text>
<Box
borderStyle="single"

View File

@@ -27,7 +27,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
const hasPending = !toolCalls.every(
(t) => t.status === ToolCallStatus.Success,
);
const borderColor = hasPending ? Colors.AccentYellow : Colors.AccentCyan;
const borderColor = hasPending ? Colors.AccentYellow : Colors.AccentPurple;
return (
<Box

View File

@@ -27,7 +27,12 @@ export interface RadioSelectItem<T> {
*/
export interface RadioButtonSelectProps<T> {
/** An array of items to display as radio options. */
items: Array<RadioSelectItem<T>>;
items: Array<
RadioSelectItem<T> & {
themeNameDisplay?: string;
themeTypeDisplay?: string;
}
>;
/** The initial index selected */
initialIndex?: number;
@@ -42,33 +47,6 @@ export interface RadioButtonSelectProps<T> {
isFocused?: boolean;
}
/**
* Custom indicator component displaying radio button style (◉/○).
*/
function RadioIndicator({
isSelected = false,
}: InkSelectIndicatorProps): React.JSX.Element {
return (
<Box marginRight={1}>
<Text color={isSelected ? Colors.AccentGreen : Colors.Gray}>
{isSelected ? '◉' : '○'}
</Text>
</Box>
);
}
/**
* Custom item component for displaying the label with appropriate color.
*/
function RadioItem({
isSelected = false,
label,
}: InkSelectItemProps): React.JSX.Element {
return (
<Text color={isSelected ? Colors.AccentGreen : Colors.Gray}>{label}</Text>
);
}
/**
* A specialized SelectInput component styled to look like radio buttons.
* It uses '◉' for selected and '○' for unselected items.
@@ -80,7 +58,7 @@ export function RadioButtonSelect<T>({
initialIndex,
onSelect,
onHighlight,
isFocused,
isFocused, // This prop indicates if the current RadioButtonSelect group is focused
}: RadioButtonSelectProps<T>): React.JSX.Element {
const handleSelect = (item: RadioSelectItem<T>) => {
onSelect(item.value);
@@ -90,11 +68,72 @@ export function RadioButtonSelect<T>({
onHighlight(item.value);
}
};
/**
* Custom indicator component displaying radio button style (◉/○).
* Color changes based on whether the item is selected and if its group is focused.
*/
function DynamicRadioIndicator({
isSelected = false,
}: InkSelectIndicatorProps): React.JSX.Element {
let indicatorColor = Colors.Foreground; // Default for not selected
if (isSelected) {
if (isFocused) {
// Group is focused, selected item is AccentGreen
indicatorColor = Colors.AccentGreen;
} else {
// Group is NOT focused, selected item is Foreground
indicatorColor = Colors.Foreground;
}
}
return (
<Box marginRight={1}>
<Text color={indicatorColor}>{isSelected ? '●' : '○'}</Text>
</Box>
);
}
/**
* Custom item component for displaying the label.
* Color changes based on whether the item is selected and if its group is focused.
* Now also handles displaying theme type with custom color.
*/
function CustomThemeItemComponent(
props: InkSelectItemProps,
): React.JSX.Element {
const { isSelected = false, label } = props;
const itemWithThemeProps = props as typeof props & {
themeNameDisplay?: string;
themeTypeDisplay?: string;
};
let textColor = Colors.Foreground;
if (isSelected) {
textColor = isFocused ? Colors.AccentGreen : Colors.Foreground;
}
if (
itemWithThemeProps.themeNameDisplay &&
itemWithThemeProps.themeTypeDisplay
) {
return (
<Text color={textColor}>
{itemWithThemeProps.themeNameDisplay}{' '}
<Text color={Colors.SubtleComment}>
{itemWithThemeProps.themeTypeDisplay}
</Text>
</Text>
);
}
return <Text color={textColor}>{label}</Text>;
}
initialIndex = initialIndex ?? 0;
return (
<SelectInput
indicatorComponent={RadioIndicator}
itemComponent={RadioItem}
indicatorComponent={DynamicRadioIndicator}
itemComponent={CustomThemeItemComponent}
items={items}
initialIndex={initialIndex}
onSelect={handleSelect}