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,398 @@
1
+ "use strict";
2
+ /**
3
+ * CheckboxTableMenu - Multi-select menu with table display
4
+ * Combines checkbox selection with formatted table layout
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.showCheckboxTableMenu = showCheckboxTableMenu;
8
+ const terminal_js_1 = require("../../core/terminal.js");
9
+ const keyboard_js_1 = require("../../core/keyboard.js");
10
+ const renderer_js_1 = require("../../core/renderer.js");
11
+ const colors_js_1 = require("../../core/colors.js");
12
+ const registry_js_1 = require("../../i18n/registry.js");
13
+ const virtual_scroll_js_1 = require("../../core/virtual-scroll.js");
14
+ /**
15
+ * Calculate column widths based on content
16
+ */
17
+ function calculateColumnWidths(columns, data, mode) {
18
+ return columns.map((col) => {
19
+ // Use specified width if provided
20
+ if (col.width)
21
+ return col.width;
22
+ if (mode === 'fixed') {
23
+ // Use default width for fixed mode
24
+ return 20;
25
+ }
26
+ // Auto-calculate based on header and data
27
+ const headerWidth = col.header.length;
28
+ const dataWidth = Math.max(...data.map(row => String(row[col.key] || '').length), 0);
29
+ // Add padding and set reasonable limits
30
+ return Math.min(Math.max(headerWidth, dataWidth) + 2, 50);
31
+ });
32
+ }
33
+ /**
34
+ * Build options array with separators inserted
35
+ */
36
+ function buildOptionsWithSeparators(data, separators) {
37
+ const result = [];
38
+ const sortedSeparators = (separators || []).sort((a, b) => a.beforeIndex - b.beforeIndex);
39
+ let sepIndex = 0;
40
+ for (let i = 0; i < data.length; i++) {
41
+ // Insert separator if needed
42
+ while (sepIndex < sortedSeparators.length && sortedSeparators[sepIndex].beforeIndex === i) {
43
+ result.push({
44
+ type: 'separator',
45
+ label: sortedSeparators[sepIndex].label,
46
+ description: sortedSeparators[sepIndex].description
47
+ });
48
+ sepIndex++;
49
+ }
50
+ // Add data row
51
+ result.push({
52
+ type: 'data',
53
+ data: data[i],
54
+ originalIndex: i
55
+ });
56
+ }
57
+ return result;
58
+ }
59
+ /**
60
+ * Render table header
61
+ */
62
+ function renderTableHeader(columns, columnWidths, checkboxWidth, showHeaderSeparator) {
63
+ let lineCount = 0;
64
+ // Header row
65
+ let headerLine = ''.padEnd(checkboxWidth); // Space for checkbox column
66
+ columns.forEach((col, index) => {
67
+ const width = columnWidths[index];
68
+ const align = col.align || 'left';
69
+ const paddedHeader = (0, renderer_js_1.padText)(col.header, width, align);
70
+ headerLine += `${colors_js_1.uiColors.primary}${colors_js_1.colors.bold}${paddedHeader}${colors_js_1.colors.reset}`;
71
+ });
72
+ process.stdout.write(` ${headerLine}\n`);
73
+ lineCount++;
74
+ // Separator line
75
+ if (showHeaderSeparator) {
76
+ const totalWidth = checkboxWidth + columnWidths.reduce((sum, w) => sum + w, 0);
77
+ process.stdout.write(` ${colors_js_1.uiColors.separator}${'─'.repeat(totalWidth)}${colors_js_1.colors.reset}\n`);
78
+ lineCount++;
79
+ }
80
+ // Blank line after header
81
+ (0, renderer_js_1.renderBlankLines)(1);
82
+ lineCount++;
83
+ return lineCount;
84
+ }
85
+ /**
86
+ * Render a table row with checkbox
87
+ */
88
+ function renderTableRow(rowData, columns, columnWidths, isSelected, isHighlighted, checkboxWidth) {
89
+ let line = '';
90
+ // Cursor indicator with background for highlighted row
91
+ if (isHighlighted) {
92
+ line += `${colors_js_1.uiColors.cursor}${colors_js_1.colors.bold}❯ ${colors_js_1.colors.reset}`;
93
+ }
94
+ else {
95
+ line += ' ';
96
+ }
97
+ // Checkbox
98
+ line += isSelected
99
+ ? `${colors_js_1.uiColors.selected}◉${colors_js_1.colors.reset} `
100
+ : `${colors_js_1.uiColors.disabled}○${colors_js_1.colors.reset} `;
101
+ // Table cells with background for highlighted row
102
+ columns.forEach((col, colIndex) => {
103
+ const value = String(rowData[col.key] || '');
104
+ const width = columnWidths[colIndex];
105
+ const align = col.align || 'left';
106
+ // Truncate if too long
107
+ const truncated = value.length > width ? value.substring(0, width - 3) + '...' : value;
108
+ const paddedValue = (0, renderer_js_1.padText)(truncated, width, align);
109
+ // Apply color and background based on state
110
+ if (isHighlighted) {
111
+ // Highlighted row: cyan text with reverse video (background)
112
+ line += `${colors_js_1.uiColors.primary}${colors_js_1.colors.bold}\x1b[7m${paddedValue}\x1b[27m${colors_js_1.colors.reset}`;
113
+ }
114
+ else if (isSelected) {
115
+ // Selected but not highlighted: normal text
116
+ line += `${colors_js_1.uiColors.textPrimary}${paddedValue}${colors_js_1.colors.reset}`;
117
+ }
118
+ else {
119
+ // Not selected: dim text
120
+ line += `${colors_js_1.uiColors.textSecondary}${paddedValue}${colors_js_1.colors.reset}`;
121
+ }
122
+ });
123
+ process.stdout.write(line + '\n');
124
+ }
125
+ /**
126
+ * Show a checkbox table menu (multi-select with table display)
127
+ * @param config - Menu configuration
128
+ * @param hints - Optional hints to display at the bottom (for Page Layout use)
129
+ * @returns Promise resolving to selected rows
130
+ */
131
+ async function showCheckboxTableMenu(config, hints) {
132
+ const { columns, data, idKey, defaultSelected = [], minSelections = 0, maxSelections, allowSelectAll = true, allowInvert = true, showBorders = false, showHeaderSeparator = true, separators, separatorAlign = 'center', widthMode = 'fixed', checkboxWidth = 4, title, prompt, separatorWidth = 30, onExit, preserveOnSelect = false } = config;
133
+ const preserveOnExit = config.preserveOnExit ?? preserveOnSelect;
134
+ // Validate data
135
+ if (!data || data.length === 0) {
136
+ throw new Error('CheckboxTableMenu requires at least one data row');
137
+ }
138
+ if (!columns || columns.length === 0) {
139
+ throw new Error('CheckboxTableMenu requires at least one column');
140
+ }
141
+ // Calculate column widths
142
+ const columnWidths = calculateColumnWidths(columns, data, widthMode);
143
+ // Calculate total table width for separators
144
+ const totalTableWidth = checkboxWidth + columnWidths.reduce((sum, w) => sum + w, 0);
145
+ // Build options with separators
146
+ const optionsWithSeparators = buildOptionsWithSeparators(data, separators);
147
+ // Initialize state
148
+ let cursorIndex = 0;
149
+ const selected = new Set();
150
+ // Map default selected (can be indices or IDs)
151
+ defaultSelected.forEach(item => {
152
+ if (typeof item === 'number') {
153
+ selected.add(item);
154
+ }
155
+ else if (idKey) {
156
+ const index = data.findIndex(row => row[idKey] === item);
157
+ if (index >= 0)
158
+ selected.add(index);
159
+ }
160
+ });
161
+ const state = (0, terminal_js_1.initTerminal)(); // Use normal mode (inline rendering)
162
+ // Get selectable indices (skip separators)
163
+ const selectableIndices = [];
164
+ optionsWithSeparators.forEach((item, index) => {
165
+ if (item.type === 'data') {
166
+ selectableIndices.push(index);
167
+ }
168
+ });
169
+ // Ensure cursorIndex points to a selectable option
170
+ if (!selectableIndices.includes(cursorIndex)) {
171
+ cursorIndex = selectableIndices[0] || 0;
172
+ }
173
+ // Helper function to get next/previous selectable index
174
+ const getNextSelectableIndex = (currentIndex, direction) => {
175
+ let nextIndex = currentIndex;
176
+ const maxAttempts = optionsWithSeparators.length;
177
+ let attempts = 0;
178
+ do {
179
+ if (direction === 'up') {
180
+ nextIndex = nextIndex > 0 ? nextIndex - 1 : optionsWithSeparators.length - 1;
181
+ }
182
+ else {
183
+ nextIndex = nextIndex < optionsWithSeparators.length - 1 ? nextIndex + 1 : 0;
184
+ }
185
+ attempts++;
186
+ } while (!selectableIndices.includes(nextIndex) && attempts < maxAttempts);
187
+ return selectableIndices.includes(nextIndex) ? nextIndex : currentIndex;
188
+ };
189
+ // Render function with virtual scrolling
190
+ const render = () => {
191
+ // Clear previous render
192
+ if (state.renderedLines > 0) {
193
+ (0, terminal_js_1.clearMenu)(state);
194
+ }
195
+ let lineCount = 0;
196
+ // Render title if provided
197
+ if (title) {
198
+ (0, renderer_js_1.renderBlankLines)(1);
199
+ lineCount++;
200
+ }
201
+ // Render prompt if provided
202
+ if (prompt) {
203
+ process.stdout.write(` ${colors_js_1.colors.dim}${prompt}${colors_js_1.colors.reset}\n`);
204
+ lineCount++;
205
+ (0, renderer_js_1.renderBlankLines)(1);
206
+ lineCount++;
207
+ }
208
+ // Render table header
209
+ lineCount += renderTableHeader(columns, columnWidths, checkboxWidth, showHeaderSeparator);
210
+ // Virtual scrolling: calculate visible range using utility
211
+ const scrollResult = (0, virtual_scroll_js_1.calculateVirtualScroll)({
212
+ items: optionsWithSeparators,
213
+ cursorIndex,
214
+ targetLines: 30,
215
+ getItemLineCount: (item, index) => {
216
+ if (item.type === 'separator') {
217
+ let lines = 1; // title line
218
+ if (index > 0)
219
+ lines++; // blank line before (except very first item)
220
+ if (item.description)
221
+ lines++; // description line
222
+ return lines;
223
+ }
224
+ return 1; // data row
225
+ }
226
+ });
227
+ const { visibleStart, visibleEnd, isScrolled } = scrollResult;
228
+ // Render visible options
229
+ for (let index = visibleStart; index < visibleEnd; index++) {
230
+ const item = optionsWithSeparators[index];
231
+ if (item.type === 'separator') {
232
+ // Add blank line before separator (except for the first visible one)
233
+ if (index > visibleStart) {
234
+ (0, renderer_js_1.renderBlankLines)(1);
235
+ lineCount++;
236
+ }
237
+ // Render separator with configured alignment
238
+ (0, renderer_js_1.renderSectionLabel)(item.label || '', totalTableWidth, separatorAlign);
239
+ lineCount++;
240
+ // Render description if provided (with same alignment)
241
+ if (item.description) {
242
+ const descLength = item.description.length;
243
+ let padding = 0;
244
+ switch (separatorAlign) {
245
+ case 'left':
246
+ padding = 2; // Just left margin
247
+ break;
248
+ case 'right':
249
+ padding = Math.max(0, totalTableWidth - descLength);
250
+ break;
251
+ case 'center':
252
+ default:
253
+ padding = Math.max(0, Math.floor((totalTableWidth - descLength) / 2)) + 2;
254
+ break;
255
+ }
256
+ const alignedDesc = ' '.repeat(padding) + item.description;
257
+ process.stdout.write(`${colors_js_1.colors.dim}${alignedDesc}${colors_js_1.colors.reset}\n`);
258
+ lineCount++;
259
+ }
260
+ }
261
+ else {
262
+ // Render data row
263
+ const originalIndex = item.originalIndex;
264
+ const isSelected = selected.has(originalIndex);
265
+ const isHighlighted = index === cursorIndex;
266
+ renderTableRow(item.data, columns, columnWidths, isSelected, isHighlighted, checkboxWidth);
267
+ lineCount++;
268
+ }
269
+ }
270
+ // Show scroll indicator if content is scrolled
271
+ if (isScrolled) {
272
+ (0, renderer_js_1.renderBlankLines)(1);
273
+ lineCount++;
274
+ // Calculate current position among selectable items
275
+ const selectableBeforeCursor = selectableIndices.filter(i => i <= cursorIndex).length;
276
+ const totalSelectable = selectableIndices.length;
277
+ const scrollText = (0, registry_js_1.t)('menus.scrollIndicator', {
278
+ current: String(selectableBeforeCursor),
279
+ total: String(totalSelectable)
280
+ });
281
+ const scrollInfo = ` ${colors_js_1.colors.dim}[${scrollText}]${colors_js_1.colors.reset}`;
282
+ process.stdout.write(scrollInfo + '\n');
283
+ lineCount++;
284
+ }
285
+ // Render hints if provided
286
+ if (hints && hints.length > 0) {
287
+ (0, renderer_js_1.renderBlankLines)(1);
288
+ lineCount++;
289
+ (0, renderer_js_1.renderHints)(hints);
290
+ lineCount += 1;
291
+ }
292
+ state.renderedLines = lineCount;
293
+ };
294
+ // Initial render
295
+ render();
296
+ // Keyboard input handling
297
+ return new Promise((resolve) => {
298
+ const onData = (key) => {
299
+ if ((0, keyboard_js_1.isCtrlC)(key)) {
300
+ state.stdin.removeListener('data', onData);
301
+ if (!preserveOnExit) {
302
+ (0, terminal_js_1.clearMenu)(state);
303
+ }
304
+ (0, terminal_js_1.restoreTerminal)(state);
305
+ if (onExit)
306
+ onExit();
307
+ process.exit(0);
308
+ }
309
+ if ((0, keyboard_js_1.isEnter)(key)) {
310
+ // Validate minimum selections
311
+ if (selected.size < minSelections) {
312
+ // TODO: Show error message
313
+ return;
314
+ }
315
+ // Clean up
316
+ state.stdin.removeListener('data', onData);
317
+ if (!preserveOnSelect) {
318
+ (0, terminal_js_1.clearMenu)(state);
319
+ }
320
+ (0, terminal_js_1.restoreTerminal)(state);
321
+ // Build result
322
+ const selectedIndices = Array.from(selected).sort((a, b) => a - b);
323
+ const selectedRows = selectedIndices.map(i => data[i]);
324
+ const result = {
325
+ indices: selectedIndices,
326
+ rows: selectedRows
327
+ };
328
+ if (idKey) {
329
+ result.ids = selectedRows.map(row => row[idKey]);
330
+ }
331
+ resolve(result);
332
+ return;
333
+ }
334
+ if ((0, keyboard_js_1.isSpace)(key)) {
335
+ // Toggle selection for current row
336
+ const currentItem = optionsWithSeparators[cursorIndex];
337
+ if (currentItem.type === 'data') {
338
+ const originalIndex = currentItem.originalIndex;
339
+ if (selected.has(originalIndex)) {
340
+ selected.delete(originalIndex);
341
+ }
342
+ else {
343
+ // Check max selections
344
+ if (!maxSelections || selected.size < maxSelections) {
345
+ selected.add(originalIndex);
346
+ }
347
+ }
348
+ render();
349
+ }
350
+ return;
351
+ }
352
+ // Arrow keys
353
+ if (key === keyboard_js_1.KEY_CODES.UP) {
354
+ cursorIndex = getNextSelectableIndex(cursorIndex, 'up');
355
+ render();
356
+ return;
357
+ }
358
+ if (key === keyboard_js_1.KEY_CODES.DOWN) {
359
+ cursorIndex = getNextSelectableIndex(cursorIndex, 'down');
360
+ render();
361
+ return;
362
+ }
363
+ // Select all (A key)
364
+ if (allowSelectAll && (key === 'a' || key === 'A')) {
365
+ if (selected.size === data.length) {
366
+ // Deselect all
367
+ selected.clear();
368
+ }
369
+ else {
370
+ // Select all
371
+ data.forEach((_, index) => {
372
+ if (!maxSelections || selected.size < maxSelections) {
373
+ selected.add(index);
374
+ }
375
+ });
376
+ }
377
+ render();
378
+ return;
379
+ }
380
+ // Invert selection (I key)
381
+ if (allowInvert && (key === 'i' || key === 'I')) {
382
+ const newSelected = new Set();
383
+ data.forEach((_, index) => {
384
+ if (!selected.has(index)) {
385
+ if (!maxSelections || newSelected.size < maxSelections) {
386
+ newSelected.add(index);
387
+ }
388
+ }
389
+ });
390
+ selected.clear();
391
+ newSelected.forEach(i => selected.add(i));
392
+ render();
393
+ return;
394
+ }
395
+ };
396
+ state.stdin.on('data', onData);
397
+ });
398
+ }
@@ -4,4 +4,5 @@
4
4
  */
