termcast 1.5.0 → 1.6.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 (68) 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-graph.d.ts +4 -4
  8. package/dist/components/bar-graph.js +2 -2
  9. package/dist/components/list.d.ts +7 -0
  10. package/dist/components/list.d.ts.map +1 -1
  11. package/dist/components/list.js +74 -11
  12. package/dist/components/list.js.map +1 -1
  13. package/dist/examples/list-detail-height-ratchet.d.ts +2 -0
  14. package/dist/examples/list-detail-height-ratchet.d.ts.map +1 -0
  15. package/dist/examples/list-detail-height-ratchet.js +26 -0
  16. package/dist/examples/list-detail-height-ratchet.js.map +1 -0
  17. package/dist/extensions/dev.d.ts.map +1 -1
  18. package/dist/extensions/dev.js +1 -0
  19. package/dist/extensions/dev.js.map +1 -1
  20. package/dist/globals.js +8 -0
  21. package/dist/globals.js.map +1 -1
  22. package/dist/package-json.d.ts +2 -0
  23. package/dist/package-json.d.ts.map +1 -1
  24. package/dist/package-json.js +20 -17
  25. package/dist/package-json.js.map +1 -1
  26. package/dist/profiler.d.ts +2 -0
  27. package/dist/profiler.d.ts.map +1 -0
  28. package/dist/profiler.js +390 -0
  29. package/dist/profiler.js.map +1 -0
  30. package/package.json +14 -15
  31. package/src/build.tsx +27 -5
  32. package/src/cli.tsx +0 -0
  33. package/src/compile.tsx +9 -1
  34. package/src/compile.vitest.tsx +8 -8
  35. package/src/components/bar-graph.tsx +9 -9
  36. package/src/components/list.tsx +92 -11
  37. package/src/examples/action-shortcut.vitest.tsx +4 -4
  38. package/src/examples/actions-context.vitest.tsx +2 -2
  39. package/src/examples/bar-graph-weekly.vitest.tsx +97 -97
  40. package/src/examples/github.vitest.tsx +17 -26
  41. package/src/examples/graph-bar-chart.vitest.tsx +36 -36
  42. package/src/examples/graph-polymarket.vitest.tsx +24 -24
  43. package/src/examples/graph-row.vitest.tsx +4 -4
  44. package/src/examples/graph-styles.vitest.tsx +65 -65
  45. package/src/examples/horizontal-bar-graph-weekly.vitest.tsx +52 -52
  46. package/src/examples/list-detail-height-ratchet.tsx +48 -0
  47. package/src/examples/list-detail-height-ratchet.vitest.tsx +161 -0
  48. package/src/examples/list-detail-metadata.vitest.tsx +49 -49
  49. package/src/examples/list-dropdown-default.vitest.tsx +27 -27
  50. package/src/examples/list-fetch-data.vitest.tsx +3 -3
  51. package/src/examples/list-loading-empty-view.vitest.tsx +1 -1
  52. package/src/examples/list-no-actions.vitest.tsx +3 -3
  53. package/src/examples/list-scrollbox.vitest.tsx +6 -6
  54. package/src/examples/list-spacing-mode.vitest.tsx +1 -1
  55. package/src/examples/list-with-detail.vitest.tsx +9 -9
  56. package/src/examples/list-with-dropdown.vitest.tsx +6 -6
  57. package/src/examples/list-with-sections.vitest.tsx +20 -20
  58. package/src/examples/list-with-toast.vitest.tsx +4 -4
  59. package/src/examples/simple-candle-chart.vitest.tsx +61 -59
  60. package/src/examples/simple-navigation.vitest.tsx +25 -25
  61. package/src/examples/simple-progress-bar.vitest.tsx +7 -7
  62. package/src/examples/swift-extension.vitest.tsx +3 -3
  63. package/src/examples/toast-action.vitest.tsx +4 -4
  64. package/src/extensions/dev.tsx +2 -1
  65. package/src/extensions/dev.vitest.tsx +17 -17
  66. package/src/globals.ts +9 -0
  67. package/src/package-json.tsx +24 -23
  68. package/src/profiler.tsx +487 -0
