@nmakarov/cli-toolkit 0.69.0 → 0.71.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.
package/dist/screen.js CHANGED
@@ -84,7 +84,43 @@ function ScreenFooter({ lines, textStyle }) {
84
84
  // src/screen/list-components.js
85
85
  import React, { useState, useEffect, useRef, createElement } from "react";
86
86
  import { Box as Box2, Text as Text2 } from "ink";
87
+
88
+ // src/screen/scrollbar.js
89
+ var BAR_THUMB = "#";
90
+ var BAR_TRACK = "|";
91
+ function scrollbarGlyphs(viewportRows, totalLines, scrollTop) {
92
+ const view = Math.max(1, Math.floor(viewportRows));
93
+ const total = Math.max(0, Math.floor(totalLines));
94
+ if (total <= view) return null;
95
+ const maxScroll = total - view;
96
+ const thumbSize = Math.max(1, Math.round(view / total * view));
97
+ const travel = Math.max(0, view - thumbSize);
98
+ const s = Math.min(Math.max(0, Math.floor(scrollTop)), maxScroll);
99
+ const thumbStart = maxScroll === 0 ? 0 : Math.round(s / maxScroll * travel);
100
+ const glyphs = [];
101
+ for (let i = 0; i < view; i++) {
102
+ glyphs.push(i >= thumbStart && i < thumbStart + thumbSize ? BAR_THUMB : BAR_TRACK);
103
+ }
104
+ return glyphs;
105
+ }
106
+ var PAGE_SCROLL_KEY_BINDINGS = [
107
+ { key: "upArrow", meta: true, caption: "page", action: "pageUp", order: 0 },
108
+ { key: "downArrow", meta: true, caption: "page", action: "pageDown", order: 0 },
109
+ { key: "upArrow", ctrl: true, caption: "page", action: "pageUp", order: 0 },
110
+ { key: "downArrow", ctrl: true, caption: "page", action: "pageDown", order: 0 },
111
+ { key: "pageUp", caption: "page", action: "pageUp", order: 0 },
112
+ { key: "pageDown", caption: "page", action: "pageDown", order: 0 }
113
+ ];
114
+
115
+ // src/screen/list-components.js
87
116
  var h2 = createElement;
117
+ function clipNameForScrollbar(name, needsBar) {
118
+ if (!needsBar || name == null) return name;
119
+ const s = String(name);
120
+ if (s.length <= 1) return s;
121
+ if (/\s$/.test(s)) return s.slice(0, -1);
122
+ return s;
123
+ }
88
124
  function MultiColumnListComponent({ items, ctx, selectedIndexRef }) {
89
125
  const [, forceUpdate] = useState({});
90
126
  const termWidth = (process.stdout.columns || 80) - 8;
@@ -298,17 +334,42 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
298
334
  });
299
335
  ctx.setAction("scrollUp", () => {
300
336
  const { scrollOffset: currentScrollOffset } = scrollStateRef.current;
301
- const newScrollOffset = Math.max(0, currentScrollOffset - 1);
302
- setScrollOffset(newScrollOffset);
337
+ setScrollOffset(Math.max(0, currentScrollOffset - 1));
303
338
  forceUpdate({});
304
339
  });
305
340
  ctx.setAction("scrollDown", () => {
306
341
  const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
307
342
  const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
308
- const newScrollOffset = Math.min(currentMaxScrollOffset, currentScrollOffset + 1);
309
- setScrollOffset(newScrollOffset);
343
+ setScrollOffset(Math.min(currentMaxScrollOffset, currentScrollOffset + 1));
344
+ forceUpdate({});
345
+ });
346
+ ctx.setAction("pageUp", () => {
347
+ const { maxHeight: vh } = scrollStateRef.current;
348
+ const page = Math.max(1, vh || 1);
349
+ const newIndex = Math.max(0, selectedIndexRef.current - page);
350
+ selectedIndexRef.current = newIndex;
351
+ const list = displayItemsRef.current;
352
+ onSelectionChangeRef.current?.(newIndex, list[newIndex]);
353
+ setScrollOffset(newIndex);
310
354
  forceUpdate({});
311
355
  });
