@termuijs/widgets 0.1.1 → 0.1.3

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/index.cjs CHANGED
@@ -20,18 +20,21 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ BarChart: () => BarChart,
23
24
  Box: () => Box,
24
25
  Gauge: () => Gauge,
25
26
  List: () => List,
26
27
  LogView: () => LogView,
27
28
  ProgressBar: () => ProgressBar,
28
29
  SPINNER_FRAMES: () => SPINNER_FRAMES,
30
+ Scrollbar: () => Scrollbar,
29
31
  Sparkline: () => Sparkline,
30
32
  Spinner: () => Spinner,
31
33
  StatusIndicator: () => StatusIndicator,
32
34
  Table: () => Table,
33
35
  Text: () => Text,
34
36
  TextInput: () => TextInput,
37
+ VirtualList: () => VirtualList,
35
38
  Widget: () => Widget
36
39
  });
37
40
  module.exports = __toCommonJS(index_exports);
@@ -286,29 +289,63 @@ var Text = class extends Widget {
286
289
  _content;
287
290
  _wrap;
288
291
  _align;
292
+ _scrollY;
293
+ _scrollX;
289
294
  constructor(content, style = {}, props = {}) {
290
295
  super(style);
291
296
  this._content = content;
292
297
  this._wrap = props.wrap ?? true;
293
298
  this._align = props.align ?? "left";
299
+ this._scrollY = props.scrollY ?? 0;
300
+ this._scrollX = props.scrollX ?? 0;
294
301
  }
295
302
  /** Update the text content */
296
303
  setContent(content) {
297
304
  this._content = content;
305
+ this.markDirty();
298
306
  }
299
307
  /** Get current text content */
300
308
  getContent() {
301
309
  return this._content;
302
310
  }
311
+ /** Set vertical scroll offset (lines to skip). */
312
+ setScrollY(offset) {
313
+ this._scrollY = Math.max(0, offset);
314
+ this.markDirty();
315
+ }
316
+ /** Set horizontal scroll offset (columns to skip). */
317
+ setScrollX(offset) {
318
+ this._scrollX = Math.max(0, offset);
319
+ this.markDirty();
320
+ }
321
+ /** Get the total number of lines after wrapping. */
322
+ getLineCount() {
323
+ const contentRect = this._getContentRect();
324
+ const text = this._wrap ? (0, import_core3.wordWrap)(this._content, contentRect.width) : this._content;
325
+ return text.split("\n").length;
326
+ }
303
327
  _renderSelf(screen) {
304
328
  const contentRect = this._getContentRect();
305
329
  const { x, y, width, height } = contentRect;
306
330
  if (width <= 0 || height <= 0) return;
307
331
  const attrs = (0, import_core3.styleToCellAttrs)(this._style);
308
332
  let text = this._wrap ? (0, import_core3.wordWrap)(this._content, width) : this._content;
309
- const lines = text.split("\n");
310
- for (let i = 0; i < Math.min(lines.length, height); i++) {
311
- let line = lines[i];
333
+ const allLines = text.split("\n");
334
+ const startLine = Math.min(this._scrollY, allLines.length);
335
+ const visibleLines = allLines.slice(startLine, startLine + height);
336
+ for (let i = 0; i < Math.min(visibleLines.length, height); i++) {
337
+ let line = visibleLines[i];
338
+ if (line === void 0) continue;
339
+ if (this._scrollX > 0) {
340
+ let skipped = 0;
341
+ let charIndex = 0;
342
+ for (const ch of line) {
343
+ if (skipped >= this._scrollX) break;
344
+ skipped++;
345
+ charIndex += ch.length;
346
+ }
347
+ line = line.slice(charIndex);
348
+ }
312
349
  const lineWidth = (0, import_core3.stringWidth)(line);
313
350
  let offsetX = 0;
314
351
  if (this._align === "center") {
@@ -343,12 +380,14 @@ var LogView = class extends Widget {
343
380
  if (this._autoScroll) {
344
381
  this._scrollToBottom();
345
382
  }
383
+ this.markDirty();
346
384
  }
347
385
  appendLine(line) {
348
386
  this._lines.push(line);
349
387
  if (this._autoScroll) {
350
388
  this._scrollToBottom();
351
389
  }
390
+ this.markDirty();
352
391
  }
353
392
  scrollUp(n = 1) {
354
393
  this._scrollOffset = Math.max(0, this._scrollOffset - n);
@@ -410,6 +449,7 @@ var List = class extends Widget {
410
449
  this._items = items;
411
450
  this._selectedIndex = Math.min(this._selectedIndex, items.length - 1);
412
451
  this._clampScroll();
452
+ this.markDirty();
413
453
  }
414
454
  /** Move selection up */
415
455
  selectPrev() {
@@ -418,6 +458,7 @@ var List = class extends Widget {
418
458
  if (next >= 0) {
419
459
  this._selectedIndex = next;
420
460
  this._clampScroll();
461
+ this.markDirty();
421
462
  }
422
463
  }
423
464
  /** Move selection down */
@@ -427,6 +468,7 @@ var List = class extends Widget {
427
468
  if (next < this._items.length) {
428
469
  this._selectedIndex = next;
429
470
  this._clampScroll();
471
+ this.markDirty();
430
472
  }
431
473
  }
432
474
  /** Confirm the current selection */
@@ -475,7 +517,10 @@ var List = class extends Widget {
475
517
  _clampScroll() {
476
518
  const rect = this._getContentRect();
477
519
  const visibleHeight = rect.height;
478
- if (visibleHeight <= 0) return;
520
+ if (visibleHeight <= 0) {
521
+ this._scrollOffset = 0;
522
+ return;
523
+ }
479
524
  if (this._selectedIndex < this._scrollOffset) {
480
525
  this._scrollOffset = this._selectedIndex;
481
526
  }
@@ -590,8 +635,173 @@ var TextInput = class extends Widget {
590
635
  }
591
636
  };
592
637
 
593
- // src/data/Table.ts
638
+ // src/input/VirtualList.ts
594
639
  var import_core7 = require("@termuijs/core");
640
+ var VirtualList = class extends Widget {
641
+ _totalItems;
642
+ _itemHeight;
643
+ _renderItem;
644
+ _onSelect;
645
+ _selectedIndex = 0;
646
+ _scrollOffset = 0;
647
+ _overscan;
648
+ _showScrollbar;
649
+ constructor(options) {
650
+ super({ border: "single", ...options.style });
651
+ this._totalItems = options.totalItems;
652
+ this._itemHeight = options.itemHeight ?? 1;
653
+ this._renderItem = options.renderItem;
654
+ this._onSelect = options.onSelect;
655
+ this._overscan = options.overscan ?? 2;
656
+ this._showScrollbar = options.showScrollbar ?? true;
657
+ this.focusable = true;
658
+ }
659
+ // ── Getters ──
660
+ get totalItems() {
661
+ return this._totalItems;
662
+ }
663
+ get selectedIndex() {
664
+ return this._selectedIndex;
665
+ }
666
+ get scrollOffset() {
667
+ return this._scrollOffset;
668
+ }
669
+ // ── Public API ──
670
+ /** Update the total item count (e.g., after data refresh) */
671
+ setTotalItems(count) {
672
+ this._totalItems = count;
673
+ this._selectedIndex = Math.min(this._selectedIndex, Math.max(0, count - 1));
674
+ this._clampScroll();
675
+ this.markDirty();
676
+ }
677
+ /** Update the render function (e.g., when data changes) */
678
+ setRenderItem(fn) {
679
+ this._renderItem = fn;
680
+ this.markDirty();
681
+ }
682
+ /** Move selection up by one */
683
+ selectPrev() {
684
+ if (this._selectedIndex > 0) {
685
+ this._selectedIndex--;
686
+ this._clampScroll();
687
+ this.markDirty();
688
+ }
689
+ }
690
+ /** Move selection down by one */
691
+ selectNext() {
692
+ if (this._selectedIndex < this._totalItems - 1) {
693
+ this._selectedIndex++;
694
+ this._clampScroll();
695
+ this.markDirty();
696
+ }
697
+ }
698
+ /** Jump to the first item */
699
+ selectFirst() {
700
+ this._selectedIndex = 0;
701
+ this._clampScroll();
702
+ this.markDirty();
703
+ }
704
+ /** Jump to the last item */
705
+ selectLast() {
706
+ this._selectedIndex = Math.max(0, this._totalItems - 1);
707
+ this._clampScroll();
708
+ this.markDirty();
709
+ }
710
+ /** Page up — move by viewport height */
711
+ pageUp() {
712
+ const rect = this._getContentRect();
713
+ const pageSize = Math.floor(rect.height / this._itemHeight);
714
+ this._selectedIndex = Math.max(0, this._selectedIndex - pageSize);
715
+ this._clampScroll();
716
+ this.markDirty();
717
+ }
718
+ /** Page down — move by viewport height */
719
+ pageDown() {
720
+ const rect = this._getContentRect();
721
+ const pageSize = Math.floor(rect.height / this._itemHeight);
722
+ this._selectedIndex = Math.min(this._totalItems - 1, this._selectedIndex + pageSize);
723
+ this._clampScroll();
724
+ this.markDirty();
725
+ }
726
+ /** Scroll to a specific index */
727
+ scrollTo(index) {
728
+ this._selectedIndex = Math.max(0, Math.min(index, this._totalItems - 1));
729
+ this._clampScroll();
730
+ this.markDirty();
731
+ }
732
+ /** Confirm the current selection */
733
+ confirm() {
734
+ if (this._totalItems > 0) {
735
+ this._onSelect?.(this._selectedIndex);
736
+ }
737
+ }
738
+ // ── Rendering ──
739
+ _renderSelf(screen) {
740
+ const rect = this._getContentRect();
741
+ const { x, y, width, height } = rect;
742
+ if (width <= 0 || height <= 0 || this._totalItems === 0) return;
743
+ const attrs = (0, import_core7.styleToCellAttrs)(this._style);
744
+ const visibleItemCount = Math.floor(height / this._itemHeight);
745
+ const startIdx = Math.max(0, this._scrollOffset - this._overscan);
746
+ const endIdx = Math.min(this._totalItems, this._scrollOffset + visibleItemCount + this._overscan);
747
+ const contentWidth = this._showScrollbar && this._totalItems > visibleItemCount ? width - 1 : width;
748
+ for (let idx = startIdx; idx < endIdx; idx++) {
749
+ const rowY = y + (idx - this._scrollOffset) * this._itemHeight;
750
+ if (rowY < y || rowY >= y + height) continue;
751
+ const isSelected = idx === this._selectedIndex;
752
+ let content;
753
+ try {
754
+ content = this._renderItem(idx);
755
+ } catch {
756
+ content = `[Error: item ${idx}]`;
757
+ }
758
+ const prefix = isSelected ? "\u25B8 " : " ";
759
+ let line = prefix + content;
760
+ line = (0, import_core7.truncate)(line, contentWidth);
761
+ const cellStyle = {
762
+ ...attrs,
763
+ bold: isSelected,
764
+ inverse: isSelected && this.isFocused
765
+ };
766
+ screen.writeString(x, rowY, line, cellStyle);
767
+ if (isSelected && this.isFocused) {
768
+ const remaining = contentWidth - (0, import_core7.stringWidth)(line);
769
+ for (let c = 0; c < remaining; c++) {
770
+ screen.setCell(x + (0, import_core7.stringWidth)(line) + c, rowY, { char: " ", ...cellStyle });
771
+ }
772
+ }
773
+ }
774
+ if (this._showScrollbar && this._totalItems > visibleItemCount) {
775
+ const scrollbarX = x + width - 1;
776
+ const totalPages = this._totalItems - visibleItemCount;
777
+ const scrollRatio = totalPages > 0 ? this._scrollOffset / totalPages : 0;
778
+ const thumbPos = Math.floor(scrollRatio * (height - 1));
779
+ for (let r = 0; r < height; r++) {
780
+ const scrollChar = r === thumbPos ? "\u2588" : "\u2591";
781
+ screen.setCell(scrollbarX, y + r, { char: scrollChar, ...attrs, dim: r !== thumbPos });
782
+ }
783
+ }
784
+ }
785
+ // ── Internal ──
786
+ _clampScroll() {
787
+ const rect = this._getContentRect();
788
+ const visibleHeight = Math.floor(rect.height / this._itemHeight);
789
+ if (visibleHeight <= 0) {
790
+ this._scrollOffset = 0;
791
+ return;
792
+ }
793
+ if (this._selectedIndex < this._scrollOffset) {
794
+ this._scrollOffset = this._selectedIndex;
795
+ }
796
+ if (this._selectedIndex >= this._scrollOffset + visibleHeight) {
797
+ this._scrollOffset = this._selectedIndex - visibleHeight + 1;
798
+ }
799
+ this._scrollOffset = Math.max(0, Math.min(this._scrollOffset, this._totalItems - visibleHeight));
800
+ }
801
+ };
802
+
803
+ // src/data/Table.ts
804
+ var import_core8 = require("@termuijs/core");
595
805
  var Table = class extends Widget {
596
806
  _columns;
597
807
  _rows;
@@ -612,13 +822,14 @@ var Table = class extends Widget {
612
822
  }
613
823
  setRows(rows) {
614
824
  this._rows = rows;
825
+ this.markDirty();
615
826
  }
616
827
  _renderSelf(screen) {
617
828
  const rect = this._getContentRect();
618
829
  const { x, y, width, height } = rect;
619
830
  if (width <= 0 || height <= 0) return;
620
- const attrs = (0, import_core7.styleToCellAttrs)(this._style);
621
- const sepWidth = (0, import_core7.stringWidth)(this._separator);
831
+ const attrs = (0, import_core8.styleToCellAttrs)(this._style);
832
+ const sepWidth = (0, import_core8.stringWidth)(this._separator);
622
833
  const colWidths = this._computeColumnWidths(
623
834
  width - (this._columns.length - 1) * sepWidth
624
835
  );
@@ -685,8 +896,8 @@ var Table = class extends Widget {
685
896
  return this._columns.map((c) => c.width ?? flexWidth);
686
897
  }
687
898
  _alignText(text, width, align) {
688
- const truncated = (0, import_core7.truncate)(text, width);
689
- const textWidth = (0, import_core7.stringWidth)(truncated);
899
+ const truncated = (0, import_core8.truncate)(text, width);
900
+ const textWidth = (0, import_core8.stringWidth)(truncated);
690
901
  const pad = Math.max(0, width - textWidth);
691
902
  switch (align) {
692
903
  case "right":
@@ -704,7 +915,7 @@ var Table = class extends Widget {
704
915
  };
705
916
 
706
917
  // src/data/Gauge.ts
707
- var import_core8 = require("@termuijs/core");
918
+ var import_core9 = require("@termuijs/core");
708
919
  var Gauge = class extends Widget {
709
920
  _label;
710
921
  _value = 0;
@@ -718,22 +929,24 @@ var Gauge = class extends Widget {
718
929
  }
719
930
  setValue(value) {
720
931
  this._value = Math.max(0, Math.min(1, value));
932
+ this.markDirty();
721
933
  }
722
934
  getValue() {
723
935
  return this._value;
724
936
  }
725
937
  setLabel(label) {
726
938
  this._label = label;
939
+ this.markDirty();
727
940
  }
728
941
  _renderSelf(screen) {
729
942
  const rect = this._getContentRect();
730
943
  const { x, y, width, height } = rect;
731
944
  if (width <= 0 || height <= 0) return;
732
- const attrs = (0, import_core8.styleToCellAttrs)(this._style);
945
+ const attrs = (0, import_core9.styleToCellAttrs)(this._style);
733
946
  const labelStr = this._label + " ";
734
947
  const percentStr = this._showLabel ? ` ${Math.round(this._value * 100)}%` : "";
735
- const labelWidth = (0, import_core8.stringWidth)(labelStr);
736
- const percentWidth = (0, import_core8.stringWidth)(percentStr);
948
+ const labelWidth = (0, import_core9.stringWidth)(labelStr);
949
+ const percentWidth = (0, import_core9.stringWidth)(percentStr);
737
950
  const barWidth = Math.max(0, width - labelWidth - percentWidth);
738
951
  screen.writeString(x, y, labelStr, { ...attrs, bold: true });
739
952
  const filled = Math.round(barWidth * this._value);
@@ -755,7 +968,7 @@ var Gauge = class extends Widget {
755
968
  };
756
969
 
757
970
  // src/data/Sparkline.ts
758
- var import_core9 = require("@termuijs/core");
971
+ var import_core10 = require("@termuijs/core");
759
972
  var SPARK_CHARS = ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
760
973
  var Sparkline = class extends Widget {
761
974
  _label;
@@ -770,15 +983,17 @@ var Sparkline = class extends Widget {
770
983
  }
771
984
  setData(data) {
772
985
  this._data = data;
986
+ this.markDirty();
773
987
  }
774
988
  pushValue(value) {
775
989
  this._data.push(value);
990
+ this.markDirty();
776
991
  }
777
992
  _renderSelf(screen) {
778
993
  const rect = this._getContentRect();
779
994
  const { x, y, width, height } = rect;
780
995
  if (width <= 0 || height <= 0) return;
781
- const attrs = (0, import_core9.styleToCellAttrs)(this._style);
996
+ const attrs = (0, import_core10.styleToCellAttrs)(this._style);
782
997
  const labelStr = this._label + " ";
783
998
  const labelWidth = labelStr.length;
784
999
  screen.writeString(x, y, labelStr, { ...attrs, bold: true });
@@ -807,7 +1022,7 @@ var Sparkline = class extends Widget {
807
1022
  };
808
1023
 
809
1024
  // src/data/StatusIndicator.ts
810
- var import_core10 = require("@termuijs/core");
1025
+ var import_core11 = require("@termuijs/core");
811
1026
  var StatusIndicator = class extends Widget {
812
1027
  _label;
813
1028
  _isUp;
@@ -833,7 +1048,7 @@ var StatusIndicator = class extends Widget {
833
1048
  const rect = this._getContentRect();
834
1049
  const { x, y, width, height } = rect;
835
1050
  if (width <= 0 || height <= 0) return;
836
- const attrs = (0, import_core10.styleToCellAttrs)(this._style);
1051
+ const attrs = (0, import_core11.styleToCellAttrs)(this._style);
837
1052
  const dot = this._isUp ? "\u25CF" : "\u25CB";
838
1053
  const statusText = this._isUp ? "Online" : "Offline";
839
1054
  const color = this._isUp ? this._upColor : this._downColor;
@@ -845,8 +1060,196 @@ var StatusIndicator = class extends Widget {
845
1060
  }
846
1061
  };
847
1062
 
1063
+ // src/data/BarChart.ts
1064
+ var import_core12 = require("@termuijs/core");
1065
+ var BarChart = class extends Widget {
1066
+ _data = [];
1067
+ _direction;
1068
+ _barWidth;
1069
+ _barGap;
1070
+ _groupGap;
1071
+ _max;
1072
+ _barColor;
1073
+ _valueColor;
1074
+ _labelColor;
1075
+ constructor(data, style = {}, opts = {}) {
1076
+ super(style);
1077
+ this._data = data;
1078
+ this._direction = opts.direction ?? "vertical";
1079
+ this._barWidth = opts.barWidth ?? 1;
1080
+ this._barGap = opts.barGap ?? 1;
1081
+ this._groupGap = opts.groupGap ?? 2;
1082
+ this._max = opts.max;
1083
+ this._barColor = opts.barColor ?? { type: "named", name: "cyan" };
1084
+ this._valueColor = opts.valueColor ?? { type: "named", name: "white" };
1085
+ this._labelColor = opts.labelColor ?? { type: "named", name: "brightBlack" };
1086
+ }
1087
+ setData(data) {
1088
+ this._data = data;
1089
+ this.markDirty();
1090
+ }
1091
+ setMax(max) {
1092
+ this._max = max;
1093
+ this.markDirty();
1094
+ }
1095
+ _renderSelf(screen) {
1096
+ const rect = this._getContentRect();
1097
+ const { x, y, width, height } = rect;
1098
+ if (width <= 0 || height <= 0 || this._data.length === 0) return;
1099
+ const maxVal = this._computeMax();
1100
+ if (maxVal === 0) return;
1101
+ if (this._direction === "vertical") {
1102
+ this._renderVertical(screen, x, y, width, height, maxVal);
1103
+ } else {
1104
+ this._renderHorizontal(screen, x, y, width, height, maxVal);
1105
+ }
1106
+ }
1107
+ _computeMax() {
1108
+ if (this._max !== void 0) return this._max;
1109
+ let max = 0;
1110
+ for (const group of this._data) {
1111
+ for (const bar of group.bars) {
1112
+ if (bar.value > max) max = bar.value;
1113
+ }
1114
+ }
1115
+ return max;
1116
+ }
1117
+ // ── Vertical Rendering ───────────────────────────
1118
+ _renderVertical(screen, ox, oy, width, height, maxVal) {
1119
+ const valueRows = 1;
1120
+ const hasLabels = this._data.some(
1121
+ (g) => g.bars.some((b) => b.label !== void 0)
1122
+ );
1123
+ const labelRows = hasLabels ? 1 : 0;
1124
+ const hasGroupLabels = this._data.some((g) => g.label !== void 0);
1125
+ const groupLabelRows = hasGroupLabels ? 1 : 0;
1126
+ const reservedRows = valueRows + labelRows + groupLabelRows;
1127
+ if (height <= reservedRows) return;
1128
+ const barAreaHeight = height - reservedRows;
1129
+ let cx = ox;
1130
+ for (let gi = 0; gi < this._data.length; gi++) {
1131
+ const group = this._data[gi];
1132
+ if (!group) continue;
1133
+ const groupStartX = cx;
1134
+ for (let bi = 0; bi < group.bars.length; bi++) {
1135
+ const bar = group.bars[bi];
1136
+ if (!bar) continue;
1137
+ if (cx + this._barWidth > ox + width) break;
1138
+ const color = bar.color ?? this._barColor;
1139
+ const scaledHeight = bar.value / maxVal * (barAreaHeight * 8);
1140
+ let remaining = Math.round(scaledHeight);
1141
+ for (let row = barAreaHeight - 1; row >= 0; row--) {
1142
+ if (remaining <= 0) break;
1143
+ const level = Math.min(remaining, 8);
1144
+ const symbol = import_core12.VERTICAL_BAR_SYMBOLS[level] ?? " ";
1145
+ const cellY = oy + row;
1146
+ for (let col = 0; col < this._barWidth; col++) {
1147
+ const cellX = cx + col;
1148
+ if (cellX < ox + width) {
1149
+ screen.setCell(cellX, cellY, { char: symbol, fg: color });
1150
+ }
1151
+ }
1152
+ remaining -= 8;
1153
+ }
1154
+ const valStr = Math.round(bar.value).toString();
1155
+ const valX = cx + Math.floor((this._barWidth - (0, import_core12.stringWidth)(valStr)) / 2);
1156
+ screen.writeString(
1157
+ Math.max(cx, valX),
1158
+ oy + barAreaHeight,
1159
+ valStr.slice(0, this._barWidth),
1160
+ { fg: this._valueColor }
1161
+ );
1162
+ if (hasLabels && bar.label) {
1163
+ const label = bar.label.slice(0, this._barWidth);
1164
+ const labelX = cx + Math.floor((this._barWidth - (0, import_core12.stringWidth)(label)) / 2);
1165
+ screen.writeString(
1166
+ Math.max(cx, labelX),
1167
+ oy + barAreaHeight + valueRows,
1168
+ label,
1169
+ { fg: this._labelColor }
1170
+ );
1171
+ }
1172
+ cx += this._barWidth;
1173
+ if (bi < group.bars.length - 1) cx += this._barGap;
1174
+ }
1175
+ if (hasGroupLabels && group.label) {
1176
+ const groupWidth = cx - groupStartX;
1177
+ const label = group.label.slice(0, groupWidth);
1178
+ const labelX = groupStartX + Math.floor((groupWidth - (0, import_core12.stringWidth)(label)) / 2);
1179
+ screen.writeString(
1180
+ Math.max(groupStartX, labelX),
1181
+ oy + height - 1,
1182
+ label,
1183
+ { fg: this._labelColor }
1184
+ );
1185
+ }
1186
+ if (gi < this._data.length - 1) cx += this._groupGap;
1187
+ }
1188
+ }
1189
+ // ── Horizontal Rendering ─────────────────────────
1190
+ _renderHorizontal(screen, ox, oy, width, height, maxVal) {
1191
+ let maxLabelWidth = 0;
1192
+ let maxValueWidth = 0;
1193
+ for (const group of this._data) {
1194
+ for (const bar of group.bars) {
1195
+ if (bar.label) {
1196
+ const w = (0, import_core12.stringWidth)(bar.label);
1197
+ if (w > maxLabelWidth) maxLabelWidth = w;
1198
+ }
1199
+ const vw = Math.round(bar.value).toString().length;
1200
+ if (vw > maxValueWidth) maxValueWidth = vw;
1201
+ }
1202
+ }
1203
+ const labelColWidth = maxLabelWidth > 0 ? maxLabelWidth + 1 : 0;
1204
+ const valueColWidth = maxValueWidth > 0 ? maxValueWidth + 1 : 0;
1205
+ const barAreaWidth = width - labelColWidth - valueColWidth;
1206
+ if (barAreaWidth <= 0) return;
1207
+ let cy = oy;
1208
+ for (let gi = 0; gi < this._data.length; gi++) {
1209
+ const group = this._data[gi];
1210
+ if (!group) continue;
1211
+ for (let bi = 0; bi < group.bars.length; bi++) {
1212
+ const bar = group.bars[bi];
1213
+ if (!bar) continue;
1214
+ for (let row = 0; row < this._barWidth; row++) {
1215
+ const cellY = cy + row;
1216
+ if (cellY >= oy + height) break;
1217
+ if (row === 0 && bar.label) {
1218
+ const label = bar.label.slice(0, maxLabelWidth);
1219
+ const padded = label.padStart(maxLabelWidth);
1220
+ screen.writeString(ox, cellY, padded, { fg: this._labelColor });
1221
+ }
1222
+ const color = bar.color ?? this._barColor;
1223
+ const scaledWidth = bar.value / maxVal * (barAreaWidth * 8);
1224
+ let remaining = Math.round(scaledWidth);
1225
+ const barStartX = ox + labelColWidth;
1226
+ for (let col = 0; col < barAreaWidth; col++) {
1227
+ if (remaining <= 0) break;
1228
+ const level = Math.min(remaining, 8);
1229
+ const symbol = import_core12.HORIZONTAL_BAR_SYMBOLS[level] ?? " ";
1230
+ screen.setCell(barStartX + col, cellY, { char: symbol, fg: color });
1231
+ remaining -= 8;
1232
+ }
1233
+ if (row === 0) {
1234
+ const valStr = Math.round(bar.value).toString();
1235
+ screen.writeString(
1236
+ ox + labelColWidth + barAreaWidth,
1237
+ cellY,
1238
+ ` ${valStr}`,
1239
+ { fg: this._valueColor }
1240
+ );
1241
+ }
1242
+ }
1243
+ cy += this._barWidth;
1244
+ if (bi < group.bars.length - 1) cy += this._barGap;
1245
+ }
1246
+ if (gi < this._data.length - 1) cy += this._groupGap;
1247
+ }
1248
+ }
1249
+ };
1250
+
848
1251
  // src/feedback/ProgressBar.ts
849
- var import_core11 = require("@termuijs/core");
1252
+ var import_core13 = require("@termuijs/core");
850
1253
  var ProgressBar = class extends Widget {
851
1254
  _value;
852
1255
  _fillChar;
@@ -868,6 +1271,7 @@ var ProgressBar = class extends Widget {
868
1271
  /** Set progress value (0–1) */
869
1272
  setValue(value) {
870
1273
  this._value = Math.max(0, Math.min(1, value));
1274
+ this.markDirty();
871
1275
  }
872
1276
  get value() {
873
1277
  return this._value;
@@ -876,7 +1280,7 @@ var ProgressBar = class extends Widget {
876
1280
  const rect = this._getContentRect();
877
1281
  const { x, y, width } = rect;
878
1282
  if (width <= 0) return;
879
- const attrs = (0, import_core11.styleToCellAttrs)(this._style);
1283
+ const attrs = (0, import_core13.styleToCellAttrs)(this._style);
880
1284
  let label = "";
881
1285
  if (this._showLabel) {
882
1286
  if (this._labelFormat === "percent") {
@@ -901,7 +1305,7 @@ var ProgressBar = class extends Widget {
901
1305
  };
902
1306
 
903
1307
  // src/feedback/Spinner.ts
904
- var import_core12 = require("@termuijs/core");
1308
+ var import_core14 = require("@termuijs/core");
905
1309
  var SPINNER_FRAMES = {
906
1310
  dots: {
907
1311
  frames: ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"],
@@ -971,7 +1375,7 @@ var Spinner = class extends Widget {
971
1375
  const rect = this._getContentRect();
972
1376
  const { x, y, width } = rect;
973
1377
  if (width <= 0) return;
974
- const attrs = (0, import_core12.styleToCellAttrs)(this._style);
1378
+ const attrs = (0, import_core14.styleToCellAttrs)(this._style);
975
1379
  const frame = this._frames[this._frameIndex];
976
1380
  screen.writeString(x, y, frame, { ...attrs, fg: this._color });
977
1381
  if (this._label) {
@@ -979,20 +1383,106 @@ var Spinner = class extends Widget {
979
1383
  }
980
1384
  }
981
1385
  };
1386
+
1387
+ // src/feedback/Scrollbar.ts
1388
+ var import_core15 = require("@termuijs/core");
1389
+ var Scrollbar = class extends Widget {
1390
+ _contentLength;
1391
+ _viewportLength;
1392
+ _position;
1393
+ _orientation;
1394
+ _thumbColor;
1395
+ _trackColor;
1396
+ _showArrows;
1397
+ constructor(style = {}, opts) {
1398
+ super(style);
1399
+ this._contentLength = opts.contentLength;
1400
+ this._viewportLength = opts.viewportLength;
1401
+ this._position = opts.position ?? 0;
1402
+ this._orientation = opts.orientation ?? "verticalRight";
1403
+ this._thumbColor = opts.thumbColor ?? { type: "named", name: "white" };
1404
+ this._trackColor = opts.trackColor ?? { type: "named", name: "brightBlack" };
1405
+ this._showArrows = opts.showArrows ?? true;
1406
+ }
1407
+ setPosition(position) {
1408
+ this._position = position;
1409
+ this.markDirty();
1410
+ }
1411
+ setContentLength(length) {
1412
+ this._contentLength = length;
1413
+ this.markDirty();
1414
+ }
1415
+ setViewportLength(length) {
1416
+ this._viewportLength = length;
1417
+ this.markDirty();
1418
+ }
1419
+ _renderSelf(screen) {
1420
+ const rect = this._getContentRect();
1421
+ const { x, y, width, height } = rect;
1422
+ if (width <= 0 || height <= 0 || this._contentLength <= 0) return;
1423
+ if (this._contentLength <= this._viewportLength) return;
1424
+ const vertical = this._orientation === "verticalRight" || this._orientation === "verticalLeft";
1425
+ const symbols = vertical ? import_core15.ScrollbarSets.VERTICAL : import_core15.ScrollbarSets.HORIZONTAL;
1426
+ const trackX = this._orientation === "verticalLeft" ? x : this._orientation === "verticalRight" ? x + width - 1 : x;
1427
+ const trackY = this._orientation === "horizontalTop" ? y : this._orientation === "horizontalBottom" ? y + height - 1 : y;
1428
+ const totalLength = vertical ? height : width;
1429
+ if (totalLength <= 0) return;
1430
+ let trackStart = 0;
1431
+ let trackLength = totalLength;
1432
+ if (this._showArrows && totalLength > 2) {
1433
+ const beginX = vertical ? trackX : x;
1434
+ const beginY = vertical ? y : trackY;
1435
+ screen.setCell(beginX, beginY, {
1436
+ char: symbols.begin,
1437
+ fg: this._trackColor
1438
+ });
1439
+ const endX = vertical ? trackX : x + totalLength - 1;
1440
+ const endY = vertical ? y + totalLength - 1 : trackY;
1441
+ screen.setCell(endX, endY, {
1442
+ char: symbols.end,
1443
+ fg: this._trackColor
1444
+ });
1445
+ trackStart = 1;
1446
+ trackLength -= 2;
1447
+ }
1448
+ if (trackLength <= 0) return;
1449
+ const thumbSize = Math.max(1, Math.floor(
1450
+ trackLength * this._viewportLength / this._contentLength
1451
+ ));
1452
+ const maxScroll = Math.max(1, this._contentLength - this._viewportLength);
1453
+ const thumbOffset = Math.min(
1454
+ trackLength - thumbSize,
1455
+ Math.floor(this._position * (trackLength - thumbSize) / maxScroll)
1456
+ );
1457
+ for (let i = 0; i < trackLength; i++) {
1458
+ const pos = trackStart + i;
1459
+ const cellX = vertical ? trackX : x + pos;
1460
+ const cellY = vertical ? y + pos : trackY;
1461
+ const isThumb = i >= thumbOffset && i < thumbOffset + thumbSize;
1462
+ screen.setCell(cellX, cellY, {
1463
+ char: isThumb ? symbols.thumb : symbols.track,
1464
+ fg: isThumb ? this._thumbColor : this._trackColor
1465
+ });
1466
+ }
1467
+ }
1468
+ };
982
1469
  // Annotate the CommonJS export names for ESM import in node:
983
1470
  0 && (module.exports = {
1471
+ BarChart,
984
1472
  Box,
985
1473
  Gauge,
986
1474
  List,
987
1475
  LogView,
988
1476
  ProgressBar,
989
1477
  SPINNER_FRAMES,
1478
+ Scrollbar,
990
1479
  Sparkline,
991
1480
  Spinner,
992
1481
  StatusIndicator,
993
1482
  Table,
994
1483
  Text,
995
1484
  TextInput,
1485
+ VirtualList,
996
1486
  Widget
997
1487
  });
998
1488
  //# sourceMappingURL=index.cjs.map