cli-menu-kit 0.1.26 → 0.2.1

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 (78) 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/header.d.ts +40 -0
  8. package/dist/components/display/header.js +331 -18
  9. package/dist/components/display/headers.d.ts +1 -0
  10. package/dist/components/display/headers.js +15 -5
  11. package/dist/components/display/hints-v2.d.ts +10 -0
  12. package/dist/components/display/hints-v2.js +34 -0
  13. package/dist/components/display/hints.d.ts +56 -0
  14. package/dist/components/display/hints.js +81 -0
  15. package/dist/components/display/index.d.ts +4 -1
  16. package/dist/components/display/index.js +17 -1
  17. package/dist/components/display/input-prompt.d.ts +35 -0
  18. package/dist/components/display/input-prompt.js +36 -0
  19. package/dist/components/display/list.d.ts +49 -0
  20. package/dist/components/display/list.js +86 -0
  21. package/dist/components/display/messages.js +5 -5
  22. package/dist/components/display/progress.d.ts +17 -0
  23. package/dist/components/display/progress.js +18 -0
  24. package/dist/components/display/summary.js +72 -10
  25. package/dist/components/display/table.d.ts +44 -0
  26. package/dist/components/display/table.js +108 -0
  27. package/dist/components/inputs/language-input.js +8 -5
  28. package/dist/components/inputs/number-input.js +19 -14
  29. package/dist/components/inputs/text-input.js +50 -13
  30. package/dist/components/menus/boolean-menu.js +34 -20
  31. package/dist/components/menus/checkbox-menu.d.ts +2 -1
  32. package/dist/components/menus/checkbox-menu.js +35 -61
  33. package/dist/components/menus/checkbox-table-menu.d.ts +12 -0
  34. package/dist/components/menus/checkbox-table-menu.js +398 -0
  35. package/dist/components/menus/index.d.ts +1 -0
  36. package/dist/components/menus/index.js +3 -1
  37. package/dist/components/menus/radio-menu-split.d.ts +34 -0
  38. package/dist/components/menus/radio-menu-split.js +258 -0
  39. package/dist/components/menus/radio-menu-v2.d.ts +11 -0
  40. package/dist/components/menus/radio-menu-v2.js +150 -0
  41. package/dist/components/menus/radio-menu.d.ts +2 -1
  42. package/dist/components/menus/radio-menu.js +100 -134
  43. package/dist/components.js +3 -3
  44. package/dist/config/index.d.ts +5 -0
  45. package/dist/config/index.js +21 -0
  46. package/dist/config/language-config.d.ts +73 -0
  47. package/dist/config/language-config.js +157 -0
  48. package/dist/config/user-config.d.ts +83 -0
  49. package/dist/config/user-config.js +185 -0
  50. package/dist/core/colors.d.ts +24 -18
  51. package/dist/core/colors.js +74 -7
  52. package/dist/core/hint-manager.d.ts +29 -0
  53. package/dist/core/hint-manager.js +65 -0
  54. package/dist/core/renderer.d.ts +2 -1
  55. package/dist/core/renderer.js +46 -22
  56. package/dist/core/screen-manager.d.ts +54 -0
  57. package/dist/core/screen-manager.js +119 -0
  58. package/dist/core/state-manager.d.ts +27 -0
  59. package/dist/core/state-manager.js +56 -0
  60. package/dist/core/terminal.d.ts +17 -1
  61. package/dist/core/terminal.js +124 -4
  62. package/dist/core/virtual-scroll.d.ts +65 -0
  63. package/dist/core/virtual-scroll.js +120 -0
  64. package/dist/features/commands.js +23 -22
  65. package/dist/i18n/languages/en.js +4 -1
  66. package/dist/i18n/languages/zh.js +4 -1
  67. package/dist/i18n/registry.d.ts +4 -3
  68. package/dist/i18n/registry.js +12 -4
  69. package/dist/i18n/types.d.ts +3 -0
  70. package/dist/index.d.ts +7 -4
  71. package/dist/index.js +49 -4
  72. package/dist/layout.d.ts +67 -0
  73. package/dist/layout.js +86 -0
  74. package/dist/page-layout.d.ts +123 -0
  75. package/dist/page-layout.js +195 -0
  76. package/dist/types/input.types.d.ts +8 -0
  77. package/dist/types/menu.types.d.ts +61 -5
  78. package/package.json +4 -1
