File size: 3,603 Bytes
52efc7b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import type React from 'react';
import { Text, Box } from 'ink';
import { theme } from '../../semantic-colors.js';
import {
  BaseSelectionList,
  type RenderItemContext,
} from './BaseSelectionList.js';
import type { SelectionListItem } from '../../hooks/useSelectionList.js';

/**
 * Represents a single option for the RadioButtonSelect.
 * Requires a label for display and a value to be returned on selection.
 */
export interface RadioSelectItem<T> extends SelectionListItem<T> {
  label: string;
  sublabel?: string;
  themeNameDisplay?: string;
  themeTypeDisplay?: string;
}

/**
 * Props for the RadioButtonSelect component.
 * @template T The type of the value associated with each radio item.
 */
export interface RadioButtonSelectProps<T> {
  /** An array of items to display as radio options. */
  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;
  /** Whether to show numbers next to items. */
  showNumbers?: boolean;
  /** Whether the hook should have priority over normal subscribers. */
  priority?: boolean;
  /** Optional custom renderer for items. */
  renderItem?: (
    item: RadioSelectItem<T>,
    context: RenderItemContext,
  ) => React.ReactNode;
}

/**
 * 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 = 0,
  onSelect,
  onHighlight,
  isFocused = true,
  showScrollArrows = false,
  maxItemsToShow = 10,
  showNumbers = true,
  priority,
  renderItem,
}: RadioButtonSelectProps<T>): React.JSX.Element {
  return (
    <BaseSelectionList<T, RadioSelectItem<T>>
      items={items}
      initialIndex={initialIndex}
      onSelect={onSelect}
      onHighlight={onHighlight}
      isFocused={isFocused}
      showNumbers={showNumbers}
      showScrollArrows={showScrollArrows}
      maxItemsToShow={maxItemsToShow}
      priority={priority}
      renderItem={
        renderItem ||
        ((item, { titleColor }) => {
          // Handle special theme display case for ThemeDialog compatibility
          if (item.themeNameDisplay && item.themeTypeDisplay) {
            return (
              <Text color={titleColor} wrap="truncate" key={item.key}>
                {item.themeNameDisplay}{' '}
                <Text color={theme.text.secondary}>
                  {item.themeTypeDisplay}
                </Text>
              </Text>
            );
          }
          // Regular label display
          return (
            <Box flexDirection="column">
              <Text color={titleColor} wrap="truncate">
                {item.label}
              </Text>
              {item.sublabel && (
                <Text color={theme.text.secondary} wrap="truncate">
                  {item.sublabel}
                </Text>
              )}
            </Box>
          );
        })
      }
    />
  );
}