diff options
| author | Miguel Solorio <[email protected]> | 2025-05-08 16:00:55 -0700 |
|---|---|---|
| committer | GitHub <[email protected]> | 2025-05-08 16:00:55 -0700 |
| commit | a685597b70242eb4c6b38d30c5356ad79418176d (patch) | |
| tree | f62cf6f0322293222c76c7cefba54fcd254ac83c /packages/cli/src | |
| parent | 6b0ac084b8557d3ad76a33df991b73196d792280 (diff) | |
UI Polish for theme selector (#294)
Diffstat (limited to 'packages/cli/src')
| -rw-r--r-- | packages/cli/src/ui/colors.ts | 3 | ||||
| -rw-r--r-- | packages/cli/src/ui/components/SuggestionsDisplay.tsx | 7 | ||||
| -rw-r--r-- | packages/cli/src/ui/components/ThemeDialog.tsx | 86 | ||||
| -rw-r--r-- | packages/cli/src/ui/components/messages/ToolGroupMessage.tsx | 2 | ||||
| -rw-r--r-- | packages/cli/src/ui/components/shared/RadioButtonSelect.tsx | 101 | ||||
| -rw-r--r-- | packages/cli/src/ui/hooks/useThemeCommand.ts | 2 | ||||
| -rw-r--r-- | packages/cli/src/ui/themes/ansi.ts | 3 | ||||
| -rw-r--r-- | packages/cli/src/ui/themes/atom-one-dark.ts | 3 | ||||
| -rw-r--r-- | packages/cli/src/ui/themes/dracula.ts | 1 | ||||
| -rw-r--r-- | packages/cli/src/ui/themes/github.ts | 1 | ||||
| -rw-r--r-- | packages/cli/src/ui/themes/googlecode.ts | 1 | ||||
| -rw-r--r-- | packages/cli/src/ui/themes/theme-manager.ts | 29 | ||||
| -rw-r--r-- | packages/cli/src/ui/themes/theme.ts | 8 | ||||
| -rw-r--r-- | packages/cli/src/ui/themes/vs.ts | 1 | ||||
| -rw-r--r-- | packages/cli/src/ui/themes/vs2015.ts | 1 | ||||
| -rw-r--r-- | packages/cli/src/ui/themes/xcode.ts | 1 |
16 files changed, 170 insertions, 80 deletions
diff --git a/packages/cli/src/ui/colors.ts b/packages/cli/src/ui/colors.ts index c5472efa..19fae1b9 100644 --- a/packages/cli/src/ui/colors.ts +++ b/packages/cli/src/ui/colors.ts @@ -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; }, diff --git a/packages/cli/src/ui/components/SuggestionsDisplay.tsx b/packages/cli/src/ui/components/SuggestionsDisplay.tsx index f0626fa9..ba25f2b6 100644 --- a/packages/cli/src/ui/components/SuggestionsDisplay.tsx +++ b/packages/cli/src/ui/components/SuggestionsDisplay.tsx @@ -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> diff --git a/packages/cli/src/ui/components/ThemeDialog.tsx b/packages/cli/src/ui/components/ThemeDialog.tsx index 7e8c5afd..20686040 100644 --- a/packages/cli/src/ui/components/ThemeDialog.tsx +++ b/packages/cli/src/ui/components/ThemeDialog.tsx @@ -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'} /> - </Box> - <Box marginTop={1}> - <Text color={Colors.SubtleComment}> - (Use ↑/↓ arrows and Enter to select, Tab to change focus) - </Text> + {/* 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} flexDirection="column"> + {/* Right Column: Preview */} + <Box flexDirection="column" width="50%" paddingLeft={3}> <Text bold>Preview</Text> <Box borderStyle="single" diff --git a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx index 401b8ee0..a9a51232 100644 --- a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx +++ b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx @@ -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 diff --git a/packages/cli/src/ui/components/shared/RadioButtonSelect.tsx b/packages/cli/src/ui/components/shared/RadioButtonSelect.tsx index 3db8b678..377be3e3 100644 --- a/packages/cli/src/ui/components/shared/RadioButtonSelect.tsx +++ b/packages/cli/src/ui/components/shared/RadioButtonSelect.tsx @@ -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; @@ -43,33 +48,6 @@ export interface RadioButtonSelectProps<T> { } /** - * 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} diff --git a/packages/cli/src/ui/hooks/useThemeCommand.ts b/packages/cli/src/ui/hooks/useThemeCommand.ts index 0f4ab93b..c32a7c2e 100644 --- a/packages/cli/src/ui/hooks/useThemeCommand.ts +++ b/packages/cli/src/ui/hooks/useThemeCommand.ts @@ -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; diff --git a/packages/cli/src/ui/themes/ansi.ts b/packages/cli/src/ui/themes/ansi.ts index 29ca6469..b5e2015e 100644 --- a/packages/cli/src/ui/themes/ansi.ts +++ b/packages/cli/src/ui/themes/ansi.ts @@ -7,7 +7,8 @@ import { ansiTheme, Theme } from './theme.js'; export const ANSI: Theme = new Theme( - 'ANSI colors only', + 'ANSI', + 'ansi', { hljs: { display: 'block', diff --git a/packages/cli/src/ui/themes/atom-one-dark.ts b/packages/cli/src/ui/themes/atom-one-dark.ts index 5599c01a..d38fbcbd 100644 --- a/packages/cli/src/ui/themes/atom-one-dark.ts +++ b/packages/cli/src/ui/themes/atom-one-dark.ts @@ -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', diff --git a/packages/cli/src/ui/themes/dracula.ts b/packages/cli/src/ui/themes/dracula.ts index e8979e70..9597e005 100644 --- a/packages/cli/src/ui/themes/dracula.ts +++ b/packages/cli/src/ui/themes/dracula.ts @@ -8,6 +8,7 @@ import { darkTheme, Theme } from './theme.js'; export const Dracula: Theme = new Theme( 'Dracula', + 'dark', { hljs: { display: 'block', diff --git a/packages/cli/src/ui/themes/github.ts b/packages/cli/src/ui/themes/github.ts index 61d7de65..2a5533bb 100644 --- a/packages/cli/src/ui/themes/github.ts +++ b/packages/cli/src/ui/themes/github.ts @@ -8,6 +8,7 @@ import { lightTheme, Theme } from './theme.js'; export const GitHub: Theme = new Theme( 'GitHub', + 'light', { hljs: { display: 'block', diff --git a/packages/cli/src/ui/themes/googlecode.ts b/packages/cli/src/ui/themes/googlecode.ts index 25dbb8a3..0729d67a 100644 --- a/packages/cli/src/ui/themes/googlecode.ts +++ b/packages/cli/src/ui/themes/googlecode.ts @@ -8,6 +8,7 @@ import { lightTheme, Theme } from './theme.js'; export const GoogleCode: Theme = new Theme( 'Google Code', + 'light', { hljs: { display: 'block', diff --git a/packages/cli/src/ui/themes/theme-manager.ts b/packages/cli/src/ui/themes/theme-manager.ts index 4a8cc32c..d1f8df9c 100644 --- a/packages/cli/src/ui/themes/theme-manager.ts +++ b/packages/cli/src/ui/themes/theme-manager.ts @@ -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, })); } diff --git a/packages/cli/src/ui/themes/theme.ts b/packages/cli/src/ui/themes/theme.ts index 88868790..582d2e9e 100644 --- a/packages/cli/src/ui/themes/theme.ts +++ b/packages/cli/src/ui/themes/theme.ts @@ -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, ) { diff --git a/packages/cli/src/ui/themes/vs.ts b/packages/cli/src/ui/themes/vs.ts index ea0d938d..2faf02a7 100644 --- a/packages/cli/src/ui/themes/vs.ts +++ b/packages/cli/src/ui/themes/vs.ts @@ -8,6 +8,7 @@ import { lightTheme, Theme } from './theme.js'; export const VS: Theme = new Theme( 'VS', + 'light', { hljs: { display: 'block', diff --git a/packages/cli/src/ui/themes/vs2015.ts b/packages/cli/src/ui/themes/vs2015.ts index 93f00ec8..34431abf 100644 --- a/packages/cli/src/ui/themes/vs2015.ts +++ b/packages/cli/src/ui/themes/vs2015.ts @@ -8,6 +8,7 @@ import { darkTheme, Theme } from './theme.js'; export const VS2015: Theme = new Theme( 'VS2015', + 'dark', { hljs: { display: 'block', diff --git a/packages/cli/src/ui/themes/xcode.ts b/packages/cli/src/ui/themes/xcode.ts index 53fd2e5b..26b8cf72 100644 --- a/packages/cli/src/ui/themes/xcode.ts +++ b/packages/cli/src/ui/themes/xcode.ts @@ -8,6 +8,7 @@ import { lightTheme, Theme } from './theme.js'; export const XCode: Theme = new Theme( 'XCode', + 'light', { hljs: { display: 'block', |