5
5
  export { showRadioMenu } from './radio-menu.js';
6
6
  export { showCheckboxMenu } from './checkbox-menu.js';
7
+ export { showCheckboxTableMenu } from './checkbox-table-menu.js';
7
8
  export { showBooleanMenu } from './boolean-menu.js';
@@ -4,10 +4,12 @@
4
4
  * Exports all menu component functions
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.showBooleanMenu = exports.showCheckboxMenu = exports.showRadioMenu = void 0;
7
+ exports.showBooleanMenu = exports.showCheckboxTableMenu = exports.showCheckboxMenu = exports.showRadioMenu = void 0;
8
8
  var radio_menu_js_1 = require("./radio-menu.js");
9
9
  Object.defineProperty(exports, "showRadioMenu", { enumerable: true, get: function () { return radio_menu_js_1.showRadioMenu; } });
10
10
  var checkbox_menu_js_1 = require("./checkbox-menu.js");
11
11
  Object.defineProperty(exports, "showCheckboxMenu", { enumerable: true, get: function () { return checkbox_menu_js_1.showCheckboxMenu; } });
12
+ var checkbox_table_menu_js_1 = require("./checkbox-table-menu.js");
13
+ Object.defineProperty(exports, "showCheckboxTableMenu", { enumerable: true, get: function () { return checkbox_table_menu_js_1.showCheckboxTableMenu; } });
12
14
  var boolean_menu_js_1 = require("./boolean-menu.js");
