cli-menu-kit 0.1.23 → 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 (60) 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/summary.js +119 -15
  18. package/dist/components/display/table.d.ts +42 -0
  19. package/dist/components/display/table.js +107 -0
  20. package/dist/components/menus/boolean-menu.js +2 -1
  21. package/dist/components/menus/checkbox-menu.d.ts +2 -1
  22. package/dist/components/menus/checkbox-menu.js +30 -59
  23. package/dist/components/menus/checkbox-table-menu.d.ts +12 -0
  24. package/dist/components/menus/checkbox-table-menu.js +395 -0
  25. package/dist/components/menus/index.d.ts +1 -0
  26. package/dist/components/menus/index.js +3 -1
  27. package/dist/components/menus/radio-menu-split.d.ts +33 -0
  28. package/dist/components/menus/radio-menu-split.js +248 -0
  29. package/dist/components/menus/radio-menu-v2.d.ts +11 -0
  30. package/dist/components/menus/radio-menu-v2.js +150 -0
  31. package/dist/components/menus/radio-menu.d.ts +2 -1
  32. package/dist/components/menus/radio-menu.js +60 -123
  33. package/dist/core/hint-manager.d.ts +29 -0
  34. package/dist/core/hint-manager.js +65 -0
  35. package/dist/core/renderer.d.ts +2 -1
  36. package/dist/core/renderer.js +22 -6
  37. package/dist/core/screen-manager.d.ts +54 -0
  38. package/dist/core/screen-manager.js +119 -0
  39. package/dist/core/state-manager.d.ts +27 -0
  40. package/dist/core/state-manager.js +56 -0
  41. package/dist/core/terminal.d.ts +4 -1
  42. package/dist/core/terminal.js +37 -4
  43. package/dist/core/virtual-scroll.d.ts +65 -0
  44. package/dist/core/virtual-scroll.js +120 -0
  45. package/dist/i18n/languages/en.js +4 -1
  46. package/dist/i18n/languages/zh.js +4 -1
  47. package/dist/i18n/registry.d.ts +4 -3
  48. package/dist/i18n/registry.js +12 -4
  49. package/dist/i18n/types.d.ts +3 -0
  50. package/dist/index.d.ts +5 -4
  51. package/dist/index.js +30 -4
  52. package/dist/layout.d.ts +68 -0
  53. package/dist/layout.js +134 -0
  54. package/dist/page-layout.d.ts +92 -0
  55. package/dist/page-layout.js +156 -0
  56. package/dist/types/display.types.d.ts +6 -0
  57. package/dist/types/menu.types.d.ts +57 -5
  58. package/package.json +1 -1
  59. package/dist/types/layout.types.d.ts +0 -56
  60. package/dist/types/layout.types.js +0 -36
