UI Polish for theme selector (#294)
This commit is contained in:
parent
6b0ac084b8
commit
a685597b70
|
@ -8,6 +8,9 @@ import { themeManager } from './themes/theme-manager.js';
|
|||
import { ColorsTheme } from './themes/theme.js';
|
||||
|
||||
export const Colors: ColorsTheme = {
|
||||
get type() {
|
||||
return themeManager.getActiveTheme().colors.type;
|
||||
},
|
||||
get Foreground() {
|
||||
return themeManager.getActiveTheme().colors.Foreground;
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -19,7 +19,7 @@ interface UseThemeCommandReturn {
|
|||
}
|
||||
|
||||
export const useThemeCommand = (
|
||||
loadedSettings: LoadedSettings, // Changed parameter
|
||||
loadedSettings: LoadedSettings,
|
||||
): UseThemeCommandReturn => {
|
||||
// Determine the effective theme
|
||||
const effectiveTheme = loadedSettings.merged.theme;
|
||||
|
|
|
@ -7,7 +7,8 @@
|
|||
import { ansiTheme, Theme } from './theme.js';
|
||||
|
||||
export const ANSI: Theme = new Theme(
|
||||
'ANSI colors only',
|
||||
'ANSI',
|
||||
'ansi',
|
||||
{
|
||||
hljs: {
|
||||
display: 'block',
|
||||
|
|
|
@ -7,7 +7,8 @@
|
|||
import { darkTheme, Theme } from './theme.js';
|
||||
|
||||
export const AtomOneDark: Theme = new Theme(
|
||||
'Atom One Dark',
|
||||
'Atom One',
|
||||
'dark',
|
||||
{
|
||||
hljs: {
|
||||
display: 'block',
|
||||
|
|
|
@ -8,6 +8,7 @@ import { darkTheme, Theme } from './theme.js';
|
|||
|
||||
export const Dracula: Theme = new Theme(
|
||||
'Dracula',
|
||||
'dark',
|
||||
{
|
||||
hljs: {
|
||||
display: 'block',
|
||||
|
|
|
@ -8,6 +8,7 @@ import { lightTheme, Theme } from './theme.js';
|
|||
|
||||
export const GitHub: Theme = new Theme(
|
||||
'GitHub',
|
||||
'light',
|
||||
{
|
||||
hljs: {
|
||||
display: 'block',
|
||||
|
|
|
@ -8,6 +8,7 @@ import { lightTheme, Theme } from './theme.js';
|
|||
|
||||
export const GoogleCode: Theme = new Theme(
|
||||
'Google Code',
|
||||
'light',
|
||||
{
|
||||
hljs: {
|
||||
display: 'block',
|
||||
|
|
|
@ -11,12 +11,12 @@ import { GoogleCode } from './googlecode.js';
|
|||
import { VS } from './vs.js';
|
||||
import { VS2015 } from './vs2015.js';
|
||||
import { XCode } from './xcode.js';
|
||||
import { Theme } from './theme.js';
|
||||
import { Theme, ThemeType } from './theme.js';
|
||||
import { ANSI } from './ansi.js';
|
||||
|
||||
export interface ThemeDisplay {
|
||||
name: string;
|
||||
active: boolean;
|
||||
type: ThemeType;
|
||||
}
|
||||
|
||||
export const DEFAULT_THEME: Theme = VS2015;
|
||||
|
@ -43,9 +43,30 @@ class ThemeManager {
|
|||
* Returns a list of available theme names.
|
||||
*/
|
||||
getAvailableThemes(): ThemeDisplay[] {
|
||||
return this.availableThemes.map((theme) => ({
|
||||
const sortedThemes = [...this.availableThemes].sort((a, b) => {
|
||||
const typeOrder = (type: ThemeType): number => {
|
||||
switch (type) {
|
||||
case 'dark':
|
||||
return 1;
|
||||
case 'light':
|
||||
return 2;
|
||||
case 'ansi':
|
||||
return 3;
|
||||
default:
|
||||
return 4;
|
||||
}
|
||||
};
|
||||
|
||||
const typeComparison = typeOrder(a.type) - typeOrder(b.type);
|
||||
if (typeComparison !== 0) {
|
||||
return typeComparison;
|
||||
}
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
return sortedThemes.map((theme) => ({
|
||||
name: theme.name,
|
||||
active: theme === this.activeTheme,
|
||||
type: theme.type,
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,11 @@
|
|||
*/
|
||||
|
||||
import type { CSSProperties } from 'react';
|
||||
|
||||
export type ThemeType = 'light' | 'dark' | 'ansi';
|
||||
|
||||
export interface ColorsTheme {
|
||||
type: ThemeType;
|
||||
Background: string;
|
||||
Foreground: string;
|
||||
LightBlue: string;
|
||||
|
@ -21,6 +25,7 @@ export interface ColorsTheme {
|
|||
}
|
||||
|
||||
export const lightTheme: ColorsTheme = {
|
||||
type: 'light',
|
||||
Background: '#FAFAFA',
|
||||
Foreground: '#3C3C43',
|
||||
LightBlue: '#ADD8E6',
|
||||
|
@ -36,6 +41,7 @@ export const lightTheme: ColorsTheme = {
|
|||
};
|
||||
|
||||
export const darkTheme: ColorsTheme = {
|
||||
type: 'dark',
|
||||
Background: '#1E1E2E',
|
||||
Foreground: '#CDD6F4',
|
||||
LightBlue: '#ADD8E6',
|
||||
|
@ -51,6 +57,7 @@ export const darkTheme: ColorsTheme = {
|
|||
};
|
||||
|
||||
export const ansiTheme: ColorsTheme = {
|
||||
type: 'ansi',
|
||||
Background: 'black',
|
||||
Foreground: 'white',
|
||||
LightBlue: 'blue',
|
||||
|
@ -250,6 +257,7 @@ export class Theme {
|
|||
*/
|
||||
constructor(
|
||||
readonly name: string,
|
||||
readonly type: ThemeType,
|
||||
rawMappings: Record<string, CSSProperties>,
|
||||
readonly colors: ColorsTheme,
|
||||
) {
|
||||
|
|
|
@ -8,6 +8,7 @@ import { lightTheme, Theme } from './theme.js';
|
|||
|
||||
export const VS: Theme = new Theme(
|
||||
'VS',
|
||||
'light',
|
||||
{
|
||||
hljs: {
|
||||
display: 'block',
|
||||
|
|
|
@ -8,6 +8,7 @@ import { darkTheme, Theme } from './theme.js';
|
|||
|
||||
export const VS2015: Theme = new Theme(
|
||||
'VS2015',
|
||||
'dark',
|
||||
{
|
||||
hljs: {
|
||||
display: 'block',
|
||||
|
|
|
@ -8,6 +8,7 @@ import { lightTheme, Theme } from './theme.js';
|
|||
|
||||
export const XCode: Theme = new Theme(
|
||||
'XCode',
|
||||
'light',
|
||||
{
|
||||
hljs: {
|
||||
display: 'block',
|
||||
|
|
Loading…
Reference in New Issue