13
15
  Object.defineProperty(exports, "showBooleanMenu", { enumerable: true, get: function () { return boolean_menu_js_1.showBooleanMenu; } });
@@ -0,0 +1,34 @@
1
+ /**
2
+ * RadioMenu - Split rendering and interaction
3
+ * Supports Page Layout V2 architecture with separate render/interact phases
4
+ * Uses ScreenManager for independent region updates
5
+ */
6
+ import { RadioMenuConfig, RadioMenuResult } from '../../types/menu.types.js';
7
+ import { TerminalState } from '../../core/terminal.js';
8
+ /**
9
+ * Menu state for split rendering
10
+ */
11
+ export interface RadioMenuState {
12
+ config: RadioMenuConfig;
13
+ selectedIndex: number;
14
+ selectableIndices: number[];
15
+ optionData: Array<{
16
+ display: string;
17
+ value: string;
18
+ isSeparator: boolean;
19
+ label?: string;
20
+ }>;
21
+ terminalState: TerminalState;
22
+ displayPrompt: string;
23
+ initialLineCount?: number;
24
+ regionId: string;
25
+ }
26
+ /**
27
+ * Render radio menu UI (non-blocking)
28
+ * Returns state for later interaction
29
+ */
30
+ export declare function renderRadioMenuUI(config: RadioMenuConfig): RadioMenuState;
31
+ /**
32
+ * Wait for user input and return result (blocking)
33
+ */
34
+ export declare function waitForRadioMenuInput(menuState: RadioMenuState): Promise<RadioMenuResult>;