@nmakarov/cli-toolkit 0.1.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.
@@ -0,0 +1,1068 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/screen/index.ts
31
+ var screen_exports = {};
32
+ __export(screen_exports, {
33
+ Divider: () => Divider,
34
+ FooterPresets: () => FooterPresets,
35
+ GridCell: () => GridCell,
36
+ InputField: () => InputField,
37
+ ListComponent: () => ListComponent,
38
+ ListItem: () => ListItem,
39
+ MultiColumnListComponent: () => MultiColumnListComponent,
40
+ MultiColumnListWithPreviewComponent: () => MultiColumnListWithPreviewComponent,
41
+ ScreenBody: () => ScreenBody,
42
+ ScreenContainer: () => ScreenContainer,
43
+ ScreenDivider: () => ScreenDivider,
44
+ ScreenFooter: () => ScreenFooter,
45
+ ScreenRow: () => ScreenRow,
46
+ ScreenTitle: () => ScreenTitle,
47
+ TextBlock: () => TextBlock,
48
+ buildBreadcrumb: () => buildBreadcrumb,
49
+ buildDetailBreadcrumb: () => buildDetailBreadcrumb,
50
+ buildFooter: () => buildFooter,
51
+ organizeFooterMessages: () => organizeFooterMessages,
52
+ showListScreen: () => showListScreen,
53
+ showMenuScreen: () => showMenuScreen,
54
+ showMultiColumnListScreen: () => showMultiColumnListScreen,
55
+ showMultiColumnListWithPreviewScreen: () => showMultiColumnListWithPreviewScreen,
56
+ showScreen: () => showScreen,
57
+ showWordGridScreen: () => showWordGridScreen
58
+ });
59
+ module.exports = __toCommonJS(screen_exports);
60
+
61
+ // src/screen/screens.ts
62
+ var import_react3 = require("react");
63
+ var import_ink3 = require("ink");
64
+
65
+ // src/screen/components.ts
66
+ var import_react = require("react");
67
+ var import_ink = require("ink");
68
+ function getScreenWidth(maxWidth = null) {
69
+ const terminalWidth = process.stdout.columns || 80;
70
+ const availableWidth = Math.max(20, terminalWidth - 4);
71
+ return maxWidth ? Math.min(availableWidth, maxWidth) : availableWidth;
72
+ }
73
+ function ScreenContainer({ children }) {
74
+ const width = getScreenWidth();
75
+ return (0, import_react.createElement)(import_ink.Box, {
76
+ flexDirection: "column",
77
+ marginTop: 1,
78
+ borderStyle: "single",
79
+ borderColor: "cyan",
80
+ paddingX: 1,
81
+ width
82
+ // Use the calculated width directly
83
+ }, children);
84
+ }
85
+ function ScreenRow({ children }) {
86
+ return (0, import_react.createElement)(import_ink.Box, { flexDirection: "column" }, children);
87
+ }
88
+ function ScreenTitle({ text }) {
89
+ return (0, import_react.createElement)(
90
+ ScreenRow,
91
+ {},
92
+ (0, import_react.createElement)(import_ink.Text, { bold: true, color: "cyan" }, text)
93
+ );
94
+ }
95
+ function ScreenDivider({ width }) {
96
+ const dividerWidth = width || getScreenWidth() - 4;
97
+ return (0, import_react.createElement)(import_ink.Text, { color: "cyan", dimColor: true }, "\u2500".repeat(dividerWidth));
98
+ }
99
+ function ScreenBody({ children, alignItems = "flex-start" }) {
100
+ return (0, import_react.createElement)(import_ink.Box, { flexDirection: "column", alignItems }, children);
101
+ }
102
+ function ScreenFooter({ lines, textStyle }) {
103
+ const defaultTextStyle = {
104
+ dimColor: true,
105
+ color: "white"
106
+ };
107
+ const finalTextStyle = { ...defaultTextStyle, ...textStyle };
108
+ const flattenAndWrap = (items, keyPrefix = "") => {
109
+ const result = [];
110
+ let keyIndex = 0;
111
+ items.forEach((item, index) => {
112
+ if (Array.isArray(item)) {
113
+ const nested = flattenAndWrap(item, `${keyPrefix}-${index}`);
114
+ result.push(...nested);
115
+ } else if (typeof item === "string") {
116
+ result.push(
117
+ (0, import_react.createElement)(import_ink.Text, { key: `${keyPrefix}-${keyIndex++}`, ...finalTextStyle }, item)
118
+ );
119
+ } else {
120
+ const element = item;
121
+ if (element.key === null || element.key === void 0) {
122
+ result.push(
123
+ (0, import_react.createElement)(import_ink.Text, { key: `${keyPrefix}-${keyIndex++}`, ...finalTextStyle }, element)
124
+ );
125
+ } else {
126
+ result.push(element);
127
+ }
128
+ }
129
+ });
130
+ return result;
131
+ };
132
+ const wrappedItems = flattenAndWrap(lines);
133
+ return (0, import_react.createElement)(
134
+ import_ink.Box,
135
+ { flexDirection: "column" },
136
+ (0, import_react.createElement)(import_ink.Box, { flexDirection: "row" }, ...wrappedItems)
137
+ );
138
+ }
139
+
140
+ // src/screen/list-components.ts
141
+ var import_react2 = __toESM(require("react"), 1);
142
+ var import_ink2 = require("ink");
143
+ var h2 = import_react2.createElement;
144
+ function MultiColumnListComponent({ items, ctx, selectedIndexRef }) {
145
+ const [, forceUpdate] = (0, import_react2.useState)({});
146
+ const termWidth = (process.stdout.columns || 80) - 8;
147
+ const maxItemLength = Math.max(...items.map((w) => w.length));
148
+ const columnWidth = maxItemLength + 3;
149
+ const columns = Math.max(1, Math.floor(termWidth / columnWidth));
150
+ const itemsPerColumn = Math.ceil(items.length / columns);
151
+ (0, import_react2.useEffect)(() => {
152
+ ctx.setAction("moveUp", () => {
153
+ selectedIndexRef.current = Math.max(0, selectedIndexRef.current - 1);
154
+ forceUpdate({});
155
+ });
156
+ ctx.setAction("moveDown", () => {
157
+ selectedIndexRef.current = Math.min(items.length - 1, selectedIndexRef.current + 1);
158
+ forceUpdate({});
159
+ });
160
+ ctx.setAction("moveLeft", () => {
161
+ if (selectedIndexRef.current === 0) {
162
+ ctx.goBack();
163
+ } else {
164
+ selectedIndexRef.current = Math.max(0, selectedIndexRef.current - itemsPerColumn);
165
+ forceUpdate({});
166
+ }
167
+ });
168
+ ctx.setAction("moveRight", () => {
169
+ selectedIndexRef.current = Math.min(items.length - 1, selectedIndexRef.current + itemsPerColumn);
170
+ forceUpdate({});
171
+ });
172
+ ctx.setKeyBinding([
173
+ { key: "leftArrow", caption: "navigate", action: "moveLeft", order: 0 },
174
+ { key: "rightArrow", caption: "navigate", action: "moveRight", order: 0 },
175
+ { key: "upArrow", caption: "navigate", action: "moveUp", order: 0 },
176
+ { key: "downArrow", caption: "navigate", action: "moveDown", order: 0 }
177
+ ]);
178
+ ctx.addFooter(`Total: ${items.length} items`);
179
+ }, []);
180
+ const selectedIndex = selectedIndexRef.current;
181
+ const rows = [];
182
+ for (let row = 0; row < itemsPerColumn; row++) {
183
+ const cols = [];
184
+ for (let col = 0; col < columns; col++) {
185
+ const index = col * itemsPerColumn + row;
186
+ if (index < items.length) {
187
+ const isSelected = index === selectedIndex;
188
+ cols.push(
189
+ h2(
190
+ import_ink2.Box,
191
+ { key: index, width: columnWidth },
192
+ h2(import_ink2.Text, {
193
+ color: isSelected ? "black" : "white",
194
+ backgroundColor: isSelected ? "cyan" : void 0,
195
+ bold: isSelected
196
+ }, items[index].padEnd(maxItemLength))
197
+ )
198
+ );
199
+ }
200
+ }
201
+ rows.push(
202
+ h2(ScreenRow, { key: row, children: h2(import_ink2.Box, { flexDirection: "row" }, ...cols) })
203
+ );
204
+ }
205
+ return h2(import_ink2.Box, { flexDirection: "column" }, ...rows);
206
+ }
207
+ function MultiColumnListWithPreviewComponent({
208
+ items,
209
+ getPreviewContent,
210
+ ctx,
211
+ selectedIndexRef
212
+ }) {
213
+ const [, forceUpdate] = (0, import_react2.useState)({});
214
+ const termWidth = (process.stdout.columns || 80) - 8;
215
+ const maxItemLength = Math.max(...items.map((w) => w.length));
216
+ const columnWidth = maxItemLength + 3;
217
+ const columns = Math.max(1, Math.floor(termWidth / columnWidth));
218
+ const itemsPerColumn = Math.ceil(items.length / columns);
219
+ (0, import_react2.useEffect)(() => {
220
+ ctx.setAction("moveUp", () => {
221
+ selectedIndexRef.current = Math.max(0, selectedIndexRef.current - 1);
222
+ forceUpdate({});
223
+ });
224
+ ctx.setAction("moveDown", () => {
225
+ selectedIndexRef.current = Math.min(items.length - 1, selectedIndexRef.current + 1);
226
+ forceUpdate({});
227
+ });
228
+ ctx.setAction("moveLeft", () => {
229
+ if (selectedIndexRef.current === 0) {
230
+ ctx.goBack();
231
+ } else {
232
+ selectedIndexRef.current = Math.max(0, selectedIndexRef.current - itemsPerColumn);
233
+ forceUpdate({});
234
+ }
235
+ });
236
+ ctx.setAction("moveRight", () => {
237
+ selectedIndexRef.current = Math.min(items.length - 1, selectedIndexRef.current + itemsPerColumn);
238
+ forceUpdate({});
239
+ });
240
+ ctx.setKeyBinding([
241
+ { key: "leftArrow", caption: "navigate", action: "moveLeft", order: 0 },
242
+ { key: "rightArrow", caption: "navigate", action: "moveRight", order: 0 },
243
+ { key: "upArrow", caption: "navigate", action: "moveUp", order: 0 },
244
+ { key: "downArrow", caption: "navigate", action: "moveDown", order: 0 }
245
+ ]);
246
+ ctx.addFooter(`Total: ${items.length} items`);
247
+ }, []);
248
+ const selectedIndex = selectedIndexRef.current;
249
+ const selectedItem = items[selectedIndex];
250
+ const rows = [];
251
+ for (let row = 0; row < itemsPerColumn; row++) {
252
+ const cols = [];
253
+ for (let col = 0; col < columns; col++) {
254
+ const index = col * itemsPerColumn + row;
255
+ if (index < items.length) {
256
+ const isSelected = index === selectedIndex;
257
+ cols.push(
258
+ h2(
259
+ import_ink2.Box,
260
+ { key: index, width: columnWidth },
261
+ h2(import_ink2.Text, {
262
+ color: isSelected ? "black" : "white",
263
+ backgroundColor: isSelected ? "cyan" : void 0,
264
+ bold: isSelected
265
+ }, items[index].padEnd(maxItemLength))
266
+ )
267
+ );
268
+ }
269
+ }
270
+ rows.push(
271
+ h2(ScreenRow, { key: row, children: h2(import_ink2.Box, { flexDirection: "row" }, ...cols) })
272
+ );
273
+ }
274
+ const previewContent = getPreviewContent ? getPreviewContent(selectedItem) : selectedItem;
275
+ const previewRows = [];
276
+ if (typeof previewContent === "string") {
277
+ previewRows.push(h2(ScreenRow, { key: "preview-string", children: h2(import_ink2.Text, { bold: true }, previewContent) }));
278
+ } else if (typeof previewContent === "object" && !import_react2.default.isValidElement(previewContent) && previewContent !== null) {
279
+ Object.entries(previewContent).forEach(([key, value], idx) => {
280
+ previewRows.push(h2(ScreenRow, { key: `preview-${key}-${idx}`, children: h2(import_ink2.Text, {}, `${key}: ${value}`) }));
281
+ });
282
+ } else if (import_react2.default.isValidElement(previewContent)) {
283
+ previewRows.push(h2(ScreenRow, { key: "preview-element", children: previewContent }));
284
+ }
285
+ return h2(
286
+ import_ink2.Box,
287
+ { flexDirection: "column" },
288
+ ...rows,
289
+ h2(ScreenRow, { key: "spacer-1", children: h2(import_ink2.Text, {}, " ") }),
290
+ h2(ScreenDivider, { key: "divider" }),
291
+ h2(ScreenRow, { key: "spacer-2", children: h2(import_ink2.Text, {}, " ") }),
292
+ ...previewRows
293
+ );
294
+ }
295
+ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sortable = false, maxHeight, sortHighlightStyle, selectionMarker = " " }) {
296
+ const [, forceUpdate] = (0, import_react2.useState)({});
297
+ const [sortOrder, setSortOrder] = (0, import_react2.useState)("none");
298
+ const [scrollOffset, setScrollOffset] = (0, import_react2.useState)(0);
299
+ const scrollStateRef = (0, import_react2.useRef)({ scrollOffset: 0, maxHeight: 0, totalItems: 0 });
300
+ const defaultGetTitle = (item) => {
301
+ return getTitle ? getTitle(item) : typeof item.value === "string" ? item.value : item.value?.title || item.name;
302
+ };
303
+ const titleGetter = getTitle || defaultGetTitle;
304
+ const displayItems = sortable && sortOrder !== "none" ? [...items].sort((a, b) => {
305
+ const titleA = titleGetter(a).toLowerCase();
306
+ const titleB = titleGetter(b).toLowerCase();
307
+ if (sortOrder === "asc") {
308
+ return titleA < titleB ? -1 : titleA > titleB ? 1 : 0;
309
+ } else {
310
+ return titleA > titleB ? -1 : titleA < titleB ? 1 : 0;
311
+ }
312
+ }) : items;
313
+ const effectiveMaxHeight = maxHeight || displayItems.length;
314
+ const canScroll = displayItems.length > effectiveMaxHeight;
315
+ const maxScrollOffset = Math.max(0, displayItems.length - effectiveMaxHeight);
316
+ const clampedScrollOffset = Math.min(Math.max(0, scrollOffset), maxScrollOffset);
317
+ const visibleItems = displayItems.slice(clampedScrollOffset, clampedScrollOffset + effectiveMaxHeight);
318
+ const canScrollUp = clampedScrollOffset > 0;
319
+ const canScrollDown = clampedScrollOffset < maxScrollOffset;
320
+ scrollStateRef.current = { scrollOffset, maxHeight: effectiveMaxHeight, totalItems: displayItems.length };
321
+ (0, import_react2.useEffect)(() => {
322
+ ctx.setAction("moveUp", () => {
323
+ const newIndex = Math.max(0, selectedIndexRef.current - 1);
324
+ selectedIndexRef.current = newIndex;
325
+ const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
326
+ const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
327
+ const currentClampedScrollOffset = Math.min(Math.max(0, currentScrollOffset), currentMaxScrollOffset);
328
+ if (newIndex < currentClampedScrollOffset) {
329
+ setScrollOffset(newIndex);
330
+ }
331
+ forceUpdate({});
332
+ });
333
+ ctx.setAction("moveDown", () => {
334
+ const currentItems = sortable && sortOrder !== "none" ? [...items].sort((a, b) => {
335
+ const titleA = titleGetter(a).toLowerCase();
336
+ const titleB = titleGetter(b).toLowerCase();
337
+ if (sortOrder === "asc") {
338
+ return titleA < titleB ? -1 : titleA > titleB ? 1 : 0;
339
+ } else {
340
+ return titleA > titleB ? -1 : titleA < titleB ? 1 : 0;
341
+ }
342
+ }) : items;
343
+ const maxIndex = currentItems.length - 1;
344
+ const newIndex = Math.min(maxIndex, selectedIndexRef.current + 1);
345
+ selectedIndexRef.current = newIndex;
346
+ const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
347
+ const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
348
+ const currentClampedScrollOffset = Math.min(Math.max(0, currentScrollOffset), currentMaxScrollOffset);
349
+ if (newIndex >= currentClampedScrollOffset + currentMaxHeight) {
350
+ setScrollOffset(newIndex - currentMaxHeight + 1);
351
+ }
352
+ forceUpdate({});
353
+ });
354
+ ctx.setAction("scrollUp", () => {
355
+ const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
356
+ const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
357
+ const newScrollOffset = Math.max(0, currentScrollOffset - 1);
358
+ setScrollOffset(newScrollOffset);
359
+ forceUpdate({});
360
+ });
361
+ ctx.setAction("scrollDown", () => {
362
+ const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
363
+ const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
364
+ const newScrollOffset = Math.min(currentMaxScrollOffset, currentScrollOffset + 1);
365
+ setScrollOffset(newScrollOffset);
366
+ forceUpdate({});
367
+ });
368
+ if (sortable) {
369
+ ctx.setAction("toggleSort", () => {
370
+ const nextSort = sortOrder === "none" ? "asc" : sortOrder === "asc" ? "desc" : "none";
371
+ const currentSelectedItem = displayItems[selectedIndexRef.current];
372
+ setSortOrder(nextSort);
373
+ const newSortedItems = nextSort !== "none" ? [...items].sort((a, b) => {
374
+ const titleA = titleGetter(a).toLowerCase();
375
+ const titleB = titleGetter(b).toLowerCase();
376
+ if (nextSort === "asc") {
377
+ return titleA < titleB ? -1 : titleA > titleB ? 1 : 0;
378
+ } else {
379
+ return titleA > titleB ? -1 : titleA < titleB ? 1 : 0;
380
+ }
381
+ }) : items;
382
+ const newIndex = newSortedItems.findIndex((item) => item === currentSelectedItem);
383
+ if (newIndex !== -1) {
384
+ selectedIndexRef.current = newIndex;
385
+ setScrollOffset(newIndex);
386
+ } else {
387
+ selectedIndexRef.current = 0;
388
+ setScrollOffset(0);
389
+ }
390
+ forceUpdate({});
391
+ });
392
+ const defaultHighlightStyle = {
393
+ color: "black",
394
+ backgroundColor: "green",
395
+ bold: true
396
+ };
397
+ const highlightStyle = { ...defaultHighlightStyle, ...sortHighlightStyle };
398
+ const sortCaption = () => {
399
+ if (sortOrder === "none") {
400
+ return h2(import_ink2.Text, {}, "s to toggle sort");
401
+ } else {
402
+ const sortLabel = sortOrder === "asc" ? "ASC" : "DESC";
403
+ return h2(
404
+ import_ink2.Text,
405
+ {},
406
+ "s to toggle ",
407
+ h2(import_ink2.Text, { color: "white", bold: true }, "sort"),
408
+ " ",
409
+ h2(import_ink2.Text, highlightStyle, ` ${sortLabel} `)
410
+ );
411
+ }
412
+ };
413
+ ctx.setKeyBinding([
414
+ { key: "upArrow", caption: "navigate", action: "moveUp", order: 0 },
415
+ { key: "downArrow", caption: "navigate", action: "moveDown", order: 0 },
416
+ {
417
+ key: "s",
418
+ caption: sortCaption,
419
+ action: "toggleSort",
420
+ order: 5
421
+ }
422
+ ]);
423
+ ctx.update();
424
+ } else {
425
+ ctx.setKeyBinding([
426
+ { key: "upArrow", caption: "navigate", action: "moveUp", order: 0 },
427
+ { key: "downArrow", caption: "navigate", action: "moveDown", order: 0 }
428
+ ]);
429
+ }
430
+ }, [sortOrder, sortable]);
431
+ const selectedIndex = selectedIndexRef.current;
432
+ const defaultRenderItem = (item, isSelected, displayIndex, actualIndex) => {
433
+ const isFirstVisible = displayIndex === 0;
434
+ const isLastVisible = displayIndex === visibleItems.length - 1;
435
+ let arrowPrefix = "";
436
+ let selectionPrefix = "";
437
+ if (isFirstVisible && canScrollUp) {
438
+ arrowPrefix = "\u2191 ";
439
+ } else if (isLastVisible && canScrollDown) {
440
+ arrowPrefix = "\u2193 ";
441
+ } else {
442
+ arrowPrefix = " ";
443
+ }
444
+ if (isSelected) {
445
+ selectionPrefix = selectionMarker;
446
+ } else {
447
+ selectionPrefix = " ".repeat(selectionMarker.length);
448
+ }
449
+ return h2(
450
+ import_ink2.Box,
451
+ { flexDirection: "row" },
452
+ // Arrow (clickable if functional, not highlighted)
453
+ h2(import_ink2.Text, {
454
+ key: `arrow-${actualIndex}`,
455
+ color: "white"
456
+ }, arrowPrefix),
457
+ // Selection marker space (always same width, not highlighted)
458
+ h2(import_ink2.Text, { key: `marker-${actualIndex}`, color: "white" }, selectionPrefix),
459
+ // Item name (highlighted if selected)
460
+ h2(import_ink2.Text, {
461
+ key: `name-${actualIndex}`,
462
+ color: isSelected ? "black" : "white",
463
+ backgroundColor: isSelected ? "cyan" : void 0,
464
+ bold: isSelected
465
+ }, item.name)
466
+ );
467
+ };
468
+ const itemRenderer = renderItem || defaultRenderItem;
469
+ return h2(
470
+ import_ink2.Box,
471
+ { flexDirection: "column" },
472
+ ...visibleItems.map((item, displayIndex) => {
473
+ const actualIndex = clampedScrollOffset + displayIndex;
474
+ const isSelected = actualIndex === selectedIndex;
475
+ if (renderItem) {
476
+ const isFirstVisible = displayIndex === 0;
477
+ const isLastVisible = displayIndex === visibleItems.length - 1;
478
+ let arrowPrefix = "";
479
+ let selectionPrefix = "";
480
+ if (isFirstVisible && canScrollUp) {
481
+ arrowPrefix = "\u2191 ";
482
+ } else if (isLastVisible && canScrollDown) {
483
+ arrowPrefix = "\u2193 ";
484
+ } else {
485
+ arrowPrefix = " ";
486
+ }
487
+ if (isSelected) {
488
+ selectionPrefix = selectionMarker;
489
+ } else {
490
+ selectionPrefix = " ".repeat(selectionMarker.length);
491
+ }
492
+ return h2(ScreenRow, {
493
+ key: `item-${actualIndex}`,
494
+ children: h2(
495
+ import_ink2.Box,
496
+ { flexDirection: "row" },
497
+ // Arrow (clickable if functional, not highlighted)
498
+ h2(import_ink2.Text, {
499
+ key: `arrow-${actualIndex}`,
500
+ color: "white"
501
+ }, arrowPrefix),
502
+ // Selection marker space (always same width, not highlighted)
503
+ h2(import_ink2.Text, { key: `marker-${actualIndex}`, color: "white" }, selectionPrefix),
504
+ // Custom rendered content
505
+ renderItem(item, isSelected, displayIndex)
506
+ )
507
+ });
508
+ } else {
509
+ return h2(ScreenRow, {
510
+ key: `item-${actualIndex}`,
511
+ children: itemRenderer(item, isSelected, displayIndex, actualIndex)
512
+ });
513
+ }
514
+ })
515
+ );
516
+ }
517
+
518
+ // src/screen/screens.ts
519
+ function groupKeyBindings(bindings) {
520
+ const groups = {};
521
+ const enabledBindings = bindings.filter((b) => b.enabled !== false);
522
+ enabledBindings.forEach((binding) => {
523
+ const caption = typeof binding.caption === "string" ? binding.caption : "";
524
+ if (!groups[caption]) {
525
+ groups[caption] = {
526
+ keys: [],
527
+ caption,
528
+ order: binding.order || 999
529
+ };
530
+ }
531
+ groups[caption].keys.push(binding.key);
532
+ });
533
+ return Object.values(groups);
534
+ }
535
+ function formatKeyBindings(bindings, mode = "long") {
536
+ const resolvedBindings = bindings.map((binding) => {
537
+ let resolvedCaption = binding.caption;
538
+ if (typeof binding.caption === "function") {
539
+ resolvedCaption = binding.caption();
540
+ }
541
+ return {
542
+ ...binding,
543
+ resolvedCaption
544
+ };
545
+ });
546
+ const groups = groupKeyBindings(resolvedBindings.map((b) => ({
547
+ ...b,
548
+ caption: typeof b.resolvedCaption === "string" ? b.resolvedCaption : ""
549
+ })));
550
+ groups.sort((a, b) => a.order - b.order);
551
+ const items = [];
552
+ groups.forEach((group) => {
553
+ const bindingWithCustom = resolvedBindings.find(
554
+ (b) => group.keys.includes(b.key) && typeof b.resolvedCaption !== "string"
555
+ );
556
+ if (bindingWithCustom && bindingWithCustom.resolvedCaption) {
557
+ items.push(bindingWithCustom.resolvedCaption);
558
+ } else {
559
+ const keyStr = formatKeys(group.keys);
560
+ if (mode === "long") {
561
+ items.push(`${keyStr} to ${group.caption}`);
562
+ } else {
563
+ items.push(keyStr);
564
+ }
565
+ }
566
+ });
567
+ return items;
568
+ }
569
+ function formatKeys(keys) {
570
+ const keyMap = {
571
+ "escape": "esc",
572
+ "leftArrow": "\u2190",
573
+ "rightArrow": "\u2192",
574
+ "upArrow": "\u2191",
575
+ "downArrow": "\u2193",
576
+ "return": "enter"
577
+ };
578
+ return keys.map((k) => keyMap[k] || k).join("/");
579
+ }
580
+ async function showScreen(config) {
581
+ const {
582
+ title,
583
+ onRender,
584
+ parentData = {}
585
+ } = config;
586
+ return new Promise((resolve) => {
587
+ let instance;
588
+ const keyBindings = [];
589
+ const actions = {};
590
+ const customFooterItems = [];
591
+ let renderResult = null;
592
+ let initialized = false;
593
+ const Screen = () => {
594
+ const [updateCounter, setUpdateCounter] = (0, import_react3.useState)(0);
595
+ if (!initialized) {
596
+ const defaultBindings = [
597
+ { key: "escape", caption: "go back", action: "back", protected: true, order: 1 },
598
+ { key: "leftArrow", caption: "go back", action: "back", protected: false, order: 1 }
599
+ // Note: 'select' is not a default - components add it if needed
600
+ ];
601
+ defaultBindings.forEach((binding) => {
602
+ keyBindings.push(binding);
603
+ });
604
+ actions.back = () => {
605
+ cleanup(null);
606
+ };
607
+ initialized = true;
608
+ }
609
+ const context = {
610
+ setAction: (actionName, handlerFn) => {
611
+ actions[actionName] = handlerFn;
612
+ },
613
+ setKeyBinding: (bindingOrBindings) => {
614
+ const bindingsToSet = Array.isArray(bindingOrBindings) ? bindingOrBindings : [bindingOrBindings];
615
+ bindingsToSet.forEach((binding) => {
616
+ const existingIndex = keyBindings.findIndex((b) => b.key === binding.key);
617
+ if (existingIndex >= 0) {
618
+ const existing = keyBindings[existingIndex];
619
+ if (existing.protected) {
620
+ console.warn(`Cannot override protected key: ${binding.key}`);
621
+ return;
622
+ }
623
+ keyBindings[existingIndex] = {
624
+ ...existing,
625
+ ...binding,
626
+ order: binding.order !== void 0 ? binding.order : existing.order,
627
+ enabled: binding.enabled !== void 0 ? binding.enabled : existing.enabled !== void 0 ? existing.enabled : true
628
+ };
629
+ } else {
630
+ keyBindings.push({
631
+ protected: false,
632
+ order: 999,
633
+ enabled: true,
634
+ ...binding
635
+ });
636
+ }
637
+ });
638
+ },
639
+ updateKeyBinding: (keyName, updates) => {
640
+ const index = keyBindings.findIndex((b) => b.key === keyName);
641
+ if (index >= 0) {
642
+ keyBindings[index] = {
643
+ ...keyBindings[index],
644
+ ...updates
645
+ };
646
+ }
647
+ },
648
+ removeKeyBinding: (keyName) => {
649
+ const index = keyBindings.findIndex((b) => b.key === keyName);
650
+ if (index >= 0) {
651
+ if (keyBindings[index].protected) {
652
+ console.warn(`Cannot remove protected key: ${keyName}`);
653
+ return;
654
+ }
655
+ keyBindings.splice(index, 1);
656
+ }
657
+ },
658
+ addFooter: (item) => {
659
+ customFooterItems.push(item);
660
+ },
661
+ clearFooter: () => {
662
+ customFooterItems.length = 0;
663
+ },
664
+ setFooter: (items) => {
665
+ customFooterItems.length = 0;
666
+ const itemsArray = Array.isArray(items) ? items : [items];
667
+ customFooterItems.push(...itemsArray);
668
+ },
669
+ update: () => {
670
+ setUpdateCounter((c) => c + 1);
671
+ },
672
+ goBack: () => {
673
+ if (actions.back) {
674
+ actions.back();
675
+ }
676
+ },
677
+ close: (result) => {
678
+ cleanup(result);
679
+ },
680
+ parentData
681
+ };
682
+ if (!renderResult) {
683
+ renderResult = onRender(context);
684
+ }
685
+ (0, import_ink3.useInput)((input, key) => {
686
+ if (key.ctrl && input === "c") {
687
+ cleanup(null);
688
+ process.exit(0);
689
+ return;
690
+ }
691
+ let matchedBinding = null;
692
+ for (const binding of keyBindings) {
693
+ let keyMatches = false;
694
+ if (key[binding.key]) {
695
+ keyMatches = true;
696
+ } else if (input === binding.key) {
697
+ keyMatches = true;
698
+ }
699
+ if (keyMatches) {
700
+ if (binding.enabled === false) {
701
+ continue;
702
+ }
703
+ if (binding.condition && !binding.condition(context)) {
704
+ continue;
705
+ }
706
+ matchedBinding = binding;
707
+ break;
708
+ }
709
+ }
710
+ if (matchedBinding && actions[matchedBinding.action]) {
711
+ const actionResult = actions[matchedBinding.action]({
712
+ input,
713
+ key,
714
+ binding: matchedBinding
715
+ });
716
+ }
717
+ });
718
+ const footerLines = [];
719
+ const bindingItems = formatKeyBindings(keyBindings, "long");
720
+ if (bindingItems.length > 0) {
721
+ const bindingsLine = [];
722
+ bindingItems.forEach((item, idx) => {
723
+ if (idx > 0) {
724
+ bindingsLine.push(", ");
725
+ }
726
+ bindingsLine.push(item);
727
+ });
728
+ const allStrings = bindingItems.every((item) => typeof item === "string");
729
+ if (allStrings) {
730
+ footerLines.push(bindingsLine.join(""));
731
+ } else {
732
+ const wrappedBindingsLine = bindingsLine.map(
733
+ (item) => typeof item === "string" ? (0, import_react3.createElement)(import_ink3.Text, {}, item) : item
734
+ );
735
+ footerLines.push(wrappedBindingsLine);
736
+ }
737
+ }
738
+ customFooterItems.forEach((item) => {
739
+ if (typeof item === "string") {
740
+ footerLines.push(item);
741
+ } else {
742
+ footerLines.push(item);
743
+ }
744
+ });
745
+ return (0, import_react3.createElement)(
746
+ ScreenContainer,
747
+ {},
748
+ (0, import_react3.createElement)(ScreenTitle, { text: title }),
749
+ (0, import_react3.createElement)(ScreenDivider),
750
+ (0, import_react3.createElement)(ScreenRow, {}, (0, import_react3.createElement)(import_ink3.Text, {}, " ")),
751
+ renderResult,
752
+ (0, import_react3.createElement)(ScreenRow, {}, (0, import_react3.createElement)(import_ink3.Text, {}, " ")),
753
+ (0, import_react3.createElement)(ScreenDivider),
754
+ (0, import_react3.createElement)(ScreenFooter, { lines: footerLines })
755
+ );
756
+ };
757
+ const cleanup = (result) => {
758
+ if (instance) instance.unmount();
759
+ setTimeout(() => resolve(result), 50);
760
+ };
761
+ instance = (0, import_ink3.render)((0, import_react3.createElement)(Screen));
762
+ });
763
+ }
764
+ async function showListScreen(config) {
765
+ const { title, items, onSelect, onEscape, parentData, initialSelectedIndex = 0, renderItem, getTitle, sortable, maxHeight, sortHighlightStyle, selectionMarker } = config;
766
+ return showScreen({
767
+ title,
768
+ parentData,
769
+ onRender: (ctx) => {
770
+ const selectedIndexRef = { current: initialSelectedIndex };
771
+ ctx.setAction("select", () => {
772
+ const selected = items[selectedIndexRef.current];
773
+ if (onSelect) {
774
+ const result = onSelect(selected.value, selectedIndexRef.current);
775
+ ctx.close(result);
776
+ }
777
+ });
778
+ if (onEscape) {
779
+ ctx.setAction("back", () => {
780
+ const result = onEscape(selectedIndexRef.current);
781
+ ctx.close(result);
782
+ });
783
+ }
784
+ ctx.setKeyBinding({ key: "return", caption: "select", action: "select", order: 2 });
785
+ return (0, import_react3.createElement)(ListComponent, { items, ctx, selectedIndexRef, renderItem, getTitle, sortable, maxHeight, sortHighlightStyle, selectionMarker });
786
+ }
787
+ });
788
+ }
789
+ async function showMultiColumnListScreen(config) {
790
+ const { title, items, onSelect, onEscape, parentData, initialSelectedIndex = 0 } = config;
791
+ return showScreen({
792
+ title,
793
+ parentData,
794
+ onRender: (ctx) => {
795
+ const selectedIndexRef = { current: initialSelectedIndex };
796
+ ctx.setAction("select", () => {
797
+ const selected = items[selectedIndexRef.current];
798
+ if (onSelect) {
799
+ const result = onSelect(selected, selectedIndexRef.current);
800
+ ctx.close(result);
801
+ }
802
+ });
803
+ if (onEscape) {
804
+ ctx.setAction("back", () => {
805
+ const result = onEscape(selectedIndexRef.current);
806
+ ctx.close(result);
807
+ });
808
+ }
809
+ ctx.setKeyBinding({ key: "return", caption: "select", action: "select", order: 2 });
810
+ return (0, import_react3.createElement)(MultiColumnListComponent, { items, ctx, selectedIndexRef });
811
+ }
812
+ });
813
+ }
814
+ async function showMultiColumnListWithPreviewScreen(config) {
815
+ const { title, items, getPreviewContent, onSelect, onEscape, parentData, initialSelectedIndex = 0 } = config;
816
+ return showScreen({
817
+ title,
818
+ parentData,
819
+ onRender: (ctx) => {
820
+ const selectedIndexRef = { current: initialSelectedIndex };
821
+ ctx.setAction("select", () => {
822
+ const selected = items[selectedIndexRef.current];
823
+ if (onSelect) {
824
+ const result = onSelect(selected, selectedIndexRef.current);
825
+ ctx.close(result);
826
+ }
827
+ });
828
+ if (onEscape) {
829
+ ctx.setAction("back", () => {
830
+ const result = onEscape(selectedIndexRef.current);
831
+ ctx.close(result);
832
+ });
833
+ }
834
+ ctx.setKeyBinding({ key: "return", caption: "select", action: "select", order: 2 });
835
+ return (0, import_react3.createElement)(MultiColumnListWithPreviewComponent, { items, getPreviewContent, ctx, selectedIndexRef });
836
+ }
837
+ });
838
+ }
839
+ var showMenuScreen = showListScreen;
840
+ var showWordGridScreen = showMultiColumnListScreen;
841
+
842
+ // src/screen/ui-elements.ts
843
+ var import_react4 = require("react");
844
+ var import_ink4 = require("ink");
845
+ function ListItem({
846
+ children,
847
+ isSelected = false,
848
+ color = "white",
849
+ backgroundColor,
850
+ bold = false,
851
+ dimColor = false
852
+ }) {
853
+ return (0, import_react4.createElement)(
854
+ import_ink4.Box,
855
+ {},
856
+ (0, import_react4.createElement)(import_ink4.Text, {
857
+ color: isSelected ? backgroundColor || "green" : color,
858
+ backgroundColor: isSelected ? color : backgroundColor,
859
+ bold: isSelected || bold,
860
+ dimColor: !isSelected && dimColor
861
+ }, children)
862
+ );
863
+ }
864
+ function TextBlock({
865
+ text,
866
+ color = "white",
867
+ dimmed = false,
868
+ bold = false,
869
+ maxWidth
870
+ }) {
871
+ return (0, import_react4.createElement)(
872
+ import_ink4.Box,
873
+ {},
874
+ (0, import_react4.createElement)(import_ink4.Text, {
875
+ color,
876
+ dimColor: dimmed,
877
+ bold
878
+ }, text)
879
+ );
880
+ }
881
+ function Divider({ character = "\u2500", width = 80 }) {
882
+ return (0, import_react4.createElement)(
883
+ import_ink4.Box,
884
+ { marginY: 1 },
885
+ (0, import_react4.createElement)(import_ink4.Text, { dimColor: true }, character.repeat(width))
886
+ );
887
+ }
888
+ function GridCell({
889
+ children,
890
+ width,
891
+ color = "white",
892
+ backgroundColor,
893
+ bold = false,
894
+ dimColor = false,
895
+ align = "left"
896
+ }) {
897
+ return (0, import_react4.createElement)(
898
+ import_ink4.Box,
899
+ { width },
900
+ (0, import_react4.createElement)(import_ink4.Text, {
901
+ color,
902
+ backgroundColor,
903
+ bold,
904
+ dimColor,
905
+ textAlign: align
906
+ }, children)
907
+ );
908
+ }
909
+ function InputField({ prompt, value, onChange, onSubmit }) {
910
+ return (0, import_react4.createElement)(
911
+ import_ink4.Box,
912
+ { flexDirection: "column" },
913
+ (0, import_react4.createElement)(import_ink4.Text, {}, prompt),
914
+ (0, import_react4.createElement)(
915
+ import_ink4.Box,
916
+ { marginTop: 1 },
917
+ (0, import_react4.createElement)(import_ink4.Text, { color: "cyan" }, " > ", value, "_")
918
+ )
919
+ );
920
+ }
921
+
922
+ // src/screen/utils.ts
923
+ function buildBreadcrumb(parts) {
924
+ if (parts.length === 0) return "";
925
+ if (parts.length === 1) return parts[0];
926
+ return parts.slice(1).map((part) => `\u2190 ${part}`).join(" ");
927
+ }
928
+ function buildDetailBreadcrumb(path, suffix = "") {
929
+ if (path.length <= 1) {
930
+ return suffix ? `\u2190 ${suffix}` : path[0] || "";
931
+ }
932
+ const breadcrumb = buildBreadcrumb(path);
933
+ return suffix ? `${breadcrumb} ${suffix}` : breadcrumb;
934
+ }
935
+
936
+ // src/screen/footer-builder.ts
937
+ function buildFooter(config = {}) {
938
+ const {
939
+ navigation = null,
940
+ actions = null,
941
+ info = null,
942
+ escape = "Esc to go back",
943
+ custom = null
944
+ } = config;
945
+ const lines = [];
946
+ const mainParts = [];
947
+ if (navigation) {
948
+ mainParts.push(navigation);
949
+ }
950
+ if (actions) {
951
+ mainParts.push(actions);
952
+ }
953
+ if (escape) {
954
+ mainParts.push(escape);
955
+ }
956
+ if (mainParts.length > 0) {
957
+ lines.push(mainParts.join(", "));
958
+ }
959
+ if (info) {
960
+ const infoLines = Array.isArray(info) ? info : [info];
961
+ lines.push(...infoLines);
962
+ }
963
+ if (custom) {
964
+ const customLines = Array.isArray(custom) ? custom : [custom];
965
+ lines.push(...customLines);
966
+ }
967
+ return lines;
968
+ }
969
+ var FooterPresets = {
970
+ /**
971
+ * Menu screen footer
972
+ */
973
+ menu: (customInfo = null) => buildFooter({
974
+ navigation: "\u2191/\u2193 to navigate",
975
+ actions: "Enter to select",
976
+ escape: "Esc to go back",
977
+ info: customInfo
978
+ }),
979
+ /**
980
+ * Word grid footer
981
+ */
982
+ wordGrid: (totalWords) => buildFooter({
983
+ navigation: "\u2191\u2193\u2190\u2192 to navigate",
984
+ actions: "Enter to select",
985
+ escape: "Esc to go back",
986
+ info: `Total: ${totalWords} words`
987
+ }),
988
+ /**
989
+ * Text input footer
990
+ */
991
+ textInput: () => buildFooter({
992
+ actions: "Type and press Enter to submit",
993
+ escape: "Esc to cancel"
994
+ }),
995
+ /**
996
+ * Info/static screen footer
997
+ */
998
+ info: () => buildFooter({
999
+ escape: "Esc to continue"
1000
+ }),
1001
+ /**
1002
+ * Main menu footer (escape exits)
1003
+ */
1004
+ mainMenu: () => buildFooter({
1005
+ navigation: "\u2191/\u2193 to navigate",
1006
+ actions: "Enter to select",
1007
+ escape: "Esc to exit"
1008
+ }),
1009
+ /**
1010
+ * Action menu footer (for word cards, etc.)
1011
+ */
1012
+ actionMenu: (hasAudio = false) => {
1013
+ const parts = buildFooter({
1014
+ navigation: "\u2191/\u2193 to navigate",
1015
+ actions: "Enter to select",
1016
+ escape: "Esc to go back"
1017
+ });
1018
+ if (hasAudio) {
1019
+ parts.push("Audio available");
1020
+ }
1021
+ return parts;
1022
+ }
1023
+ };
1024
+ function organizeFooterMessages(messages) {
1025
+ if (!messages || messages.length === 0) {
1026
+ return ["Esc to go back"];
1027
+ }
1028
+ const navigation = messages.filter((m) => m.includes("\u2191") || m.includes("\u2193") || m.includes("\u2190") || m.includes("\u2192"));
1029
+ const actions = messages.filter((m) => m.includes("Enter") || m.includes("select") || m.includes("submit"));
1030
+ const escape = messages.filter((m) => m.includes("Esc"));
1031
+ const others = messages.filter(
1032
+ (m) => !navigation.includes(m) && !actions.includes(m) && !escape.includes(m)
1033
+ );
1034
+ const lines = [];
1035
+ const mainLine = [...navigation, ...actions, ...escape].join(", ");
1036
+ if (mainLine) lines.push(mainLine);
1037
+ lines.push(...others);
1038
+ return lines;
1039
+ }
1040
+ // Annotate the CommonJS export names for ESM import in node:
1041
+ 0 && (module.exports = {
1042
+ Divider,
1043
+ FooterPresets,
1044
+ GridCell,
1045
+ InputField,
1046
+ ListComponent,
1047
+ ListItem,
1048
+ MultiColumnListComponent,
1049
+ MultiColumnListWithPreviewComponent,
1050
+ ScreenBody,
1051
+ ScreenContainer,
1052
+ ScreenDivider,
1053
+ ScreenFooter,
1054
+ ScreenRow,
1055
+ ScreenTitle,
1056
+ TextBlock,
1057
+ buildBreadcrumb,
1058
+ buildDetailBreadcrumb,
1059
+ buildFooter,
1060
+ organizeFooterMessages,
1061
+ showListScreen,
1062
+ showMenuScreen,
1063
+ showMultiColumnListScreen,
1064
+ showMultiColumnListWithPreviewScreen,
1065
+ showScreen,
1066
+ showWordGridScreen
1067
+ });
1068
+ //# sourceMappingURL=screen.cjs.map