termcast 1.3.49 → 1.3.51

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 (164) hide show
  1. package/dist/apis/environment.d.ts +1 -0
  2. package/dist/apis/environment.d.ts.map +1 -1
  3. package/dist/apis/environment.js +5 -0
  4. package/dist/apis/environment.js.map +1 -1
  5. package/dist/app.d.ts +33 -0
  6. package/dist/app.d.ts.map +1 -0
  7. package/dist/app.js +1125 -0
  8. package/dist/app.js.map +1 -0
  9. package/dist/cli.js +80 -0
  10. package/dist/cli.js.map +1 -1
  11. package/dist/components/detail.d.ts.map +1 -1
  12. package/dist/components/detail.js +20 -17
  13. package/dist/components/detail.js.map +1 -1
  14. package/dist/components/dropdown.d.ts.map +1 -1
  15. package/dist/components/dropdown.js +3 -2
  16. package/dist/components/dropdown.js.map +1 -1
  17. package/dist/components/footer.d.ts +6 -0
  18. package/dist/components/footer.d.ts.map +1 -1
  19. package/dist/components/footer.js +15 -6
  20. package/dist/components/footer.js.map +1 -1
  21. package/dist/components/form/checkbox.d.ts.map +1 -1
  22. package/dist/components/form/checkbox.js +1 -13
  23. package/dist/components/form/checkbox.js.map +1 -1
  24. package/dist/components/form/date-picker.js +2 -2
  25. package/dist/components/form/date-picker.js.map +1 -1
  26. package/dist/components/form/description.js +1 -1
  27. package/dist/components/form/description.js.map +1 -1
  28. package/dist/components/form/dropdown.d.ts.map +1 -1
  29. package/dist/components/form/dropdown.js +19 -3
  30. package/dist/components/form/dropdown.js.map +1 -1
  31. package/dist/components/form/file-picker.d.ts.map +1 -1
  32. package/dist/components/form/file-picker.js +22 -4
  33. package/dist/components/form/file-picker.js.map +1 -1
  34. package/dist/components/form/index.d.ts +3 -1
  35. package/dist/components/form/index.d.ts.map +1 -1
  36. package/dist/components/form/index.js +6 -4
  37. package/dist/components/form/index.js.map +1 -1
  38. package/dist/components/form/password-field.js +3 -3
  39. package/dist/components/form/password-field.js.map +1 -1
  40. package/dist/components/form/text-area.d.ts.map +1 -1
  41. package/dist/components/form/text-area.js +29 -6
  42. package/dist/components/form/text-area.js.map +1 -1
  43. package/dist/components/form/text-field.js +3 -3
  44. package/dist/components/form/text-field.js.map +1 -1
  45. package/dist/components/heatmap.d.ts +80 -0
  46. package/dist/components/heatmap.d.ts.map +1 -0
  47. package/dist/components/heatmap.js +405 -0
  48. package/dist/components/heatmap.js.map +1 -0
  49. package/dist/components/list.d.ts +2 -0
  50. package/dist/components/list.d.ts.map +1 -1
  51. package/dist/components/list.js +80 -52
  52. package/dist/components/list.js.map +1 -1
  53. package/dist/components/markdown.d.ts +7 -0
  54. package/dist/components/markdown.d.ts.map +1 -0
  55. package/dist/components/markdown.js +19 -0
  56. package/dist/components/markdown.js.map +1 -0
  57. package/dist/components/metadata.d.ts.map +1 -1
  58. package/dist/components/metadata.js +4 -1
  59. package/dist/components/metadata.js.map +1 -1
  60. package/dist/components/progress-bar.d.ts +37 -0
  61. package/dist/components/progress-bar.d.ts.map +1 -0
  62. package/dist/components/progress-bar.js +34 -0
  63. package/dist/components/progress-bar.js.map +1 -0
  64. package/dist/components/table.d.ts +3 -2
  65. package/dist/components/table.d.ts.map +1 -1
  66. package/dist/components/table.js +78 -63
  67. package/dist/components/table.js.map +1 -1
  68. package/dist/diagram-parser.d.ts +17 -3
  69. package/dist/diagram-parser.d.ts.map +1 -1
  70. package/dist/diagram-parser.js +17 -3
  71. package/dist/diagram-parser.js.map +1 -1
  72. package/dist/examples/list-slot.d.ts +2 -0
  73. package/dist/examples/list-slot.d.ts.map +1 -0
  74. package/dist/examples/list-slot.js +14 -0
  75. package/dist/examples/list-slot.js.map +1 -0
  76. package/dist/examples/list-with-dropdown.js +2 -4
  77. package/dist/examples/list-with-dropdown.js.map +1 -1
  78. package/dist/examples/simple-heatmap.d.ts +2 -0
  79. package/dist/examples/simple-heatmap.d.ts.map +1 -0
  80. package/dist/examples/simple-heatmap.js +37 -0
  81. package/dist/examples/simple-heatmap.js.map +1 -0
  82. package/dist/examples/simple-progress-bar.d.ts +2 -0
  83. package/dist/examples/simple-progress-bar.d.ts.map +1 -0
  84. package/dist/examples/simple-progress-bar.js +36 -0
  85. package/dist/examples/simple-progress-bar.js.map +1 -0
  86. package/dist/index.d.ts +6 -0
  87. package/dist/index.d.ts.map +1 -1
  88. package/dist/index.js +6 -0
  89. package/dist/index.js.map +1 -1
  90. package/dist/internal/date-picker-widget.d.ts.map +1 -1
  91. package/dist/internal/date-picker-widget.js +5 -4
  92. package/dist/internal/date-picker-widget.js.map +1 -1
  93. package/dist/internal/navigation.d.ts.map +1 -1
  94. package/dist/internal/navigation.js +7 -2
  95. package/dist/internal/navigation.js.map +1 -1
  96. package/dist/internal/providers.d.ts.map +1 -1
  97. package/dist/internal/providers.js +42 -4
  98. package/dist/internal/providers.js.map +1 -1
  99. package/dist/logger.js +6 -1
  100. package/dist/logger.js.map +1 -1
  101. package/dist/state.d.ts +2 -0
  102. package/dist/state.d.ts.map +1 -1
  103. package/dist/state.js +31 -2
  104. package/dist/state.js.map +1 -1
  105. package/dist/theme.d.ts +1 -0
  106. package/dist/theme.d.ts.map +1 -1
  107. package/dist/theme.js +23 -1
  108. package/dist/theme.js.map +1 -1
  109. package/dist/utils.d.ts.map +1 -1
  110. package/dist/utils.js +6 -1
  111. package/dist/utils.js.map +1 -1
  112. package/package.json +4 -4
  113. package/src/apis/environment.tsx +6 -0
  114. package/src/app.tsx +1487 -0
  115. package/src/assets/default-app-icon.png +0 -0
  116. package/src/cli.tsx +105 -0
  117. package/src/components/detail.tsx +32 -22
  118. package/src/components/dropdown.tsx +3 -2
  119. package/src/components/footer.tsx +37 -7
  120. package/src/components/form/checkbox.tsx +2 -17
  121. package/src/components/form/date-picker.tsx +2 -2
  122. package/src/components/form/description.tsx +1 -1
  123. package/src/components/form/dropdown.tsx +22 -3
  124. package/src/components/form/file-picker.tsx +33 -10
  125. package/src/components/form/index.tsx +10 -6
  126. package/src/components/form/password-field.tsx +3 -3
  127. package/src/components/form/text-area.tsx +31 -6
  128. package/src/components/form/text-field.tsx +3 -3
  129. package/src/components/heatmap.tsx +584 -0
  130. package/src/components/list.tsx +135 -72
  131. package/src/components/markdown.tsx +30 -0
  132. package/src/components/metadata.tsx +9 -2
  133. package/src/components/progress-bar.tsx +112 -0
  134. package/src/components/table.tsx +88 -71
  135. package/src/diagram-parser.tsx +17 -3
  136. package/src/examples/bar-graph-weekly.vitest.tsx +4 -4
  137. package/src/examples/detail-metadata-showcase.vitest.tsx +12 -12
  138. package/src/examples/form-basic.vitest.tsx +117 -16
  139. package/src/examples/graph-bar-chart.vitest.tsx +2 -2
  140. package/src/examples/graph-row.vitest.tsx +10 -10
  141. package/src/examples/internal/descendants-rerender.vitest.tsx +94 -46
  142. package/src/examples/internal/simple-scrollbox.vitest.tsx +38 -14
  143. package/src/examples/list-dropdown-default.vitest.tsx +78 -58
  144. package/src/examples/list-slot.tsx +38 -0
  145. package/src/examples/list-with-detail.vitest.tsx +8 -8
  146. package/src/examples/list-with-dropdown.tsx +2 -2
  147. package/src/examples/list-with-dropdown.vitest.tsx +16 -16
  148. package/src/examples/list-with-sections.vitest.tsx +45 -32
  149. package/src/examples/simple-detail-table.vitest.tsx +2 -2
  150. package/src/examples/simple-file-picker.vitest.tsx +1 -1
  151. package/src/examples/simple-grid.vitest.tsx +27 -53
  152. package/src/examples/simple-heatmap.tsx +63 -0
  153. package/src/examples/simple-heatmap.vitest.tsx +88 -0
  154. package/src/examples/simple-progress-bar.tsx +82 -0
  155. package/src/examples/simple-progress-bar.vitest.tsx +72 -0
  156. package/src/examples/table-edge-cases.vitest.tsx +1 -1
  157. package/src/index.tsx +19 -0
  158. package/src/internal/date-picker-widget.tsx +23 -12
  159. package/src/internal/navigation.tsx +7 -2
  160. package/src/internal/providers.tsx +48 -3
  161. package/src/logger.tsx +6 -1
  162. package/src/state.tsx +38 -2
  163. package/src/theme.tsx +26 -2
  164. package/src/utils.tsx +6 -1
