termcast 1.3.52 → 1.3.54

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 (50) hide show
  1. package/dist/app.d.ts.map +1 -1
  2. package/dist/app.js +209 -7
  3. package/dist/app.js.map +1 -1
  4. package/dist/cli.js +4 -4
  5. package/dist/cli.js.map +1 -1
  6. package/dist/components/candle-chart.d.ts +110 -0
  7. package/dist/components/candle-chart.d.ts.map +1 -0
  8. package/dist/components/candle-chart.js +295 -0
  9. package/dist/components/candle-chart.js.map +1 -0
  10. package/dist/components/list.d.ts.map +1 -1
  11. package/dist/components/list.js +3 -0
  12. package/dist/components/list.js.map +1 -1
  13. package/dist/components/table.d.ts +2 -0
  14. package/dist/components/table.d.ts.map +1 -1
  15. package/dist/components/table.js +41 -4
  16. package/dist/components/table.js.map +1 -1
  17. package/dist/examples/simple-candle-chart-data.d.ts +9064 -0
  18. package/dist/examples/simple-candle-chart-data.d.ts.map +1 -0
  19. package/dist/examples/simple-candle-chart-data.js +12683 -0
  20. package/dist/examples/simple-candle-chart-data.js.map +1 -0
  21. package/dist/examples/simple-candle-chart.d.ts +2 -0
  22. package/dist/examples/simple-candle-chart.d.ts.map +1 -0
  23. package/dist/examples/simple-candle-chart.js +125 -0
  24. package/dist/examples/simple-candle-chart.js.map +1 -0
  25. package/dist/index.d.ts +2 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +2 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/internal/dialog.d.ts +1 -0
  30. package/dist/internal/dialog.d.ts.map +1 -1
  31. package/dist/internal/dialog.js +4 -0
  32. package/dist/internal/dialog.js.map +1 -1
  33. package/dist/state.d.ts +1 -0
  34. package/dist/state.d.ts.map +1 -1
  35. package/dist/state.js.map +1 -1
  36. package/package.json +1 -1
  37. package/src/app.tsx +269 -8
  38. package/src/cli.tsx +5 -5
  39. package/src/components/candle-chart.tsx +410 -0
  40. package/src/components/list.tsx +3 -0
  41. package/src/components/table.tsx +46 -4
  42. package/src/examples/simple-candle-chart-data.ts +12683 -0
  43. package/src/examples/simple-candle-chart.tsx +363 -0
  44. package/src/examples/simple-candle-chart.vitest.tsx +269 -0
  45. package/src/examples/simple-detail-table.vitest.tsx +2 -2
  46. package/src/examples/simple-table-wrap.vitest.tsx +19 -19
  47. package/src/examples/table-flex-grow.vitest.tsx +8 -8
  48. package/src/index.tsx +7 -0
  49. package/src/internal/dialog.tsx +5 -0
  50. package/src/state.tsx +1 -0
