cli-menu-kit 0.1.26 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/api.d.ts +23 -5
  2. package/dist/api.js +16 -4
  3. package/dist/component-factories.d.ts +59 -0
  4. package/dist/component-factories.js +141 -0
  5. package/dist/components/display/header-v2.d.ts +13 -0
  6. package/dist/components/display/header-v2.js +43 -0
  7. package/dist/components/display/hints-v2.d.ts +10 -0
  8. package/dist/components/display/hints-v2.js +34 -0
  9. package/dist/components/display/hints.d.ts +56 -0
  10. package/dist/components/display/hints.js +81 -0
  11. package/dist/components/display/index.d.ts +3 -0
  12. package/dist/components/display/index.js +15 -1
  13. package/dist/components/display/input-prompt.d.ts +35 -0
  14. package/dist/components/display/input-prompt.js +36 -0
  15. package/dist/components/display/list.d.ts +49 -0
  16. package/dist/components/display/list.js +86 -0
  17. package/dist/components/display/table.d.ts +42 -0
  18. package/dist/components/display/table.js +107 -0
  19. package/dist/components/menus/boolean-menu.js +2 -1
  20. package/dist/components/menus/checkbox-menu.d.ts +2 -1
  21. package/dist/components/menus/checkbox-menu.js +30 -59
  22. package/dist/components/menus/checkbox-table-menu.d.ts +12 -0
  23. package/dist/components/menus/checkbox-table-menu.js +395 -0
  24. package/dist/components/menus/index.d.ts +1 -0
  25. package/dist/components/menus/index.js +3 -1
  26. package/dist/components/menus/radio-menu-split.d.ts +33 -0
  27. package/dist/components/menus/radio-menu-split.js +248 -0
  28. package/dist/components/menus/radio-menu-v2.d.ts +11 -0
  29. package/dist/components/menus/radio-menu-v2.js +150 -0
  30. package/dist/components/menus/radio-menu.d.ts +2 -1
  31. package/dist/components/menus/radio-menu.js +60 -123
  32. package/dist/core/hint-manager.d.ts +29 -0
  33. package/dist/core/hint-manager.js +65 -0
  34. package/dist/core/renderer.d.ts +2 -1
  35. package/dist/core/renderer.js +22 -6
  36. package/dist/core/screen-manager.d.ts +54 -0
  37. package/dist/core/screen-manager.js +119 -0
  38. package/dist/core/state-manager.d.ts +27 -0
  39. package/dist/core/state-manager.js +56 -0
  40. package/dist/core/terminal.d.ts +4 -1
  41. package/dist/core/terminal.js +37 -4
  42. package/dist/core/virtual-scroll.d.ts +65 -0
  43. package/dist/core/virtual-scroll.js +120 -0
  44. package/dist/i18n/languages/en.js +4 -1
  45. package/dist/i18n/languages/zh.js +4 -1
  46. package/dist/i18n/registry.d.ts +4 -3
  47. package/dist/i18n/registry.js +12 -4
  48. package/dist/i18n/types.d.ts +3 -0
  49. package/dist/index.d.ts +5 -4
  50. package/dist/index.js +30 -4
  51. package/dist/layout.d.ts +68 -0
  52. package/dist/layout.js +134 -0
  53. package/dist/page-layout.d.ts +92 -0
  54. package/dist/page-layout.js +156 -0
  55. package/dist/types/menu.types.d.ts +57 -5
  56. package/package.json +1 -1
  57. package/dist/types/layout.types.d.ts +0 -56
  58. package/dist/types/layout.types.js +0 -36