@@ -33,7 +33,7 @@ test('list with dropdown navigation', async () => {
33
33
  "
34
34
 
35
35
 
36
- Search Beers ───────────────────────────────────────────────────
36
+ Search Beers ──────────────────────────────────────────── ◆ Acme
37
37
 
38
38
  > Search... All ▾
39
39
 
@@ -49,7 +49,7 @@ test('list with dropdown navigation', async () => {
49
49
  Apple Juice Juice
50
50
 
51
51
 
52
- ↑↓ navigate ^p dropdown ^k actions
52
+ ↑↓ navigate ^k actions ^p select drink type
53
53
 
54
54
 
55
55
 
@@ -69,7 +69,7 @@ test('list with dropdown navigation', async () => {
69
69
  "
70
70
 
71
71
 
72
- Search Beers ───────────────────────────────────────────────────
72
+ Search Beers ──────────────────────────────────────────── ◆ Acme
73
73
  ╭────────────────────────────────────────────────────────────────╮
74
74
  │ │
75
75
  │ Select Drink Type esc │
@@ -103,7 +103,7 @@ test('list with dropdown navigation', async () => {
103
103
  "
104
104
 
105
105
 
106
- Search Beers ───────────────────────────────────────────────────
106
+ Search Beers ──────────────────────────────────────────── ◆ Acme
107
107
  ╭────────────────────────────────────────────────────────────────╮
108
108
  │ │
109
109
  │ Select Drink Type esc │
@@ -135,7 +135,7 @@ test('list with dropdown navigation', async () => {
135
135
  "
136
136
 
137
137
 
138
- Search Beers ───────────────────────────────────────────────────
138
+ Search Beers ──────────────────────────────────────────── ◆ Acme
139
139
  ╭────────────────────────────────────────────────────────────────╮
140
140
  │ │
141
141
  │ Select Drink Type esc │
@@ -167,7 +167,7 @@ test('list with dropdown navigation', async () => {
167
167
  "
168
168
 
169
169
 
170
- Search Beers ───────────────────────────────────────────────────
170
+ Search Beers ──────────────────────────────────────────── ◆ Acme
171
171
  ╭────────────────────────────────────────────────────────────────╮
172
172
  │ │
173
173
  │ Select Drink Type esc │
@@ -203,7 +203,7 @@ test('list with dropdown navigation', async () => {
203
203
  "
204
204
 
205
205
 
206
- Search Beers ───────────────────────────────────────────────────
206
+ Search Beers ──────────────────────────────────────────── ◆ Acme
207
207
 
208
208
  > Search... Wine ▾
209
209
 
@@ -215,7 +215,7 @@ test('list with dropdown navigation', async () => {
215
215
 
216
216
 
217
217
 
218
- ↑↓ navigate ^p dropdown ^k actions
218
+ ↑↓ navigate ^k actions ^p select drink type
219
219
 
220
220
 
221
221
 
@@ -253,7 +253,7 @@ test('small screen: dropdown accessory wastes vertical space', async () => {
253
253
  "
254
254
 
255
255
 
256
- Search Beers ─────────────────────────────────────────
256
+ Search Beers ────────────────────────────────── ◆ Acme
257
257
 
258
258
  > Search... All ▾
259
259
 
@@ -265,7 +265,7 @@ test('small screen: dropdown accessory wastes vertical space', async () => {
265
265
  Pinot Noir Wine
266
266
 
267
267
 
268
- ↑↓ navigate ^p dropdown ^k actions"
268
+ ↑↓ navigate ^k actions ^p select drink type"
269
269
  `)
270
270
  } finally {
271
271
  smallSession.close()
@@ -292,7 +292,7 @@ test('list with dropdown search and filter', async () => {
292
292
  "
293
293
 
294
294
 
295
- Search Beers ───────────────────────────────────────────────────
295
+ Search Beers ──────────────────────────────────────────── ◆ Acme
296
296
  ╭────────────────────────────────────────────────────────────────╮
297
297
  │ │
298
298
  │ Select Drink Type esc │
@@ -334,7 +334,7 @@ test('list with dropdown search and filter', async () => {
334
334
  "
335
335
 
336
336
 
337
- Search Beers ───────────────────────────────────────────────────
337
+ Search Beers ──────────────────────────────────────────── ◆ Acme
338
338
 
339
339
  > Search... All ▾
340
340
 
@@ -370,7 +370,7 @@ test('list with dropdown search and filter', async () => {
370
370
  "
371
371
 
372
372
 
373
- Search Beers ───────────────────────────────────────────────────
373
+ Search Beers ──────────────────────────────────────────── ◆ Acme
374
374
 
375
375
  > Search... Wine ▾
376
376
 
@@ -382,7 +382,7 @@ test('list with dropdown search and filter', async () => {
382
382
 
383
383
 
384
384
 
385
- ↑↓ navigate ^p dropdown ^k actions
385
+ ↑↓ navigate ^k actions ^p select drink type
386
386
 
387
387
 
388
388
 
@@ -406,7 +406,7 @@ test('list with dropdown search and filter', async () => {
406
406
  "
407
407
 
408
408
 
409
- Search Beers ───────────────────────────────────────────────────
409
+ Search Beers ──────────────────────────────────────────── ◆ Acme
410
410
 
411
411
  > pinot Wine ▾
412
412
 
@@ -418,7 +418,7 @@ test('list with dropdown search and filter', async () => {
418
418
 
419
419
 
420
420
 
421
- ↑↓ navigate ^p dropdown ^k actions
421
+ ↑↓ navigate ^k actions ^p select drink type
422
422
 
423
423
 
424
424
 
@@ -334,67 +334,79 @@ test('list click functionality', async () => {
334
334
  },
335
335
  })
336
336
 
337
- // Click on "Banana" item (visible in initial view)
337
+ // Click on "Banana" should immediately execute first action (navigate to detail)
338
338
  await session.click('Banana', { first: true })
339
339
 
340
- const afterClickBananaSnapshot = await session.text()
341
- expect(afterClickBananaSnapshot).toMatchInlineSnapshot(`
340
+ const afterClickBanana = await session.text()
341
+ expect(afterClickBanana).toMatchInlineSnapshot(`
342
342
  "
343
343
 
344
344
 
345
- Simple List Example ────────────────────────────────────────────
346
345
 
347
- > Search items...
348
346
 
349
- Fruits
350
- Apple Red and sweet Fresh Popular
351
- ›Banana Yellow and nutritious Ripe
352
- Orange Citrus and juicy Fresh
353
- Grape Sweet clusters Seasonal
354
- Mango Tropical delight Imported
355
- Pineapple Sweet and tangy
356
- Strawberry Red and sweet Popular
347
+ Banana
357
348
 
349
+ A yellow tropical fruit that's nutritious and energy-rich.
358
350
 
351
+ Benefits
352
+
353
+ - High in potassium
354
+ - Natural energy booster
355
+ - Aids digestion
356
+
357
+
358
+
359
+ esc go back ^k actions
359
360
 
360
- ↵ view details ↑↓ navigate ^k actions
361
361
 
362
362
  "
363
363
  `)
364
+ expect(afterClickBanana).toContain('Banana')
365
+ expect(afterClickBanana).toContain('go back')
364
366
 
365
- // Click on "Apple" item
367
+ // Go back to list
368
+ await session.press('escape')
369
+ await session.text({ waitFor: (text) => /search/i.test(text) })
370
+
371
+ // Click on "Apple" — should also immediately execute (Apple has actions)
366
372
  await session.click('Apple', { first: true })
367
373
 
368
- const afterClickAppleSnapshot = await session.text()
369
- expect(afterClickAppleSnapshot).toMatchInlineSnapshot(`
374
+ const afterClickApple = await session.text()
375
+ expect(afterClickApple).toMatchInlineSnapshot(`
370
376
  "
371
377
 
372
378
 
373
- Simple List Example ────────────────────────────────────────────
374
379
 
375
- > Search items...
376
380
 
377
- Fruits
378
- ›Apple Red and sweet Fresh Popular
379
- Banana Yellow and nutritious Ripe
380
- Orange Citrus and juicy Fresh
381
- Grape Sweet clusters Seasonal
382
- Mango Tropical delight Imported
383
- Pineapple Sweet and tangy
384
- Strawberry Red and sweet Popular
381
+ Apple
385
382
 
383
+ A delicious red fruit that's sweet and crunchy.
386
384
 
385
+ Nutrition Facts
386
+
387
+ - High in fiber
388
+ - Rich in antioxidants
389
+ - Good source of vitamin C
390
+
391
+
392
+
393
+ esc go back ^k actions
387
394
 
388
- ↵ view details ↑↓ navigate ^k actions
389
395
 
390
396
  "
391
397
  `)
398
+ expect(afterClickApple).toContain('Apple')
399
+ expect(afterClickApple).toContain('go back')
400
+
401
+ // Go back to list
402
+ await session.press('escape')
403
+ await session.text({ waitFor: (text) => /search/i.test(text) })
392
404
 
393
- // Click on "Grape" item (visible in initial view)
405
+ // Click on "Grape" no actions, should just select
394
406
  await session.click('Grape', { first: true })
395
407
 
396
- const afterClickGrapeSnapshot = await session.text()
397
- expect(afterClickGrapeSnapshot).toMatchInlineSnapshot(`
408
+ const afterClickGrape = await session.text()
409
+ expect(afterClickGrape).toMatchInlineSnapshot(`
398
410
  "
399
411
 
400
412
 
@@ -417,7 +429,8 @@ test('list click functionality', async () => {
417
429
 
418
430
  "
419
431
  `)
420
- }, 10000)
432
+ expect(afterClickGrape).toContain('Grape')
433
+ }, 15000)
421
434
 
422
435
  test('list actions panel with ctrl+k', async () => {
423
436
  await session.text({
@@ -66,7 +66,6 @@ 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
70
69
 
71
70
 
72
71
 
@@ -83,6 +82,7 @@ test('markdown tables render with borderless layout', async () => {
83
82
 
84
83
 
85
84
 
85
+ esc go back ^k actions powered by termcast.app
86
86
 
87
87
  "
88
88
  `)
@@ -166,7 +166,6 @@ 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
170
169
 
171
170
 
172
171
 
@@ -183,6 +182,7 @@ test('two tables render side by side in a Row', async () => {
183
182
 
184
183
 
185
184
 
185
+ esc go back ^k actions powered by termcast.app
186
186
 
187
187
  "
188
188
  `)
@@ -271,7 +271,7 @@ test('selecting first item with enter adds it to the list', async () => {
271
271
  │ Enter file path...
272
272
 
273
273
  │ Selected files:
274
- │ • /Users/morse/Documents/GitHub/termcast/termcast/src
274
+ │ • /Users/morse/Documents/GitHub/termcast/termcast/src
275
275
  │ Choose a folder for output
276
276
 
277
277
  ◇ Select Single File
@@ -522,30 +522,27 @@ test('grid mouse interaction', async () => {
522
522
  Simple Grid Example ────────────────────────────────────────────
523
523
 
524
524
  > Search items...
525
- ╭────────────────────────────────────────────────────────────────╮
526
- │ │
527
- │ Actions esc │
528
- │ │
529
- │ > Search actions... │
530
- │ │
531
- │ ›Show Details │
532
- │ Copy Emoji ⌃C │
533
- │ │
534
- │ Settings │
535
- │ Change Theme... │
536
- │ Toggle Console Logs │
537
- │ │
538
- │ │
539
- │ │
540
- │ │
541
- │ │
542
- │ ↵ select ↑↓ navigate │
543
- │ │
544
- ╰────────────────────────────────────────────────────────────────╯"
545
- `)
546
525
 
547
- // Close the actions panel first
548
- await session.press('esc')
526
+
527
+ Animals
528
+ 🐕 Dog
529
+ 🐱 Cat
530
+ 🐰 Rabbit
531
+
532
+ Others
533
+ 🏠 House
534
+ 🚗 Car
535
+ 🚀 Rocket
536
+ ›⭐ Star
537
+ 🌙 Moon
538
+ ☀ Sun
539
+
540
+
541
+
542
+ ↵ show details ↑↓ navigate ^k actions
543
+
544
+ "
545
+ `)
549
546
 
550
547
  // Navigate back up to make Apple visible.
551
548
  // Grid is implemented via List, which uses edge-triggered pagination.
@@ -562,36 +559,13 @@ test('grid mouse interaction', async () => {
562
559
  timeout: 5000,
563
560
  })
564
561
 
565
- // Click on "Apple" to go back to first section
562
+ // Click on "Apple" - it's already selected (first item after scrolling up),
563
+ // so clicking executes the first action (Show Details) which logs to console.
564
+ // Verify no actions dialog opened (unlike old behavior).
566
565
  await session.click('Apple', { first: true })
567
566
 
568
567
  const afterClickAppleSnapshot = await session.text()
569
- expect(afterClickAppleSnapshot).toMatchInlineSnapshot(`
570
- "
571
-
572
-
573
- Simple Grid Example ────────────────────────────────────────────
574
-
575
- > Search items...
576
- ╭────────────────────────────────────────────────────────────────╮
577
- │ │
578
- │ Actions esc │
579
- │ │
580
- │ > Search actions... │
581
- │ │
582
- │ ›Show Details │
583
- │ Copy Emoji ⌃C │
584
- │ │
585
- │ Settings │
586
- │ Change Theme... │
587
- │ Toggle Console Logs │
588
- │ │
589
- │ │
590
- │ │
591
- │ │
592
- │ │
593
- │ ↵ select ↑↓ navigate │
594
- │ │
595
- ╰────────────────────────────────────────────────────────────────╯"
596
- `)
597
- }, 10000)
568
+ // Apple should still be selected, no actions dialog visible
569
+ expect(afterClickAppleSnapshot).toContain('›🍎 Apple')
570
+ expect(afterClickAppleSnapshot).not.toContain('Actions')
571
+ }, 30000)
@@ -0,0 +1,63 @@
1
+ // Example: CalendarHeatmap component showcase with various color combinations.
2
+ // Shows month splits, width-based truncation, and different color palettes.
3
+ // Demonstrates Markdown component interleaved with Heatmaps for descriptions.
4
+
5
+ import { CalendarHeatmap, Color, Detail, Markdown } from 'termcast'
6
+ import type { CalendarHeatmapData } from 'termcast'
7
+ import { renderWithProviders } from '../utils'
8
+
9
+ function createRangeData(start: Date, dayCount: number, offset: number): CalendarHeatmapData[] {
10
+ return Array.from({ length: dayCount }, (_, index) => {
11
+ const date = new Date(start)
12
+ date.setDate(start.getDate() + index)
13
+
14
+ const dayOfWeek = date.getDay()
15
+ const weekendPenalty = dayOfWeek === 0 || dayOfWeek === 6 ? 0.35 : 1
16
+ const wave = (Math.sin((index + offset) / 6) + 1) / 2
17
+ const value = Math.round((1 + wave * 7) * weekendPenalty)
18
+
19
+ return {
20
+ date,
21
+ value,
22
+ }
23
+ })
24
+ }
25
+
26
+ const summerData = createRangeData(new Date(2025, 5, 1), 110, 0)
27
+ const winterData = createRangeData(new Date(2026, 0, 5), 45, 31)
28
+ const longHistoryData = createRangeData(new Date(2021, 0, 1), 1800, 13)
29
+ const recentData = createRangeData(new Date(2025, 9, 1), 150, 7)
30
+ const shortBurst = createRangeData(new Date(2025, 8, 1), 180, 3)
31
+ const journalData = [...summerData, ...winterData]
32
+
33
+ function SimpleHeatmap() {
34
+ return (
35
+ <Detail
36
+ markdown={[
37
+ '# Calendar Heatmap Color Showcase',
38
+ '',
39
+ 'Each heatmap demonstrates a different color combination.',
40
+ 'Data has a late-fall gap to show that empty weeks are skipped.',
41
+ 'Last heatmap renders multi-year data to verify width truncation.',
42
+ ].join('\n')}
43
+ metadata={
44
+ <Detail.Metadata>
45
+ <Markdown content="**Long history** — 5 years of daily data in purple. Months that don't fit the terminal width are truncated from the left." />
46
+ <CalendarHeatmap data={longHistoryData} color={Color.Purple} />
47
+ <Markdown content="**Journal** — summer + winter entries in green, with a fall gap between the two ranges." />
48
+ <CalendarHeatmap data={journalData} color={Color.Green} />
49
+ <Markdown content="**Recent activity** — last 150 days in red, showing the sine-wave pattern clearly." />
50
+ <CalendarHeatmap data={recentData} color={Color.Red} />
51
+ <Markdown content="**Short burst** — 180 days in blue on a purple empty background." />
52
+ <CalendarHeatmap data={shortBurst} color={Color.Blue} emptyColor={Color.Purple} />
53
+ <Markdown content="**Warm tones** — orange cells on magenta empty, same journal data." />
54
+ <CalendarHeatmap data={journalData} color={Color.Orange} emptyColor={Color.Magenta} />
55
+ <Markdown content="**Yellow on blue** — high-contrast palette for the recent data set." />
56
+ <CalendarHeatmap data={recentData} color={Color.Yellow} emptyColor={Color.Blue} />
57
+ </Detail.Metadata>
58
+ }
59
+ />
60
+ )
61
+ }
62
+
63
+ renderWithProviders(<SimpleHeatmap />)
@@ -0,0 +1,88 @@
1
+ // E2E tests for CalendarHeatmap with normal and overflow data ranges.
2
+ // Verifies month truncation does not overflow terminal width.
3
+
4
+ import { test, expect, afterEach, beforeEach } from 'vitest'
5
+ import { launchTerminal, Session } from 'tuistory/src'
6
+
7
+ let session: Session
8
+
9
+ beforeEach(async () => {
10
+ session = await launchTerminal({
11
+ command: 'bun',
12
+ args: ['src/examples/simple-heatmap.tsx'],
13
+ cols: 88,
14
+ rows: 50,
15
+ })
16
+ })
17
+
18
+ afterEach(() => {
19
+ session?.close()
20
+ })
21
+
22
+ test('renders calendar heatmaps with various color combinations', async () => {
23
+ const text = await session.text({
24
+ waitFor: (text) => {
25
+ return text.includes('Calendar Heatmap Color Showcase') && text.includes('Less')
26
+ },
27
+ timeout: 10000,
28
+ })
29
+
30
+ expect(text).toMatchInlineSnapshot(`
31
+ "
32
+
33
+
34
+
35
+
36
+ Calendar Heatmap Color Showcase
37
+
38
+ Each heatmap demonstrates a different color combination.
39
+ Data has a late-fall gap to show that empty weeks are skipped.
40
+ Last heatmap renders multi-year data to verify width truncation.
41
+
42
+ Long history — 5 years of daily data in purple. Months that don't fit the
43
+ terminal width are truncated from the left.
44
+
45
+ May Jun Jul Aug Sep Oct Nov
46
+ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼
47
+ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ Mon
48
+ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼
49
+ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ Wed
50
+ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼
51
+ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ Fri
52
+ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼
53
+ Less ◼ ◼ ◼ ◼ ◼ More
54
+
55
+ Journal — summer + winter entries in green, with a fall gap between the two
56
+ ranges.
57
+
58
+ Jun Jul Aug Sep Jan Feb
59
+ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼
60
+ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ Mon
61
+ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼
62
+ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ Wed
63
+ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼
64
+ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ Fri
65
+ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼
66
+ Less ◼ ◼ ◼ ◼ ◼ More
67
+
68
+ Recent activity — last 150 days in red, showing the sine-wave pattern clearly.
69
+
70
+ Se Oct Nov Dec Jan Feb
71
+ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼
72
+ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ Mon
73
+ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼
74
+ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ Wed
75
+ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼
76
+ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ ◼ Fri
77
+
78
+
79
+ esc go back ^k actions powered by termcast.app
80
+
81
+ "
82
+ `)
83
+
84
+ const maxLineLength = text.split('\n').reduce((maxLength, line) => {
85
+ return Math.max(maxLength, line.length)
86
+ }, 0)
87
+ expect(maxLineLength).toBeLessThanOrEqual(88)
88
+ }, 30000)
@@ -0,0 +1,82 @@
1
+ // Example: ProgressBar rendered inside List.Item.Detail.Metadata.
2
+ // Shows usage-style rows with title, bar+percentage in one line, and reset labels.
3
+
4
+ import React from 'react'
5
+ import { List, ProgressBar } from 'termcast'
6
+ import { renderWithProviders } from '../utils'
7
+
8
+ interface UsageItem {
9
+ title: string
10
+ subtitle: string
11
+ sessionUsage: number
12
+ sessionReset: string
13
+ weekUsage: number
14
+ weekReset: string
15
+ }
16
+
17
+ const usageItems: UsageItem[] = [
18
+ {
19
+ title: 'OpenAI account',
20
+ subtitle: 'default workspace',
21
+ sessionUsage: 37,
22
+ sessionReset: 'Resets 9pm (Asia/Bangkok)',
23
+ weekUsage: 7,
24
+ weekReset: 'Resets Feb 27, 1pm (Asia/Bangkok)',
25
+ },
26
+ {
27
+ title: 'Anthropic account',
28
+ subtitle: 'research workspace',
29
+ sessionUsage: 82,
30
+ sessionReset: 'Resets 11pm (Europe/Rome)',
31
+ weekUsage: 46,
32
+ weekReset: 'Resets Mar 1, 9am (Europe/Rome)',
33
+ },
34
+ {
35
+ title: 'Google account',
36
+ subtitle: 'sandbox workspace',
37
+ sessionUsage: 15,
38
+ sessionReset: 'Resets 6pm (America/New_York)',
39
+ weekUsage: 24,
40
+ weekReset: 'Resets Mar 3, 8am (America/New_York)',
41
+ },
42
+ ]
43
+
44
+ function SimpleProgressBar() {
45
+ return (
46
+ <List navigationTitle="ProgressBar Metadata" isShowingDetail={true}>
47
+ {usageItems.map((item) => {
48
+ return (
49
+ <List.Item
50
+ key={item.title}
51
+ title={item.title}
52
+ subtitle={item.subtitle}
53
+ detail={
54
+ <List.Item.Detail
55
+ metadata={
56
+ <List.Item.Detail.Metadata>
57
+ <ProgressBar
58
+ title="Current session"
59
+ value={item.sessionUsage}
60
+ percentageSuffix="used"
61
+ label={item.sessionReset}
62
+ />
63
+ <ProgressBar
64
+ title="Current week (all models)"
65
+ value={item.weekUsage}
66
+ percentageSuffix="used"
67
+ label={item.weekReset}
68
+ barCharacter="▁"
69
+ trackCharacter="▁"
70
+ />
71
+ </List.Item.Detail.Metadata>
72
+ }
73
+ />
74
+ }
75
+ />
76
+ )
77
+ })}
78
+ </List>
79
+ )
80
+ }
81
+
82
+ renderWithProviders(<SimpleProgressBar />)