summaryrefslogtreecommitdiff
path: root/packages/cli/src/ui/components/shared
diff options
context:
space:
mode:
Diffstat (limited to 'packages/cli/src/ui/components/shared')
-rw-r--r--packages/cli/src/ui/components/shared/RadioButtonSelect.tsx193
1 files changed, 104 insertions, 89 deletions
diff --git a/packages/cli/src/ui/components/shared/RadioButtonSelect.tsx b/packages/cli/src/ui/components/shared/RadioButtonSelect.tsx
index fab0615c..c3829bb4 100644
--- a/packages/cli/src/ui/components/shared/RadioButtonSelect.tsx
+++ b/packages/cli/src/ui/components/shared/RadioButtonSelect.tsx
@@ -4,12 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import React from 'react';
-import { Text, Box } from 'ink';
-import SelectInput, {
- type ItemProps as InkSelectItemProps,
- type IndicatorProps as InkSelectIndicatorProps,
-} from 'ink-select-input';
+import React, { useEffect, useState } from 'react';
+import { Text, Box, useInput } from 'ink';
import { Colors } from '../../colors.js';
/**
@@ -20,6 +16,8 @@ export interface RadioSelectItem<T> {
label: string;
value: T;
disabled?: boolean;
+ themeNameDisplay?: string;
+ themeTypeDisplay?: string;
}
/**
@@ -28,115 +26,132 @@ export interface RadioSelectItem<T> {
*/
export interface RadioButtonSelectProps<T> {
/** An array of items to display as radio options. */
- items: Array<
- RadioSelectItem<T> & {
- themeNameDisplay?: string;
- themeTypeDisplay?: string;
- }
- >;
-
+ items: Array<RadioSelectItem<T>>;
/** The initial index selected */
initialIndex?: number;
-
/** Function called when an item is selected. Receives the `value` of the selected item. */
onSelect: (value: T) => void;
-
/** Function called when an item is highlighted. Receives the `value` of the selected item. */
onHighlight?: (value: T) => void;
-
/** Whether this select input is currently focused and should respond to input. */
isFocused?: boolean;
+ /** Whether to show the scroll arrows. */
+ showScrollArrows?: boolean;
+ /** The maximum number of items to show at once. */
+ maxItemsToShow?: number;
}
/**
- * A specialized SelectInput component styled to look like radio buttons.
- * It uses '◉' for selected and '○' for unselected items.
+ * A custom component that displays a list of items with radio buttons,
+ * supporting scrolling and keyboard navigation.
*
* @template T The type of the value associated with each radio item.
*/
export function RadioButtonSelect<T>({
items,
- initialIndex,
+ initialIndex = 0,
onSelect,
onHighlight,
- isFocused, // This prop indicates if the current RadioButtonSelect group is focused
+ isFocused,
+ showScrollArrows = true,
+ maxItemsToShow = 10,
}: RadioButtonSelectProps<T>): React.JSX.Element {
- const handleSelect = (item: RadioSelectItem<T>) => {
- onSelect(item.value);
- };
- const handleHighlight = (item: RadioSelectItem<T>) => {
- if (onHighlight) {
- onHighlight(item.value);
- }
- };
+ const [activeIndex, setActiveIndex] = useState(initialIndex);
+ const [scrollOffset, setScrollOffset] = useState(0);
- /**
- * 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 {
- return (
- <Box minWidth={2} flexShrink={0}>
- <Text color={isSelected ? Colors.AccentGreen : Colors.Foreground}>
- {isSelected ? '●' : '○'}
- </Text>
- </Box>
+ useEffect(() => {
+ const newScrollOffset = Math.max(
+ 0,
+ Math.min(activeIndex - maxItemsToShow + 1, items.length - maxItemsToShow),
);
- }
+ if (activeIndex < scrollOffset) {
+ setScrollOffset(activeIndex);
+ } else if (activeIndex >= scrollOffset + maxItemsToShow) {
+ setScrollOffset(newScrollOffset);
+ }
+ }, [activeIndex, items.length, scrollOffset, maxItemsToShow]);
- /**
- * 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;
- disabled?: boolean;
- };
+ useInput(
+ (input, key) => {
+ if (input === 'k' || key.upArrow) {
+ const newIndex = activeIndex > 0 ? activeIndex - 1 : items.length - 1;
+ setActiveIndex(newIndex);
+ onHighlight?.(items[newIndex]!.value);
+ }
+ if (input === 'j' || key.downArrow) {
+ const newIndex = activeIndex < items.length - 1 ? activeIndex + 1 : 0;
+ setActiveIndex(newIndex);
+ onHighlight?.(items[newIndex]!.value);
+ }
+ if (key.return) {
+ onSelect(items[activeIndex]!.value);
+ }
- let textColor = Colors.Foreground;
- if (isSelected) {
- textColor = Colors.AccentGreen;
- } else if (itemWithThemeProps.disabled === true) {
- textColor = Colors.Gray;
- }
+ // Enable selection directly from number keys.
+ if (/^[1-9]$/.test(input)) {
+ const targetIndex = Number.parseInt(input, 10) - 1;
+ if (targetIndex >= 0 && targetIndex < visibleItems.length) {
+ const selectedItem = visibleItems[targetIndex];
+ if (selectedItem) {
+ onSelect?.(selectedItem.value);
+ }
+ }
+ }
+ },
+ { isActive: isFocused && items.length > 0 },
+ );
- if (
- itemWithThemeProps.themeNameDisplay &&
- itemWithThemeProps.themeTypeDisplay
- ) {
- return (
- <Text color={textColor} wrap="truncate">
- {itemWithThemeProps.themeNameDisplay}{' '}
- <Text color={Colors.Gray}>{itemWithThemeProps.themeTypeDisplay}</Text>
+ const visibleItems = items.slice(scrollOffset, scrollOffset + maxItemsToShow);
+
+ return (
+ <Box flexDirection="column">
+ {showScrollArrows && (
+ <Text color={scrollOffset > 0 ? Colors.Foreground : Colors.Gray}>
+ ▲
</Text>
- );
- }
+ )}
+ {visibleItems.map((item, index) => {
+ const itemIndex = scrollOffset + index;
+ const isSelected = activeIndex === itemIndex;
- return (
- <Text color={textColor} wrap="truncate">
- {label}
- </Text>
- );
- }
+ let textColor = Colors.Foreground;
+ if (isSelected) {
+ textColor = Colors.AccentGreen;
+ } else if (item.disabled) {
+ textColor = Colors.Gray;
+ }
- initialIndex = initialIndex ?? 0;
- return (
- <SelectInput
- indicatorComponent={DynamicRadioIndicator}
- itemComponent={CustomThemeItemComponent}
- items={items}
- initialIndex={initialIndex}
- onSelect={handleSelect}
- onHighlight={handleHighlight}
- isFocused={isFocused}
- />
+ return (
+ <Box key={item.label}>
+ <Box minWidth={2} flexShrink={0}>
+ <Text color={isSelected ? Colors.AccentGreen : Colors.Foreground}>
+ {isSelected ? '●' : '○'}
+ </Text>
+ </Box>
+ {item.themeNameDisplay && item.themeTypeDisplay ? (
+ <Text color={textColor} wrap="truncate">
+ {item.themeNameDisplay}{' '}
+ <Text color={Colors.Gray}>{item.themeTypeDisplay}</Text>
+ </Text>
+ ) : (
+ <Text color={textColor} wrap="truncate">
+ {item.label}
+ </Text>
+ )}
+ </Box>
+ );
+ })}
+ {showScrollArrows && (
+ <Text
+ color={
+ scrollOffset + maxItemsToShow < items.length
+ ? Colors.Foreground
+ : Colors.Gray
+ }
+ >
+ ▼
+ </Text>
+ )}
+ </Box>
);
}