cli-menu-kit 0.1.1 → 0.1.9

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.
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Header component for CLI applications
3
+ * Displays ASCII art, title, description, version and URL
4
+ */
5
+ /**
6
+ * Header configuration
7
+ */
8
+ export interface HeaderConfig {
9
+ /** ASCII art lines (array of strings) */
10
+ asciiArt?: string[];
11
+ /** Application title */
12
+ title?: string;
13
+ /** Application description */
14
+ description?: string;
15
+ /** Version number */
16
+ version?: string;
17
+ /** Project URL */
18
+ url?: string;
19
+ /** Optional menu title (e.g., "请选择功能:") */
20
+ menuTitle?: string;
21
+ /** Box width (default: 60) */
22
+ boxWidth?: number;
23
+ /** Header color (default: cyan) */
24
+ color?: string;
25
+ }
26
+ /**
27
+ * Render a boxed header with ASCII art, title, and description
28
+ * @param config - Header configuration
29
+ */
30
+ export declare function renderHeader(config: HeaderConfig): void;
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ /**
3
+ * Header component for CLI applications
4
+ * Displays ASCII art, title, description, version and URL
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.renderHeader = renderHeader;
8
+ const colors_js_1 = require("../../core/colors.js");
9
+ const terminal_js_1 = require("../../core/terminal.js");
10
+ /**
11
+ * Render a boxed header with ASCII art, title, and description
12
+ * @param config - Header configuration
13
+ */
14
+ function renderHeader(config) {
15
+ const { asciiArt = [], title = '', description = '', version, url, menuTitle, boxWidth = 60, color = colors_js_1.colors.cyan } = config;
16
+ const boldColor = `${color}${colors_js_1.colors.bold}`;
17
+ // Top border
18
+ (0, terminal_js_1.writeLine)('');
19
+ (0, terminal_js_1.writeLine)(`${boldColor}╔${'═'.repeat(boxWidth - 2)}╗${colors_js_1.colors.reset}`);
20
+ // Empty line
21
+ (0, terminal_js_1.writeLine)(`${boldColor}║${' '.repeat(boxWidth - 2)}║${colors_js_1.colors.reset}`);
22
+ // ASCII art (left-aligned with 2 spaces padding)
23
+ if (asciiArt.length > 0) {
24
+ asciiArt.forEach(line => {
25
+ const paddedLine = ` ${line}`.padEnd(boxWidth - 2, ' ');
26
+ (0, terminal_js_1.writeLine)(`${boldColor}║${paddedLine}║${colors_js_1.colors.reset}`);
27
+ });
28
+ (0, terminal_js_1.writeLine)(`${boldColor}║${' '.repeat(boxWidth - 2)}║${colors_js_1.colors.reset}`);
29
+ }
30
+ // Title (left-aligned with 2 spaces padding)
31
+ if (title) {
32
+ const paddedTitle = ` ${title}`.padEnd(boxWidth - 2, ' ');
33
+ (0, terminal_js_1.writeLine)(`${boldColor}║${paddedTitle}║${colors_js_1.colors.reset}`);
34
+ (0, terminal_js_1.writeLine)(`${boldColor}║${' '.repeat(boxWidth - 2)}║${colors_js_1.colors.reset}`);
35
+ }
36
+ // Description (left-aligned with 2 spaces padding)
37
+ if (description) {
38
+ const paddedDesc = ` ${description}`.padEnd(boxWidth - 2, ' ');
39
+ (0, terminal_js_1.writeLine)(`${boldColor}║${paddedDesc}║${colors_js_1.colors.reset}`);
40
+ (0, terminal_js_1.writeLine)(`${boldColor}║${' '.repeat(boxWidth - 2)}║${colors_js_1.colors.reset}`);
41
+ }
42
+ // Bottom border
43
+ (0, terminal_js_1.writeLine)(`${boldColor}╚${'═'.repeat(boxWidth - 2)}╝${colors_js_1.colors.reset}`);
44
+ // Version and URL (outside the box, dimmed)
45
+ if (version || url) {
46
+ const versionText = version ? `Version: ${version}` : '';
47
+ const urlText = url || '';
48
+ const separator = version && url ? ' | ' : '';
49
+ (0, terminal_js_1.writeLine)(`${colors_js_1.colors.dim} ${versionText}${separator}${urlText}${colors_js_1.colors.reset}`);
50
+ }
51
+ (0, terminal_js_1.writeLine)('');
52
+ // Menu title (optional)
53
+ if (menuTitle) {
54
+ (0, terminal_js_1.writeLine)(`${color} ${menuTitle}${colors_js_1.colors.reset}`);
55
+ }
56
+ }
@@ -6,3 +6,4 @@ export { renderSimpleHeader, renderAsciiHeader, createSimpleHeader, createAsciiH
6
6
  export { renderProgressIndicator, renderStageHeader, renderStageSeparator, createProgressIndicator, createStageHeader, createStageSeparator } from './progress.js';
7
7
  export { renderMessage, showSuccess, showError, showWarning, showInfo, showQuestion, createMessage } from './messages.js';
8
8
  export { renderSummaryTable, createSummaryTable, createSimpleSummary } from './summary.js';
9
+ export { renderHeader, type HeaderConfig } from './header.js';
@@ -4,7 +4,7 @@
4
4
  * Exports all display component functions
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.createSimpleSummary = exports.createSummaryTable = exports.renderSummaryTable = exports.createMessage = exports.showQuestion = exports.showInfo = exports.showWarning = exports.showError = exports.showSuccess = exports.renderMessage = exports.createStageSeparator = exports.createStageHeader = exports.createProgressIndicator = exports.renderStageSeparator = exports.renderStageHeader = exports.renderProgressIndicator = exports.createAsciiHeader = exports.createSimpleHeader = exports.renderAsciiHeader = exports.renderSimpleHeader = void 0;
7
+ exports.renderHeader = exports.createSimpleSummary = exports.createSummaryTable = exports.renderSummaryTable = exports.createMessage = exports.showQuestion = exports.showInfo = exports.showWarning = exports.showError = exports.showSuccess = exports.renderMessage = exports.createStageSeparator = exports.createStageHeader = exports.createProgressIndicator = exports.renderStageSeparator = exports.renderStageHeader = exports.renderProgressIndicator = exports.createAsciiHeader = exports.createSimpleHeader = exports.renderAsciiHeader = exports.renderSimpleHeader = void 0;
8
8
  var headers_js_1 = require("./headers.js");
9
9
  Object.defineProperty(exports, "renderSimpleHeader", { enumerable: true, get: function () { return headers_js_1.renderSimpleHeader; } });
10
10
  Object.defineProperty(exports, "renderAsciiHeader", { enumerable: true, get: function () { return headers_js_1.renderAsciiHeader; } });
@@ -29,3 +29,5 @@ var summary_js_1 = require("./summary.js");
29
29
  Object.defineProperty(exports, "renderSummaryTable", { enumerable: true, get: function () { return summary_js_1.renderSummaryTable; } });
30
30
  Object.defineProperty(exports, "createSummaryTable", { enumerable: true, get: function () { return summary_js_1.createSummaryTable; } });
31
31
  Object.defineProperty(exports, "createSimpleSummary", { enumerable: true, get: function () { return summary_js_1.createSimpleSummary; } });
32
+ var header_js_1 = require("./header.js");
33
+ Object.defineProperty(exports, "renderHeader", { enumerable: true, get: function () { return header_js_1.renderHeader; } });
@@ -9,13 +9,32 @@ const layout_types_js_1 = require("../../types/layout.types.js");
9
9
  const terminal_js_1 = require("../../core/terminal.js");
10
10
  const keyboard_js_1 = require("../../core/keyboard.js");
11
11
  const renderer_js_1 = require("../../core/renderer.js");
12
+ const registry_js_1 = require("../../i18n/registry.js");
13
+ /**
14
+ * Generate hints based on menu configuration
15
+ */
16
+ function generateHints(allowSelectAll, allowInvert) {
17
+ const hints = [(0, registry_js_1.t)('hints.arrows'), (0, registry_js_1.t)('hints.space')];
18
+ if (allowSelectAll) {
19
+ hints.push((0, registry_js_1.t)('hints.selectAll'));
20
+ }
21
+ if (allowInvert) {
22
+ hints.push((0, registry_js_1.t)('hints.invert'));
23
+ }
24
+ hints.push((0, registry_js_1.t)('hints.enter'));
25
+ return hints;
26
+ }
12
27
  /**
13
28
  * Show a checkbox menu (multi-select)
14
29
  * @param config - Menu configuration
15
30
  * @returns Promise resolving to selected options
16
31
  */
17
32
  async function showCheckboxMenu(config) {
18
- const { options, title, prompt = '空格选中/取消,回车确认', hints = ['↑↓ 方向键', '空格 选中/取消', 'A 全选', 'I 反选', '⏎ 确认'], layout = { ...layout_types_js_1.LAYOUT_PRESETS.SUB_MENU, order: ['input', 'options', 'hints'] }, defaultSelected = [], minSelections = 0, maxSelections, allowSelectAll = true, allowInvert = true, onExit } = config;
33
+ const { options, title, prompt, hints, layout = { ...layout_types_js_1.LAYOUT_PRESETS.SUB_MENU, order: ['input', 'options', 'hints'] }, defaultSelected = [], minSelections = 0, maxSelections, allowSelectAll = true, allowInvert = true, onExit } = config;
34
+ // Use i18n for default prompt if not provided
35
+ const displayPrompt = prompt || (0, registry_js_1.t)('menus.multiSelectPrompt');
36
+ // Generate hints dynamically if not provided
37
+ const displayHints = hints || generateHints(allowSelectAll, allowInvert);
19
38
  // Validate options
20
39
  if (!options || options.length === 0) {
21
40
  throw new Error('CheckboxMenu requires at least one option');
@@ -24,8 +43,48 @@ async function showCheckboxMenu(config) {
24
43
  let cursorIndex = 0;
25
44
  const selected = new Set(defaultSelected);
26
45
  const state = (0, terminal_js_1.initTerminal)();
27
- // Extract option values
28
- const optionValues = options.map(opt => typeof opt === 'string' ? opt : opt.label);
46
+ // Separate selectable options from separators
47
+ const selectableIndices = [];
48
+ const optionData = [];
49
+ options.forEach((opt, index) => {
50
+ if (typeof opt === 'object' && 'type' in opt && opt.type === 'separator') {
51
+ optionData.push({ value: '', isSeparator: true, label: opt.label });
52
+ }
53
+ else {
54
+ let value;
55
+ if (typeof opt === 'string') {
56
+ value = opt;
57
+ }
58
+ else if ('value' in opt) {
59
+ value = opt.value ?? opt.label ?? '';
60
+ }
61
+ else {
62
+ value = opt.label ?? '';
63
+ }
64
+ optionData.push({ value, isSeparator: false });
65
+ selectableIndices.push(index);
66
+ }
67
+ });
68
+ // Ensure cursorIndex points to a selectable option
69
+ if (!selectableIndices.includes(cursorIndex)) {
70
+ cursorIndex = selectableIndices[0] || 0;
71
+ }
72
+ // Helper function to get next/previous selectable index
73
+ const getNextSelectableIndex = (currentIndex, direction) => {
74
+ let nextIndex = currentIndex;
75
+ const maxAttempts = options.length;
76
+ let attempts = 0;
77
+ do {
78
+ if (direction === 'up') {
79
+ nextIndex = nextIndex > 0 ? nextIndex - 1 : options.length - 1;
80
+ }
81
+ else {
82
+ nextIndex = nextIndex < options.length - 1 ? nextIndex + 1 : 0;
83
+ }
84
+ attempts++;
85
+ } while (!selectableIndices.includes(nextIndex) && attempts < maxAttempts);
86
+ return selectableIndices.includes(nextIndex) ? nextIndex : currentIndex;
87
+ };
29
88
  // Render function
30
89
  const render = () => {
31
90
  (0, terminal_js_1.clearMenu)(state);
@@ -42,20 +101,26 @@ async function showCheckboxMenu(config) {
42
101
  case 'input':
43
102
  if (layout.visible.input) {
44
103
  const selectedCount = selected.size;
45
- const displayValue = `${selectedCount} 项已选`;
46
- (0, renderer_js_1.renderInputPrompt)(prompt, displayValue);
104
+ const displayValue = `${selectedCount} ${(0, registry_js_1.t)('menus.selectedCount')}`;
105
+ (0, renderer_js_1.renderInputPrompt)(displayPrompt, displayValue);
47
106
  lineCount++;
48
107
  }
49
108
  break;
50
109
  case 'options':
51
- optionValues.forEach((option, index) => {
52
- (0, renderer_js_1.renderOption)(option, selected.has(index), index === cursorIndex);
110
+ optionData.forEach((item, index) => {
111
+ if (item.isSeparator) {
112
+ // Render section label
113
+ (0, renderer_js_1.renderSectionLabel)(item.label);
114
+ }
115
+ else {
116
+ (0, renderer_js_1.renderOption)(item.value, selected.has(index), index === cursorIndex);
117
+ }
53
118
  lineCount++;
54
119
  });
55
120
  break;
56
121
  case 'hints':
57
- if (layout.visible.hints && hints.length > 0) {
58
- (0, renderer_js_1.renderHints)(hints);
122
+ if (layout.visible.hints && displayHints.length > 0) {
123
+ (0, renderer_js_1.renderHints)(displayHints);
59
124
  lineCount++;
60
125
  }
61
126
  break;
@@ -100,42 +165,54 @@ async function showCheckboxMenu(config) {
100
165
  const indices = Array.from(selected).sort((a, b) => a - b);
101
166
  const values = indices.map(i => {
102
167
  const option = options[i];
103
- return typeof option === 'string' ? option : option.value || option.label;
168
+ if (typeof option === 'string') {
169
+ return option;
170
+ }
171
+ else if ('type' in option && option.type === 'separator') {
172
+ return '';
173
+ }
174
+ else if ('value' in option) {
175
+ return option.value ?? option.label ?? '';
176
+ }
177
+ else {
178
+ return option.label ?? '';
179
+ }
104
180
  });
105
181
  resolve({ indices, values });
106
182
  return;
107
183
  }
108
184
  // Handle Space (toggle selection)
109
185
  if ((0, keyboard_js_1.isSpace)(key)) {
110
- if (selected.has(cursorIndex)) {
111
- selected.delete(cursorIndex);
112
- }
113
- else {
114
- // Check max selections
115
- if (!maxSelections || selected.size < maxSelections) {
116
- selected.add(cursorIndex);
186
+ // Only toggle if cursor is on a selectable item
187
+ if (selectableIndices.includes(cursorIndex)) {
188
+ if (selected.has(cursorIndex)) {
189
+ selected.delete(cursorIndex);
117
190
  }
191
+ else {
192
+ // Check max selections
193
+ if (!maxSelections || selected.size < maxSelections) {
194
+ selected.add(cursorIndex);
195
+ }
196
+ }
197
+ render();
118
198
  }
119
- render();
120
199
  return;
121
200
  }
122
201
  // Handle arrow keys
123
202
  if (key === keyboard_js_1.KEY_CODES.UP) {
124
- cursorIndex = cursorIndex > 0 ? cursorIndex - 1 : options.length - 1;
203
+ cursorIndex = getNextSelectableIndex(cursorIndex, 'up');
125
204
  render();
126
205
  return;
127
206
  }
128
207
  if (key === keyboard_js_1.KEY_CODES.DOWN) {
129
- cursorIndex = cursorIndex < options.length - 1 ? cursorIndex + 1 : 0;
208
+ cursorIndex = getNextSelectableIndex(cursorIndex, 'down');
130
209
  render();
131
210
  return;
132
211
  }
133
212
  // Handle 'A' (select all)
134
213
  if (allowSelectAll && (0, keyboard_js_1.normalizeLetter)(key) === 'a') {
135
- if (!maxSelections || maxSelections >= options.length) {
136
- for (let i = 0; i < options.length; i++) {
137
- selected.add(i);
138
- }
214
+ if (!maxSelections || maxSelections >= selectableIndices.length) {
215
+ selectableIndices.forEach(i => selected.add(i));
139
216
  render();
140
217
  }
141
218
  return;
@@ -143,13 +220,13 @@ async function showCheckboxMenu(config) {
143
220
  // Handle 'I' (invert selection)
144
221
  if (allowInvert && (0, keyboard_js_1.normalizeLetter)(key) === 'i') {
145
222
  const newSelected = new Set();
146
- for (let i = 0; i < options.length; i++) {
223
+ selectableIndices.forEach(i => {
147
224
  if (!selected.has(i)) {
148
225
  if (!maxSelections || newSelected.size < maxSelections) {
149
226
  newSelected.add(i);
150
227
  }
151
228
  }
152
- }
229
+ });
153
230
  selected.clear();
154
231
  newSelected.forEach(i => selected.add(i));
155
232
  render();
@@ -10,13 +10,32 @@ const terminal_js_1 = require("../../core/terminal.js");
10
10
  const keyboard_js_1 = require("../../core/keyboard.js");
11
11
  const renderer_js_1 = require("../../core/renderer.js");
12
12
  const colors_js_1 = require("../../core/colors.js");
13
+ const registry_js_1 = require("../../i18n/registry.js");
14
+ /**
15
+ * Generate hints based on menu configuration
16
+ */
17
+ function generateHints(allowNumberKeys, allowLetterKeys) {
18
+ const hints = [(0, registry_js_1.t)('hints.arrows')];
19
+ if (allowNumberKeys) {
20
+ hints.push((0, registry_js_1.t)('hints.numbers'));
21
+ }
22
+ if (allowLetterKeys) {
23
+ hints.push((0, registry_js_1.t)('hints.letters'));
24
+ }
25
+ hints.push((0, registry_js_1.t)('hints.enter'));
26
+ return hints;
27
+ }
13
28
  /**
14
29
  * Show a radio menu (single-select)
15
30
  * @param config - Menu configuration
16
31
  * @returns Promise resolving to selected option
17
32
  */
18
33
  async function showRadioMenu(config) {
19
- const { options, title, prompt = '输入选项或用↑↓选择,回车确认', hints = ['↑↓ 方向键', '0-9 输入序号', '⏎ 确认'], layout = layout_types_js_1.LAYOUT_PRESETS.MAIN_MENU, defaultIndex = 0, allowNumberKeys = true, allowLetterKeys = false, onExit } = config;
34
+ const { options, title, prompt, hints, layout = layout_types_js_1.LAYOUT_PRESETS.MAIN_MENU, defaultIndex = 0, allowNumberKeys = true, allowLetterKeys = false, onExit } = config;
35
+ // Use i18n for default prompt if not provided
36
+ const displayPrompt = prompt || (0, registry_js_1.t)('menus.selectPrompt');
37
+ // Generate hints dynamically if not provided
38
+ const displayHints = hints || generateHints(allowNumberKeys, allowLetterKeys);
20
39
  // Validate options
21
40
  if (!options || options.length === 0) {
22
41
  throw new Error('RadioMenu requires at least one option');
@@ -24,8 +43,48 @@ async function showRadioMenu(config) {
24
43
  // Initialize state
25
44
  let selectedIndex = Math.max(0, Math.min(defaultIndex, options.length - 1));
26
45
  const state = (0, terminal_js_1.initTerminal)();
27
- // Extract option values
28
- const optionValues = options.map(opt => typeof opt === 'string' ? opt : opt.label);
46
+ // Separate selectable options from separators
47
+ const selectableIndices = [];
48
+ const optionData = [];
49
+ options.forEach((opt, index) => {
50
+ if (typeof opt === 'object' && 'type' in opt && opt.type === 'separator') {
51
+ optionData.push({ value: '', isSeparator: true, label: opt.label });
52
+ }
53
+ else {
54
+ let value;
55
+ if (typeof opt === 'string') {
56
+ value = opt;
57
+ }
58
+ else if ('value' in opt) {
59
+ value = opt.value ?? opt.label ?? '';
60
+ }
61
+ else {
62
+ value = opt.label ?? '';
63
+ }
64
+ optionData.push({ value, isSeparator: false });
65
+ selectableIndices.push(index);
66
+ }
67
+ });
68
+ // Ensure selectedIndex points to a selectable option
69
+ if (!selectableIndices.includes(selectedIndex)) {
70
+ selectedIndex = selectableIndices[0] || 0;
71
+ }
72
+ // Helper function to get next/previous selectable index
73
+ const getNextSelectableIndex = (currentIndex, direction) => {
74
+ let nextIndex = currentIndex;
75
+ const maxAttempts = options.length;
76
+ let attempts = 0;
77
+ do {
78
+ if (direction === 'up') {
79
+ nextIndex = nextIndex > 0 ? nextIndex - 1 : options.length - 1;
80
+ }
81
+ else {
82
+ nextIndex = nextIndex < options.length - 1 ? nextIndex + 1 : 0;
83
+ }
84
+ attempts++;
85
+ } while (!selectableIndices.includes(nextIndex) && attempts < maxAttempts);
86
+ return selectableIndices.includes(nextIndex) ? nextIndex : currentIndex;
87
+ };
29
88
  // Render function
30
89
  const render = () => {
31
90
  (0, terminal_js_1.clearMenu)(state);
@@ -46,12 +105,28 @@ async function showRadioMenu(config) {
46
105
  }
47
106
  break;
48
107
  case 'options':
49
- optionValues.forEach((option, index) => {
50
- // Extract number prefix if exists
51
- const match = option.match(/^(\d+)\.\s*/);
52
- const prefix = match ? '' : `${index + 1}. `;
53
- // For radio menus, don't show selection indicator (pass undefined instead of false)
54
- (0, renderer_js_1.renderOption)(option, undefined, index === selectedIndex, prefix);
108
+ optionData.forEach((item, index) => {
109
+ if (item.isSeparator) {
110
+ // Render section label
111
+ (0, renderer_js_1.renderSectionLabel)(item.label);
112
+ }
113
+ else {
114
+ // Check if option starts with a number or letter prefix
115
+ const numberMatch = item.value.match(/^(\d+)\.\s*/);
116
+ const letterMatch = item.value.match(/^([a-zA-Z])\.\s*/);
117
+ // Don't add prefix if option already has number or letter prefix
118
+ const prefix = (numberMatch || letterMatch) ? '' : `${selectableIndices.indexOf(index) + 1}. `;
119
+ // Check if this is an Exit option (contains "Exit" or "Quit")
120
+ const isExitOption = /\b(exit|quit)\b/i.test(item.value);
121
+ const displayValue = isExitOption ? `${colors_js_1.colors.red}${item.value}${colors_js_1.colors.reset}` : item.value;
122
+ // For radio menus, don't show selection indicator (pass undefined instead of false)
123
+ (0, renderer_js_1.renderOption)(displayValue, undefined, index === selectedIndex, prefix);
124
+ // Add blank line after last item before next separator
125
+ const nextIndex = index + 1;
126
+ if (nextIndex < optionData.length && optionData[nextIndex].isSeparator) {
127
+ (0, terminal_js_1.writeLine)('');
128
+ }
129
+ }
55
130
  lineCount++;
56
131
  });
57
132
  break;
@@ -59,21 +134,23 @@ async function showRadioMenu(config) {
59
134
  if (layout.visible.input) {
60
135
  // Calculate display value (current selection number)
61
136
  let displayValue = '';
62
- const currentOption = optionValues[selectedIndex];
63
- const match = currentOption?.match(/^([^.]+)\./);
64
- if (match) {
65
- displayValue = match[1];
66
- }
67
- else {
68
- displayValue = String(selectedIndex + 1);
137
+ const currentItem = optionData[selectedIndex];
138
+ if (currentItem && !currentItem.isSeparator) {
139
+ const match = currentItem.value.match(/^([^.]+)\./);
140
+ if (match) {
141
+ displayValue = match[1];
142
+ }
143
+ else {
144
+ displayValue = String(selectableIndices.indexOf(selectedIndex) + 1);
145
+ }
69
146
  }
70
- (0, renderer_js_1.renderInputPrompt)(prompt, displayValue);
147
+ (0, renderer_js_1.renderInputPrompt)(displayPrompt, displayValue);
71
148
  lineCount++;
72
149
  }
73
150
  break;
74
151
  case 'hints':
75
- if (layout.visible.hints && hints.length > 0) {
76
- (0, renderer_js_1.renderHints)(hints);
152
+ if (layout.visible.hints && displayHints.length > 0) {
153
+ (0, renderer_js_1.renderHints)(displayHints);
77
154
  lineCount++;
78
155
  }
79
156
  break;
@@ -111,9 +188,19 @@ async function showRadioMenu(config) {
111
188
  (0, terminal_js_1.clearMenu)(state);
112
189
  (0, terminal_js_1.restoreTerminal)(state);
113
190
  const selectedOption = options[selectedIndex];
114
- const value = typeof selectedOption === 'string'
115
- ? selectedOption
116
- : selectedOption.value || selectedOption.label;
191
+ let value;
192
+ if (typeof selectedOption === 'string') {
193
+ value = selectedOption;
194
+ }
195
+ else if ('type' in selectedOption && selectedOption.type === 'separator') {
196
+ value = '';
197
+ }
198
+ else if ('value' in selectedOption) {
199
+ value = selectedOption.value ?? selectedOption.label ?? '';
200
+ }
201
+ else {
202
+ value = selectedOption.label ?? '';
203
+ }
117
204
  resolve({
118
205
  index: selectedIndex,
119
206
  value
@@ -122,20 +209,20 @@ async function showRadioMenu(config) {
122
209
  }
123
210
  // Handle arrow keys
124
211
  if (key === keyboard_js_1.KEY_CODES.UP) {
125
- selectedIndex = selectedIndex > 0 ? selectedIndex - 1 : options.length - 1;
212
+ selectedIndex = getNextSelectableIndex(selectedIndex, 'up');
126
213
  render();
127
214
  return;
128
215
  }
129
216
  if (key === keyboard_js_1.KEY_CODES.DOWN) {
130
- selectedIndex = selectedIndex < options.length - 1 ? selectedIndex + 1 : 0;
217
+ selectedIndex = getNextSelectableIndex(selectedIndex, 'down');
131
218
  render();
132
219
  return;
133
220
  }
134
221
  // Handle number keys
135
222
  if (allowNumberKeys && (0, keyboard_js_1.isNumberKey)(key)) {
136
223
  const num = parseInt(key, 10);
137
- if (num > 0 && num <= options.length) {
138
- selectedIndex = num - 1;
224
+ if (num > 0 && num <= selectableIndices.length) {
225
+ selectedIndex = selectableIndices[num - 1];
139
226
  render();
140
227
  }
141
228
  return;
@@ -143,11 +230,14 @@ async function showRadioMenu(config) {
143
230
  // Handle letter keys
144
231
  if (allowLetterKeys && (0, keyboard_js_1.isLetterKey)(key)) {
145
232
  const letter = (0, keyboard_js_1.normalizeLetter)(key);
146
- const index = optionValues.findIndex(opt => {
147
- const match = opt.match(/^([a-zA-Z])\./i);
233
+ const index = selectableIndices.find(idx => {
234
+ const item = optionData[idx];
235
+ if (item.isSeparator)
236
+ return false;
237
+ const match = item.value.match(/^([a-zA-Z])\./i);
148
238
  return match && match[1].toLowerCase() === letter;
149
239
  });
150
- if (index !== -1) {
240
+ if (index !== undefined) {
151
241
  selectedIndex = index;
152
242
  render();
153
243
  }
@@ -39,6 +39,11 @@ export declare function renderHints(hints: string[]): void;
39
39
  * @param width - Width of separator (default: terminal width)
40
40
  */
41
41
  export declare function renderSeparator(char?: string, width?: number): void;
42
+ /**
43
+ * Render a section label (menu grouping)
44
+ * @param label - Label text (optional)
45
+ */
46
+ export declare function renderSectionLabel(label?: string): void;
42
47
  /**
43
48
  * Render a message with icon
44
49
  * @param type - Message type (success, error, warning, info, question)
@@ -10,6 +10,7 @@ exports.renderOption = renderOption;
10
10
  exports.renderInputPrompt = renderInputPrompt;
11
11
  exports.renderHints = renderHints;
12
12
  exports.renderSeparator = renderSeparator;
13
+ exports.renderSectionLabel = renderSectionLabel;
13
14
  exports.renderMessage = renderMessage;
14
15
  exports.renderProgress = renderProgress;
15
16
  exports.renderBox = renderBox;
@@ -79,7 +80,7 @@ function renderOption(text, isSelected, isHighlighted, prefix) {
79
80
  * @param showCursor - Whether to show cursor
80
81
  */
81
82
  function renderInputPrompt(prompt, value, showCursor = false) {
82
- let line = ` ${colors_js_1.colors.dim}${prompt}${colors_js_1.colors.reset} `;
83
+ let line = ` ${prompt} `;
83
84
  if (value) {
84
85
  line += `${colors_js_1.colors.cyan}${value}${colors_js_1.colors.reset}`;
85
86
  }
@@ -95,7 +96,19 @@ function renderInputPrompt(prompt, value, showCursor = false) {
95
96
  function renderHints(hints) {
96
97
  if (hints.length === 0)
97
98
  return;
98
- const hintLine = ` ${colors_js_1.colors.dim}${hints.join(' ')}${colors_js_1.colors.reset}`;
99
+ // Format each hint: symbols/numbers in black, text in gray
100
+ const formattedHints = hints.map(hint => {
101
+ const parts = hint.split(' ');
102
+ if (parts.length >= 2) {
103
+ // First part (symbols/numbers) in normal color, rest in dim
104
+ const symbols = parts[0];
105
+ const text = parts.slice(1).join(' ');
106
+ return `${symbols} ${colors_js_1.colors.dim}${text}${colors_js_1.colors.reset}`;
107
+ }
108
+ // If no space, keep entire hint in dim
109
+ return `${colors_js_1.colors.dim}${hint}${colors_js_1.colors.reset}`;
110
+ });
111
+ const hintLine = ` ${formattedHints.join(' • ')}`;
99
112
  (0, terminal_js_1.writeLine)(hintLine);
100
113
  }
101
114
  /**
@@ -108,6 +121,26 @@ function renderSeparator(char = '─', width) {
108
121
  const sepWidth = width || termWidth;
109
122
  (0, terminal_js_1.writeLine)(char.repeat(sepWidth));
110
123
  }
124
+ /**
125
+ * Render a section label (menu grouping)
126
+ * @param label - Label text (optional)
127
+ */
128
+ function renderSectionLabel(label) {
129
+ if (label) {
130
+ const totalWidth = 50; // Fixed total width for consistency
131
+ const padding = 2; // Spaces around label
132
+ const labelWithPadding = ` ${label} `;
133
+ const labelLength = labelWithPadding.length;
134
+ const dashesTotal = totalWidth - labelLength;
135
+ const dashesLeft = Math.floor(dashesTotal / 2);
136
+ const dashesRight = dashesTotal - dashesLeft;
137
+ const line = ` ${colors_js_1.colors.dim}${'─'.repeat(dashesLeft)}${labelWithPadding}${'─'.repeat(dashesRight)}${colors_js_1.colors.reset}`;
138
+ (0, terminal_js_1.writeLine)(line);
139
+ }
140
+ else {
141
+ (0, terminal_js_1.writeLine)('');
142
+ }
143
+ }
111
144
  /**
112
145
  * Render a message with icon
113
146
  * @param type - Message type (success, error, warning, info, question)
@@ -16,10 +16,11 @@ exports.en = {
16
16
  space: 'Space Toggle',
17
17
  enter: '⏎ Confirm',
18
18
  numbers: '0-9 Number keys',
19
- letters: 'A-Z Letter keys',
19
+ letters: 'Letter Shortcuts',
20
20
  selectAll: 'A Select all',
21
21
  invert: 'I Invert',
22
- yesNo: 'Y/N Quick keys'
22
+ yesNo: 'Y/N Quick keys',
23
+ exit: 'Ctrl+C Exit'
23
24
  },
24
25
  messages: {
25
26
  success: 'Success',
@@ -16,10 +16,11 @@ exports.zh = {
16
16
  space: '空格 选中/取消',
17
17
  enter: '⏎ 确认',
18
18
  numbers: '0-9 输入序号',
19
- letters: 'A-Z 字母选择',
19
+ letters: '字母 快捷键',
20
20
  selectAll: 'A 全选',
21
21
  invert: 'I 反选',
22
- yesNo: 'Y/N 快捷键'
22
+ yesNo: 'Y/N 快捷键',
23
+ exit: 'Ctrl+C 退出'
23
24
  },
24
25
  messages: {
25
26
  success: '成功',
@@ -24,6 +24,7 @@ export interface LanguageMap {
24
24
  selectAll: string;
25
25
  invert: string;
26
26
  yesNo: string;
27
+ exit: string;
27
28
  };
28
29
  messages: {
29
30
  success: string;
package/dist/index.d.ts CHANGED
@@ -6,7 +6,7 @@ export { menuAPI as menu, inputAPI as input, wizardAPI as wizard } from './api.j
6
6
  export { default } from './api.js';
7
7
  export { showRadioMenu, showCheckboxMenu, showBooleanMenu } from './components/menus/index.js';
8
8
  export { showTextInput, showNumberInput, showLanguageSelector, showModifyField } from './components/inputs/index.js';
9
- export { renderSimpleHeader, renderAsciiHeader, createSimpleHeader, createAsciiHeader, renderProgressIndicator, renderStageHeader, renderStageSeparator, createProgressIndicator, createStageHeader, createStageSeparator, renderMessage, showSuccess, showError, showWarning, showInfo, showQuestion, createMessage, renderSummaryTable, createSummaryTable, createSimpleSummary } from './components/display/index.js';
9
+ export { renderSimpleHeader, renderAsciiHeader, createSimpleHeader, createAsciiHeader, renderProgressIndicator, renderStageHeader, renderStageSeparator, createProgressIndicator, createStageHeader, createStageSeparator, renderMessage, showSuccess, showError, showWarning, showInfo, showQuestion, createMessage, renderSummaryTable, createSummaryTable, createSimpleSummary, renderHeader, type HeaderConfig } from './components/display/index.js';
10
10
  export { runWizard, createWizard, WizardConfig, WizardStep, WizardResult } from './features/wizard.js';
11
11
  export { registerCommand, unregisterCommand, clearCustomCommands, isCommand, parseCommand, handleCommand, getAvailableCommands, showCommandHelp } from './features/commands.js';
12
12
  export { getCurrentLanguage, setLanguage, t, registerLanguage, getAvailableLanguages, getCurrentLanguageMap } from './i18n/registry.js';
package/dist/index.js CHANGED
@@ -21,8 +21,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
21
21
  return (mod && mod.__esModule) ? mod : { "default": mod };
22
22
  };
23
23
  Object.defineProperty(exports, "__esModule", { value: true });
24
- exports.applyGradient = exports.createGradient = exports.colors = exports.getCurrentLanguageMap = exports.getAvailableLanguages = exports.registerLanguage = exports.t = exports.setLanguage = exports.getCurrentLanguage = exports.showCommandHelp = exports.getAvailableCommands = exports.handleCommand = exports.parseCommand = exports.isCommand = exports.clearCustomCommands = exports.unregisterCommand = exports.registerCommand = exports.createWizard = exports.runWizard = exports.createSimpleSummary = exports.createSummaryTable = exports.renderSummaryTable = exports.createMessage = exports.showQuestion = exports.showInfo = exports.showWarning = exports.showError = exports.showSuccess = exports.renderMessage = exports.createStageSeparator = exports.createStageHeader = exports.createProgressIndicator = exports.renderStageSeparator = exports.renderStageHeader = exports.renderProgressIndicator = exports.createAsciiHeader = exports.createSimpleHeader = exports.renderAsciiHeader = exports.renderSimpleHeader = exports.showModifyField = exports.showLanguageSelector = exports.showNumberInput = exports.showTextInput = exports.showBooleanMenu = exports.showCheckboxMenu = exports.showRadioMenu = exports.default = exports.wizard = exports.input = exports.menu = void 0;
25
- exports.LAYOUT_PRESETS = exports.KEY_CODES = exports.colorize = void 0;
24
+ exports.createGradient = exports.colors = exports.getCurrentLanguageMap = exports.getAvailableLanguages = exports.registerLanguage = exports.t = exports.setLanguage = exports.getCurrentLanguage = exports.showCommandHelp = exports.getAvailableCommands = exports.handleCommand = exports.parseCommand = exports.isCommand = exports.clearCustomCommands = exports.unregisterCommand = exports.registerCommand = exports.createWizard = exports.runWizard = exports.renderHeader = exports.createSimpleSummary = exports.createSummaryTable = exports.renderSummaryTable = exports.createMessage = exports.showQuestion = exports.showInfo = exports.showWarning = exports.showError = exports.showSuccess = exports.renderMessage = exports.createStageSeparator = exports.createStageHeader = exports.createProgressIndicator = exports.renderStageSeparator = exports.renderStageHeader = exports.renderProgressIndicator = exports.createAsciiHeader = exports.createSimpleHeader = exports.renderAsciiHeader = exports.renderSimpleHeader = exports.showModifyField = exports.showLanguageSelector = exports.showNumberInput = exports.showTextInput = exports.showBooleanMenu = exports.showCheckboxMenu = exports.showRadioMenu = exports.default = exports.wizard = exports.input = exports.menu = void 0;
25
+ exports.LAYOUT_PRESETS = exports.KEY_CODES = exports.colorize = exports.applyGradient = void 0;
26
26
  // Export unified API
27
27
  var api_js_1 = require("./api.js");
28
28
  Object.defineProperty(exports, "menu", { enumerable: true, get: function () { return api_js_1.menuAPI; } });
@@ -63,6 +63,7 @@ Object.defineProperty(exports, "createMessage", { enumerable: true, get: functio
63
63
  Object.defineProperty(exports, "renderSummaryTable", { enumerable: true, get: function () { return index_js_3.renderSummaryTable; } });
64
64
  Object.defineProperty(exports, "createSummaryTable", { enumerable: true, get: function () { return index_js_3.createSummaryTable; } });
65
65
  Object.defineProperty(exports, "createSimpleSummary", { enumerable: true, get: function () { return index_js_3.createSimpleSummary; } });
66
+ Object.defineProperty(exports, "renderHeader", { enumerable: true, get: function () { return index_js_3.renderHeader; } });
66
67
  // Export features
67
68
  var wizard_js_1 = require("./features/wizard.js");
68
69
  Object.defineProperty(exports, "runWizard", { enumerable: true, get: function () { return wizard_js_1.runWizard; } });
@@ -3,11 +3,14 @@
3
3
  */
4
4
  import { MenuLayout } from './layout.types.js';
5
5
  /**
6
- * Menu option (can be string or object with label)
6
+ * Menu option (can be string, object with label, or section header)
7
7
  */
8
8
  export type MenuOption = string | {
9
9
  label: string;
10
10
  value?: string;
11
+ } | {
12
+ type: 'separator';
13
+ label?: string;
11
14
  };
12
15
  /**
13
16
  * Base menu configuration
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cli-menu-kit",
3
- "version": "0.1.1",
3
+ "version": "0.1.9",
4
4
  "description": "A lightweight, customizable CLI menu system with keyboard shortcuts and real-time rendering",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",