@@ -0,0 +1,363 @@
1
+ // Example: CandleChart component showing realistic crypto OHLC data.
2
+ // List of markets with candlestick charts in the side detail panel.
3
+ // Green bars = bullish (close >= open), red bars = bearish (close < open).
4
+ // Wicks extend from body to high/low extremes.
5
+ //
6
+ // Demonstrates:
7
+ // - CandleChart in List.Item.Detail (side panel)
8
+ // - CandleChart in full-page Detail (pushed on Enter)
9
+ // - Mixed component types: CandleChart + Graph overlay, CandleChart + BarChart volume,
10
+ // Row for side-by-side comparisons, metadata with tags
11
+ // - Realistic crypto market action from frozen Coinbase daily OHLCV data
12
+
13
+ import React from 'react'
14
+ import { Action, ActionPanel, List, Detail, Color, CandleChart, Graph, BarChart, Row } from 'termcast'
15
+ import type { CandleData } from 'termcast'
16
+ import { useNavigation } from 'termcast/src/internal/navigation'
17
+ import { renderWithProviders } from '../utils'
18
+ import { cryptoMarketData } from './simple-candle-chart-data'
19
+
20
+ interface Market {
21
+ symbol: string
22
+ displaySymbol: string
23
+ name: string
24
+ sector: string
25
+ currentPrice: number
26
+ change: number
27
+ priceDecimals: number
28
+ candles: CandleData[]
29
+ closingPrices: number[]
30
+ volume: number[]
31
+ }
32
+
33
+ interface MixedItem {
34
+ title: string
35
+ subtitle: string
36
+ market: Market
37
+ mode: 'candle-only' | 'candle-with-line' | 'candle-with-volume' | 'side-by-side'
38
+ }
39
+
40
+ const tickers: Market[] = cryptoMarketData.map((market) => {
41
+ const candles: CandleData[] = market.candles.map((candle) => {
42
+ return {
43
+ open: candle.open,
44
+ close: candle.close,
45
+ high: candle.high,
46
+ low: candle.low,
47
+ }
48
+ })
49
+
50
+ return {
51
+ symbol: market.symbol,
52
+ displaySymbol: market.symbol.replace('-USD', ''),
53
+ name: market.name,
54
+ sector: market.sector,
55
+ currentPrice: market.currentPrice,
56
+ change: market.change,
57
+ priceDecimals: market.priceDecimals,
58
+ candles,
59
+ closingPrices: candles.map((candle) => {
60
+ return candle.close
61
+ }),
62
+ volume: [...market.volume],
63
+ }
64
+ })
65
+
66
+ const mixedItems: MixedItem[] = [
67
+ { title: 'BTC - Candles', subtitle: 'Real BTC/USD hourly candles', market: tickers[0]!, mode: 'candle-only' },
68
+ { title: 'ETH - Candle + Line', subtitle: 'Candles plus closing line', market: tickers[1]!, mode: 'candle-with-line' },
69
+ { title: 'SOL - Candle + Volume', subtitle: 'Candles plus volume split', market: tickers[2]!, mode: 'candle-with-volume' },
70
+ { title: 'BTC vs ETH', subtitle: 'Side-by-side crypto leaders', market: tickers[0]!, mode: 'side-by-side' },
71
+ { title: 'DOGE - Candle + Line', subtitle: 'Low-priced asset formatting', market: tickers[4]!, mode: 'candle-with-line' },
72
+ ]
73
+
74
+ // 300 hourly candles ≈ 12.5 days
75
+ const xLabels = ['12d', '8d', '4d', 'Now']
76
+
77
+ function formatNumber({ value, decimals }: { value: number; decimals: number }): string {
78
+ return new Intl.NumberFormat('en-US', {
79
+ minimumFractionDigits: decimals,
80
+ maximumFractionDigits: decimals,
81
+ }).format(value)
82
+ }
83
+
84
+ function formatPrice({ value, decimals }: { value: number; decimals: number }): string {
85
+ return `$${formatNumber({ value, decimals })}`
86
+ }
87
+
88
+ function formatPercent(change: number): string {
89
+ return change >= 0 ? `+${change.toFixed(1)}%` : `${change.toFixed(1)}%`
90
+ }
91
+
92
+ function changeColor(change: number): Color.ColorLike {
93
+ if (change > 0) return Color.Green
94
+ if (change < 0) return Color.Red
95
+ return Color.SecondaryText
96
+ }
97
+
98
+ function yAxisFormatter({ market }: { market: Market }): (value: number) => string {
99
+ return (value) => {
100
+ return formatPrice({ value, decimals: market.priceDecimals })
101
+ }
102
+ }
103
+
104
+ function ChartForMode({ item, height }: { item: MixedItem; height: number }): any {
105
+ const { market } = item
106
+ const yFormat = yAxisFormatter({ market })
107
+
108
+ switch (item.mode) {
109
+ case 'candle-only': {
110
+ return (
111
+ <CandleChart
112
+ data={market.candles}
113
+ height={height}
114
+ xLabels={xLabels}
115
+ yTicks={4}
116
+ yFormat={yFormat}
117
+ />
118
+ )
119
+ }
120
+ case 'candle-with-line': {
121
+ return (
122
+ <>
123
+ <CandleChart
124
+ data={market.candles}
125
+ height={height}
126
+ xLabels={xLabels}
127
+ yTicks={4}
128
+ yFormat={yFormat}
129
+ />
130
+ <Graph
131
+ height={Math.max(5, height - 3)}
132
+ xLabels={xLabels}
133
+ yTicks={3}
134
+ yFormat={yFormat}
135
+ variant="area"
136
+ >
137
+ <Graph.Line data={market.closingPrices} color={Color.Blue} title="Close" />
138
+ </Graph>
139
+ </>
140
+ )
141
+ }
142
+ case 'candle-with-volume': {
143
+ const firstHalf = market.volume.slice(0, 15).reduce((sum, value) => {
144
+ return sum + value
145
+ }, 0)
146
+ const secondHalf = market.volume.slice(15).reduce((sum, value) => {
147
+ return sum + value
148
+ }, 0)
149
+
150
+ return (
151
+ <>
152
+ <CandleChart
153
+ data={market.candles}
154
+ height={height}
155
+ xLabels={xLabels}
156
+ yTicks={4}
157
+ yFormat={yFormat}
158
+ />
159
+ <BarChart height={1}>
160
+ <BarChart.Segment value={firstHalf} label="First half" color={Color.Blue} />
161
+ <BarChart.Segment value={secondHalf} label="Second half" color={Color.Orange} />
162
+ </BarChart>
163
+ </>
164
+ )
165
+ }
166
+ case 'side-by-side': {
167
+ const eth = tickers[1]!
168
+ return (
169
+ <Row>
170
+ <CandleChart
171
+ data={market.candles}
172
+ height={height}
173
+ xLabels={['30d', 'Now']}
174
+ yTicks={3}
175
+ yFormat={yAxisFormatter({ market })}
176
+ />
177
+ <CandleChart
178
+ data={eth.candles}
179
+ height={height}
180
+ xLabels={['30d', 'Now']}
181
+ yTicks={3}
182
+ yFormat={yAxisFormatter({ market: eth })}
183
+ />
184
+ </Row>
185
+ )
186
+ }
187
+ }
188
+ }
189
+
190
+ function CandleDetailView({ item }: { item: MixedItem }): any {
191
+ const { pop } = useNavigation()
192
+ const { market } = item
193
+ const changeStr = formatPercent(market.change)
194
+
195
+ const markdown = [
196
+ `# ${market.displaySymbol} - ${market.name}`,
197
+ '',
198
+ `**Category:** ${market.sector}`,
199
+ '',
200
+ `**Price:** ${formatPrice({ value: market.currentPrice, decimals: market.priceDecimals })} `,
201
+ `**24h change:** ${changeStr}`,
202
+ '',
203
+ `**Mode:** \`${item.mode}\``,
204
+ '',
205
+ '300 hourly candles from Coinbase Exchange, frozen so the example stays deterministic.',
206
+ ].join('\n')
207
+
208
+ return (
209
+ <Detail
210
+ navigationTitle={`${market.displaySymbol} Detail`}
211
+ markdown={markdown}
212
+ metadata={
213
+ <Detail.Metadata>
214
+ <ChartForMode item={item} height={15} />
215
+ <Detail.Metadata.Separator />
216
+ <Detail.Metadata.Label
217
+ title="Price"
218
+ text={{ value: formatPrice({ value: market.currentPrice, decimals: market.priceDecimals }), color: changeColor(market.change) }}
219
+ />
220
+ <Detail.Metadata.Label
221
+ title="Change"
222
+ text={{ value: changeStr, color: changeColor(market.change) }}
223
+ />
224
+ <Detail.Metadata.Label title="Category" text={market.sector} />
225
+ <Detail.Metadata.Separator />
226
+ <Detail.Metadata.TagList title="Components">
227
+ <Detail.Metadata.TagList.Item text="CandleChart" color={Color.Green} />
228
+ {item.mode === 'candle-with-line' && (
229
+ <Detail.Metadata.TagList.Item text="Graph" color={Color.Blue} />
230
+ )}
231
+ {item.mode === 'candle-with-volume' && (
232
+ <Detail.Metadata.TagList.Item text="BarChart" color={Color.Orange} />
233
+ )}
234
+ {item.mode === 'side-by-side' && (
235
+ <Detail.Metadata.TagList.Item text="Row" color={Color.Purple} />
236
+ )}
237
+ </Detail.Metadata.TagList>
238
+ </Detail.Metadata>
239
+ }
240
+ actions={
241
+ <ActionPanel>
242
+ <Action title="Go Back" onAction={() => { pop() }} />
243
+ </ActionPanel>
244
+ }
245
+ />
246
+ )
247
+ }
248
+
249
+ function SimpleCandleChart() {
250
+ const { push } = useNavigation()
251
+
252
+ return (
253
+ <List
254
+ navigationTitle="Crypto Markets"
255
+ searchBarPlaceholder="Search markets..."
256
+ isShowingDetail={true}
257
+ >
258
+ <List.Section title="Watchlist">
259
+ {tickers.map((market) => {
260
+ const changeStr = formatPercent(market.change)
261
+ return (
262
+ <List.Item
263
+ key={market.symbol}
264
+ id={market.symbol}
265
+ title={market.displaySymbol}
266
+ subtitle={market.name}
267
+ accessories={[
268
+ { text: { value: formatPrice({ value: market.currentPrice, decimals: market.priceDecimals }), color: changeColor(market.change) } },
269
+ { text: { value: changeStr, color: changeColor(market.change) } },
270
+ ]}
271
+ detail={
272
+ <List.Item.Detail
273
+ metadata={
274
+ <List.Item.Detail.Metadata>
275
+ <CandleChart
276
+ data={market.candles}
277
+ height={10}
278
+ xLabels={xLabels}
279
+ yTicks={4}
280
+ yFormat={yAxisFormatter({ market })}
281
+ />
282
+ <List.Item.Detail.Metadata.Label
283
+ title="Price"
284
+ text={{ value: formatPrice({ value: market.currentPrice, decimals: market.priceDecimals }), color: changeColor(market.change) }}
285
+ />
286
+ <List.Item.Detail.Metadata.Label
287
+ title="Change"
288
+ text={{ value: changeStr, color: changeColor(market.change) }}
289
+ />
290
+ <List.Item.Detail.Metadata.Label title="Category" text={market.sector} />
291
+ <List.Item.Detail.Metadata.Separator />
292
+ <List.Item.Detail.Metadata.Label title={`${market.symbol} Hourly OHLC`} />
293
+ </List.Item.Detail.Metadata>
294
+ }
295
+ />
296
+ }
297
+ actions={
298
+ <ActionPanel>
299
+ <Action
300
+ title="Open Detail"
301
+ onAction={() => {
302
+ push(
303
+ <CandleDetailView
304
+ item={{ title: market.displaySymbol, subtitle: market.name, market, mode: 'candle-only' }}
305
+ />,
306
+ )
307
+ }}
308
+ />
309
+ </ActionPanel>
310
+ }
311
+ />
312
+ )
313
+ })}
314
+ </List.Section>
315
+
316
+ <List.Section title="Mixed Components">
317
+ {mixedItems.map((item) => {
318
+ const { market } = item
319
+ const changeStr = formatPercent(market.change)
320
+ return (
321
+ <List.Item
322
+ key={item.title}
323
+ id={item.title}
324
+ title={item.title}
325
+ subtitle={item.subtitle}
326
+ accessories={[
327
+ { text: { value: formatPrice({ value: market.currentPrice, decimals: market.priceDecimals }), color: changeColor(market.change) } },
328
+ ]}
329
+ detail={
330
+ <List.Item.Detail
331
+ metadata={
332
+ <List.Item.Detail.Metadata>
333
+ <ChartForMode item={item} height={8} />
334
+ <List.Item.Detail.Metadata.Separator />
335
+ <List.Item.Detail.Metadata.Label
336
+ title="Price"
337
+ text={{ value: formatPrice({ value: market.currentPrice, decimals: market.priceDecimals }), color: changeColor(market.change) }}
338
+ />
339
+ <List.Item.Detail.Metadata.Label
340
+ title="Change"
341
+ text={{ value: changeStr, color: changeColor(market.change) }}
342
+ />
343
+ </List.Item.Detail.Metadata>
344
+ }
345
+ />
346
+ }
347
+ actions={
348
+ <ActionPanel>
349
+ <Action
350
+ title="Open Detail"
351
+ onAction={() => { push(<CandleDetailView item={item} />) }}
352
+ />
353
+ </ActionPanel>
354
+ }
355
+ />
356
+ )
357
+ })}
358
+ </List.Section>
359
+ </List>
360
+ )
361
+ }
362
+
363
+ renderWithProviders(<SimpleCandleChart />)
@@ -0,0 +1,269 @@
1
+ // E2E tests for CandleChart component showing realistic crypto OHLC candles.
2
+ // Verifies candle body (▌/▘/▖) and wick (│) characters render,
3
+ // Y-axis labels, X-axis labels, and navigation between tickers.
4
+ // Also tests full-page Detail view and mixed component types
5
+ // (CandleChart + Graph, CandleChart + BarChart, Row side-by-side).
6
+
7
+ import { test, expect, afterEach, beforeEach } from 'vitest'
8
+ import { launchTerminal, Session } from 'tuistory/src'
9
+
10
+ let session: Session
11
+
12
+ beforeEach(async () => {
13
+ session = await launchTerminal({
14
+ command: 'bun',
15
+ args: ['src/examples/simple-candle-chart.tsx'],
16
+ cols: 100,
17
+ rows: 30,
18
+ })
19
+ })
20
+
21
+ afterEach(() => {
22
+ session?.close()
23
+ })
24
+
25
+ test('candle chart renders in list detail with axes', async () => {
26
+ const text = await session.text({
27
+ waitFor: (text) => {
28
+ return text.includes('BTC') && text.includes('$') && text.includes('12d')
29
+ },
30
+ timeout: 10000,
31
+ })
32
+
33
+ expect(text).toMatchInlineSnapshot(`
34
+ "
35
+
36
+
37
+ Crypto Markets ───────────────────────────────────────────────────────────────────────────────
38
+
39
+ > Search markets...
40
+
41
+ Watchlist │ $74,678│ │
42
+ ›BTC Bitcoin │ │ ▌▌▖│
43
+ ETH Ethereum │ │ ▌│▘▌▖│
44
+ SOL Solana │ $70,438│ │ │ ▌ │▘▘▌
45
+ XRP XRP │ │ ▖▖▖▖ ▌▌ ▖▖▌ ▌││
46
+ DOGE Dogecoin │ │ ▌││▌▖▖ ▖▖│││▌▘▌▌│ ▘▘▘▌
47
+ BNB BNB │ $66,197│▖▖ │▌ ▘▘▘▌▖▖▌▘▌▖▌▌▌ ▘▘
48
+ │ ││▌▖ │▌▘ ▘▘▌▌ ▘▘▘▘
49
+ Mixed Components │ │ │▌▌▘▘ ▘▘
50
+ BTC - Candles Real BTC/USD hourly candles │ $61,957│ ││
51
+ ETH - Candle + Line Candles plus closing line │ 12d 8d 4d Now
52
+ SOL - Candle + Volume Candles plus volume spli │
53
+ BTC vs ETH Side-by-side crypto leaders │ Price: $67,641
54
+ DOGE - Candle + Line Low-priced asset formatti │
55
+ │ Change: -0.2%
56
+
57
+ │ Category: Store of Value
58
+
59
+ │ ────────────────────────────────────────────
60
+
61
+ ↵ open detail ↑↓ navigate ^k actions │ BTC-USD Hourly OHLC
62
+
63
+
64
+ "
65
+ `)
66
+
67
+ expect(text).toContain('BTC')
68
+ expect(text).toContain('12d')
69
+ expect(text).toContain('Now')
70
+ expect(text).toContain('$')
71
+ }, 30000)
72
+
73
+ test('push to full-page detail view on Enter', async () => {
74
+ await session.text({ waitFor: (t) => t.includes('BTC'), timeout: 10000 })
75
+ // Press Enter to push to Detail view
76
+ session.sendKey('return')
77
+
78
+ const text = await session.text({
79
+ waitFor: (t) => t.includes('candle-only') && t.includes('Go Back'),
80
+ timeout: 10000,
81
+ })
82
+
83
+ expect(text).toMatchInlineSnapshot(`
84
+ "
85
+
86
+
87
+
88
+
89
+ BTC - Bitcoin
90
+
91
+ Category: Store of Value
92
+
93
+ Price: $67,641
94
+ 24h change: -0.2%
95
+
96
+ Mode: candle-only
97
+
98
+ 300 hourly candles from Coinbase Exchange, frozen so the example stays deterministic.
99
+
100
+ $74,678│ │
101
+ │ ▖▌▌│ │
102
+ │ ▌│▘▌▖▖▌▌
103
+ │ │▌ ││ ▌│││
104
+ │ ▌▘ ▘▌▖▌▌▖▖
105
+ $70,438│ │ │ │▌ │││▌
106
+ │ ▌▌ │ ▌▌▖▖ ││││▌▘ ▘▌
107
+ │ ▌▘▌▌▘▌▖│ ││ │ ▌││▘▌▖│▌▌▖▖▌ ▌▖▖▖▖▖▖▖
108
+ │ ▖▌ │││ ▌▌▘▌▌▌ │▖▖▖ ││ ││ ▌ │▌│▌││▘▘ ││ ▘▘│▘
109
+ $66,197│▖▖▖ │ ▌ ▘▘ │ ▘▌▖ │▌▘ ▘▌▌▌│▖▌▌▖▌ ▘▌▌
110
+
111
+
112
+ esc go back ^k actions ↵ Go Back powered by termcast.app
113
+
114
+ "
115
+ `)
116
+
117
+ // Full-page detail has larger chart and markdown content
118
+ expect(text).toContain('Bitcoin')
119
+ expect(text).toContain('candle-only')
120
+ }, 30000)
121
+
122
+ test('candle + line overlay (mixed components)', async () => {
123
+ await session.text({ waitFor: (t) => t.includes('Mixed Components'), timeout: 10000 })
124
+ // Navigate past Watchlist (6 items) to Mixed Components section,
125
+ // then one more item to ETH - Candle + Line.
126
+ for (let i = 0; i < 7; i++) {
127
+ session.sendKey('down')
128
+ }
129
+
130
+ const text = await session.text({
131
+ waitFor: (t) => t.includes('›ETH - Candle + Line') && t.includes('closing line'),
132
+ timeout: 10000,
133
+ })
134
+
135
+ expect(text).toMatchInlineSnapshot(`
136
+ "
137
+
138
+
139
+ Crypto Markets ───────────────────────────────────────────────────────────────────────────────
140
+
141
+ > Search markets...
142
+
143
+ Watchlist │ $2,220│ │
144
+ BTC Bitcoin │ │ │ ▌▌▖▖
145
+ ETH Ethereum │ $2,073│ ▖▖▖ │ ▖▖ ▖▌▘▘▌▖▖▖
146
+ SOL Solana │ │ │▌│▘▌▌▌ ▖▖ ▌▘▌││▌│ │││▌
147
+ XRP XRP │ │ │▌▘ │ ▌▖ ▖▌▘▌▖▌ ▌▌▘▘ ▌▌▘▘
148
+ DOGE Dogecoin │ $1,927│▖▖ ▖▌ │▘▌▖▌ │││ │
149
+ BNB BNB │ │▘▘▌▖▌▘│ ▘▘
150
+ │ $1,780│ ▘▘
151
+ Mixed Components │ 12d 8d 4d Now
152
+ BTC - Candles Real BTC/USD hourly candles │
153
+ ›ETH - Candle + Line Candles plus closing line │ $2,197│ ⢠⣆⣠⡀
154
+ SOL - Candle + Volume Candles plus volume spli │ │ ⣴⣤⣤ ⣀ ⢠⣀ ⢠⣿⣿⣿⣷⣶⣦⡀
155
+ BTC vs ETH Side-by-side crypto leaders │ $1,997│ ⢰⣿⣿⣿⣿⣿⣇ ⣠⣷⣴⣄⣀⢸⣿⣶⣠⣤⣼⣿⣿⣿⣿⣿⣿⣧⣤⣤⣤
156
+ DOGE - Candle + Line Low-priced asset formatti │ │⣶⡄ ⣦⣾⣿⣿⣿⣿⣿⣿⣶⣶⢠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
157
+ │ $1,797│⣿⣿⣷⣦⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
158
+ │ 12d 8d 4d Now
159
+
160
+ │ ────────────────────────────────────────────
161
+
162
+ │ Price: $1,971
163
+
164
+ ↵ open detail ↑↓ navigate ^k actions │ Change: -0.3%
165
+
166
+ "
167
+ `)
168
+
169
+ expect(text).toContain('Candle + Line')
170
+ }, 30000)
171
+
172
+ test('candle + volume bar chart (mixed components)', async () => {
173
+ await session.text({ waitFor: (t) => t.includes('Mixed Components'), timeout: 10000 })
174
+ // Navigate to "Candle + Volume" item (8 downs from top)
175
+ for (let i = 0; i < 8; i++) {
176
+ session.sendKey('down')
177
+ }
178
+
179
+ const text = await session.text({
180
+ waitFor: (t) => t.includes('›SOL - Candle + Volume') && t.includes('half'),
181
+ timeout: 10000,
182
+ })
183
+
184
+ expect(text).toMatchInlineSnapshot(`
185
+ "
186
+
187
+
188
+ Crypto Markets ───────────────────────────────────────────────────────────────────────────────
189
+
190
+ > Search markets...
191
+
192
+ Watchlist │ $95.03│ ││
193
+ BTC Bitcoin │ │ │ │ ▖▖▖▖
194
+ ETH Ethereum │ $88.26│ ▌▌▖ ││ │ ▖▖ ▌▘▘▘▌▌▌│
195
+ SOL Solana │ │ ▖▌│▘▌▌▌ │▌▌▖▖▌▘▌▖▌▘ ▘▌│││
196
+ XRP XRP │ │ │▌ │ ▌▖│ ▌▘▘▘▌▌ ▘▘││ ▘▘▘▌
197
+ DOGE Dogecoin │ $81.48│▖▖ ▌▘ │▘▌▌▘ │
198
+ BNB BNB │ │▘▘▌▖▌▘ ▘▘
199
+ │ $74.71│ ▘▘│
200
+ Mixed Components │ 12d 8d 4d Now
201
+ BTC - Candles Real BTC/USD hourly candles │
202
+ ETH - Candle + Line Candles plus closing line │ ┌Second half: 95.3%┐
203
+ ›SOL - Candle + Volume Candles plus volume spli │
204
+ BTC vs ETH Side-by-side crypto leaders │
205
+ DOGE - Candle + Line Low-priced asset formatti │ ────────────────────────────────────────────
206
+
207
+ │ Price: $83.31
208
+
209
+ ↵ open detail ↑↓ navigate ^k actions │ Change: -0.4%
210
+
211
+
212
+
213
+
214
+
215
+ "
216
+ `)
217
+
218
+ expect(text).toContain('Candle + Volume')
219
+ }, 30000)
220
+
221
+ test('side-by-side candle charts in Row', async () => {
222
+ await session.text({ waitFor: (t) => t.includes('Mixed Components'), timeout: 10000 })
223
+ // Navigate to "BTC vs ETH" item (9 downs from top)
224
+ for (let i = 0; i < 9; i++) {
225
+ session.sendKey('down')
226
+ }
227
+
228
+ const text = await session.text({
229
+ waitFor: (t) => t.includes('›BTC vs ETH'),
230
+ timeout: 10000,
231
+ })
232
+
233
+ expect(text).toMatchInlineSnapshot(`
234
+ "
235
+
236
+
237
+ Crypto Markets ───────────────────────────────────────────────────────────────────────────────
238
+
239
+ > Search markets...
240
+
241
+ Watchlist │ $74,678│ ││ $2,220│ │
242
+ BTC Bitcoin │ │ ▌▌ │ │ ││
243
+ ETH Ethereum │ │ │ │ ▌▘▌ │ ▖▖│ │ ▌▌▖
244
+ SOL Solana │ │ ▖▖ ││▌ ▌│ │ ▌▘▌ ││││▌│▌
245
+ XRP XRP │ $68,318│ ▌▘▌ ││▌▘▘ ▘▘ $2,000│ ▌│▌ ▌▌▌▌▌ ▘▘
246
+ DOGE Dogecoin │ │▖ ▌│▌▖▌▘▘│ ││ ▌ ▘▌▌│││
247
+ BNB BNB │ │▌▌▘ ▘▘ │▌▖▌ ││
248
+ │ $61,957│ │ │ $1,780│││
249
+ Mixed Components │ 30d Now 30d Now
250
+ BTC - Candles Real BTC/USD hourly candles │
251
+ ETH - Candle + Line Candles plus closing line │ ────────────────────────────────────────────
252
+ SOL - Candle + Volume Candles plus volume spli │
253
+ ›BTC vs ETH Side-by-side crypto leaders │ Price: $67,641
254
+ DOGE - Candle + Line Low-priced asset formatti │
255
+ │ Change: -0.2%
256
+
257
+
258
+ ↵ open detail ↑↓ navigate ^k actions │
259
+
260
+
261
+
262
+
263
+
264
+ "
265
+ `)
266
+
267
+ expect(text).toContain('BTC vs ETH')
268
+ expect(text).toContain('Side-by-side')
269
+ }, 30000)
@@ -66,6 +66,7 @@ test('markdown tables render with borderless layout', async () => {
66
66
  ap-south-1 89ms /api/health 500
67
67
 
68
68
 
69
+ esc go back ^k actions powered by termcast.app
69
70
 
70
71
 
71
72
 
@@ -82,7 +83,6 @@ test('markdown tables render with borderless layout', async () => {
82
83
 
83
84
 
84
85
 
85
- esc go back ^k actions powered by termcast.app
86
86
 
87
87
  "
88
88
  `)
@@ -166,6 +166,7 @@ test('two tables render side by side in a Row', async () => {
166
166
  ap-south-1 89ms /api/health 500
167
167
 
168
168
 
169
+ esc go back ^k actions powered by termcast.app
169
170
 
170
171
 
171
172
 
@@ -182,7 +183,6 @@ test('two tables render side by side in a Row', async () => {
182
183
 
183
184
 
184
185
 
185
- esc go back ^k actions powered by termcast.app
186
186
 
187
187
  "
188
188
  `)