@@ -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,
@@ -225,8 +225,17 @@ function CurrentItemDetail(props: {
225
225
  }): any {
226
226
  const theme = useTheme()
227
227
  const descendantsMap = useListDescendantsRerender()
228
-
229
- if (!props.isShowingDetail) return null
228
+ const boxRef = React.useRef<BoxRenderable>(null)
229
+ // Grow-only height ratchet: once the detail panel reaches a certain height,
230
+ // it never shrinks below that. This prevents the footer from jumping up
231
+ // when navigating from a tall detail to a short one.
232
+ const maxHeightRef = React.useRef(0)
233
+
234
+ if (!props.isShowingDetail) {
235
+ // Reset ratchet when detail is hidden so next show starts fresh
236
+ maxHeightRef.current = 0
237
+ return null
238
+ }
230
239
 
231
240
  const currentItem = Object.values(descendantsMap)
232
241
  .find((item) => item.index === props.selectedIndex)
@@ -236,6 +245,14 @@ function CurrentItemDetail(props: {
236
245
 
237
246
  return (
238
247
  <box
248
+ ref={boxRef}
249
+ minHeight={maxHeightRef.current || undefined}
250
+ onSizeChange={() => {
251
+ const h = boxRef.current?.height ?? 0
252
+ if (h > maxHeightRef.current) {
253
+ maxHeightRef.current = h
254
+ }
255
+ }}
239
256
  style={{
240
257
  width: '50%',
241
258
  paddingLeft: 1,
@@ -406,6 +423,13 @@ export interface ListProps
406
423
  searchBarPlaceholder?: string
407
424
  selectedItemId?: string
408
425
  isShowingDetail?: boolean
426
+ /**
427
+ * Minimum terminal width in columns required to show the detail panel.
428
+ * When the terminal is narrower than this value, the detail panel is
429
+ * automatically hidden even if `isShowingDetail` is true.
430
+ * @default 80
431
+ */
432
+ detailMinWidth?: number
409
433
  /**
410
434
  * Controls the vertical spacing of list items.
411
435
  * - 'default': Single-line items with title and subtitle on same row
@@ -1060,6 +1084,7 @@ export const List: ListType = (props) => {
1060
1084
  isLoading,
1061
1085
  navigationTitle,
1062
1086
  isShowingDetail,
1087
+ detailMinWidth = 80,
1063
1088
  selectedItemId,
1064
1089
  searchBarAccessory,
1065
1090
  logo,
@@ -1070,6 +1095,9 @@ export const List: ListType = (props) => {
1070
1095
  } = props
1071
1096
 
1072
1097
  const theme = useTheme()
1098
+ const { width: terminalWidth } = useTerminalDimensions()
1099
+ const effectiveIsShowingDetail = isShowingDetail && terminalWidth >= detailMinWidth
1100
+
1073
1101
  const currentStackSelectedListIndex = useStore((state) => {
1074
1102
  const stack = state.navigationStack
1075
1103
  const currentItem = stack[stack.length - 1]
@@ -1262,14 +1290,14 @@ export const List: ListType = (props) => {
1262
1290
  setSelectedIndex: setSelectedIndexWithPersistence,
1263
1291
  searchText,
1264
1292
  isFiltering: isFilteringEnabled,
1265
- isShowingDetail,
1293
+ isShowingDetail: effectiveIsShowingDetail,
1266
1294
  customEmptyViewRef,
1267
1295
  isLoading,
1268
1296
  hasDropdown: !!searchBarAccessory,
1269
1297
  spacingMode,
1270
1298
  accessoryTagWidths: accessoryTagsLayout,
1271
1299
  }),
1272
- [isDropdownOpen, selectedIndex, searchText, isFilteringEnabled, isShowingDetail, isLoading, searchBarAccessory, spacingMode, accessoryTagsLayout],
1300
+ [isDropdownOpen, selectedIndex, searchText, isFilteringEnabled, effectiveIsShowingDetail, isLoading, searchBarAccessory, spacingMode, accessoryTagsLayout],
1273
1301
  )
1274
1302
 
1275
1303
  // Handle selectedItemId prop changes (before paint to avoid flash)
@@ -1356,6 +1384,46 @@ export const List: ListType = (props) => {
1356
1384
  }
1357
1385
  }
1358
1386
 
1387
+ // Trigger pagination when the user mouse-scrolls near the bottom of the list.
1388
+ // Only fires on scroll-down so scrolling up near the bottom doesn't spuriously
1389
+ // re-trigger onLoadMore. Uses queueMicrotask because opentui calls onMouseScroll
1390
+ // before ScrollBox.onMouseEvent updates scrollTop in the same call stack;
1391
+ // a microtask runs after the synchronous handler chain finishes.
1392
+ const checkScrollPagination = (event: OpenTUIMouseEvent) => {
1393
+ if (event.scroll?.direction !== 'down') return
1394
+
1395
+ queueMicrotask(() => {
1396
+ const scrollBox = scrollBoxRef.current
1397
+ if (!scrollBox || !props.pagination?.hasMore) return
1398
+
1399
+ // Reset pagination lock when new items arrive (same logic as in move())
1400
+ const items = Object.values(descendantsContext.map.current)
1401
+ .filter((item) => item.index !== -1 && item.props?.visible !== false)
1402
+ if (items.length !== prevItemCountRef.current) {
1403
+ prevItemCountRef.current = items.length
1404
+ paginationCalledRef.current = false
1405
+ }
1406
+
1407
+ if (paginationCalledRef.current) return
1408
+
1409
+ const scrollTop = scrollBox.scrollTop || 0
1410
+ const viewportHeight = scrollBox.viewport?.height || 0
1411
+ const contentHeight = scrollBox.scrollHeight || 0
1412
+
1413
+ // Nothing to paginate if content fits in viewport
1414
+ if (contentHeight <= viewportHeight) return
1415
+
1416
+ // Trigger when within 20% of the bottom (or 3 rows, whichever is larger)
1417
+ const threshold = Math.max(3, Math.floor(viewportHeight * 0.2))
1418
+ const distanceFromBottom = contentHeight - (scrollTop + viewportHeight)
1419
+
1420
+ if (distanceFromBottom <= threshold) {
1421
+ paginationCalledRef.current = true
1422
+ props.pagination.onLoadMore()
1423
+ }
1424
+ })
1425
+ }
1426
+
1359
1427
  const move = (direction: -1 | 1) => {
1360
1428
  // Get all visible items
1361
1429
  const items = Object.values(descendantsContext.map.current)
@@ -1612,7 +1680,8 @@ export const List: ListType = (props) => {
1612
1680
  return
1613
1681
  }
1614
1682
 
1615
- // Ctrl+d for half-page down, Ctrl+u for half-page up
1683
+ // Ctrl+d / Ctrl+u for half-page down/up
1684
+ // Ctrl+f / Ctrl+b for full-page down/up
1616
1685
  if (evt.ctrl && evt.name === 'd') {
1617
1686
  const viewportHeight = scrollBoxRef.current?.viewport?.height || 20
1618
1687
  moveByN(Math.floor(viewportHeight / 2))
@@ -1625,6 +1694,18 @@ export const List: ListType = (props) => {
1625
1694
  evt.stopPropagation()
1626
1695
  return
1627
1696
  }
1697
+ if (evt.ctrl && evt.name === 'f') {
1698
+ const viewportHeight = scrollBoxRef.current?.viewport?.height || 20
1699
+ moveByN(viewportHeight)
1700
+ evt.stopPropagation()
1701
+ return
1702
+ }
1703
+ if (evt.ctrl && evt.name === 'b') {
1704
+ const viewportHeight = scrollBoxRef.current?.viewport?.height || 20
1705
+ moveByN(-viewportHeight)
1706
+ evt.stopPropagation()
1707
+ return
1708
+ }
1628
1709
 
1629
1710
  // / to enter search mode
1630
1711
  if (evt.sequence === '/' && !evt.ctrl && !evt.meta) {
@@ -1799,14 +1880,15 @@ export const List: ListType = (props) => {
1799
1880
  {/* Main content area with optional detail view */}
1800
1881
  <box style={{ flexDirection: 'row', flexGrow: 1, flexShrink: 1 }}>
1801
1882
  {/* List content - render children which will register themselves */}
1802
- <box style={{ width: isShowingDetail ? '50%' : '100%', flexGrow: 1, flexShrink: 1, flexDirection: 'column' }}>
1883
+ <box style={{ width: effectiveIsShowingDetail ? '50%' : '100%', flexGrow: 1, flexShrink: 1, flexDirection: 'column' }}>
1803
1884
  {/* Scrollable list items */}
1804
1885
  <ScrollBox
1805
1886
  ref={scrollBoxRef}
1806
1887
  focused={false}
1807
1888
  flexGrow={1}
1808
1889
  flexShrink={1}
1809
- minHeight={6}
1890
+ minHeight={10}
1891
+ onMouseScroll={checkScrollPagination}
1810
1892
  style={{
1811
1893
  rootOptions: {
1812
1894
  backgroundColor: undefined,
@@ -1836,7 +1918,7 @@ export const List: ListType = (props) => {
1836
1918
  {/* Detail panel on the right */}
1837
1919
  <CurrentItemDetail
1838
1920
  selectedIndex={selectedIndex}
1839
- isShowingDetail={isShowingDetail}
1921
+ isShowingDetail={effectiveIsShowingDetail}
1840
1922
  />
1841
1923
  </box>
1842
1924
  </box>
@@ -1990,8 +2072,7 @@ const ListItem: ListItemType = (props) => {
1990
2072
  }
1991
2073
  }
1992
2074
 
1993
- // Don't show accessories if we're showing detail
1994
- const showAccessories = !props.detail && props.accessories
2075
+ const showAccessories = Boolean(props.accessories)
1995
2076
 
1996
2077
  // Get icon string and color from props.icon (can be string or object with value/tintColor)
1997
2078
  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 :vim
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 :vim │
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 :vim │
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
  `)
@@ -54,22 +54,13 @@ afterEach(() => {
54
54
  })
55
55
 
56
56
  test.skipIf(!extensionExists)('github extension shows command list on launch', async () => {
57
- // Wait for command list to appear (extension has multiple commands)
58
- // Don't match "Commands" alone - it falsely matches "Building 18 commands..." build log
59
57
  const initialView = await session.text({
60
- waitFor: (text) => /My Pu.*uest|Search .*sitories/i.test(text),
58
+ waitFor: (text) => text.includes('My Pul...quests') && text.includes('Search Repositorie'),
61
59
  timeout: 30000,
62
60
  })
63
61
 
64
- // Wait for the full command list to render.
65
- // The list can paint the first item before all descendants are registered.
66
- await session.text({
67
- waitFor: (text) => text.includes('My Pu...uest') && text.includes('Search ...sitories'),
68
- timeout: 30000,
69
- })
70
-
71
- expect(initialView).toContain('My Pu...uest')
72
- expect(initialView).toContain('Search ...sitories')
62
+ expect(initialView).toContain('My Pul...quests')
63
+ expect(initialView).toContain('Search Repositorie')
73
64
  expect(initialView).toMatchInlineSnapshot(`
74
65
  "
75
66
 
@@ -79,19 +70,19 @@ test.skipIf(!extensionExists)('github extension shows command list on launch', a
79
70
  > Search commands...
80
71
 
81
72
  Commands
82
- ›My Pu...uest List pull requests you cre...d in, or were mentioned in. view
83
- Search ...RequestsSearch recent pull reque...lly in all repositories. view
84
- Create ...RequestCreate a pull request in...your GitHub repositories. view
73
+ ›My Pul...questsList pull requests you cr... in, or were mentioned in. view
74
+ Search Pull Request Search recent pull requ...ly in all repositories. view
75
+ Create Pull Reques Create a pull request i...our GitHub repositories. view
85
76
  My IssuesList issues created by you, ...ned to you or mentioning you. view
86
77
  Search Issues Search recent issues globally in all repositories. view
87
78
  Create Issue Create an issue in one of your GitHub repositories. view
88
79
  Create Branch Create a branch in one of your GitHub repositories view
89
- Search ...sitoriesSearch in your public or...te repositories by name. view
80
+ Search Repositorie Search in your public o...te repositories by name. view
90
81
  My Latest Repositories List your repositories by latest updated view
91
82
  My Starred Repositories List repositories you have starred view
92
83
  Workflow Runs Manage workflow runs for a selected GitHub repository. view
93
- Noti...ionsList inbox notifications fr...es or a selected repository. view
94
- Search ...ussionsSearch recent Discussion...bally in all repositories view
84
+ Notification List inbox notifications f...s or a selected repository. view
85
+ Search DiscussionsSearch recent Discussion...ally in all repositories view
95
86
  My Discussions Show your Discussions view
96
87
  My Projects Show your Projects view
97
88
 
@@ -108,7 +99,7 @@ test.skipIf(!extensionExists)('github extension shows command list on launch', a
108
99
  test.skipIf(!extensionExists)('github extension can navigate commands', async () => {
109
100
  // Wait for command list
110
101
  await session.text({
111
- waitFor: (text) => text.includes('My Pu...uest') && text.includes('Search ...sitories'),
102
+ waitFor: (text) => text.includes('My Pul...quests') && text.includes('Search Repositorie'),
112
103
  timeout: 30000,
113
104
  })
114
105
 
@@ -128,19 +119,19 @@ test.skipIf(!extensionExists)('github extension can navigate commands', async ()
128
119
  > Search commands...
129
120
 
130
121
  Commands
131
- My Pu...uest List pull requests you cre...d in, or were mentioned in. view
132
- Search ...RequestsSearch recent pull reque...lly in all repositories. view
133
- ›Create ...RequestCreate a pull request in...your GitHub repositories. view
122
+ My Pul...questsList pull requests you cr... in, or were mentioned in. view
123
+ Search Pull Request Search recent pull requ...ly in all repositories. view
124
+ ›Create Pull Reques Create a pull request i...our GitHub repositories. view
134
125
  My IssuesList issues created by you, ...ned to you or mentioning you. view
135
126
  Search Issues Search recent issues globally in all repositories. view
136
127
  Create Issue Create an issue in one of your GitHub repositories. view
137
128
  Create Branch Create a branch in one of your GitHub repositories view
138
- Search ...sitoriesSearch in your public or...te repositories by name. view
129
+ Search Repositorie Search in your public o...te repositories by name. view
139
130
  My Latest Repositories List your repositories by latest updated view
140
131
  My Starred Repositories List repositories you have starred view
141
132
  Workflow Runs Manage workflow runs for a selected GitHub repository. view
142
- Noti...ionsList inbox notifications fr...es or a selected repository. view
143
- Search ...ussionsSearch recent Discussion...bally in all repositories view
133
+ Notification List inbox notifications f...s or a selected repository. view
134
+ Search DiscussionsSearch recent Discussion...ally in all repositories view
144
135
  My Discussions Show your Discussions view
145
136
  My Projects Show your Projects view
146
137
 
@@ -239,11 +230,11 @@ test.skipIf(!extensionExists)('github extension can search commands', async () =
239
230
 
240
231
 
241
232
 
242
- ↵ run command ↑↓ navigate ^k actions :vim powered by termcast.app
243
233
 
244
234
 
245
235
 
246
236
 
237
+ ↵ run command ↑↓ navigate ^k actions :vim powered by termcast.app
247
238
 
248
239
 
249
240