@@ -0,0 +1,395 @@
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.colors.cyan}${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.colors.dim}${'─'.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.colors.cyan}${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.colors.green}◉${colors_js_1.colors.reset} `
100
+ : `${colors_js_1.colors.dim}○${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.colors.cyan}${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.colors.reset}${paddedValue}${colors_js_1.colors.reset}`;
117
+ }
118
+ else {
119
+ // Not selected: dim text
120
+ line += `${colors_js_1.colors.dim}${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
+ // Validate data
134
+ if (!data || data.length === 0) {
135
+ throw new Error('CheckboxTableMenu requires at least one data row');
136
+ }
137
+ if (!columns || columns.length === 0) {
138
+ throw new Error('CheckboxTableMenu requires at least one column');
139
+ }
140
+ // Calculate column widths
141
+ const columnWidths = calculateColumnWidths(columns, data, widthMode);
142
+ // Calculate total table width for separators
143
+ const totalTableWidth = checkboxWidth + columnWidths.reduce((sum, w) => sum + w, 0);
144
+ // Build options with separators
145
+ const optionsWithSeparators = buildOptionsWithSeparators(data, separators);
146
+ // Initialize state
147
+ let cursorIndex = 0;
148
+ const selected = new Set();
149
+ // Map default selected (can be indices or IDs)
150
+ defaultSelected.forEach(item => {
151
+ if (typeof item === 'number') {
152
+ selected.add(item);
153
+ }
154
+ else if (idKey) {
155
+ const index = data.findIndex(row => row[idKey] === item);
156
+ if (index >= 0)
157
+ selected.add(index);
158
+ }
159
+ });
160
+ const state = (0, terminal_js_1.initTerminal)(); // Use normal mode (inline rendering)
161
+ // Get selectable indices (skip separators)
162
+ const selectableIndices = [];
163
+ optionsWithSeparators.forEach((item, index) => {
164
+ if (item.type === 'data') {
165
+ selectableIndices.push(index);
166
+ }
167
+ });
168
+ // Ensure cursorIndex points to a selectable option
169
+ if (!selectableIndices.includes(cursorIndex)) {
170
+ cursorIndex = selectableIndices[0] || 0;
171
+ }
172
+ // Helper function to get next/previous selectable index
173
+ const getNextSelectableIndex = (currentIndex, direction) => {
174
+ let nextIndex = currentIndex;
175
+ const maxAttempts = optionsWithSeparators.length;
176
+ let attempts = 0;
177
+ do {
178
+ if (direction === 'up') {
179
+ nextIndex = nextIndex > 0 ? nextIndex - 1 : optionsWithSeparators.length - 1;
180
+ }
181
+ else {
182
+ nextIndex = nextIndex < optionsWithSeparators.length - 1 ? nextIndex + 1 : 0;
183
+ }
184
+ attempts++;
185
+ } while (!selectableIndices.includes(nextIndex) && attempts < maxAttempts);
186
+ return selectableIndices.includes(nextIndex) ? nextIndex : currentIndex;
187
+ };
188
+ // Render function with virtual scrolling
189
+ const render = () => {
190
+ // Clear previous render
191
+ if (state.renderedLines > 0) {
192
+ (0, terminal_js_1.clearMenu)(state);
193
+ }
194
+ let lineCount = 0;
195
+ // Render title if provided
196
+ if (title) {
197
+ (0, renderer_js_1.renderBlankLines)(1);
198
+ lineCount++;
199
+ }
200
+ // Render prompt if provided
201
+ if (prompt) {
202
+ process.stdout.write(` ${colors_js_1.colors.dim}${prompt}${colors_js_1.colors.reset}\n`);
203
+ lineCount++;
204
+ (0, renderer_js_1.renderBlankLines)(1);
205
+ lineCount++;
206
+ }
207
+ // Render table header
208
+ lineCount += renderTableHeader(columns, columnWidths, checkboxWidth, showHeaderSeparator);
209
+ // Virtual scrolling: calculate visible range using utility
210
+ const scrollResult = (0, virtual_scroll_js_1.calculateVirtualScroll)({
211
+ items: optionsWithSeparators,
212
+ cursorIndex,
213
+ targetLines: 30,
214
+ getItemLineCount: (item, index) => {
215
+ if (item.type === 'separator') {
216
+ let lines = 1; // title line
217
+ if (index > 0)
218
+ lines++; // blank line before (except very first item)
219
+ if (item.description)
220
+ lines++; // description line
221
+ return lines;
222
+ }
223
+ return 1; // data row
224
+ }
225
+ });
226
+ const { visibleStart, visibleEnd, isScrolled } = scrollResult;
227
+ // Render visible options
228
+ for (let index = visibleStart; index < visibleEnd; index++) {
229
+ const item = optionsWithSeparators[index];
230
+ if (item.type === 'separator') {
231
+ // Add blank line before separator (except for the first visible one)
232
+ if (index > visibleStart) {
233
+ (0, renderer_js_1.renderBlankLines)(1);
234
+ lineCount++;
235
+ }
236
+ // Render separator with configured alignment
237
+ (0, renderer_js_1.renderSectionLabel)(item.label || '', totalTableWidth, separatorAlign);
238
+ lineCount++;
239
+ // Render description if provided (with same alignment)
240
+ if (item.description) {
241
+ const descLength = item.description.length;
242
+ let padding = 0;
243
+ switch (separatorAlign) {
244
+ case 'left':
245
+ padding = 2; // Just left margin
246
+ break;
247
+ case 'right':
248
+ padding = Math.max(0, totalTableWidth - descLength);
249
+ break;
250
+ case 'center':
251
+ default:
252
+ padding = Math.max(0, Math.floor((totalTableWidth - descLength) / 2)) + 2;
253
+ break;
254
+ }
255
+ const alignedDesc = ' '.repeat(padding) + item.description;
256
+ process.stdout.write(`${colors_js_1.colors.dim}${alignedDesc}${colors_js_1.colors.reset}\n`);
257
+ lineCount++;
258
+ }
259
+ }
260
+ else {
261
+ // Render data row
262
+ const originalIndex = item.originalIndex;
263
+ const isSelected = selected.has(originalIndex);
264
+ const isHighlighted = index === cursorIndex;
265
+ renderTableRow(item.data, columns, columnWidths, isSelected, isHighlighted, checkboxWidth);
266
+ lineCount++;
267
+ }
268
+ }
269
+ // Show scroll indicator if content is scrolled
270
+ if (isScrolled) {
271
+ (0, renderer_js_1.renderBlankLines)(1);
272
+ lineCount++;
273
+ // Calculate current position among selectable items
274
+ const selectableBeforeCursor = selectableIndices.filter(i => i <= cursorIndex).length;
275
+ const totalSelectable = selectableIndices.length;
276
+ const scrollText = (0, registry_js_1.t)('menus.scrollIndicator', {
277
+ current: String(selectableBeforeCursor),
278
+ total: String(totalSelectable)
279
+ });
280
+ const scrollInfo = ` ${colors_js_1.colors.dim}[${scrollText}]${colors_js_1.colors.reset}`;
281
+ process.stdout.write(scrollInfo + '\n');
282
+ lineCount++;
283
+ }
284
+ // Render hints if provided
285
+ if (hints && hints.length > 0) {
286
+ (0, renderer_js_1.renderBlankLines)(1);
287
+ lineCount++;
288
+ (0, renderer_js_1.renderHints)(hints);
289
+ lineCount += 1;
290
+ }
291
+ state.renderedLines = lineCount;
292
+ };
293
+ // Initial render
294
+ render();
295
+ // Keyboard input handling
296
+ return new Promise((resolve) => {
297
+ const onData = (key) => {
298
+ if ((0, keyboard_js_1.isCtrlC)(key)) {
299
+ state.stdin.removeListener('data', onData);
300
+ (0, terminal_js_1.clearMenu)(state);
301
+ (0, terminal_js_1.restoreTerminal)(state);
302
+ if (onExit)
303
+ onExit();
304
+ process.exit(0);
305
+ }
306
+ if ((0, keyboard_js_1.isEnter)(key)) {
307
+ // Validate minimum selections
308
+ if (selected.size < minSelections) {
309
+ // TODO: Show error message
310
+ return;
311
+ }
312
+ // Clean up
313
+ state.stdin.removeListener('data', onData);
314
+ if (!preserveOnSelect) {
315
+ (0, terminal_js_1.clearMenu)(state);
316
+ }
317
+ (0, terminal_js_1.restoreTerminal)(state);
318
+ // Build result
319
+ const selectedIndices = Array.from(selected).sort((a, b) => a - b);
320
+ const selectedRows = selectedIndices.map(i => data[i]);
321
+ const result = {
322
+ indices: selectedIndices,
323
+ rows: selectedRows
324
+ };
325
+ if (idKey) {
326
+ result.ids = selectedRows.map(row => row[idKey]);
327
+ }
328
+ resolve(result);
329
+ return;
330
+ }
331
+ if ((0, keyboard_js_1.isSpace)(key)) {
332
+ // Toggle selection for current row
333
+ const currentItem = optionsWithSeparators[cursorIndex];
334
+ if (currentItem.type === 'data') {
335
+ const originalIndex = currentItem.originalIndex;
336
+ if (selected.has(originalIndex)) {
337
+ selected.delete(originalIndex);
338
+ }
339
+ else {
340
+ // Check max selections
341
+ if (!maxSelections || selected.size < maxSelections) {
342
+ selected.add(originalIndex);
343
+ }
344
+ }
345
+ render();
346
+ }
347
+ return;
348
+ }
349
+ // Arrow keys
350
+ if (key === keyboard_js_1.KEY_CODES.UP) {
351
+ cursorIndex = getNextSelectableIndex(cursorIndex, 'up');
352
+ render();
353
+ return;
354
+ }
355
+ if (key === keyboard_js_1.KEY_CODES.DOWN) {
356
+ cursorIndex = getNextSelectableIndex(cursorIndex, 'down');
357
+ render();
358
+ return;
359
+ }
360
+ // Select all (A key)
361
+ if (allowSelectAll && (key === 'a' || key === 'A')) {
362
+ if (selected.size === data.length) {
363
+ // Deselect all
364
+ selected.clear();
365
+ }
366
+ else {
367
+ // Select all
368
+ data.forEach((_, index) => {
369
+ if (!maxSelections || selected.size < maxSelections) {
370
+ selected.add(index);
371
+ }
372
+ });
373
+ }
374
+ render();
375
+ return;
376
+ }
377
+ // Invert selection (I key)
378
+ if (allowInvert && (key === 'i' || key === 'I')) {
379
+ const newSelected = new Set();
380
+ data.forEach((_, index) => {
381
+ if (!selected.has(index)) {
382
+ if (!maxSelections || newSelected.size < maxSelections) {
383
+ newSelected.add(index);
384
+ }
385
+ }
386
+ });
387
+ selected.clear();
388
+ newSelected.forEach(i => selected.add(i));
389
+ render();
390
+ return;
391
+ }
392
+ };
393
+ state.stdin.on('data', onData);
394
+ });
395
+ }
@@ -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,33 @@
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
+ value: string;
17
+ isSeparator: boolean;
18
+ label?: string;
19
+ }>;
20
+ terminalState: TerminalState;
21
+ displayPrompt: string;
22
+ initialLineCount?: number;
23
+ regionId: string;
24
+ }
25
+ /**
26
+ * Render radio menu UI (non-blocking)
27
+ * Returns state for later interaction
28
+ */
29
+ export declare function renderRadioMenuUI(config: RadioMenuConfig): RadioMenuState;
30
+ /**
31
+ * Wait for user input and return result (blocking)
32
+ */
33
+ export declare function waitForRadioMenuInput(menuState: RadioMenuState): Promise<RadioMenuResult>;