356
+ ctx.setAction("pageDown", () => {
357
+ const { maxHeight: vh, totalItems } = scrollStateRef.current;
358
+ const page = Math.max(1, vh || 1);
359
+ const maxIndex = Math.max(0, totalItems - 1);
360
+ const newIndex = Math.min(maxIndex, selectedIndexRef.current + page);
361
+ selectedIndexRef.current = newIndex;
362
+ const list = displayItemsRef.current;
363
+ onSelectionChangeRef.current?.(newIndex, list[newIndex]);
364
+ const maxScroll = Math.max(0, totalItems - page);
365
+ setScrollOffset(Math.min(maxScroll, Math.max(0, newIndex - page + 1)));
366
+ forceUpdate({});
367
+ });
368
+ const navBindings = [
369
+ { key: "upArrow", caption: "navigate", action: "moveUp", order: 0 },
370
+ { key: "downArrow", caption: "navigate", action: "moveDown", order: 0 },
371
+ ...PAGE_SCROLL_KEY_BINDINGS
372
+ ];
312
373
  if (sortable) {
313
374
  ctx.setAction("toggleSort", () => {
314
375
  const nextSort = sortOrder === "none" ? "asc" : sortOrder === "asc" ? "desc" : "none";
@@ -355,8 +416,7 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
355
416
  }
356
417
  };
357
418
  ctx.setKeyBinding([
358
- { key: "upArrow", caption: "navigate", action: "moveUp", order: 0 },
359
- { key: "downArrow", caption: "navigate", action: "moveDown", order: 0 },
419
+ ...navBindings,
360
420
  {
361
421
  key: "s",
362
422
  caption: sortCaption,
@@ -366,47 +426,47 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
366
426
  ]);
367
427
  ctx.update();
368
428
  } else {
369
- ctx.setKeyBinding([
370
- { key: "upArrow", caption: "navigate", action: "moveUp", order: 0 },
371
- { key: "downArrow", caption: "navigate", action: "moveDown", order: 0 }
372
- ]);
429
+ ctx.setKeyBinding(navBindings);
373
430
  }
374
431
  }, [sortOrder, sortable]);
375
432
  const selectedIndex = selectedIndexRef.current;
