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';
|
import { ColorsTheme } from './themes/theme.js';
|
||||||
|
|
||||||
export const Colors: ColorsTheme = {
|
export const Colors: ColorsTheme = {
|
||||||
|
get type() {
|
||||||
|
return themeManager.getActiveTheme().colors.type;
|
||||||
|
},
|
||||||
get Foreground() {
|
get Foreground() {
|
||||||
return themeManager.getActiveTheme().colors.Foreground;
|
return themeManager.getActiveTheme().colors.Foreground;
|
||||||
},
|
},
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Box, Text } from 'ink';
|
import { Box, Text } from 'ink';
|
||||||
|
import { Colors } from '../colors.js';
|
||||||
export interface Suggestion {
|
export interface Suggestion {
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
|
@ -48,7 +49,7 @@ export function SuggestionsDisplay({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box borderStyle="round" flexDirection="column" paddingX={1} width={width}>
|
<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) => {
|
{visibleSuggestions.map((suggestion, index) => {
|
||||||
const originalIndex = startIndex + index;
|
const originalIndex = startIndex + index;
|
||||||
|
@ -56,8 +57,8 @@ export function SuggestionsDisplay({
|
||||||
return (
|
return (
|
||||||
<Text
|
<Text
|
||||||
key={`${suggestion}-${originalIndex}`}
|
key={`${suggestion}-${originalIndex}`}
|
||||||
color={isActive ? 'black' : 'white'}
|
color={isActive ? Colors.Background : Colors.Foreground}
|
||||||
backgroundColor={isActive ? 'blue' : undefined}
|
backgroundColor={isActive ? Colors.AccentBlue : undefined}
|
||||||
>
|
>
|
||||||
{suggestion.label}
|
{suggestion.label}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
|
@ -32,16 +32,22 @@ export function ThemeDialog({
|
||||||
SettingScope.User,
|
SettingScope.User,
|
||||||
);
|
);
|
||||||
|
|
||||||
const themeItems = themeManager.getAvailableThemes().map((theme) => ({
|
// Generate theme items
|
||||||
label: theme.active ? `${theme.name} (Active)` : theme.name,
|
const themeItems = themeManager.getAvailableThemes().map((theme) => {
|
||||||
|
const typeString = theme.type.charAt(0).toUpperCase() + theme.type.slice(1);
|
||||||
|
return {
|
||||||
|
label: theme.name,
|
||||||
value: theme.name,
|
value: theme.name,
|
||||||
}));
|
themeNameDisplay: theme.name,
|
||||||
|
themeTypeDisplay: typeString,
|
||||||
|
};
|
||||||
|
});
|
||||||
const [selectInputKey, setSelectInputKey] = useState(Date.now());
|
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(
|
const initialThemeIndex = themeItems.findIndex(
|
||||||
(item) =>
|
(item) => item.value === (settings.merged.theme || DEFAULT_THEME.name),
|
||||||
item.value ===
|
|
||||||
(settings.forScope(selectedScope).settings.theme || DEFAULT_THEME.name),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const scopeItems = [
|
const scopeItems = [
|
||||||
|
@ -88,24 +94,26 @@ export function ThemeDialog({
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
borderStyle="round"
|
borderStyle="round"
|
||||||
borderColor={Colors.AccentCyan}
|
borderColor={Colors.AccentPurple}
|
||||||
flexDirection="column"
|
flexDirection="row"
|
||||||
padding={1}
|
padding={1}
|
||||||
width="50%"
|
width="100%"
|
||||||
>
|
>
|
||||||
|
{/* Left Column: Selection */}
|
||||||
|
<Box flexDirection="column" width="50%" paddingRight={2}>
|
||||||
<Text bold={focusedSection === 'theme'}>
|
<Text bold={focusedSection === 'theme'}>
|
||||||
{focusedSection === 'theme' ? '> ' : ' '}Select Theme{' '}
|
{focusedSection === 'theme' ? '> ' : ' '}Select Theme{' '}
|
||||||
<Text color={Colors.SubtleComment}>{otherScopeModifiedMessage}</Text>
|
<Text color={Colors.SubtleComment}>{otherScopeModifiedMessage}</Text>
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<RadioButtonSelect
|
<RadioButtonSelect
|
||||||
key={selectInputKey}
|
key={selectInputKey}
|
||||||
items={themeItems}
|
items={themeItems}
|
||||||
initialIndex={initialThemeIndex}
|
initialIndex={initialThemeIndex}
|
||||||
onSelect={handleThemeSelect} // Use the wrapper handler
|
onSelect={handleThemeSelect}
|
||||||
onHighlight={onHighlight}
|
onHighlight={onHighlight}
|
||||||
isFocused={focusedSection === 'theme'}
|
isFocused={focusedSection === 'theme'}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Scope Selection */}
|
{/* Scope Selection */}
|
||||||
<Box marginTop={1} flexDirection="column">
|
<Box marginTop={1} flexDirection="column">
|
||||||
<Text bold={focusedSection === 'scope'}>
|
<Text bold={focusedSection === 'scope'}>
|
||||||
|
@ -125,8 +133,10 @@ export function ThemeDialog({
|
||||||
(Use ↑/↓ arrows and Enter to select, Tab to change focus)
|
(Use ↑/↓ arrows and Enter to select, Tab to change focus)
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Box marginTop={1} flexDirection="column">
|
{/* Right Column: Preview */}
|
||||||
|
<Box flexDirection="column" width="50%" paddingLeft={3}>
|
||||||
<Text bold>Preview</Text>
|
<Text bold>Preview</Text>
|
||||||
<Box
|
<Box
|
||||||
borderStyle="single"
|
borderStyle="single"
|
||||||
|
|
|
@ -27,7 +27,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
||||||
const hasPending = !toolCalls.every(
|
const hasPending = !toolCalls.every(
|
||||||
(t) => t.status === ToolCallStatus.Success,
|
(t) => t.status === ToolCallStatus.Success,
|
||||||
);
|
);
|
||||||
const borderColor = hasPending ? Colors.AccentYellow : Colors.AccentCyan;
|
const borderColor = hasPending ? Colors.AccentYellow : Colors.AccentPurple;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
|
|
|
@ -27,7 +27,12 @@ export interface RadioSelectItem<T> {
|
||||||
*/
|
*/
|
||||||
export interface RadioButtonSelectProps<T> {
|
export interface RadioButtonSelectProps<T> {
|
||||||
/** An array of items to display as radio options. */
|
/** 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 */
|
/** The initial index selected */
|
||||||
initialIndex?: number;
|
initialIndex?: number;
|
||||||
|
@ -42,33 +47,6 @@ export interface RadioButtonSelectProps<T> {
|
||||||
isFocused?: boolean;
|
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.
|
* A specialized SelectInput component styled to look like radio buttons.
|
||||||
* It uses '◉' for selected and '○' for unselected items.
|
* It uses '◉' for selected and '○' for unselected items.
|
||||||
|
@ -80,7 +58,7 @@ export function RadioButtonSelect<T>({
|
||||||
initialIndex,
|
initialIndex,
|
||||||
onSelect,
|
onSelect,
|
||||||
onHighlight,
|
onHighlight,
|
||||||
isFocused,
|
isFocused, // This prop indicates if the current RadioButtonSelect group is focused
|
||||||
}: RadioButtonSelectProps<T>): React.JSX.Element {
|
}: RadioButtonSelectProps<T>): React.JSX.Element {
|
||||||
const handleSelect = (item: RadioSelectItem<T>) => {
|
const handleSelect = (item: RadioSelectItem<T>) => {
|
||||||
onSelect(item.value);
|
onSelect(item.value);
|
||||||
|
@ -90,11 +68,72 @@ export function RadioButtonSelect<T>({
|
||||||
onHighlight(item.value);
|
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;
|
initialIndex = initialIndex ?? 0;
|
||||||
return (
|
return (
|
||||||
<SelectInput
|
<SelectInput
|
||||||
indicatorComponent={RadioIndicator}
|
indicatorComponent={DynamicRadioIndicator}
|
||||||
itemComponent={RadioItem}
|
itemComponent={CustomThemeItemComponent}
|
||||||
items={items}
|
items={items}
|
||||||
initialIndex={initialIndex}
|
initialIndex={initialIndex}
|
||||||
onSelect={handleSelect}
|
onSelect={handleSelect}
|
||||||
|
|
|
@ -19,7 +19,7 @@ interface UseThemeCommandReturn {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useThemeCommand = (
|
export const useThemeCommand = (
|
||||||
loadedSettings: LoadedSettings, // Changed parameter
|
loadedSettings: LoadedSettings,
|
||||||
): UseThemeCommandReturn => {
|
): UseThemeCommandReturn => {
|
||||||
// Determine the effective theme
|
// Determine the effective theme
|
||||||
const effectiveTheme = loadedSettings.merged.theme;
|
const effectiveTheme = loadedSettings.merged.theme;
|
||||||
|
|
|
@ -7,7 +7,8 @@
|
||||||
import { ansiTheme, Theme } from './theme.js';
|
import { ansiTheme, Theme } from './theme.js';
|
||||||
|
|
||||||
export const ANSI: Theme = new Theme(
|
export const ANSI: Theme = new Theme(
|
||||||
'ANSI colors only',
|
'ANSI',
|
||||||
|
'ansi',
|
||||||
{
|
{
|
||||||
hljs: {
|
hljs: {
|
||||||
display: 'block',
|
display: 'block',
|
||||||
|
|
|
@ -7,7 +7,8 @@
|
||||||
import { darkTheme, Theme } from './theme.js';
|
import { darkTheme, Theme } from './theme.js';
|
||||||
|
|
||||||
export const AtomOneDark: Theme = new Theme(
|
export const AtomOneDark: Theme = new Theme(
|
||||||
'Atom One Dark',
|
'Atom One',
|
||||||
|
'dark',
|
||||||
{
|
{
|
||||||
hljs: {
|
hljs: {
|
||||||
display: 'block',
|
display: 'block',
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { darkTheme, Theme } from './theme.js';
|
||||||
|
|
||||||
export const Dracula: Theme = new Theme(
|
export const Dracula: Theme = new Theme(
|
||||||
'Dracula',
|
'Dracula',
|
||||||
|
'dark',
|
||||||
{
|
{
|
||||||
hljs: {
|
hljs: {
|
||||||
display: 'block',
|
display: 'block',
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { lightTheme, Theme } from './theme.js';
|
||||||
|
|
||||||
export const GitHub: Theme = new Theme(
|
export const GitHub: Theme = new Theme(
|
||||||
'GitHub',
|
'GitHub',
|
||||||
|
'light',
|
||||||
{
|
{
|
||||||
hljs: {
|
hljs: {
|
||||||
display: 'block',
|
display: 'block',
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { lightTheme, Theme } from './theme.js';
|
||||||
|
|
||||||
export const GoogleCode: Theme = new Theme(
|
export const GoogleCode: Theme = new Theme(
|
||||||
'Google Code',
|
'Google Code',
|
||||||
|
'light',
|
||||||
{
|
{
|
||||||
hljs: {
|
hljs: {
|
||||||
display: 'block',
|
display: 'block',
|
||||||
|
|
|
@ -11,12 +11,12 @@ import { GoogleCode } from './googlecode.js';
|
||||||
import { VS } from './vs.js';
|
import { VS } from './vs.js';
|
||||||
import { VS2015 } from './vs2015.js';
|
import { VS2015 } from './vs2015.js';
|
||||||
import { XCode } from './xcode.js';
|
import { XCode } from './xcode.js';
|
||||||
import { Theme } from './theme.js';
|
import { Theme, ThemeType } from './theme.js';
|
||||||
import { ANSI } from './ansi.js';
|
import { ANSI } from './ansi.js';
|
||||||
|
|
||||||
export interface ThemeDisplay {
|
export interface ThemeDisplay {
|
||||||
name: string;
|
name: string;
|
||||||
active: boolean;
|
type: ThemeType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_THEME: Theme = VS2015;
|
export const DEFAULT_THEME: Theme = VS2015;
|
||||||
|
@ -43,9 +43,30 @@ class ThemeManager {
|
||||||
* Returns a list of available theme names.
|
* Returns a list of available theme names.
|
||||||
*/
|
*/
|
||||||
getAvailableThemes(): ThemeDisplay[] {
|
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,
|
name: theme.name,
|
||||||
active: theme === this.activeTheme,
|
type: theme.type,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,11 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { CSSProperties } from 'react';
|
import type { CSSProperties } from 'react';
|
||||||
|
|
||||||
|
export type ThemeType = 'light' | 'dark' | 'ansi';
|
||||||
|
|
||||||
export interface ColorsTheme {
|
export interface ColorsTheme {
|
||||||
|
type: ThemeType;
|
||||||
Background: string;
|
Background: string;
|
||||||
Foreground: string;
|
Foreground: string;
|
||||||
LightBlue: string;
|
LightBlue: string;
|
||||||
|
@ -21,6 +25,7 @@ export interface ColorsTheme {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const lightTheme: ColorsTheme = {
|
export const lightTheme: ColorsTheme = {
|
||||||
|
type: 'light',
|
||||||
Background: '#FAFAFA',
|
Background: '#FAFAFA',
|
||||||
Foreground: '#3C3C43',
|
Foreground: '#3C3C43',
|
||||||
LightBlue: '#ADD8E6',
|
LightBlue: '#ADD8E6',
|
||||||
|
@ -36,6 +41,7 @@ export const lightTheme: ColorsTheme = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const darkTheme: ColorsTheme = {
|
export const darkTheme: ColorsTheme = {
|
||||||
|
type: 'dark',
|
||||||
Background: '#1E1E2E',
|
Background: '#1E1E2E',
|
||||||
Foreground: '#CDD6F4',
|
Foreground: '#CDD6F4',
|
||||||
LightBlue: '#ADD8E6',
|
LightBlue: '#ADD8E6',
|
||||||
|
@ -51,6 +57,7 @@ export const darkTheme: ColorsTheme = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ansiTheme: ColorsTheme = {
|
export const ansiTheme: ColorsTheme = {
|
||||||
|
type: 'ansi',
|
||||||
Background: 'black',
|
Background: 'black',
|
||||||
Foreground: 'white',
|
Foreground: 'white',
|
||||||
LightBlue: 'blue',
|
LightBlue: 'blue',
|
||||||
|
@ -250,6 +257,7 @@ export class Theme {
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
readonly name: string,
|
readonly name: string,
|
||||||
|
readonly type: ThemeType,
|
||||||
rawMappings: Record<string, CSSProperties>,
|
rawMappings: Record<string, CSSProperties>,
|
||||||
readonly colors: ColorsTheme,
|
readonly colors: ColorsTheme,
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { lightTheme, Theme } from './theme.js';
|
||||||
|
|
||||||
export const VS: Theme = new Theme(
|
export const VS: Theme = new Theme(
|
||||||
'VS',
|
'VS',
|
||||||
|
'light',
|
||||||
{
|
{
|
||||||
hljs: {
|
hljs: {
|
||||||
display: 'block',
|
display: 'block',
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { darkTheme, Theme } from './theme.js';
|
||||||
|
|
||||||
export const VS2015: Theme = new Theme(
|
export const VS2015: Theme = new Theme(
|
||||||
'VS2015',
|
'VS2015',
|
||||||
|
'dark',
|
||||||
{
|
{
|
||||||
hljs: {
|
hljs: {
|
||||||
display: 'block',
|
display: 'block',
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { lightTheme, Theme } from './theme.js';
|
||||||
|
|
||||||
export const XCode: Theme = new Theme(
|
export const XCode: Theme = new Theme(
|
||||||
'XCode',
|
'XCode',
|
||||||
|
'light',
|
||||||
{
|
{
|
||||||
hljs: {
|
hljs: {
|
||||||
display: 'block',
|
display: 'block',
|
||||||
|
|
Loading…
Reference in New Issue