termcast 1.5.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/dist/build.d.ts.map +1 -1
  2. package/dist/build.js +22 -5
  3. package/dist/build.js.map +1 -1
  4. package/dist/compile.d.ts.map +1 -1
  5. package/dist/compile.js +7 -1
  6. package/dist/compile.js.map +1 -1
  7. package/dist/components/bar-chart.d.ts.map +1 -1
  8. package/dist/components/bar-chart.js +14 -3
  9. package/dist/components/bar-chart.js.map +1 -1
  10. package/dist/components/bar-graph.d.ts +4 -4
  11. package/dist/components/bar-graph.d.ts.map +1 -1
  12. package/dist/components/bar-graph.js +23 -5
  13. package/dist/components/bar-graph.js.map +1 -1
  14. package/dist/components/candle-chart.d.ts +15 -0
  15. package/dist/components/candle-chart.d.ts.map +1 -1
  16. package/dist/components/candle-chart.js +41 -3
  17. package/dist/components/candle-chart.js.map +1 -1
  18. package/dist/components/chart-tooltip.d.ts +83 -0
  19. package/dist/components/chart-tooltip.d.ts.map +1 -0
  20. package/dist/components/chart-tooltip.js +127 -0
  21. package/dist/components/chart-tooltip.js.map +1 -0
  22. package/dist/components/dotted-line-graph.d.ts +11 -0
  23. package/dist/components/dotted-line-graph.d.ts.map +1 -1
  24. package/dist/components/dotted-line-graph.js +43 -2
  25. package/dist/components/dotted-line-graph.js.map +1 -1
  26. package/dist/components/graph.d.ts +11 -0
  27. package/dist/components/graph.d.ts.map +1 -1
  28. package/dist/components/graph.js +53 -4
  29. package/dist/components/graph.js.map +1 -1
  30. package/dist/components/horizontal-bar-graph.d.ts.map +1 -1
  31. package/dist/components/horizontal-bar-graph.js +16 -5
  32. package/dist/components/horizontal-bar-graph.js.map +1 -1
  33. package/dist/components/list.d.ts +7 -0
  34. package/dist/components/list.d.ts.map +1 -1
  35. package/dist/components/list.js +75 -14
  36. package/dist/components/list.js.map +1 -1
  37. package/dist/examples/chart-tooltips.d.ts +2 -0
  38. package/dist/examples/chart-tooltips.d.ts.map +1 -0
  39. package/dist/examples/chart-tooltips.js +16 -0
  40. package/dist/examples/chart-tooltips.js.map +1 -0
  41. package/dist/examples/list-detail-height-ratchet.d.ts +2 -0
  42. package/dist/examples/list-detail-height-ratchet.d.ts.map +1 -0
  43. package/dist/examples/list-detail-height-ratchet.js +26 -0
  44. package/dist/examples/list-detail-height-ratchet.js.map +1 -0
  45. package/dist/extensions/dev.d.ts.map +1 -1
  46. package/dist/extensions/dev.js +1 -0
  47. package/dist/extensions/dev.js.map +1 -1
  48. package/dist/globals.js +8 -0
  49. package/dist/globals.js.map +1 -1
  50. package/dist/package-json.d.ts +2 -0
  51. package/dist/package-json.d.ts.map +1 -1
  52. package/dist/package-json.js +20 -17
  53. package/dist/package-json.js.map +1 -1
  54. package/dist/profiler.d.ts +2 -0
  55. package/dist/profiler.d.ts.map +1 -0
  56. package/dist/profiler.js +390 -0
  57. package/dist/profiler.js.map +1 -0
  58. package/package.json +14 -15
  59. package/src/build.tsx +27 -5
  60. package/src/cli.tsx +0 -0
  61. package/src/compile.tsx +9 -1
  62. package/src/compile.vitest.tsx +8 -8
  63. package/src/components/bar-chart.tsx +23 -3
  64. package/src/components/bar-graph.tsx +32 -13
  65. package/src/components/candle-chart.tsx +63 -16
  66. package/src/components/chart-tooltip.tsx +191 -0
  67. package/src/components/dotted-line-graph.tsx +49 -3
  68. package/src/components/graph.tsx +76 -18
  69. package/src/components/horizontal-bar-graph.tsx +24 -4
  70. package/src/components/list.tsx +93 -20
  71. package/src/examples/action-shortcut.vitest.tsx +4 -4
  72. package/src/examples/actions-context.vitest.tsx +2 -2
  73. package/src/examples/bar-graph-weekly.vitest.tsx +97 -97
  74. package/src/examples/chart-tooltips.tsx +54 -0
  75. package/src/examples/form-basic.vitest.tsx +8 -8
  76. package/src/examples/github.vitest.tsx +19 -28
  77. package/src/examples/graph-bar-chart.vitest.tsx +40 -40
  78. package/src/examples/graph-polymarket.vitest.tsx +24 -24
  79. package/src/examples/graph-row.vitest.tsx +8 -8
  80. package/src/examples/graph-styles.vitest.tsx +65 -65
  81. package/src/examples/horizontal-bar-graph-weekly.vitest.tsx +52 -52
  82. package/src/examples/list-detail-height-ratchet.tsx +48 -0
  83. package/src/examples/list-detail-height-ratchet.vitest.tsx +161 -0
  84. package/src/examples/list-detail-metadata.vitest.tsx +49 -49
  85. package/src/examples/list-dropdown-default.vitest.tsx +27 -27
  86. package/src/examples/list-fetch-data.vitest.tsx +3 -3
  87. package/src/examples/list-item-accessories.vitest.tsx +2 -2
  88. package/src/examples/list-loading-empty-view.vitest.tsx +1 -1
  89. package/src/examples/list-no-actions.vitest.tsx +3 -3
  90. package/src/examples/list-scrollbox.vitest.tsx +6 -6
  91. package/src/examples/list-spacing-mode.vitest.tsx +3 -3
  92. package/src/examples/list-with-detail.vitest.tsx +11 -11
  93. package/src/examples/list-with-dropdown.vitest.tsx +7 -7
  94. package/src/examples/list-with-sections.vitest.tsx +32 -32
  95. package/src/examples/list-with-toast.vitest.tsx +4 -4
  96. package/src/examples/simple-candle-chart.vitest.tsx +63 -61
  97. package/src/examples/simple-grid.vitest.tsx +13 -13
  98. package/src/examples/simple-navigation.vitest.tsx +25 -25
  99. package/src/examples/simple-progress-bar.vitest.tsx +8 -8
  100. package/src/examples/swift-extension.vitest.tsx +3 -3
  101. package/src/examples/toast-action.vitest.tsx +4 -4
  102. package/src/extensions/dev.tsx +2 -1
  103. package/src/extensions/dev.vitest.tsx +17 -17
  104. package/src/globals.ts +9 -0
  105. package/src/package-json.tsx +24 -23
  106. package/src/profiler.tsx +487 -0
