@termuijs/widgets 0.1.1 → 0.1.2

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.js CHANGED
@@ -257,29 +257,63 @@ var Text = class extends Widget {
257
257
  _content;
258
258
  _wrap;
259
259
  _align;
260
+ _scrollY;
261
+ _scrollX;
260
262
  constructor(content, style = {}, props = {}) {
261
263
  super(style);
262
264
  this._content = content;
263
265
  this._wrap = props.wrap ?? true;
264
266
  this._align = props.align ?? "left";
267
+ this._scrollY = props.scrollY ?? 0;
268
+ this._scrollX = props.scrollX ?? 0;
265
269
  }
266
270
  /** Update the text content */
267
271
  setContent(content) {
268
272
  this._content = content;
273
+ this.markDirty();
269
274
  }
270
275
  /** Get current text content */
271
276
  getContent() {
272
277
  return this._content;
273
278
  }
279
+ /** Set vertical scroll offset (lines to skip). */
280
+ setScrollY(offset) {
281
+ this._scrollY = Math.max(0, offset);
282
+ this.markDirty();
283
+ }
284
+ /** Set horizontal scroll offset (columns to skip). */
285
+ setScrollX(offset) {
286
+ this._scrollX = Math.max(0, offset);
287
+ this.markDirty();
288
+ }
289
+ /** Get the total number of lines after wrapping. */
290
+ getLineCount() {
291
+ const contentRect = this._getContentRect();
292
+ const text = this._wrap ? wordWrap(this._content, contentRect.width) : this._content;
293
+ return text.split("\n").length;
294
+ }
274
295
  _renderSelf(screen) {
275
296
  const contentRect = this._getContentRect();
276
297
  const { x, y, width, height } = contentRect;
277
298
  if (width <= 0 || height <= 0) return;
278
299
  const attrs = styleToCellAttrs3(this._style);
279
300
  let text = this._wrap ? wordWrap(this._content, width) : this._content;
280
- const lines = text.split("\n");
281
- for (let i = 0; i < Math.min(lines.length, height); i++) {
282
- let line = lines[i];
301
+ const allLines = text.split("\n");
302
+ const startLine = Math.min(this._scrollY, allLines.length);
303
+ const visibleLines = allLines.slice(startLine, startLine + height);
304
+ for (let i = 0; i < Math.min(visibleLines.length, height); i++) {
305
+ let line = visibleLines[i];
306
+ if (line === void 0) continue;
307
+ if (this._scrollX > 0) {
308
+ let skipped = 0;
309
+ let charIndex = 0;
310
+ for (const ch of line) {
311
+ if (skipped >= this._scrollX) break;
312
+ skipped++;
313
+ charIndex += ch.length;
314
+ }
315
+ line = line.slice(charIndex);
316
+ }
283
317
  const lineWidth = stringWidth(line);
284
318
  let offsetX = 0;
285
319
  if (this._align === "center") {
@@ -314,12 +348,14 @@ var LogView = class extends Widget {
314
348
  if (this._autoScroll) {
315
349
  this._scrollToBottom();
316
350
  }
351
+ this.markDirty();
317
352
  }
318
353
  appendLine(line) {
319
354
  this._lines.push(line);
320
355
  if (this._autoScroll) {
321
356
  this._scrollToBottom();
322
357
  }
358
+ this.markDirty();
323
359
  }
324
360
  scrollUp(n = 1) {
325
361
  this._scrollOffset = Math.max(0, this._scrollOffset - n);
@@ -381,6 +417,7 @@ var List = class extends Widget {
381
417
  this._items = items;
382
418
  this._selectedIndex = Math.min(this._selectedIndex, items.length - 1);
383
419
  this._clampScroll();
420
+ this.markDirty();
384
421
  }
385
422
  /** Move selection up */
386
423
  selectPrev() {
@@ -389,6 +426,7 @@ var List = class extends Widget {
389
426
  if (next >= 0) {
390
427
  this._selectedIndex = next;
391
428
  this._clampScroll();
429
+ this.markDirty();
392
430
  }
393
431
  }
394
432
  /** Move selection down */
@@ -398,6 +436,7 @@ var List = class extends Widget {
398
436
  if (next < this._items.length) {
399
437
  this._selectedIndex = next;
400
438
  this._clampScroll();
439
+ this.markDirty();
401
440
  }
402
441
  }
403
442
  /** Confirm the current selection */
@@ -446,7 +485,10 @@ var List = class extends Widget {
446
485
  _clampScroll() {
447
486
  const rect = this._getContentRect();
448
487
  const visibleHeight = rect.height;
449
- if (visibleHeight <= 0) return;
488
+ if (visibleHeight <= 0) {
489
+ this._scrollOffset = 0;
490
+ return;
491
+ }
450
492
  if (this._selectedIndex < this._scrollOffset) {
451
493
  this._scrollOffset = this._selectedIndex;
452
494
  }
@@ -561,8 +603,173 @@ var TextInput = class extends Widget {
561
603
  }
562
604
  };
563
605
 
606
+ // src/input/VirtualList.ts
607
+ import { styleToCellAttrs as styleToCellAttrs7, truncate as truncate4, stringWidth as stringWidth4 } from "@termuijs/core";
608
+ var VirtualList = class extends Widget {
609
+ _totalItems;
610
+ _itemHeight;
611
+ _renderItem;
612
+ _onSelect;
613
+ _selectedIndex = 0;
614
+ _scrollOffset = 0;
615
+ _overscan;
616
+ _showScrollbar;
617
+ constructor(options) {
618
+ super({ border: "single", ...options.style });
619
+ this._totalItems = options.totalItems;
620
+ this._itemHeight = options.itemHeight ?? 1;
621
+ this._renderItem = options.renderItem;
622
+ this._onSelect = options.onSelect;
623
+ this._overscan = options.overscan ?? 2;
624
+ this._showScrollbar = options.showScrollbar ?? true;
625
+ this.focusable = true;
626
+ }
627
+ // ── Getters ──
628
+ get totalItems() {
629
+ return this._totalItems;
630
+ }
631
+ get selectedIndex() {
632
+ return this._selectedIndex;
633
+ }
634
+ get scrollOffset() {
635
+ return this._scrollOffset;
636
+ }
637
+ // ── Public API ──
638
+ /** Update the total item count (e.g., after data refresh) */
639
+ setTotalItems(count) {
640
+ this._totalItems = count;
641
+ this._selectedIndex = Math.min(this._selectedIndex, Math.max(0, count - 1));
642
+ this._clampScroll();
643
+ this.markDirty();
644
+ }
645
+ /** Update the render function (e.g., when data changes) */
646
+ setRenderItem(fn) {
647
+ this._renderItem = fn;
648
+ this.markDirty();
649
+ }
650
+ /** Move selection up by one */
651
+ selectPrev() {
652
+ if (this._selectedIndex > 0) {
653
+ this._selectedIndex--;
654
+ this._clampScroll();
655
+ this.markDirty();
656
+ }
657
+ }
658
+ /** Move selection down by one */
659
+ selectNext() {
660
+ if (this._selectedIndex < this._totalItems - 1) {
661
+ this._selectedIndex++;
662
+ this._clampScroll();
663
+ this.markDirty();
664
+ }
665
+ }
666
+ /** Jump to the first item */
667
+ selectFirst() {
668
+ this._selectedIndex = 0;
669
+ this._clampScroll();
670
+ this.markDirty();
671
+ }
672
+ /** Jump to the last item */
673
+ selectLast() {
674
+ this._selectedIndex = Math.max(0, this._totalItems - 1);
675
+ this._clampScroll();
676
+ this.markDirty();
677
+ }
678
+ /** Page up — move by viewport height */
679
+ pageUp() {
680
+ const rect = this._getContentRect();
681
+ const pageSize = Math.floor(rect.height / this._itemHeight);
682
+ this._selectedIndex = Math.max(0, this._selectedIndex - pageSize);
683
+ this._clampScroll();
684
+ this.markDirty();
685
+ }
686
+ /** Page down — move by viewport height */
687
+ pageDown() {
688
+ const rect = this._getContentRect();
689
+ const pageSize = Math.floor(rect.height / this._itemHeight);
690
+ this._selectedIndex = Math.min(this._totalItems - 1, this._selectedIndex + pageSize);
691
+ this._clampScroll();
692
+ this.markDirty();
693
+ }
694
+ /** Scroll to a specific index */
695
+ scrollTo(index) {
696
+ this._selectedIndex = Math.max(0, Math.min(index, this._totalItems - 1));
697
+ this._clampScroll();
698
+ this.markDirty();
699
+ }
700
+ /** Confirm the current selection */
701
+ confirm() {
702
+ if (this._totalItems > 0) {
703
+ this._onSelect?.(this._selectedIndex);
704
+ }
705
+ }
706
+ // ── Rendering ──
707
+ _renderSelf(screen) {
708
+ const rect = this._getContentRect();
709
+ const { x, y, width, height } = rect;
710
+ if (width <= 0 || height <= 0 || this._totalItems === 0) return;
711
+ const attrs = styleToCellAttrs7(this._style);
712
+ const visibleItemCount = Math.floor(height / this._itemHeight);
713
+ const startIdx = Math.max(0, this._scrollOffset - this._overscan);
714
+ const endIdx = Math.min(this._totalItems, this._scrollOffset + visibleItemCount + this._overscan);
715
+ const contentWidth = this._showScrollbar && this._totalItems > visibleItemCount ? width - 1 : width;
716
+ for (let idx = startIdx; idx < endIdx; idx++) {
717
+ const rowY = y + (idx - this._scrollOffset) * this._itemHeight;
718
+ if (rowY < y || rowY >= y + height) continue;
719
+ const isSelected = idx === this._selectedIndex;
720
+ let content;
721
+ try {
722
+ content = this._renderItem(idx);
723
+ } catch {
724
+ content = `[Error: item ${idx}]`;
725
+ }
726
+ const prefix = isSelected ? "\u25B8 " : " ";
727
+ let line = prefix + content;
728
+ line = truncate4(line, contentWidth);
729
+ const cellStyle = {
730
+ ...attrs,
731
+ bold: isSelected,
732
+ inverse: isSelected && this.isFocused
733
+ };
734
+ screen.writeString(x, rowY, line, cellStyle);
735
+ if (isSelected && this.isFocused) {
736
+ const remaining = contentWidth - stringWidth4(line);
737
+ for (let c = 0; c < remaining; c++) {
738
+ screen.setCell(x + stringWidth4(line) + c, rowY, { char: " ", ...cellStyle });
739
+ }
740
+ }
741
+ }
742
+ if (this._showScrollbar && this._totalItems > visibleItemCount) {
743
+ const scrollbarX = x + width - 1;
744
+ const totalPages = this._totalItems - visibleItemCount;
745
+ const scrollRatio = totalPages > 0 ? this._scrollOffset / totalPages : 0;
746
+ const thumbPos = Math.floor(scrollRatio * (height - 1));
747
+ for (let r = 0; r < height; r++) {
748
+ const scrollChar = r === thumbPos ? "\u2588" : "\u2591";
749
+ screen.setCell(scrollbarX, y + r, { char: scrollChar, ...attrs, dim: r !== thumbPos });
750
+ }
751
+ }
752
+ }
753
+ // ── Internal ──
754
+ _clampScroll() {
755
+ const rect = this._getContentRect();
756
+ const visibleHeight = Math.floor(rect.height / this._itemHeight);
757
+ if (visibleHeight <= 0) {
758
+ this._scrollOffset = 0;
759
+ return;
760
+ }
761
+ if (this._selectedIndex < this._scrollOffset) {
762
+ this._scrollOffset = this._selectedIndex;
763
+ }
764
+ if (this._selectedIndex >= this._scrollOffset + visibleHeight) {
765
+ this._scrollOffset = this._selectedIndex - visibleHeight + 1;
766
+ }
767
+ this._scrollOffset = Math.max(0, Math.min(this._scrollOffset, this._totalItems - visibleHeight));
768
+ }
769
+ };
770
+
564
771
  // src/data/Table.ts
565
- import { styleToCellAttrs as styleToCellAttrs7, stringWidth as stringWidth4, truncate as truncate4 } from "@termuijs/core";
772
+ import { styleToCellAttrs as styleToCellAttrs8, stringWidth as stringWidth5, truncate as truncate5 } from "@termuijs/core";
566
773
  var Table = class extends Widget {
567
774
  _columns;
568
775
  _rows;
@@ -583,13 +790,14 @@ var Table = class extends Widget {
583
790
  }
584
791
  setRows(rows) {
585
792
  this._rows = rows;
793
+ this.markDirty();
586
794
  }
587
795
  _renderSelf(screen) {
588
796
  const rect = this._getContentRect();
589
797
  const { x, y, width, height } = rect;
590
798
  if (width <= 0 || height <= 0) return;
591
- const attrs = styleToCellAttrs7(this._style);
592
- const sepWidth = stringWidth4(this._separator);
799
+ const attrs = styleToCellAttrs8(this._style);
800
+ const sepWidth = stringWidth5(this._separator);
593
801
  const colWidths = this._computeColumnWidths(
594
802
  width - (this._columns.length - 1) * sepWidth
595
803
  );
@@ -656,8 +864,8 @@ var Table = class extends Widget {
656
864
  return this._columns.map((c) => c.width ?? flexWidth);
657
865
  }
658
866
  _alignText(text, width, align) {
659
- const truncated = truncate4(text, width);
660
- const textWidth = stringWidth4(truncated);
867
+ const truncated = truncate5(text, width);
868
+ const textWidth = stringWidth5(truncated);
661
869
  const pad = Math.max(0, width - textWidth);
662
870
  switch (align) {
663
871
  case "right":
@@ -675,7 +883,7 @@ var Table = class extends Widget {
675
883
  };
676
884
 
677
885
  // src/data/Gauge.ts
678
- import { styleToCellAttrs as styleToCellAttrs8, stringWidth as stringWidth5 } from "@termuijs/core";
886
+ import { styleToCellAttrs as styleToCellAttrs9, stringWidth as stringWidth6 } from "@termuijs/core";
679
887
  var Gauge = class extends Widget {
680
888
  _label;
681
889
  _value = 0;
@@ -689,22 +897,24 @@ var Gauge = class extends Widget {
689
897
  }
690
898
  setValue(value) {
691
899
  this._value = Math.max(0, Math.min(1, value));
900
+ this.markDirty();
692
901
  }
693
902
  getValue() {
694
903
  return this._value;
695
904
  }
696
905
  setLabel(label) {
697
906
  this._label = label;
907
+ this.markDirty();
698
908
  }
699
909
  _renderSelf(screen) {
700
910
  const rect = this._getContentRect();
701
911
  const { x, y, width, height } = rect;
702
912
  if (width <= 0 || height <= 0) return;
703
- const attrs = styleToCellAttrs8(this._style);
913
+ const attrs = styleToCellAttrs9(this._style);
704
914
  const labelStr = this._label + " ";
705
915
  const percentStr = this._showLabel ? ` ${Math.round(this._value * 100)}%` : "";
706
- const labelWidth = stringWidth5(labelStr);
707
- const percentWidth = stringWidth5(percentStr);
916
+ const labelWidth = stringWidth6(labelStr);
917
+ const percentWidth = stringWidth6(percentStr);
708
918
  const barWidth = Math.max(0, width - labelWidth - percentWidth);
709
919
  screen.writeString(x, y, labelStr, { ...attrs, bold: true });
710
920
  const filled = Math.round(barWidth * this._value);
@@ -726,7 +936,7 @@ var Gauge = class extends Widget {
726
936
  };
727
937
 
728
938
  // src/data/Sparkline.ts
729
- import { styleToCellAttrs as styleToCellAttrs9 } from "@termuijs/core";
939
+ import { styleToCellAttrs as styleToCellAttrs10 } from "@termuijs/core";
730
940
  var SPARK_CHARS = ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
731
941
  var Sparkline = class extends Widget {
732
942
  _label;
@@ -741,15 +951,17 @@ var Sparkline = class extends Widget {
741
951
  }
742
952
  setData(data) {
743
953
  this._data = data;
954
+ this.markDirty();
744
955
  }
745
956
  pushValue(value) {
746
957
  this._data.push(value);
958
+ this.markDirty();
747
959
  }
748
960
  _renderSelf(screen) {
749
961
  const rect = this._getContentRect();
750
962
  const { x, y, width, height } = rect;
751
963
  if (width <= 0 || height <= 0) return;
752
- const attrs = styleToCellAttrs9(this._style);
964
+ const attrs = styleToCellAttrs10(this._style);
753
965
  const labelStr = this._label + " ";
754
966
  const labelWidth = labelStr.length;
755
967
  screen.writeString(x, y, labelStr, { ...attrs, bold: true });
@@ -778,7 +990,7 @@ var Sparkline = class extends Widget {
778
990
  };
779
991
 
780
992
  // src/data/StatusIndicator.ts
781
- import { styleToCellAttrs as styleToCellAttrs10 } from "@termuijs/core";
993
+ import { styleToCellAttrs as styleToCellAttrs11 } from "@termuijs/core";
782
994
  var StatusIndicator = class extends Widget {
783
995
  _label;
784
996
  _isUp;
@@ -804,7 +1016,7 @@ var StatusIndicator = class extends Widget {
804
1016
  const rect = this._getContentRect();
805
1017
  const { x, y, width, height } = rect;
806
1018
  if (width <= 0 || height <= 0) return;
807
- const attrs = styleToCellAttrs10(this._style);
1019
+ const attrs = styleToCellAttrs11(this._style);
808
1020
  const dot = this._isUp ? "\u25CF" : "\u25CB";
809
1021
  const statusText = this._isUp ? "Online" : "Offline";
810
1022
  const color = this._isUp ? this._upColor : this._downColor;
@@ -816,8 +1028,200 @@ var StatusIndicator = class extends Widget {
816
1028
  }
817
1029
  };
818
1030
 
1031
+ // src/data/BarChart.ts
1032
+ import {
1033
+ stringWidth as stringWidth7,
1034
+ VERTICAL_BAR_SYMBOLS,
1035
+ HORIZONTAL_BAR_SYMBOLS
1036
+ } from "@termuijs/core";
1037
+ var BarChart = class extends Widget {
1038
+ _data = [];
1039
+ _direction;
1040
+ _barWidth;
1041
+ _barGap;
1042
+ _groupGap;
1043
+ _max;
1044
+ _barColor;
1045
+ _valueColor;
1046
+ _labelColor;
1047
+ constructor(data, style = {}, opts = {}) {
1048
+ super(style);
1049
+ this._data = data;
1050
+ this._direction = opts.direction ?? "vertical";
1051
+ this._barWidth = opts.barWidth ?? 1;
1052
+ this._barGap = opts.barGap ?? 1;
1053
+ this._groupGap = opts.groupGap ?? 2;
1054
+ this._max = opts.max;
1055
+ this._barColor = opts.barColor ?? { type: "named", name: "cyan" };
1056
+ this._valueColor = opts.valueColor ?? { type: "named", name: "white" };
1057
+ this._labelColor = opts.labelColor ?? { type: "named", name: "brightBlack" };
1058
+ }
1059
+ setData(data) {
1060
+ this._data = data;
1061
+ this.markDirty();
1062
+ }
1063
+ setMax(max) {
1064
+ this._max = max;
1065
+ this.markDirty();
1066
+ }
1067
+ _renderSelf(screen) {
1068
+ const rect = this._getContentRect();
1069
+ const { x, y, width, height } = rect;
1070
+ if (width <= 0 || height <= 0 || this._data.length === 0) return;
1071
+ const maxVal = this._computeMax();
1072
+ if (maxVal === 0) return;
1073
+ if (this._direction === "vertical") {
1074
+ this._renderVertical(screen, x, y, width, height, maxVal);
1075
+ } else {
1076
+ this._renderHorizontal(screen, x, y, width, height, maxVal);
1077
+ }
1078
+ }
1079
+ _computeMax() {
1080
+ if (this._max !== void 0) return this._max;
1081
+ let max = 0;
1082
+ for (const group of this._data) {
1083
+ for (const bar of group.bars) {
1084
+ if (bar.value > max) max = bar.value;
1085
+ }
1086
+ }
1087
+ return max;
1088
+ }
1089
+ // ── Vertical Rendering ───────────────────────────
1090
+ _renderVertical(screen, ox, oy, width, height, maxVal) {
1091
+ const valueRows = 1;
1092
+ const hasLabels = this._data.some(
1093
+ (g) => g.bars.some((b) => b.label !== void 0)
1094
+ );
1095
+ const labelRows = hasLabels ? 1 : 0;
1096
+ const hasGroupLabels = this._data.some((g) => g.label !== void 0);
1097
+ const groupLabelRows = hasGroupLabels ? 1 : 0;
1098
+ const reservedRows = valueRows + labelRows + groupLabelRows;
1099
+ if (height <= reservedRows) return;
1100
+ const barAreaHeight = height - reservedRows;
1101
+ let cx = ox;
1102
+ for (let gi = 0; gi < this._data.length; gi++) {
1103
+ const group = this._data[gi];
1104
+ if (!group) continue;
1105
+ const groupStartX = cx;
1106
+ for (let bi = 0; bi < group.bars.length; bi++) {
1107
+ const bar = group.bars[bi];
1108
+ if (!bar) continue;
1109
+ if (cx + this._barWidth > ox + width) break;
1110
+ const color = bar.color ?? this._barColor;
1111
+ const scaledHeight = bar.value / maxVal * (barAreaHeight * 8);
1112
+ let remaining = Math.round(scaledHeight);
1113
+ for (let row = barAreaHeight - 1; row >= 0; row--) {
1114
+ if (remaining <= 0) break;
1115
+ const level = Math.min(remaining, 8);
1116
+ const symbol = VERTICAL_BAR_SYMBOLS[level] ?? " ";
1117
+ const cellY = oy + row;
1118
+ for (let col = 0; col < this._barWidth; col++) {
1119
+ const cellX = cx + col;
1120
+ if (cellX < ox + width) {
1121
+ screen.setCell(cellX, cellY, { char: symbol, fg: color });
1122
+ }
1123
+ }
1124
+ remaining -= 8;
1125
+ }
1126
+ const valStr = Math.round(bar.value).toString();
1127
+ const valX = cx + Math.floor((this._barWidth - stringWidth7(valStr)) / 2);
1128
+ screen.writeString(
1129
+ Math.max(cx, valX),
1130
+ oy + barAreaHeight,
1131
+ valStr.slice(0, this._barWidth),
1132
+ { fg: this._valueColor }
1133
+ );
1134
+ if (hasLabels && bar.label) {
1135
+ const label = bar.label.slice(0, this._barWidth);
1136
+ const labelX = cx + Math.floor((this._barWidth - stringWidth7(label)) / 2);
1137
+ screen.writeString(
1138
+ Math.max(cx, labelX),
1139
+ oy + barAreaHeight + valueRows,
1140
+ label,
1141
+ { fg: this._labelColor }
1142
+ );
1143
+ }
1144
+ cx += this._barWidth;
1145
+ if (bi < group.bars.length - 1) cx += this._barGap;
1146
+ }
1147
+ if (hasGroupLabels && group.label) {
1148
+ const groupWidth = cx - groupStartX;
1149
+ const label = group.label.slice(0, groupWidth);
1150
+ const labelX = groupStartX + Math.floor((groupWidth - stringWidth7(label)) / 2);
1151
+ screen.writeString(
1152
+ Math.max(groupStartX, labelX),
1153
+ oy + height - 1,
1154
+ label,
1155
+ { fg: this._labelColor }
1156
+ );
1157
+ }
1158
+ if (gi < this._data.length - 1) cx += this._groupGap;
1159
+ }
1160
+ }
1161
+ // ── Horizontal Rendering ─────────────────────────
1162
+ _renderHorizontal(screen, ox, oy, width, height, maxVal) {
1163
+ let maxLabelWidth = 0;
1164
+ let maxValueWidth = 0;
1165
+ for (const group of this._data) {
1166
+ for (const bar of group.bars) {
1167
+ if (bar.label) {
1168
+ const w = stringWidth7(bar.label);
1169
+ if (w > maxLabelWidth) maxLabelWidth = w;
1170
+ }
1171
+ const vw = Math.round(bar.value).toString().length;
1172
+ if (vw > maxValueWidth) maxValueWidth = vw;
1173
+ }
1174
+ }
1175
+ const labelColWidth = maxLabelWidth > 0 ? maxLabelWidth + 1 : 0;
1176
+ const valueColWidth = maxValueWidth > 0 ? maxValueWidth + 1 : 0;
1177
+ const barAreaWidth = width - labelColWidth - valueColWidth;
1178
+ if (barAreaWidth <= 0) return;
1179
+ let cy = oy;
1180
+ for (let gi = 0; gi < this._data.length; gi++) {
1181
+ const group = this._data[gi];
1182
+ if (!group) continue;
1183
+ for (let bi = 0; bi < group.bars.length; bi++) {
1184
+ const bar = group.bars[bi];
1185
+ if (!bar) continue;
1186
+ for (let row = 0; row < this._barWidth; row++) {
1187
+ const cellY = cy + row;
1188
+ if (cellY >= oy + height) break;
1189
+ if (row === 0 && bar.label) {
1190
+ const label = bar.label.slice(0, maxLabelWidth);
1191
+ const padded = label.padStart(maxLabelWidth);
1192
+ screen.writeString(ox, cellY, padded, { fg: this._labelColor });
1193
+ }
1194
+ const color = bar.color ?? this._barColor;
1195
+ const scaledWidth = bar.value / maxVal * (barAreaWidth * 8);
1196
+ let remaining = Math.round(scaledWidth);
1197
+ const barStartX = ox + labelColWidth;
1198
+ for (let col = 0; col < barAreaWidth; col++) {
1199
+ if (remaining <= 0) break;
1200
+ const level = Math.min(remaining, 8);
1201
+ const symbol = HORIZONTAL_BAR_SYMBOLS[level] ?? " ";
1202
+ screen.setCell(barStartX + col, cellY, { char: symbol, fg: color });
1203
+ remaining -= 8;
1204
+ }
1205
+ if (row === 0) {
1206
+ const valStr = Math.round(bar.value).toString();
1207
+ screen.writeString(
1208
+ ox + labelColWidth + barAreaWidth,
1209
+ cellY,
1210
+ ` ${valStr}`,
1211
+ { fg: this._valueColor }
1212
+ );
1213
+ }
1214
+ }
1215
+ cy += this._barWidth;
1216
+ if (bi < group.bars.length - 1) cy += this._barGap;
1217
+ }
1218
+ if (gi < this._data.length - 1) cy += this._groupGap;
1219
+ }
1220
+ }
1221
+ };
1222
+
819
1223
  // src/feedback/ProgressBar.ts
820
- import { styleToCellAttrs as styleToCellAttrs11 } from "@termuijs/core";
1224
+ import { styleToCellAttrs as styleToCellAttrs13 } from "@termuijs/core";
821
1225
  var ProgressBar = class extends Widget {
822
1226
  _value;
823
1227
  _fillChar;
@@ -839,6 +1243,7 @@ var ProgressBar = class extends Widget {
839
1243
  /** Set progress value (0–1) */
840
1244
  setValue(value) {
841
1245
  this._value = Math.max(0, Math.min(1, value));
1246
+ this.markDirty();
842
1247
  }
843
1248
  get value() {
844
1249
  return this._value;
@@ -847,7 +1252,7 @@ var ProgressBar = class extends Widget {
847
1252
  const rect = this._getContentRect();
848
1253
  const { x, y, width } = rect;
849
1254
  if (width <= 0) return;
850
- const attrs = styleToCellAttrs11(this._style);
1255
+ const attrs = styleToCellAttrs13(this._style);
851
1256
  let label = "";
852
1257
  if (this._showLabel) {
853
1258
  if (this._labelFormat === "percent") {
@@ -872,7 +1277,7 @@ var ProgressBar = class extends Widget {
872
1277
  };
873
1278
 
874
1279
  // src/feedback/Spinner.ts
875
- import { styleToCellAttrs as styleToCellAttrs12 } from "@termuijs/core";
1280
+ import { styleToCellAttrs as styleToCellAttrs14 } from "@termuijs/core";
876
1281
  var SPINNER_FRAMES = {
877
1282
  dots: {
878
1283
  frames: ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"],
@@ -942,7 +1347,7 @@ var Spinner = class extends Widget {
942
1347
  const rect = this._getContentRect();
943
1348
  const { x, y, width } = rect;
944
1349
  if (width <= 0) return;
945
- const attrs = styleToCellAttrs12(this._style);
1350
+ const attrs = styleToCellAttrs14(this._style);
946
1351
  const frame = this._frames[this._frameIndex];
947
1352
  screen.writeString(x, y, frame, { ...attrs, fg: this._color });
948
1353
  if (this._label) {
@@ -950,19 +1355,107 @@ var Spinner = class extends Widget {
950
1355
  }
951
1356
  }
952
1357
  };
1358
+
1359
+ // src/feedback/Scrollbar.ts
1360
+ import {
1361
+ ScrollbarSets
1362
+ } from "@termuijs/core";
1363
+ var Scrollbar = class extends Widget {
1364
+ _contentLength;
1365
+ _viewportLength;
1366
+ _position;
1367
+ _orientation;
1368
+ _thumbColor;
1369
+ _trackColor;
1370
+ _showArrows;
1371
+ constructor(style = {}, opts) {
1372
+ super(style);
1373
+ this._contentLength = opts.contentLength;
1374
+ this._viewportLength = opts.viewportLength;
1375
+ this._position = opts.position ?? 0;
1376
+ this._orientation = opts.orientation ?? "verticalRight";
1377
+ this._thumbColor = opts.thumbColor ?? { type: "named", name: "white" };
1378
+ this._trackColor = opts.trackColor ?? { type: "named", name: "brightBlack" };
1379
+ this._showArrows = opts.showArrows ?? true;
1380
+ }
1381
+ setPosition(position) {
1382
+ this._position = position;
1383
+ this.markDirty();
1384
+ }
1385
+ setContentLength(length) {
1386
+ this._contentLength = length;
1387
+ this.markDirty();
1388
+ }
1389
+ setViewportLength(length) {
1390
+ this._viewportLength = length;
1391
+ this.markDirty();
1392
+ }
1393
+ _renderSelf(screen) {
1394
+ const rect = this._getContentRect();
1395
+ const { x, y, width, height } = rect;
1396
+ if (width <= 0 || height <= 0 || this._contentLength <= 0) return;
1397
+ if (this._contentLength <= this._viewportLength) return;
1398
+ const vertical = this._orientation === "verticalRight" || this._orientation === "verticalLeft";
1399
+ const symbols = vertical ? ScrollbarSets.VERTICAL : ScrollbarSets.HORIZONTAL;
1400
+ const trackX = this._orientation === "verticalLeft" ? x : this._orientation === "verticalRight" ? x + width - 1 : x;
1401
+ const trackY = this._orientation === "horizontalTop" ? y : this._orientation === "horizontalBottom" ? y + height - 1 : y;
1402
+ const totalLength = vertical ? height : width;
1403
+ if (totalLength <= 0) return;
1404
+ let trackStart = 0;
1405
+ let trackLength = totalLength;
1406
+ if (this._showArrows && totalLength > 2) {
1407
+ const beginX = vertical ? trackX : x;
1408
+ const beginY = vertical ? y : trackY;
1409
+ screen.setCell(beginX, beginY, {
1410
+ char: symbols.begin,
1411
+ fg: this._trackColor
1412
+ });
1413
+ const endX = vertical ? trackX : x + totalLength - 1;
1414
+ const endY = vertical ? y + totalLength - 1 : trackY;
1415
+ screen.setCell(endX, endY, {
1416
+ char: symbols.end,
1417
+ fg: this._trackColor
1418
+ });
1419
+ trackStart = 1;
1420
+ trackLength -= 2;
1421
+ }
1422
+ if (trackLength <= 0) return;
1423
+ const thumbSize = Math.max(1, Math.floor(
1424
+ trackLength * this._viewportLength / this._contentLength
1425
+ ));
1426
+ const maxScroll = Math.max(1, this._contentLength - this._viewportLength);
1427
+ const thumbOffset = Math.min(
1428
+ trackLength - thumbSize,
1429
+ Math.floor(this._position * (trackLength - thumbSize) / maxScroll)
1430
+ );
1431
+ for (let i = 0; i < trackLength; i++) {
1432
+ const pos = trackStart + i;
1433
+ const cellX = vertical ? trackX : x + pos;
1434
+ const cellY = vertical ? y + pos : trackY;
1435
+ const isThumb = i >= thumbOffset && i < thumbOffset + thumbSize;
1436
+ screen.setCell(cellX, cellY, {
1437
+ char: isThumb ? symbols.thumb : symbols.track,
1438
+ fg: isThumb ? this._thumbColor : this._trackColor
1439
+ });
1440
+ }
1441
+ }
1442
+ };
953
1443
  export {
1444
+ BarChart,
954
1445
  Box,
955
1446
  Gauge,
956
1447
  List,
957
1448
  LogView,
958
1449
  ProgressBar,
959
1450
  SPINNER_FRAMES,
1451
+ Scrollbar,
960
1452
  Sparkline,
961
1453
  Spinner,
962
1454
  StatusIndicator,
963
1455
  Table,
964
1456
  Text,
965
1457
  TextInput,
1458
+ VirtualList,
966
1459
  Widget
967
1460
  };
968
1461
  //# sourceMappingURL=index.js.map