@@ -0,0 +1,248 @@
1
+ "use strict";
2
+ /**
3
+ * RadioMenu - Split rendering and interaction
4
+ * Supports Page Layout V2 architecture with separate render/interact phases
5
+ * Uses ScreenManager for independent region updates
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.renderRadioMenuUI = renderRadioMenuUI;
9
+ exports.waitForRadioMenuInput = waitForRadioMenuInput;
10
+ const terminal_js_1 = require("../../core/terminal.js");
11
+ const keyboard_js_1 = require("../../core/keyboard.js");
12
+ const renderer_js_1 = require("../../core/renderer.js");
13
+ const colors_js_1 = require("../../core/colors.js");
14
+ const registry_js_1 = require("../../i18n/registry.js");
15
+ /**
16
+ * Render radio menu UI (non-blocking)
17
+ * Returns state for later interaction
18
+ */
19
+ function renderRadioMenuUI(config) {
20
+ const { options, title, prompt, defaultIndex = 0, separatorWidth = 30 } = config;
21
+ // Use i18n for default prompt if not provided
22
+ const displayPrompt = prompt || (0, registry_js_1.t)('menus.selectPrompt');
23
+ // Validate options
24
+ if (!options || options.length === 0) {
25
+ throw new Error('RadioMenu requires at least one option');
26
+ }
27
+ // Initialize state
28
+ let selectedIndex = Math.max(0, Math.min(defaultIndex, options.length - 1));
29
+ // Separate selectable options from separators
30
+ const selectableIndices = [];
31
+ const optionData = [];
32
+ options.forEach((opt, index) => {
33
+ if (typeof opt === 'object' && 'type' in opt && opt.type === 'separator') {
34
+ optionData.push({ value: '', isSeparator: true, label: opt.label });
35
+ }
36
+ else {
37
+ let value;
38
+ if (typeof opt === 'string') {
39
+ value = opt;
40
+ }
41
+ else if ('value' in opt) {
42
+ value = opt.value ?? opt.label ?? '';
43
+ }
44
+ else {
45
+ value = opt.label ?? '';
46
+ }
47
+ optionData.push({ value, isSeparator: false });
48
+ selectableIndices.push(index);
49
+ }
50
+ });
51
+ // Ensure selectedIndex points to a selectable option
52
+ if (!selectableIndices.includes(selectedIndex)) {
53
+ selectedIndex = selectableIndices[0] || 0;
54
+ }
55
+ // Render menu UI NOW (in Phase 1, before terminal initialization)
56
+ let lineCount = 0;
57
+ // Render menu title if provided
58
+ if (title) {
59
+ (0, renderer_js_1.renderHeader)(` ${title}`, colors_js_1.colors.cyan);
60
+ lineCount++;
61
+ (0, renderer_js_1.renderBlankLines)(1);
62
+ lineCount++;
63
+ }
64
+ // Render options
65
+ optionData.forEach((item, index) => {
66
+ if (item.isSeparator) {
67
+ (0, renderer_js_1.renderSectionLabel)(item.label, separatorWidth);
68
+ }
69
+ else {
70
+ const numberMatch = item.value.match(/^(\d+)\.\s*/);
71
+ const letterMatch = item.value.match(/^([a-zA-Z])\.\s*/);
72
+ const prefix = (numberMatch || letterMatch) ? '' : `${selectableIndices.indexOf(index) + 1}. `;
73
+ const isExitOption = /\b(exit|quit)\b/i.test(item.value);
74
+ const displayValue = isExitOption ? `${colors_js_1.uiColors.error}${item.value}${colors_js_1.colors.reset}` : item.value;
75
+ (0, renderer_js_1.renderOption)(displayValue, undefined, index === selectedIndex, prefix);
76
+ const nextIndex = index + 1;
77
+ if (nextIndex < optionData.length && optionData[nextIndex].isSeparator) {
78
+ (0, terminal_js_1.writeLine)('');
79
+ lineCount++;
80
+ }
81
+ }
82
+ lineCount++;
83
+ });
84
+ // Don't render input prompt here - it should be in footer
85
+ // Just render the menu options
86
+ // Generate a unique region ID for this menu
87
+ const regionId = `menu-${Date.now()}`;
88
+ // Store the line count for later use in interact phase
89
+ return {
90
+ config,
91
+ selectedIndex,
92
+ selectableIndices,
93
+ optionData,
94
+ terminalState: null, // Will be initialized in interact phase
95
+ displayPrompt,
96
+ initialLineCount: lineCount,
97
+ regionId
98
+ };
99
+ }
100
+ /**
101
+ * Wait for user input and return result (blocking)
102
+ */
103
+ async function waitForRadioMenuInput(menuState) {
104
+ const { config, selectableIndices, optionData, displayPrompt } = menuState;
105
+ let selectedIndex = menuState.selectedIndex;
106
+ // Initialize terminal NOW (in interact phase, after all rendering is done)
107
+ const state = (0, terminal_js_1.initTerminal)();
108
+ const { allowNumberKeys = true, allowLetterKeys = false, onExit, preserveOnSelect = false } = config;
109
+ // Helper function to get next/previous selectable index
110
+ const getNextSelectableIndex = (currentIndex, direction) => {
111
+ let nextIndex = currentIndex;
112
+ const maxAttempts = optionData.length;
113
+ let attempts = 0;
114
+ do {
115
+ if (direction === 'up') {
116
+ nextIndex = nextIndex > 0 ? nextIndex - 1 : optionData.length - 1;
117
+ }
118
+ else {
119
+ nextIndex = nextIndex < optionData.length - 1 ? nextIndex + 1 : 0;
120
+ }
121
+ attempts++;
122
+ } while (!selectableIndices.includes(nextIndex) && attempts < maxAttempts);
123
+ return selectableIndices.includes(nextIndex) ? nextIndex : currentIndex;
124
+ };
125
+ // Render function (updates display using ScreenManager)
126
+ const render = () => {
127
+ // Move to menu region and clear it
128
+ // screenManager.moveTo(menuState.regionId);
129
+ // screenManager.clearRegion(menuState.regionId);
130
+ let lineCount = 0;
131
+ // Render menu title if provided
132
+ if (config.title) {
133
+ (0, renderer_js_1.renderHeader)(` ${config.title}`, colors_js_1.colors.cyan);
134
+ lineCount++;
135
+ (0, renderer_js_1.renderBlankLines)(1);
136
+ lineCount++;
137
+ }
138
+ // Render options
139
+ optionData.forEach((item, index) => {
140
+ if (item.isSeparator) {
141
+ (0, renderer_js_1.renderSectionLabel)(item.label, config.separatorWidth || 30);
142
+ }
143
+ else {
144
+ const numberMatch = item.value.match(/^(\d+)\.\s*/);
145
+ const letterMatch = item.value.match(/^([a-zA-Z])\.\s*/);
146
+ const prefix = (numberMatch || letterMatch) ? '' : `${selectableIndices.indexOf(index) + 1}. `;
147
+ const isExitOption = /\b(exit|quit)\b/i.test(item.value);
148
+ const displayValue = isExitOption ? `${colors_js_1.uiColors.error}${item.value}${colors_js_1.colors.reset}` : item.value;
149
+ (0, renderer_js_1.renderOption)(displayValue, undefined, index === selectedIndex, prefix);
150
+ const nextIndex = index + 1;
151
+ if (nextIndex < optionData.length && optionData[nextIndex].isSeparator) {
152
+ (0, terminal_js_1.writeLine)('');
153
+ lineCount++;
154
+ }
155
+ }
156
+ lineCount++;
157
+ });
158
+ // Update region size if it changed
159
+ // if (lineCount !== menuState.initialLineCount) {
160
+ // screenManager.updateRegionSize(menuState.regionId, lineCount);
161
+ // menuState.initialLineCount = lineCount;
162
+ // }
163
+ // Update global state with current selection
164
+ // const currentItem = optionData[selectedIndex];
165
+ // if (currentItem && !currentItem.isSeparator) {
166
+ // const match = currentItem.value.match(/^([^.]+)\./);
167
+ // const displayValue = match ? match[1] : String(selectableIndices.indexOf(selectedIndex) + 1);
168
+ // globalState.setState('menu.selectedValue', displayValue);
169
+ // globalState.setState('menu.selectedIndex', selectedIndex);
170
+ // }
171
+ };
172
+ // Register the menu region with ScreenManager
173
+ // if (menuState.initialLineCount) {
174
+ // screenManager.registerRegion(menuState.regionId, menuState.initialLineCount);
175
+ // }
176
+ // Don't render initially - menu was already rendered in Phase 1
177
+ // Just initialize the state
178
+ state.renderedLines = menuState.initialLineCount || 0;
179
+ // Handle keyboard input
180
+ return new Promise((resolve) => {
181
+ const onData = (key) => {
182
+ // Handle Ctrl+C
183
+ if ((0, keyboard_js_1.isCtrlC)(key)) {
184
+ state.stdin.removeListener('data', onData);
185
+ // screenManager.clearRegion(menuState.regionId);
186
+ (0, terminal_js_1.restoreTerminal)(state);
187
+ if (onExit) {
188
+ onExit();
189
+ }
190
+ else {
191
+ console.log('\nšŸ‘‹ å†č§!');
192
+ }
193
+ process.exit(0);
194
+ }
195
+ // Handle arrow keys
196
+ if (key === keyboard_js_1.KEY_CODES.UP) {
197
+ selectedIndex = getNextSelectableIndex(selectedIndex, 'up');
198
+ render();
199
+ }
200
+ else if (key === keyboard_js_1.KEY_CODES.DOWN) {
201
+ selectedIndex = getNextSelectableIndex(selectedIndex, 'down');
202
+ render();
203
+ }
204
+ // Handle Enter
205
+ if ((0, keyboard_js_1.isEnter)(key)) {
206
+ const selectedItem = optionData[selectedIndex];
207
+ if (selectedItem && !selectedItem.isSeparator) {
208
+ state.stdin.removeListener('data', onData);
209
+ if (!preserveOnSelect) {
210
+ // screenManager.clearRegion(menuState.regionId);
211
+ }
212
+ (0, terminal_js_1.restoreTerminal)(state);
213
+ resolve({
214
+ value: selectedItem.value,
215
+ index: selectableIndices.indexOf(selectedIndex)
216
+ });
217
+ }
218
+ }
219
+ // Handle number keys
220
+ if (allowNumberKeys && (0, keyboard_js_1.isNumberKey)(key)) {
221
+ const num = parseInt(key, 10);
222
+ const targetIndex = num === 0 ? 9 : num - 1;
223
+ if (targetIndex < selectableIndices.length) {
224
+ selectedIndex = selectableIndices[targetIndex];
225
+ render();
226
+ }
227
+ }
228
+ // Handle letter keys
229
+ if (allowLetterKeys) {
230
+ const letter = (0, keyboard_js_1.normalizeLetter)(key);
231
+ if (letter) {
232
+ const matchingIndex = selectableIndices.find(idx => {
233
+ const item = optionData[idx];
234
+ if (!item || item.isSeparator)
235
+ return false;
236
+ const normalized = (0, keyboard_js_1.normalizeLetter)(item.value.charAt(0));
237
+ return normalized === letter;
238
+ });
239
+ if (matchingIndex !== undefined) {
240
+ selectedIndex = matchingIndex;
241
+ render();
242
+ }
243
+ }
244
+ }
245
+ };
246
+ state.stdin.on('data', onData);
247
+ });
248
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Radio Menu Component - New Architecture Version
3
+ * Renders menu directly in new architecture without calling old showRadioMenu
4
+ */
5
+ import { Component } from '../../layout.js';
6
+ import type { RadioMenuConfig, RadioMenuResult } from '../../types/menu.types.js';
7
+ export interface RadioMenuComponentConfig {
8
+ menuConfig: RadioMenuConfig;
9
+ onResult: (result: RadioMenuResult) => Promise<void>;
10
+ }
11
+ export declare function createRadioMenuComponentV2(config: RadioMenuComponentConfig): Component;
@@ -0,0 +1,150 @@
1
+ "use strict";
2
+ /**
3
+ * Radio Menu Component - New Architecture Version
4
+ * Renders menu directly in new architecture without calling old showRadioMenu
5
+ */
6
+ var __importDefault = (this && this.__importDefault) || function (mod) {
7
+ return (mod && mod.__esModule) ? mod : { "default": mod };
8
+ };
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.createRadioMenuComponentV2 = createRadioMenuComponentV2;
11
+ const readline_1 = __importDefault(require("readline"));
12
+ const layout_js_1 = require("../../layout.js");
13
+ const colors_js_1 = require("../../core/colors.js");
14
+ function createRadioMenuComponentV2(config) {
15
+ let selectedIndex = 0;
16
+ const options = config.menuConfig.options;
17
+ // Parse options
18
+ const selectableIndices = [];
19
+ const optionData = [];
20
+ options.forEach((opt, index) => {
21
+ if (typeof opt === 'object' && 'type' in opt && opt.type === 'separator') {
22
+ optionData.push({ value: '', isSeparator: true, label: opt.label });
23
+ }
24
+ else {
25
+ const value = typeof opt === 'string' ? opt : opt.value || '';
26
+ optionData.push({ value, isSeparator: false });
27
+ selectableIndices.push(index);
28
+ }
29
+ });
30
+ // Render function
31
+ const renderMenu = (rect) => {
32
+ const lines = [];
33
+ optionData.forEach((item, index) => {
34
+ if (item.isSeparator) {
35
+ // Render separator
36
+ const label = item.label || '';
37
+ const line = `${colors_js_1.colors.dim}${'─'.repeat(5)} ${label} ${'─'.repeat(Math.max(0, 20 - label.length))}${colors_js_1.colors.reset}`;
38
+ lines.push(line);
39
+ }
40
+ else {
41
+ // Render option
42
+ const isSelected = index === selectedIndex;
43
+ const prefix = isSelected ? `${colors_js_1.colors.green}>${colors_js_1.colors.reset}` : ' ';
44
+ // Add number prefix
45
+ const numberMatch = item.value.match(/^(\d+)\.\s*/);
46
+ const letterMatch = item.value.match(/^([a-zA-Z])\.\s*/);
47
+ const hasPrefix = numberMatch || letterMatch;
48
+ const displayPrefix = hasPrefix ? '' : `${selectableIndices.indexOf(index) + 1}. `;
49
+ // Check if exit option
50
+ const isExitOption = /\b(exit|quit)\b/i.test(item.value);
51
+ const displayValue = isExitOption ? `${colors_js_1.uiColors.error}${item.value}${colors_js_1.colors.reset}` : item.value;
52
+ const text = isSelected
53
+ ? `${colors_js_1.colors.green}${colors_js_1.colors.bold}${displayPrefix}${displayValue}${colors_js_1.colors.reset}`
54
+ : `${displayPrefix}${displayValue}`;
55
+ lines.push(`${prefix} ${text}`);
56
+ }
57
+ });
58
+ // Fill remaining lines
59
+ while (lines.length < rect.height) {
60
+ lines.push('');
61
+ }
62
+ return lines;
63
+ };
64
+ return {
65
+ type: 'radio-menu',
66
+ regionId: 'main',
67
+ render: renderMenu,
68
+ interact: async () => {
69
+ return new Promise((resolve) => {
70
+ readline_1.default.emitKeypressEvents(process.stdin);
71
+ if (process.stdin.isTTY)
72
+ process.stdin.setRawMode(true);
73
+ const onKey = async (str, key) => {
74
+ if (key.ctrl && key.name === 'c') {
75
+ cleanup();
76
+ process.exit(0);
77
+ }
78
+ let needsUpdate = false;
79
+ // Arrow key navigation
80
+ if (key.name === 'up') {
81
+ // Move to previous selectable option
82
+ const currentPos = selectableIndices.indexOf(selectedIndex);
83
+ if (currentPos > 0) {
84
+ selectedIndex = selectableIndices[currentPos - 1];
85
+ needsUpdate = true;
86
+ }
87
+ }
88
+ else if (key.name === 'down') {
89
+ // Move to next selectable option
90
+ const currentPos = selectableIndices.indexOf(selectedIndex);
91
+ if (currentPos < selectableIndices.length - 1) {
92
+ selectedIndex = selectableIndices[currentPos + 1];
93
+ needsUpdate = true;
94
+ }
95
+ }
96
+ // Number key selection
97
+ else if (config.menuConfig.allowNumberKeys && str && /^[0-9]$/.test(str)) {
98
+ const num = parseInt(str, 10);
99
+ if (num > 0 && num <= selectableIndices.length) {
100
+ selectedIndex = selectableIndices[num - 1];
101
+ needsUpdate = true;
102
+ }
103
+ }
104
+ // Letter key selection
105
+ else if (config.menuConfig.allowLetterKeys && str && /^[a-zA-Z]$/.test(str)) {
106
+ // Find option starting with this letter
107
+ const letter = str.toLowerCase();
108
+ const matchIndex = selectableIndices.find(idx => {
109
+ const value = optionData[idx].value.toLowerCase();
110
+ return value.startsWith(letter) || value.match(new RegExp(`^\\d+\\.\\s*${letter}`, 'i'));
111
+ });
112
+ if (matchIndex !== undefined) {
113
+ selectedIndex = matchIndex;
114
+ needsUpdate = true;
115
+ }
116
+ }
117
+ // Enter to confirm
118
+ else if (key.name === 'return') {
119
+ cleanup();
120
+ const result = {
121
+ value: optionData[selectedIndex].value,
122
+ index: selectableIndices.indexOf(selectedIndex)
123
+ };
124
+ await config.onResult(result);
125
+ resolve();
126
+ return;
127
+ }
128
+ // Update display if selection changed
129
+ if (needsUpdate) {
130
+ const layout = (0, layout_js_1.computeLayout)();
131
+ const lines = renderMenu(layout.main);
132
+ layout_js_1.screenManager.renderRegion('main', lines);
133
+ // Update hint
134
+ layout_js_1.hintManager.set('menu', `Selected: ${optionData[selectedIndex].value}`, 10);
135
+ }
136
+ };
137
+ function cleanup() {
138
+ process.stdin.off('keypress', onKey);
139
+ if (process.stdin.isTTY)
140
+ process.stdin.setRawMode(false);
141
+ layout_js_1.hintManager.clear('menu');
142
+ }
143
+ process.stdin.on('keypress', onKey);
144
+ // Set initial hint
145
+ layout_js_1.hintManager.set('menu', `Selected: ${optionData[selectedIndex].value}`, 10);
146
+ });
147
+ },
148
+ config
149
+ };
150
+ }
@@ -6,6 +6,7 @@ import { RadioMenuConfig, RadioMenuResult } from '../../types/menu.types.js';
6
6
  /**
7
7
  * Show a radio menu (single-select)
8
8
  * @param config - Menu configuration
9
+ * @param hints - Optional hints to display at the bottom (for Page Layout use)
9
10
  * @returns Promise resolving to selected option
10
11
  */
11
- export declare function showRadioMenu(config: RadioMenuConfig): Promise<RadioMenuResult>;
12
+ export declare function showRadioMenu(config: RadioMenuConfig, hints?: string[]): Promise<RadioMenuResult>;
@@ -5,61 +5,22 @@
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.showRadioMenu = showRadioMenu;
8
- const layout_types_js_1 = require("../../types/layout.types.js");
9
8
  const terminal_js_1 = require("../../core/terminal.js");
10
9
  const keyboard_js_1 = require("../../core/keyboard.js");
11
10
  const renderer_js_1 = require("../../core/renderer.js");
11
+ const renderer_js_2 = require("../../core/renderer.js");
12
12
  const colors_js_1 = require("../../core/colors.js");
13
13
  const registry_js_1 = require("../../i18n/registry.js");
14
- /**
15
- * Generate hints based on menu configuration and actual options
16
- */
17
- function generateHints(options, allowNumberKeys, allowLetterKeys) {
18
- const hints = [];
19
- // Count selectable options
20
- const selectableCount = options.filter(opt => !(typeof opt === 'object' && 'type' in opt && opt.type === 'separator')).length;
21
- // Only show arrow hints if there are multiple options
22
- if (selectableCount > 1) {
23
- hints.push((0, registry_js_1.t)('hints.arrows'));
24
- }
25
- // Check if there are actually number-prefixed options
26
- if (allowNumberKeys) {
27
- const hasNumberOptions = options.some(opt => {
28
- if (typeof opt === 'string') {
29
- return /^\d+\./.test(opt);
30
- }
31
- return false;
32
- });
33
- if (hasNumberOptions) {
34
- hints.push((0, registry_js_1.t)('hints.numbers'));
35
- }
36
- }
37
- // Check if there are actually letter-prefixed options
38
- if (allowLetterKeys) {
39
- const hasLetterOptions = options.some(opt => {
40
- if (typeof opt === 'string') {
41
- return /^[a-zA-Z]\./.test(opt);
42
- }
43
- return false;
44
- });
45
- if (hasLetterOptions) {
46
- hints.push((0, registry_js_1.t)('hints.letters'));
47
- }
48
- }
49
- hints.push((0, registry_js_1.t)('hints.enter'));
50
- return hints;
51
- }
52
14
  /**
53
15
  * Show a radio menu (single-select)
54
16
  * @param config - Menu configuration
17
+ * @param hints - Optional hints to display at the bottom (for Page Layout use)
55
18
  * @returns Promise resolving to selected option
56
19
  */
57
- async function showRadioMenu(config) {
58
- const { options, title, prompt, hints, layout = layout_types_js_1.LAYOUT_PRESETS.MAIN_MENU, defaultIndex = 0, allowNumberKeys = true, allowLetterKeys = false, separatorWidth = 30, onExit, preserveOnSelect = false } = config;
20
+ async function showRadioMenu(config, hints) {
21
+ const { options, title, prompt, defaultIndex = 0, allowNumberKeys = true, allowLetterKeys = false, separatorWidth = 30, onExit, preserveOnSelect = false } = config;
59
22
  // Use i18n for default prompt if not provided
60
23
  const displayPrompt = prompt || (0, registry_js_1.t)('menus.selectPrompt');
61
- // Generate hints dynamically if not provided
62
- const displayHints = hints || generateHints(options, allowNumberKeys, allowLetterKeys);
63
24
  // Validate options
64
25
  if (!options || options.length === 0) {
65
26
  throw new Error('RadioMenu requires at least one option');
@@ -89,8 +50,6 @@ async function showRadioMenu(config) {
89
50
  selectableIndices.push(index);
90
51
  }
91
52
  });
92
- // Use MINIMAL layout for single-option menus
93
- const effectiveLayout = selectableIndices.length === 1 ? layout_types_js_1.LAYOUT_PRESETS.MINIMAL : layout;
94
53
  // Ensure selectedIndex points to a selectable option
95
54
  if (!selectableIndices.includes(selectedIndex)) {
96
55
  selectedIndex = selectableIndices[0] || 0;
@@ -115,80 +74,63 @@ async function showRadioMenu(config) {
115
74
  const render = () => {
116
75
  (0, terminal_js_1.clearMenu)(state);
117
76
  let lineCount = 0;
118
- // Render based on layout order
119
- effectiveLayout.order.forEach(element => {
120
- // Add spacing before element
121
- const spacingKey = `before${element.charAt(0).toUpperCase() + element.slice(1)}`;
122
- if (effectiveLayout.spacing?.[spacingKey]) {
123
- (0, renderer_js_1.renderBlankLines)(effectiveLayout.spacing[spacingKey]);
124
- lineCount += effectiveLayout.spacing[spacingKey];
125
- }
126
- switch (element) {
127
- case 'header':
128
- if (effectiveLayout.visible.header && title) {
129
- (0, renderer_js_1.renderHeader)(` ${title}`, colors_js_1.colors.cyan);
130
- lineCount++;
131
- }
132
- break;
133
- case 'options':
134
- optionData.forEach((item, index) => {
135
- if (item.isSeparator) {
136
- // Render section label with configured width
137
- (0, renderer_js_1.renderSectionLabel)(item.label, separatorWidth);
138
- }
139
- else {
140
- // Check if option starts with a number or letter prefix
141
- const numberMatch = item.value.match(/^(\d+)\.\s*/);
142
- const letterMatch = item.value.match(/^([a-zA-Z])\.\s*/);
143
- // Don't add prefix if option already has number or letter prefix
144
- const prefix = (numberMatch || letterMatch) ? '' : `${selectableIndices.indexOf(index) + 1}. `;
145
- // Check if this is an Exit option (contains "Exit" or "Quit")
146
- const isExitOption = /\b(exit|quit)\b/i.test(item.value);
147
- const displayValue = isExitOption ? `${colors_js_1.uiColors.error}${item.value}${colors_js_1.colors.reset}` : item.value;
148
- // For radio menus, don't show selection indicator (pass undefined instead of false)
149
- (0, renderer_js_1.renderOption)(displayValue, undefined, index === selectedIndex, prefix);
150
- // Add blank line after last item before next separator
151
- const nextIndex = index + 1;
152
- if (nextIndex < optionData.length && optionData[nextIndex].isSeparator) {
153
- (0, terminal_js_1.writeLine)('');
154
- lineCount++; // Count the blank line
155
- }
156
- }
157
- lineCount++;
158
- });
159
- break;
160
- case 'input':
161
- if (effectiveLayout.visible.input) {
162
- // Calculate display value (current selection number)
163
- let displayValue = '';
164
- const currentItem = optionData[selectedIndex];
165
- if (currentItem && !currentItem.isSeparator) {
166
- const match = currentItem.value.match(/^([^.]+)\./);
167
- if (match) {
168
- displayValue = match[1];
169
- }
170
- else {
171
- displayValue = String(selectableIndices.indexOf(selectedIndex) + 1);
172
- }
173
- }
174
- (0, renderer_js_1.renderInputPrompt)(displayPrompt, displayValue);
175
- lineCount++;
176
- }
177
- break;
178
- case 'hints':
179
- if (effectiveLayout.visible.hints && displayHints.length > 0) {
180
- (0, renderer_js_1.renderHints)(displayHints);
181
- lineCount++;
182
- }
183
- break;
77
+ // Render title if provided
78
+ if (title) {
79
+ (0, renderer_js_1.renderHeader)(` ${title}`, colors_js_1.colors.cyan);
80
+ lineCount++;
81
+ (0, renderer_js_1.renderBlankLines)(1);
82
+ lineCount++;
83
+ }
84
+ // Render options
85
+ optionData.forEach((item, index) => {
86
+ if (item.isSeparator) {
87
+ // Render section label with configured width
88
+ (0, renderer_js_1.renderSectionLabel)(item.label, separatorWidth);
184
89
  }
185
- // Add spacing after element
186
- const afterSpacingKey = `after${element.charAt(0).toUpperCase() + element.slice(1)}`;
187
- if (effectiveLayout.spacing?.[afterSpacingKey]) {
188
- (0, renderer_js_1.renderBlankLines)(effectiveLayout.spacing[afterSpacingKey]);
189
- lineCount += effectiveLayout.spacing[afterSpacingKey];
90
+ else {
91
+ // Check if option starts with a number or letter prefix
92
+ const numberMatch = item.value.match(/^(\d+)\.\s*/);
93
+ const letterMatch = item.value.match(/^([a-zA-Z])\.\s*/);
94
+ // Don't add prefix if option already has number or letter prefix
95
+ const prefix = (numberMatch || letterMatch) ? '' : `${selectableIndices.indexOf(index) + 1}. `;
96
+ // Check if this is an Exit option (contains "Exit" or "Quit")
97
+ const isExitOption = /\b(exit|quit)\b/i.test(item.value);
98
+ const displayValue = isExitOption ? `${colors_js_1.uiColors.error}${item.value}${colors_js_1.colors.reset}` : item.value;
99
+ // For radio menus, don't show selection indicator (pass undefined instead of false)
100
+ (0, renderer_js_1.renderOption)(displayValue, undefined, index === selectedIndex, prefix);
101
+ // Add blank line after last item before next separator
102
+ const nextIndex = index + 1;
103
+ if (nextIndex < optionData.length && optionData[nextIndex].isSeparator) {
104
+ (0, terminal_js_1.writeLine)('');
105
+ lineCount++; // Count the blank line
106
+ }
190
107
  }
108
+ lineCount++;
191
109
  });
110
+ // Render input prompt
111
+ (0, renderer_js_1.renderBlankLines)(1);
112
+ lineCount++;
113
+ // Calculate display value (current selection number)
114
+ let displayValue = '';
115
+ const currentItem = optionData[selectedIndex];
116
+ if (currentItem && !currentItem.isSeparator) {
117
+ const match = currentItem.value.match(/^([^.]+)\./);
118
+ if (match) {
119
+ displayValue = match[1];
120
+ }
121
+ else {
122
+ displayValue = String(selectableIndices.indexOf(selectedIndex) + 1);
123
+ }
124
+ }
125
+ (0, renderer_js_1.renderInputPrompt)(displayPrompt, displayValue);
126
+ lineCount++;
127
+ // Render hints if provided (for Page Layout footer)
128
+ if (hints && hints.length > 0) {
129
+ (0, renderer_js_1.renderBlankLines)(1);
130
+ lineCount++;
131
+ (0, renderer_js_2.renderHints)(hints);
132
+ lineCount++;
133
+ }
192
134
  state.renderedLines = lineCount;
193
135
  };
194
136
  // Initial render
@@ -199,14 +141,9 @@ async function showRadioMenu(config) {
199
141
  // Handle Ctrl+C
200
142
  if ((0, keyboard_js_1.isCtrlC)(key)) {
201
143
  state.stdin.removeListener('data', onData);
202
- (0, terminal_js_1.clearMenu)(state);
203
144
  (0, terminal_js_1.restoreTerminal)(state);
204
- if (onExit) {
205
- onExit();
206
- }
207
- else {
208
- console.log('\nšŸ‘‹ å†č§!');
209
- }
145
+ // Don't clear menu on Ctrl+C - just exit directly
146
+ console.log('\n');
210
147
  process.exit(0);
211
148
  }
212
149
  // Handle Enter