433
+ const needsBar = displayItems.length > effectiveMaxHeight;
434
+ const barGlyphs = needsBar ? scrollbarGlyphs(effectiveMaxHeight, displayItems.length, clampedScrollOffset) : null;
435
+ const appendBar = (rowContent, displayIndex) => {
436
+ if (!barGlyphs) return rowContent;
437
+ const glyph = barGlyphs[displayIndex] ?? BAR_TRACK;
438
+ return h2(
439
+ Box2,
440
+ { flexDirection: "row" },
441
+ rowContent,
442
+ h2(Text2, { color: glyph === BAR_THUMB ? "cyan" : "gray" }, glyph)
443
+ );
444
+ };
376
445
  const defaultRenderItem = (item, isSelected, displayIndex, actualIndex) => {
377
446
  const isFirstVisible = displayIndex === 0;
378
447
  const isLastVisible = displayIndex === visibleItems.length - 1;
379
- let arrowPrefix = "";
380
- let selectionPrefix = "";
381
- if (isFirstVisible && canScrollUp) {
382
- arrowPrefix = "\u2191 ";
383
- } else if (isLastVisible && canScrollDown) {
384
- arrowPrefix = "\u2193 ";
385
- } else {
386
- arrowPrefix = " ";
387
- }
388
- if (isSelected) {
389
- selectionPrefix = selectionMarker;
390
- } else {
391
- selectionPrefix = " ".repeat(selectionMarker.length);
448
+ let arrowPrefix = " ";
449
+ if (!needsBar) {
450
+ if (isFirstVisible && canScrollUp) arrowPrefix = "\u2191 ";
451
+ else if (isLastVisible && canScrollDown) arrowPrefix = "\u2193 ";
392
452
  }
453
+ const selectionPrefix = isSelected ? selectionMarker : " ".repeat(selectionMarker.length);
454
+ const label = clipNameForScrollbar(item.name, needsBar);
393
455
  return h2(
394
456
  Box2,
395
457
  { flexDirection: "row" },
396
- // Arrow (clickable if functional, not highlighted)
397
- h2(Text2, {
398
- key: `arrow-${actualIndex}`,
399
- color: "white"
400
- }, arrowPrefix),
401
- // Selection marker space (always same width, not highlighted)
458
+ h2(Text2, { key: `arrow-${actualIndex}`, color: "white" }, arrowPrefix),
402
459
  h2(Text2, { key: `marker-${actualIndex}`, color: "white" }, selectionPrefix),
403
- // Item name (highlighted if selected)
404
- h2(Text2, {
405
- key: `name-${actualIndex}`,
406
- color: isSelected ? "black" : "white",
407
- backgroundColor: isSelected ? "cyan" : void 0,
408
- bold: isSelected
409
- }, item.name)
460
+ h2(
461
+ Text2,
462
+ {
463
+ key: `name-${actualIndex}`,
464
+ color: isSelected ? "black" : "white",
465
+ backgroundColor: isSelected ? "cyan" : void 0,
466
+ bold: isSelected
467
+ },
468
+ label
469
+ )
410
470
  );
411
471
  };
412
472
  const itemRenderer = renderItem || defaultRenderItem;
@@ -419,42 +479,32 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
419
479
  if (renderItem) {
420
480
  const isFirstVisible = displayIndex === 0;
421
481
  const isLastVisible = displayIndex === visibleItems.length - 1;
422
- let arrowPrefix = "";
423
- let selectionPrefix = "";
424
- if (isFirstVisible && canScrollUp) {
425
- arrowPrefix = "\u2191 ";
426
- } else if (isLastVisible && canScrollDown) {
427
- arrowPrefix = "\u2193 ";
428
- } else {
429
- arrowPrefix = " ";
430
- }
431
- if (isSelected) {
432
- selectionPrefix = selectionMarker;
433
- } else {
434
- selectionPrefix = " ".repeat(selectionMarker.length);
482
+ let arrowPrefix = " ";
483
+ if (!needsBar) {
484
+ if (isFirstVisible && canScrollUp) arrowPrefix = "\u2191 ";
485
+ else if (isLastVisible && canScrollDown) arrowPrefix = "\u2193 ";
435
486
  }
487
+ const selectionPrefix = isSelected ? selectionMarker : " ".repeat(selectionMarker.length);
488
+ const clipped = needsBar ? { ...item, name: clipNameForScrollbar(item.name, true) } : item;
489
+ const row = h2(
490
+ Box2,
491
+ { flexDirection: "row" },
492
+ h2(Text2, { key: `arrow-${actualIndex}`, color: "white" }, arrowPrefix),
493
+ h2(Text2, { key: `marker-${actualIndex}`, color: "white" }, selectionPrefix),
494
+ renderItem(clipped, isSelected, displayIndex)
495
+ );
436
496
  return h2(ScreenRow, {
437
497
  key: `item-${actualIndex}`,
438
- children: h2(
439
- Box2,
440
- { flexDirection: "row" },
441
- // Arrow (clickable if functional, not highlighted)
442
- h2(Text2, {
443
- key: `arrow-${actualIndex}`,
444
- color: "white"
445
- }, arrowPrefix),
446
- // Selection marker space (always same width, not highlighted)
447
- h2(Text2, { key: `marker-${actualIndex}`, color: "white" }, selectionPrefix),
448
- // Custom rendered content
449
- renderItem(item, isSelected, displayIndex)
450
- )
451
- });
452
- } else {
453
- return h2(ScreenRow, {
454
- key: `item-${actualIndex}`,
455
- children: itemRenderer(item, isSelected, displayIndex, actualIndex)
498
+ children: appendBar(row, displayIndex)
456
499
  });
457
500
  }
501
+ return h2(ScreenRow, {
502
+ key: `item-${actualIndex}`,
503
+ children: appendBar(
504
+ itemRenderer(item, isSelected, displayIndex, actualIndex),
505
+ displayIndex
506
+ )
507
+ });
458
508
  })