@@ -0,0 +1,258 @@
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({ display: '', value: '', isSeparator: true, label: opt.label });
35
+ }
36
+ else {
37
+ let display;
38
+ let value;
39
+ if (typeof opt === 'string') {
40
+ display = opt;
41
+ value = opt;
42
+ }
43
+ else if ('value' in opt) {
44
+ display = opt.label ?? String(opt.value ?? '');
45
+ value = opt.value ?? opt.label ?? '';
46
+ }
47
+ else {
48
+ display = opt.label ?? '';
49
+ value = opt.label ?? '';
50
+ }
51
+ optionData.push({ display, value, isSeparator: false });
52
+ selectableIndices.push(index);
53
+ }
54
+ });
55
+ // Ensure selectedIndex points to a selectable option
56
+ if (!selectableIndices.includes(selectedIndex)) {
57
+ selectedIndex = selectableIndices[0] || 0;
58
+ }
59
+ // Render menu UI NOW (in Phase 1, before terminal initialization)
60
+ let lineCount = 0;
61
+ // Render menu title if provided
62
+ if (title) {
63
+ (0, renderer_js_1.renderHeader)(` ${title}`, colors_js_1.uiColors.primary);
64
+ lineCount++;
65
+ (0, renderer_js_1.renderBlankLines)(1);
66
+ lineCount++;
67
+ }
68
+ // Render options
69
+ optionData.forEach((item, index) => {
70
+ if (item.isSeparator) {
71
+ (0, renderer_js_1.renderSectionLabel)(item.label, separatorWidth);
72
+ }
73
+ else {
74
+ const numberMatch = item.display.match(/^(\d+)\.\s*/);
75
+ const letterMatch = item.display.match(/^([a-zA-Z])\.\s*/);
76
+ const prefix = (numberMatch || letterMatch) ? '' : `${selectableIndices.indexOf(index) + 1}. `;
77
+ const isExitOption = /\b(exit|quit)\b/i.test(item.display);
78
+ const isCurrent = index === selectedIndex;
79
+ const displayValue = isExitOption && !isCurrent
80
+ ? `${colors_js_1.uiColors.error}${item.display}${colors_js_1.colors.reset}`
81
+ : item.display;
82
+ (0, renderer_js_1.renderOption)(displayValue, undefined, isCurrent, prefix);
83
+ const nextIndex = index + 1;
84
+ if (nextIndex < optionData.length && optionData[nextIndex].isSeparator) {
85
+ (0, terminal_js_1.writeLine)('');
86
+ lineCount++;
87
+ }
88
+ }
89
+ lineCount++;
90
+ });
91
+ // Don't render input prompt here - it should be in footer
92
+ // Just render the menu options
93
+ // Generate a unique region ID for this menu
94
+ const regionId = `menu-${Date.now()}`;
95
+ // Store the line count for later use in interact phase
96
+ return {
97
+ config,
98
+ selectedIndex,
99
+ selectableIndices,
100
+ optionData,
101
+ terminalState: null, // Will be initialized in interact phase
102
+ displayPrompt,
103
+ initialLineCount: lineCount,
104
+ regionId
105
+ };
106
+ }
107
+ /**
108
+ * Wait for user input and return result (blocking)
109
+ */
110
+ async function waitForRadioMenuInput(menuState) {
111
+ const { config, selectableIndices, optionData, displayPrompt } = menuState;
112
+ let selectedIndex = menuState.selectedIndex;
113
+ // Initialize terminal NOW (in interact phase, after all rendering is done)
114
+ const state = (0, terminal_js_1.initTerminal)();
115
+ const { allowNumberKeys = true, allowLetterKeys = false, onExit, preserveOnSelect = false } = config;
116
+ // Helper function to get next/previous selectable index
117
+ const getNextSelectableIndex = (currentIndex, direction) => {
118
+ let nextIndex = currentIndex;
119
+ const maxAttempts = optionData.length;
120
+ let attempts = 0;
121
+ do {
122
+ if (direction === 'up') {
123
+ nextIndex = nextIndex > 0 ? nextIndex - 1 : optionData.length - 1;
124
+ }
125
+ else {
126
+ nextIndex = nextIndex < optionData.length - 1 ? nextIndex + 1 : 0;
127
+ }
128
+ attempts++;
129
+ } while (!selectableIndices.includes(nextIndex) && attempts < maxAttempts);
130
+ return selectableIndices.includes(nextIndex) ? nextIndex : currentIndex;
131
+ };
132
+ // Render function (updates display using ScreenManager)
133
+ const render = () => {
134
+ // Move to menu region and clear it
135
+ // screenManager.moveTo(menuState.regionId);
136
+ // screenManager.clearRegion(menuState.regionId);
137
+ let lineCount = 0;
138
+ // Render menu title if provided
139
+ if (config.title) {
140
+ (0, renderer_js_1.renderHeader)(` ${config.title}`, colors_js_1.uiColors.primary);
141
+ lineCount++;
142
+ (0, renderer_js_1.renderBlankLines)(1);
143
+ lineCount++;
144
+ }
145
+ // Render options
146
+ optionData.forEach((item, index) => {
147
+ if (item.isSeparator) {
148
+ (0, renderer_js_1.renderSectionLabel)(item.label, config.separatorWidth || 30);
149
+ }
150
+ else {
151
+ const numberMatch = item.display.match(/^(\d+)\.\s*/);
152
+ const letterMatch = item.display.match(/^([a-zA-Z])\.\s*/);
153
+ const prefix = (numberMatch || letterMatch) ? '' : `${selectableIndices.indexOf(index) + 1}. `;
154
+ const isExitOption = /\b(exit|quit)\b/i.test(item.display);
155
+ const isCurrent = index === selectedIndex;
156
+ const displayValue = isExitOption && !isCurrent
157
+ ? `${colors_js_1.uiColors.error}${item.display}${colors_js_1.colors.reset}`
158
+ : item.display;
159
+ (0, renderer_js_1.renderOption)(displayValue, undefined, isCurrent, prefix);
160
+ const nextIndex = index + 1;
161
+ if (nextIndex < optionData.length && optionData[nextIndex].isSeparator) {
162
+ (0, terminal_js_1.writeLine)('');
163
+ lineCount++;
164
+ }
165
+ }
166
+ lineCount++;
167
+ });
168
+ // Update region size if it changed
169
+ // if (lineCount !== menuState.initialLineCount) {
170
+ // screenManager.updateRegionSize(menuState.regionId, lineCount);
171
+ // menuState.initialLineCount = lineCount;
172
+ // }
173
+ // Update global state with current selection
174
+ // const currentItem = optionData[selectedIndex];
175
+ // if (currentItem && !currentItem.isSeparator) {
176
+ // const match = currentItem.value.match(/^([^.]+)\./);
177
+ // const displayValue = match ? match[1] : String(selectableIndices.indexOf(selectedIndex) + 1);
178
+ // globalState.setState('menu.selectedValue', displayValue);
179
+ // globalState.setState('menu.selectedIndex', selectedIndex);
180
+ // }
181
+ };
182
+ // Register the menu region with ScreenManager
183
+ // if (menuState.initialLineCount) {
184
+ // screenManager.registerRegion(menuState.regionId, menuState.initialLineCount);
185
+ // }
186
+ // Don't render initially - menu was already rendered in Phase 1
187
+ // Just initialize the state
188
+ state.renderedLines = menuState.initialLineCount || 0;
189
+ // Handle keyboard input
190
+ return new Promise((resolve) => {
191
+ const onData = (key) => {
192
+ // Handle Ctrl+C
193
+ if ((0, keyboard_js_1.isCtrlC)(key)) {
194
+ state.stdin.removeListener('data', onData);
195
+ // screenManager.clearRegion(menuState.regionId);
196
+ (0, terminal_js_1.restoreTerminal)(state);
197
+ if (onExit) {
198
+ onExit();
199
+ }
200
+ else {
201
+ console.log(`\n${(0, registry_js_1.t)('messages.goodbye')}`);
202
+ }
203
+ process.exit(0);
204
+ }
205
+ // Handle arrow keys
206
+ if (key === keyboard_js_1.KEY_CODES.UP) {
207
+ selectedIndex = getNextSelectableIndex(selectedIndex, 'up');
208
+ render();
209
+ }
210
+ else if (key === keyboard_js_1.KEY_CODES.DOWN) {
211
+ selectedIndex = getNextSelectableIndex(selectedIndex, 'down');
212
+ render();
213
+ }
214
+ // Handle Enter
215
+ if ((0, keyboard_js_1.isEnter)(key)) {
216
+ const selectedItem = optionData[selectedIndex];
217
+ if (selectedItem && !selectedItem.isSeparator) {
218
+ state.stdin.removeListener('data', onData);
219
+ if (!preserveOnSelect) {
220
+ // screenManager.clearRegion(menuState.regionId);
221
+ }
222
+ (0, terminal_js_1.restoreTerminal)(state);
223
+ resolve({
224
+ value: selectedItem.value,
225
+ index: selectableIndices.indexOf(selectedIndex)
226
+ });
227
+ }
228
+ }
229
+ // Handle number keys
230
+ if (allowNumberKeys && (0, keyboard_js_1.isNumberKey)(key)) {
231
+ const num = parseInt(key, 10);
232
+ const targetIndex = num === 0 ? 9 : num - 1;
233
+ if (targetIndex < selectableIndices.length) {
234
+ selectedIndex = selectableIndices[targetIndex];
235
+ render();
236
+ }
237
+ }
238
+ // Handle letter keys
239
+ if (allowLetterKeys) {
240
+ const letter = (0, keyboard_js_1.normalizeLetter)(key);
241
+ if (letter) {
242
+ const matchingIndex = selectableIndices.find(idx => {
243
+ const item = optionData[idx];
244
+ if (!item || item.isSeparator)
245
+ return false;
246
+ const normalized = (0, keyboard_js_1.normalizeLetter)(item.display.charAt(0));
247
+ return normalized === letter;
248
+ });
249
+ if (matchingIndex !== undefined) {
250
+ selectedIndex = matchingIndex;
251
+ render();
252
+ }
253
+ }
254
+ }
255
+ };
256
+ state.stdin.on('data', onData);
257
+ });
258
+ }
@@ -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>;