@@ -6,10 +6,12 @@
6
6
  * colored series rows and percentages.
7
7
  */
8
8
 
9
- import React, { ReactNode, useMemo } from 'react'
9
+ import React, { ReactNode, useMemo, useRef } from 'react'
10
10
  import { BoxProps } from '@opentui/react'
11
+ import type { MouseEvent as OpenTUIMouseEvent } from '@opentui/core'
11
12
  import { Color, resolveColor } from 'termcast/src/colors'
12
13
  import { getThemePalette, useTheme } from 'termcast/src/theme'
14
+ import { ChartTooltip, useChartTooltip, formatTooltipLine } from 'termcast/src/components/chart-tooltip'
13
15
 
14
16
  export interface HorizontalBarGraphSeriesProps {
15
17
  /** One value per row/category position. */
@@ -97,6 +99,8 @@ const HorizontalBarGraph: HorizontalBarGraphType = (props) => {
97
99
  } = props
98
100
 
99
101
  const palette = getThemePalette(theme)
102
+ const containerRef = useRef<any>(null)
103
+ const { tooltip, show: showTooltip, hide: hideTooltip } = useChartTooltip()
100
104
 
101
105
  const seriesList = useMemo<Array<{ data: number[]; color: string; title?: string }>>(() => {
102
106
  const childArray = React.Children.toArray(children)
@@ -189,7 +193,8 @@ const HorizontalBarGraph: HorizontalBarGraphType = (props) => {
189
193
  const chartHeight = headerHeight + visibleRows.length
190
194
 
191
195
  return (
192
- <box flexDirection="column" width="100%" flexShrink={0} {...rest}>
196
+ <box ref={containerRef} flexDirection="column" width="100%" flexShrink={0} {...rest} onMouseOut={hideTooltip}>
197
+ <ChartTooltip tooltip={tooltip} containerRef={containerRef} />
193
198
  {showHeader && (
194
199
  <>
195
200
  <box flexDirection="row" height={1} flexShrink={0}>
@@ -238,7 +243,22 @@ const HorizontalBarGraph: HorizontalBarGraphType = (props) => {
238
243
  }
239
244
  const series = seriesList[seriesIndex]!
240
245
  return (
241
- <box key={seriesIndex} flexGrow={value} flexBasis={0} flexShrink={1} overflow="hidden">
246
+ <box
247
+ key={seriesIndex}
248
+ flexGrow={value}
249
+ flexBasis={0}
250
+ flexShrink={1}
251
+ overflow="hidden"
252
+ onMouseMove={(evt: OpenTUIMouseEvent) => {
253
+ const label = row.label
254
+ const seriesTitle = series.title || `#${seriesIndex + 1}`
255
+ showTooltip({
256
+ x: evt.x,
257
+ y: evt.y,
258
+ lines: [label, formatTooltipLine(seriesTitle, value)],
259
+ })
260
+ }}
261
+ >
242
262
  <box position="absolute" width="100%" height="100%" overflow="hidden">
243
263
  <text fg={series.color} wrapMode="none">{barCharacter.repeat(200)}</text>
244
264
  </box>
@@ -246,7 +266,7 @@ const HorizontalBarGraph: HorizontalBarGraphType = (props) => {
246
266
  )
247
267
  })}
248
268
  {maxTotal > row.total && (
249
- <box flexGrow={maxTotal - row.total} flexBasis={0} flexShrink={1} />
269
+ <box flexGrow={maxTotal - row.total} flexBasis={0} flexShrink={1} onMouseMove={hideTooltip} />
250
270
  )}
251
271
  </box>
252
272
  </box>
@@ -6,7 +6,7 @@ import {
6
6
  TextareaRenderable,
7
7
  } from '@opentui/core'
8
8
  import type { MouseEvent as OpenTUIMouseEvent } from '@opentui/core'
9
- import { useKeyboard, flushSync } from '@opentui/react'
9
+ import { useKeyboard, flushSync, useTerminalDimensions } from '@opentui/react'
10
10
  import React, {
11
11
  ReactElement,
12
12
  ReactNode,
@@ -164,15 +164,7 @@ function ListFooter(): any {
164
164
  </text>
165
165
  </Hoverable>
166
166
  )}
167
- {!isVim && (
168
- <Hoverable
169
- onMouseDown={() => {
170
- executeVimCommand('vim')
171
- }}
172
- >
173
- <text flexShrink={0} fg={theme.textMuted}>:vim</text>
174
- </Hoverable>
175
- )}
167
+
176
168
  </box>
177
169
  )
178
170
 
@@ -225,8 +217,17 @@ function CurrentItemDetail(props: {
225
217
  }): any {
226
218
  const theme = useTheme()
227
219
  const descendantsMap = useListDescendantsRerender()
228
-
229
- if (!props.isShowingDetail) return null
220
+ const boxRef = React.useRef<BoxRenderable>(null)
221
+ // Grow-only height ratchet: once the detail panel reaches a certain height,
222
+ // it never shrinks below that. This prevents the footer from jumping up
223
+ // when navigating from a tall detail to a short one.
224
+ const maxHeightRef = React.useRef(0)
225
+
226
+ if (!props.isShowingDetail) {
227
+ // Reset ratchet when detail is hidden so next show starts fresh
228
+ maxHeightRef.current = 0
229
+ return null
230
+ }
230
231
 
231
232
  const currentItem = Object.values(descendantsMap)
232
233
  .find((item) => item.index === props.selectedIndex)
@@ -236,6 +237,14 @@ function CurrentItemDetail(props: {
236
237
 
237
238
  return (
238
239
  <box
240
+ ref={boxRef}
241
+ minHeight={maxHeightRef.current || undefined}
242
+ onSizeChange={() => {
243
+ const h = boxRef.current?.height ?? 0
244
+ if (h > maxHeightRef.current) {
245
+ maxHeightRef.current = h
246
+ }
247
+ }}
239
248
  style={{
240
249
  width: '50%',
241
250
  paddingLeft: 1,
@@ -406,6 +415,13 @@ export interface ListProps
406
415
  searchBarPlaceholder?: string
407
416
  selectedItemId?: string
408
417
  isShowingDetail?: boolean
418
+ /**
419
+ * Minimum terminal width in columns required to show the detail panel.
420
+ * When the terminal is narrower than this value, the detail panel is
421
+ * automatically hidden even if `isShowingDetail` is true.
422
+ * @default 80
423
+ */
424
+ detailMinWidth?: number
409
425
  /**
410
426
  * Controls the vertical spacing of list items.
411
427
  * - 'default': Single-line items with title and subtitle on same row
@@ -1060,6 +1076,7 @@ export const List: ListType = (props) => {
1060
1076
  isLoading,
1061
1077
  navigationTitle,
1062
1078
  isShowingDetail,
1079
+ detailMinWidth = 80,
1063
1080
  selectedItemId,
1064
1081
  searchBarAccessory,
1065
1082
  logo,
@@ -1070,6 +1087,9 @@ export const List: ListType = (props) => {
1070
1087
  } = props
1071
1088
 
1072
1089
  const theme = useTheme()
1090
+ const { width: terminalWidth } = useTerminalDimensions()
1091
+ const effectiveIsShowingDetail = isShowingDetail && terminalWidth >= detailMinWidth
1092
+
1073
1093
  const currentStackSelectedListIndex = useStore((state) => {
1074
1094
  const stack = state.navigationStack
1075
1095
  const currentItem = stack[stack.length - 1]
@@ -1262,14 +1282,14 @@ export const List: ListType = (props) => {
1262
1282
  setSelectedIndex: setSelectedIndexWithPersistence,
1263
1283
  searchText,
1264
1284
  isFiltering: isFilteringEnabled,
1265
- isShowingDetail,
1285
+ isShowingDetail: effectiveIsShowingDetail,
1266
1286
  customEmptyViewRef,
1267
1287
  isLoading,
1268
1288
  hasDropdown: !!searchBarAccessory,
1269
1289
  spacingMode,
1270
1290
  accessoryTagWidths: accessoryTagsLayout,
1271
1291
  }),
1272
- [isDropdownOpen, selectedIndex, searchText, isFilteringEnabled, isShowingDetail, isLoading, searchBarAccessory, spacingMode, accessoryTagsLayout],
1292
+ [isDropdownOpen, selectedIndex, searchText, isFilteringEnabled, effectiveIsShowingDetail, isLoading, searchBarAccessory, spacingMode, accessoryTagsLayout],
1273
1293
  )
1274
1294
 
1275
1295
  // Handle selectedItemId prop changes (before paint to avoid flash)
@@ -1356,6 +1376,46 @@ export const List: ListType = (props) => {
1356
1376
  }
1357
1377
  }
1358
1378
 
1379
+ // Trigger pagination when the user mouse-scrolls near the bottom of the list.
1380
+ // Only fires on scroll-down so scrolling up near the bottom doesn't spuriously
1381
+ // re-trigger onLoadMore. Uses queueMicrotask because opentui calls onMouseScroll
1382
+ // before ScrollBox.onMouseEvent updates scrollTop in the same call stack;
1383
+ // a microtask runs after the synchronous handler chain finishes.
1384
+ const checkScrollPagination = (event: OpenTUIMouseEvent) => {
1385
+ if (event.scroll?.direction !== 'down') return
1386
+
1387
+ queueMicrotask(() => {
1388
+ const scrollBox = scrollBoxRef.current
1389
+ if (!scrollBox || !props.pagination?.hasMore) return
1390
+
1391
+ // Reset pagination lock when new items arrive (same logic as in move())
1392
+ const items = Object.values(descendantsContext.map.current)
1393
+ .filter((item) => item.index !== -1 && item.props?.visible !== false)
1394
+ if (items.length !== prevItemCountRef.current) {
1395
+ prevItemCountRef.current = items.length
1396
+ paginationCalledRef.current = false
1397
+ }
1398
+
1399
+ if (paginationCalledRef.current) return
1400
+
1401
+ const scrollTop = scrollBox.scrollTop || 0
1402
+ const viewportHeight = scrollBox.viewport?.height || 0
1403
+ const contentHeight = scrollBox.scrollHeight || 0
1404
+
1405
+ // Nothing to paginate if content fits in viewport
1406
+ if (contentHeight <= viewportHeight) return
1407
+
1408
+ // Trigger when within 20% of the bottom (or 3 rows, whichever is larger)
1409
+ const threshold = Math.max(3, Math.floor(viewportHeight * 0.2))
1410
+ const distanceFromBottom = contentHeight - (scrollTop + viewportHeight)
1411
+
1412
+ if (distanceFromBottom <= threshold) {
1413
+ paginationCalledRef.current = true
1414
+ props.pagination.onLoadMore()
1415
+ }
1416
+ })
1417
+ }
1418
+
1359
1419
  const move = (direction: -1 | 1) => {
1360
1420
  // Get all visible items
1361
1421
  const items = Object.values(descendantsContext.map.current)
@@ -1612,7 +1672,8 @@ export const List: ListType = (props) => {
1612
1672
  return
1613
1673
  }
1614
1674
 
1615
- // Ctrl+d for half-page down, Ctrl+u for half-page up
1675
+ // Ctrl+d / Ctrl+u for half-page down/up
1676
+ // Ctrl+f / Ctrl+b for full-page down/up
1616
1677
  if (evt.ctrl && evt.name === 'd') {
1617
1678
  const viewportHeight = scrollBoxRef.current?.viewport?.height || 20
1618
1679
  moveByN(Math.floor(viewportHeight / 2))
@@ -1625,6 +1686,18 @@ export const List: ListType = (props) => {
1625
1686
  evt.stopPropagation()
1626
1687
  return
1627
1688
  }
1689
+ if (evt.ctrl && evt.name === 'f') {
1690
+ const viewportHeight = scrollBoxRef.current?.viewport?.height || 20
1691
+ moveByN(viewportHeight)
1692
+ evt.stopPropagation()
1693
+ return
1694
+ }
1695
+ if (evt.ctrl && evt.name === 'b') {
1696
+ const viewportHeight = scrollBoxRef.current?.viewport?.height || 20
1697
+ moveByN(-viewportHeight)
1698
+ evt.stopPropagation()
1699
+ return
1700
+ }
1628
1701
 
1629
1702
  // / to enter search mode
1630
1703
  if (evt.sequence === '/' && !evt.ctrl && !evt.meta) {
@@ -1799,14 +1872,15 @@ export const List: ListType = (props) => {
1799
1872
  {/* Main content area with optional detail view */}
1800
1873
  <box style={{ flexDirection: 'row', flexGrow: 1, flexShrink: 1 }}>
1801
1874
  {/* List content - render children which will register themselves */}
1802
- <box style={{ width: isShowingDetail ? '50%' : '100%', flexGrow: 1, flexShrink: 1, flexDirection: 'column' }}>
1875
+ <box style={{ width: effectiveIsShowingDetail ? '50%' : '100%', flexGrow: 1, flexShrink: 1, flexDirection: 'column' }}>
1803
1876
  {/* Scrollable list items */}
1804
1877
  <ScrollBox
1805
1878
  ref={scrollBoxRef}
1806
1879
  focused={false}
1807
1880
  flexGrow={1}
1808
1881
  flexShrink={1}
1809
- minHeight={6}
1882
+ minHeight={10}
1883
+ onMouseScroll={checkScrollPagination}
1810
1884
  style={{
1811
1885
  rootOptions: {
1812
1886
  backgroundColor: undefined,
@@ -1836,7 +1910,7 @@ export const List: ListType = (props) => {
1836
1910
  {/* Detail panel on the right */}
1837
1911
  <CurrentItemDetail
1838
1912
  selectedIndex={selectedIndex}
1839
- isShowingDetail={isShowingDetail}
1913
+ isShowingDetail={effectiveIsShowingDetail}
1840
1914
  />
1841
1915
  </box>
1842
1916
  </box>
@@ -1990,8 +2064,7 @@ const ListItem: ListItemType = (props) => {
1990
2064
  }
1991
2065
  }
1992
2066
 
1993
- // Don't show accessories if we're showing detail
1994
- const showAccessories = !props.detail && props.accessories
2067
+ const showAccessories = Boolean(props.accessories)
1995
2068
 
1996
2069
  // Get icon string and color from props.icon (can be string or object with value/tintColor)
1997
2070
  const { iconValue, iconColor } = (() => {
@@ -49,7 +49,7 @@ test('ctrl+r shortcut should trigger Refresh action directly', async () => {
49
49
 
50
50
  > Search...
51
51
 
52
- Refre...nt: 1Press ctrl+r to refresh ...nter then select Refresh
52
+ Refres...unt: 1Press ctrl+r to refresh...ter then select Refresh
53
53
 
54
54
 
55
55
 
@@ -57,11 +57,11 @@ test('ctrl+r shortcut should trigger Refresh action directly', async () => {
57
57
 
58
58
 
59
59
 
60
- ↵ refresh ↑↓ navigate ^k actions :vim
61
60
 
62
61
 
63
62
 
64
63
 
64
+ ↵ refresh ↑↓ navigate ^k actions
65
65
  "
66
66
  `)
67
67
  }, 30000)
@@ -85,6 +85,7 @@ test('action shortcut is displayed in action panel', async () => {
85
85
  expect(actionsPanel).toMatchInlineSnapshot(`
86
86
  "
87
87
 
88
+
88
89
  ╭────────────────────────────────────────────────────────────────╮
89
90
  │ │
90
91
  │ Actions esc │
@@ -102,8 +103,7 @@ test('action shortcut is displayed in action panel', async () => {
102
103
  │ │
103
104
  │ │
104
105
  │ │
105
- │ ↵ select ↑↓ navigate │
106
- │ │"
106
+ │ ↵ select ↑↓ navigate │"
107
107
  `)
108
108
  }, 30000)
109
109
 
@@ -38,6 +38,7 @@ test('actions preserve React context through portal', async () => {
38
38
  expect(actionsPanel).toMatchInlineSnapshot(`
39
39
  "
40
40
 
41
+
41
42
  ╭────────────────────────────────────────────────────────────────╮
42
43
  │ │
43
44
  │ Actions esc │
@@ -55,8 +56,7 @@ test('actions preserve React context through portal', async () => {
55
56
  │ │
56
57
  │ │
57
58
  │ │
58
- │ ↵ select ↑↓ navigate │
59
- │ │"
59
+ │ ↵ select ↑↓ navigate │"
60
60
  `)
61
61
 
62
62
  // Select "Show Counter" (first action, already selected)
@@ -1,5 +1,5 @@
1
1
  // E2E tests for BarGraph vertical stacked bar chart.
2
- // Bar segments use chars so they show in text snapshots without filling cells.
2
+ // Bar segments use (full block) chars for solid, gap-free columns.
3
3
 
4
4
  import { test, expect, afterEach, beforeEach } from 'vitest'
5
5
  import { launchTerminal, Session } from 'tuistory/src'
@@ -35,28 +35,28 @@ test('bar graph renders bars, labels, and legend', async () => {
35
35
 
36
36
  > Search...
37
37
 
38
- Weekl...affic3 channels...oss 6 days │ 110.0│ ▃▃▃
39
- Revenu... Regio EMEA / A... Americas │ │▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃
40
- Server Load CPU / Memory / IO │ 82.5│▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃
41
- Many ...s (20)Overflow ...th 20 bars │ │▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃
42
- Many Series (8) Legend overflow test │ 55.0│▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃
43
- Lon...belsLabels wide... bar columns │ │▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃
44
- Week 1 vs Week 2 Two graphs in a Row │ 27.5│▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃
45
- │▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃
46
- │ 0.0│▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃
47
- │ Mon Tue Wed Thu Fri Sat
48
- ↵ open detail ↑↓ navigate ^k act │ ■ Direct ■ Organic ■ Referral
49
-
50
-
51
-
52
-
53
-
54
-
55
-
56
-
57
-
58
-
59
-
38
+ Weekly Traffi 3 channel...oss 6 days │ 110.0│ ██
39
+ Revenue by Regio EMEA / A...Americas │ │██ ██ ██ ██
40
+ Server Load CPU / Memory / IO │ 82.5│██ ██ ██ ██
41
+ Many C...ns (20)Overflow...h 20 bars │ │██ ██ ██ ██ ██
42
+ Many Series (8) Legend overflow test │ 55.0│██ ██ ██ ██ ██ ██
43
+ Long Label Labels wide...bar columns │ │██ ██ ██ ██ ██ ██
44
+ Week 1 vs Week 2 Two graphs in a Row │ 27.5│██ ██ ██ ██ ██ ██
45
+ │██ ██ ██ ██ ██ ██
46
+ │ 0.0│██ ██ ██ ██ ██ ██
47
+ │ Mon Tue Wed Thu FriSat
48
+ │ ■ Direct ■ Organic ■ Referral
49
+
50
+
51
+
52
+
53
+
54
+
55
+
56
+
57
+
58
+
59
+ ↵ open detail ↑↓ navigate ^k act │
60
60
 
61
61
  "
62
62
  `)
@@ -64,7 +64,7 @@ test('bar graph renders bars, labels, and legend', async () => {
64
64
  expect(text).toContain('Mon')
65
65
  expect(text).toContain('Direct')
66
66
  expect(text).toContain('0.0│')
67
- expect(text).toContain('')
67
+ expect(text).toContain('')
68
68
  }, 30000)
69
69
 
70
70
  test('many columns (20) clips with overflow hidden', async () => {
@@ -75,7 +75,7 @@ test('many columns (20) clips with overflow hidden', async () => {
75
75
  session.sendKey('down')
76
76
 
77
77
  await session.text({
78
- waitFor: (t) => t.includes('›Many ...s (20)'),
78
+ waitFor: (t) => t.includes('›Many') && t.includes('(20)'),
79
79
  timeout: 10000,
80
80
  })
81
81
  await session.waitIdle()
@@ -83,13 +83,13 @@ test('many columns (20) clips with overflow hidden', async () => {
83
83
 
84
84
  // Bar graph rendering has non-deterministic ANSI highlights, so use toContain checks
85
85
  // instead of inline snapshot for the bars area
86
- expect(text).toContain('›Many ...s (20)')
86
+ expect(text).toContain('›Many')
87
87
  expect(text).toContain('BarGraph Showcase')
88
- expect(text).toContain('')
88
+ expect(text).toContain('')
89
89
 
90
90
  // Some labels visible, overflow clips the rest
91
91
  expect(text).toContain('D')
92
- expect(text).toContain('')
92
+ expect(text).toContain('')
93
93
  }, 30000)
94
94
 
95
95
  test('many series (8) bottom legend clips on one row', async () => {
@@ -113,28 +113,28 @@ test('many series (8) bottom legend clips on one row', async () => {
113
113
 
114
114
  > Search...
115
115
 
116
- Weekl...affic3 channels...oss 6 days │ 328.0│▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃
117
- Revenu... Regio EMEA / A... Americas │ │▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃
118
- Server Load CPU / Memory / IO │ 246.0│▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃
119
- Many ...s (20)Overflow ...th 20 bars │ │▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃
120
- ›Many Series (8) Legend overflow test │ 164.0│▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃
121
- Lon...belsLabels wide... bar columns │ │▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃
122
- Week 1 vs Week 2 Two graphs in a Row │ 82.0│▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃
123
- │▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃
124
- │ 0.0│▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃
125
- │ Mon Tue Wed Thu Fri Sat
126
- ↑↓ navigate ^k actions :vim │ ■ Series 1 ■ Series 2 ■ Series 3
127
-
128
-
129
-
130
-
131
-
132
-
133
-
134
-
135
-
136
-
137
-
116
+ Weekly Traffi 3 channel...oss 6 days │ 328.0│██ ██ ██ ██ ██
117
+ Revenue by Regio EMEA / A...Americas │ │██ ██ ██ ██ ██ ██
118
+ Server Load CPU / Memory / IO │ 246.0│██ ██ ██ ██ ██ ██
119
+ Many C...ns (20)Overflow...h 20 bars │ │██ ██ ██ ██ ██ ██
120
+ ›Many Series (8) Legend overflow test │ 164.0│██ ██ ██ ██ ██ ██
121
+ Long Label Labels wide...bar columns │ │██ ██ ██ ██ ██ ██
122
+ Week 1 vs Week 2 Two graphs in a Row │ 82.0│██ ██ ██ ██ ██ ██
123
+ │██ ██ ██ ██ ██ ██
124
+ │ 0.0│██ ██ ██ ██ ██ ██
125
+ │ Mon Tue Wed Thu FriSat
126
+ │ ■ Series 1 ■ Series 2 ■ Series 3
127
+
128
+
129
+
130
+
131
+
132
+
133
+
134
+
135
+
136
+
137
+ ↑↓ navigate ^k actions │
138
138
 
139
139
  "
140
140
  `)
@@ -142,11 +142,11 @@ test('many series (8) bottom legend clips on one row', async () => {
142
142
  // Bottom legend is a single clipped row by default.
143
143
  expect(text).toContain('Series 1')
144
144
  expect(text).toContain('Series 3')
145
- expect(text).toContain('')
145
+ expect(text).toContain('')
146
146
  }, 30000)
147
147
 
148
148
  test('long labels truncated by overflow hidden', async () => {
149
- await session.text({ waitFor: (t) => t.includes('Lon...bels'), timeout: 10000 })
149
+ await session.text({ waitFor: (t) => t.includes('Labels wide'), timeout: 10000 })
150
150
  // Navigate: Weekly, Revenue, Server, Many Columns, Many Series, Long Labels = 5 downs
151
151
  session.sendKey('down')
152
152
  session.sendKey('down')
@@ -155,7 +155,7 @@ test('long labels truncated by overflow hidden', async () => {
155
155
  session.sendKey('down')
156
156
 
157
157
  const text = await session.text({
158
- waitFor: (t) => t.includes('›Lon...bels'),
158
+ waitFor: (t) => t.includes('›Long Label') || t.includes('›Lon...bels'),
159
159
  timeout: 10000,
160
160
  })
161
161
 
@@ -167,34 +167,34 @@ test('long labels truncated by overflow hidden', async () => {
167
167
 
168
168
  > Search...
169
169
 
170
- Weekl...affic3 channels...oss 6 days │ 75.0│ ▃▃▃
171
- Revenu... Regio EMEA / A... Americas │ │▃▃▃ ▃▃▃ ▃▃▃
172
- Server Load CPU / Memory / IO │ 56.3│▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃
173
- Many ...s (20)Overflow ...th 20 bars │ │▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃
174
- Many Series (8) Legend overflow test │ 37.5│▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃
175
- Lon...belsLabels wide... bar columns │ │▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃
176
- Week 1 vs Week 2 Two graphs in a Row │ 18.8│▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃
177
- │▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃
178
- │ 0.0│▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃ ▃▃▃
170
+ Weekly Traffi 3 channel...oss 6 days │ 75.0│ ██
171
+ Revenue by Regio EMEA / A...Americas │ │██ ██ ██
172
+ Server Load CPU / Memory / IO │ 56.3│██ ██ ██ ██
173
+ Many C...ns (20)Overflow...h 20 bars │ │██ ██ ██ ██
174
+ Many Series (8) Legend overflow test │ 37.5│██ ██ ██ ██ ██
175
+ Long Label Labels wide...bar columns │ │██ ██ ██ ██ ██ ██
176
+ Week 1 vs Week 2 Two graphs in a Row │ 18.8│██ ██ ██ ██ ██ ██
177
+ │██ ██ ██ ██ ██ ██
178
+ │ 0.0│██ ██ ██ ██ ██ ██
179
179
  │ Monday Thursday
180
- ↑↓ navigate ^k actions :vim │ ■ Views ■ Clicks
181
-
182
-
183
-
184
-
185
-
186
-
187
-
188
-
189
-
190
-
191
-
180
+ │ ■ Views ■ Clicks
181
+
182
+
183
+
184
+
185
+
186
+
187
+
188
+
189
+
190
+
191
+ ↑↓ navigate ^k actions │
192
192
 
193
193
  "
194
194
  `)
195
195
 
196
- expect(text).toContain('›Lon...bels')
197
- expect(text).toContain('')
196
+ expect(text).toContain('Labels wide')
197
+ expect(text).toContain('')
198
198
  }, 30000)
199
199
 
200
200
  test('side-by-side bar graphs in a Row', async () => {
@@ -220,28 +220,28 @@ test('side-by-side bar graphs in a Row', async () => {
220
220
 
221
221
  > Search...
222
222
 
223
- Weekl...affic3 channels...oss 6 days │ 110.0│ 130.0│
224
- Revenu... Regio EMEA / A... Americas │ │▃▃▃ ▃▃▃ │▃▃▃
225
- Server Load CPU / Memory / IO │ 82.5│▃▃▃ ▃▃▃ 97.5│▃▃▃ ▃▃▃
226
- Many ...s (20)Overflow ...th 20 bars │ │▃▃▃ ▃▃▃ ▃▃▃ │▃▃▃ ▃▃▃ ▃▃
227
- Many Series (8) Legend overflow test │ 55.0│▃▃▃ ▃▃▃ ▃▃▃ 65.0│▃▃▃ ▃▃▃ ▃▃
228
- Lon...belsLabels wide... bar columns │ │▃▃▃ ▃▃▃ ▃▃▃ │▃▃▃ ▃▃▃ ▃▃
229
- ›Week 1 vs Week 2 Two graphs in a Row │ 27.5│▃▃▃ ▃▃▃ ▃▃▃ 32.5│▃▃▃ ▃▃▃ ▃▃
230
- │▃▃▃ ▃▃▃ ▃▃▃ │▃▃▃ ▃▃▃ ▃▃
231
- │ 0.0│▃▃▃ ▃▃▃ ▃▃▃ 0.0│▃▃▃ ▃▃▃ ▃▃
223
+ Weekly Traffi 3 channel...oss 6 days │ 110.0│ 130.0│
224
+ Revenue by Regio EMEA / A...Americas │ │██ ██ │██
225
+ Server Load CPU / Memory / IO │ 82.5│██ ██ 97.5│██ ██
226
+ Many C...ns (20)Overflow...h 20 bars │ │██ ██ ██ │██ ██ ██
227
+ Many Series (8) Legend overflow test │ 55.0│██ ██ ██ 65.0│██ ██ ██
228
+ Long Label Labels wide...bar columns │ │██ ██ ██ │██ ██ ██
229
+ ›Week 1 vs Week 2 Two graphs in a Row │ 27.5│██ ██ ██ 32.5│██ ██ ██
230
+ │██ ██ ██ │██ ██ ██
231
+ │ 0.0│██ ██ ██ 0.0│██ ██ ██
232
232
  │ Mon Tue Wed Mon Tue We
233
- ↵ open detail ↑↓ navigate ^k act │ ■ Direct ■ Organ ■ Direct ■ Orga
234
-
235
-
236
-
237
-
238
-
239
-
240
-
241
-
242
-
243
-
244
-
233
+ │ ■ Direct ■ Organ ■ Direct ■ Orga
234
+
235
+
236
+
237
+
238
+
239
+
240
+
241
+
242
+
243
+
244
+ ↵ open detail ↑↓ navigate ^k act │
245
245
 
246
246
  "
247
247
  `)