459
509
  );
460
510
  }
@@ -914,8 +964,6 @@ function InputField({ prompt, value, onChange: _onChange, onSubmit: _onSubmit })
914
964
  import { useState as useState3, useEffect as useEffect2, useMemo, useRef as useRef2, createElement as createElement2 } from "react";
915
965
  import { Box as Box4, Text as Text5 } from "ink";
916
966
  var h5 = createElement2;
917
- var BAR_THUMB = "#";
918
- var BAR_TRACK = "|";
919
967
  function wrapTextLines(text, cols) {
920
968
  const w = Math.max(1, Math.floor(Number(cols) || 1));
921
969
  const out = [];
@@ -933,21 +981,6 @@ function wrapTextLines(text, cols) {
933
981
  }
934
982
  return out;
935
983
  }
936
- function scrollbarGlyphs(viewportRows, totalLines, scrollTop) {
937
- const view = Math.max(1, Math.floor(viewportRows));
938
- const total = Math.max(0, Math.floor(totalLines));
939
- if (total <= view) return null;
940
- const maxScroll = total - view;
941
- const thumbSize = Math.max(1, Math.round(view / total * view));
942
- const travel = Math.max(0, view - thumbSize);
943
- const s = Math.min(Math.max(0, Math.floor(scrollTop)), maxScroll);
944
- const thumbStart = maxScroll === 0 ? 0 : Math.round(s / maxScroll * travel);
945
- const glyphs = [];
946
- for (let i = 0; i < view; i++) {
947
- glyphs.push(i >= thumbStart && i < thumbStart + thumbSize ? BAR_THUMB : BAR_TRACK);
948
- }
949
- return glyphs;
950
- }
951
984
  function padEndVisible(s, w) {
952
985
  const t = String(s ?? "");
953
986
  if (t.length >= w) return t.slice(0, w);
@@ -956,12 +989,7 @@ function padEndVisible(s, w) {
956
989
  var SCROLL_KEYS = [
957
990
  { key: "upArrow", caption: "scroll", action: "scrollUp", order: 0 },
958
991
  { key: "downArrow", caption: "scroll", action: "scrollDown", order: 0 },
959
- { key: "upArrow", meta: true, caption: "page", action: "pageUp", order: 0 },
960
- { key: "downArrow", meta: true, caption: "page", action: "pageDown", order: 0 },
961
- { key: "upArrow", ctrl: true, caption: "page", action: "pageUp", order: 0 },
962
- { key: "downArrow", ctrl: true, caption: "page", action: "pageDown", order: 0 },
963
- { key: "pageUp", caption: "page", action: "pageUp", order: 0 },
964
- { key: "pageDown", caption: "page", action: "pageDown", order: 0 }
992
+ ...PAGE_SCROLL_KEY_BINDINGS
965
993
  ];
966
994
  function ScrollableText({
967
995
  ctx,
@@ -1194,6 +1222,8 @@ if (typeof window === "undefined") {
1194
1222
  });
1195
1223
  }
1196
1224
  export {
1225
+ BAR_THUMB,
1226
+ BAR_TRACK,
1197
1227
  Box5 as Box,
1198
1228
  Divider,
1199
1229
  FooterPresets,
@@ -1203,6 +1233,7 @@ export {
1203
1233
  ListItem,
1204
1234
  MultiColumnListComponent,
1205
1235
  MultiColumnListWithPreviewComponent,
1236
+ PAGE_SCROLL_KEY_BINDINGS,
1206
1237
  React2 as React,
1207
1238
  ScreenBody,
1208
1239
  ScreenContainer,