termcast 1.3.50 → 1.3.52

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 (178) 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 +1130 -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/compile.d.ts.map +1 -1
  12. package/dist/compile.js +5 -2
  13. package/dist/compile.js.map +1 -1
  14. package/dist/components/actions.d.ts +4 -1
  15. package/dist/components/actions.d.ts.map +1 -1
  16. package/dist/components/actions.js +8 -5
  17. package/dist/components/actions.js.map +1 -1
  18. package/dist/components/detail.d.ts.map +1 -1
  19. package/dist/components/detail.js +21 -18
  20. package/dist/components/detail.js.map +1 -1
  21. package/dist/components/dropdown.d.ts.map +1 -1
  22. package/dist/components/dropdown.js +3 -2
  23. package/dist/components/dropdown.js.map +1 -1
  24. package/dist/components/footer.d.ts +6 -0
  25. package/dist/components/footer.d.ts.map +1 -1
  26. package/dist/components/footer.js +15 -6
  27. package/dist/components/footer.js.map +1 -1
  28. package/dist/components/form/checkbox.d.ts.map +1 -1
  29. package/dist/components/form/checkbox.js +1 -13
  30. package/dist/components/form/checkbox.js.map +1 -1
  31. package/dist/components/form/date-picker.js +2 -2
  32. package/dist/components/form/date-picker.js.map +1 -1
  33. package/dist/components/form/description.js +1 -1
  34. package/dist/components/form/description.js.map +1 -1
  35. package/dist/components/form/dropdown.d.ts.map +1 -1
  36. package/dist/components/form/dropdown.js +19 -3
  37. package/dist/components/form/dropdown.js.map +1 -1
  38. package/dist/components/form/file-picker.d.ts.map +1 -1
  39. package/dist/components/form/file-picker.js +22 -4
  40. package/dist/components/form/file-picker.js.map +1 -1
  41. package/dist/components/form/index.d.ts +3 -1
  42. package/dist/components/form/index.d.ts.map +1 -1
  43. package/dist/components/form/index.js +7 -5
  44. package/dist/components/form/index.js.map +1 -1
  45. package/dist/components/form/password-field.js +3 -3
  46. package/dist/components/form/password-field.js.map +1 -1
  47. package/dist/components/form/text-area.d.ts.map +1 -1
  48. package/dist/components/form/text-area.js +29 -6
  49. package/dist/components/form/text-area.js.map +1 -1
  50. package/dist/components/form/text-field.js +3 -3
  51. package/dist/components/form/text-field.js.map +1 -1
  52. package/dist/components/graph.d.ts.map +1 -1
  53. package/dist/components/graph.js +21 -25
  54. package/dist/components/graph.js.map +1 -1
  55. package/dist/components/heatmap.d.ts +80 -0
  56. package/dist/components/heatmap.d.ts.map +1 -0
  57. package/dist/components/heatmap.js +424 -0
  58. package/dist/components/heatmap.js.map +1 -0
  59. package/dist/components/list.d.ts +2 -0
  60. package/dist/components/list.d.ts.map +1 -1
  61. package/dist/components/list.js +91 -58
  62. package/dist/components/list.js.map +1 -1
  63. package/dist/components/markdown.d.ts +7 -0
  64. package/dist/components/markdown.d.ts.map +1 -0
  65. package/dist/components/markdown.js +19 -0
  66. package/dist/components/markdown.js.map +1 -0
  67. package/dist/components/metadata.d.ts.map +1 -1
  68. package/dist/components/metadata.js +4 -1
  69. package/dist/components/metadata.js.map +1 -1
  70. package/dist/components/progress-bar.d.ts +37 -0
  71. package/dist/components/progress-bar.d.ts.map +1 -0
  72. package/dist/components/progress-bar.js +34 -0
  73. package/dist/components/progress-bar.js.map +1 -0
  74. package/dist/components/table.d.ts +3 -2
  75. package/dist/components/table.d.ts.map +1 -1
  76. package/dist/components/table.js +78 -63
  77. package/dist/components/table.js.map +1 -1
  78. package/dist/diagram-parser.d.ts +17 -3
  79. package/dist/diagram-parser.d.ts.map +1 -1
  80. package/dist/diagram-parser.js +17 -3
  81. package/dist/diagram-parser.js.map +1 -1
  82. package/dist/examples/list-slot.d.ts +2 -0
  83. package/dist/examples/list-slot.d.ts.map +1 -0
  84. package/dist/examples/list-slot.js +14 -0
  85. package/dist/examples/list-slot.js.map +1 -0
  86. package/dist/examples/list-with-dropdown.js +2 -4
  87. package/dist/examples/list-with-dropdown.js.map +1 -1
  88. package/dist/examples/simple-heatmap.d.ts +2 -0
  89. package/dist/examples/simple-heatmap.d.ts.map +1 -0
  90. package/dist/examples/simple-heatmap.js +37 -0
  91. package/dist/examples/simple-heatmap.js.map +1 -0
  92. package/dist/examples/simple-progress-bar.d.ts +2 -0
  93. package/dist/examples/simple-progress-bar.d.ts.map +1 -0
  94. package/dist/examples/simple-progress-bar.js +36 -0
  95. package/dist/examples/simple-progress-bar.js.map +1 -0
  96. package/dist/index.d.ts +6 -0
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +6 -0
  99. package/dist/index.js.map +1 -1
  100. package/dist/internal/date-picker-widget.d.ts.map +1 -1
  101. package/dist/internal/date-picker-widget.js +5 -4
  102. package/dist/internal/date-picker-widget.js.map +1 -1
  103. package/dist/internal/navigation.d.ts.map +1 -1
  104. package/dist/internal/navigation.js +7 -2
  105. package/dist/internal/navigation.js.map +1 -1
  106. package/dist/internal/providers.d.ts.map +1 -1
  107. package/dist/internal/providers.js +42 -4
  108. package/dist/internal/providers.js.map +1 -1
  109. package/dist/logger.js +6 -1
  110. package/dist/logger.js.map +1 -1
  111. package/dist/state.d.ts +2 -0
  112. package/dist/state.d.ts.map +1 -1
  113. package/dist/state.js +31 -2
  114. package/dist/state.js.map +1 -1
  115. package/dist/theme.d.ts +1 -0
  116. package/dist/theme.d.ts.map +1 -1
  117. package/dist/theme.js +23 -1
  118. package/dist/theme.js.map +1 -1
  119. package/dist/utils.d.ts.map +1 -1
  120. package/dist/utils.js +6 -1
  121. package/dist/utils.js.map +1 -1
  122. package/package.json +3 -3
  123. package/src/apis/environment.tsx +6 -0
  124. package/src/app.tsx +1492 -0
  125. package/src/assets/default-app-icon.png +0 -0
  126. package/src/cli.tsx +105 -0
  127. package/src/compile.tsx +5 -2
  128. package/src/components/actions.tsx +9 -6
  129. package/src/components/detail.tsx +33 -23
  130. package/src/components/dropdown.tsx +3 -2
  131. package/src/components/footer.tsx +40 -7
  132. package/src/components/form/checkbox.tsx +2 -17
  133. package/src/components/form/date-picker.tsx +2 -2
  134. package/src/components/form/description.tsx +1 -1
  135. package/src/components/form/dropdown.tsx +22 -3
  136. package/src/components/form/file-picker.tsx +33 -10
  137. package/src/components/form/index.tsx +11 -7
  138. package/src/components/form/password-field.tsx +3 -3
  139. package/src/components/form/text-area.tsx +31 -6
  140. package/src/components/form/text-field.tsx +3 -3
  141. package/src/components/graph.tsx +21 -24
  142. package/src/components/heatmap.tsx +602 -0
  143. package/src/components/list.tsx +147 -78
  144. package/src/components/markdown.tsx +30 -0
  145. package/src/components/metadata.tsx +9 -2
  146. package/src/components/progress-bar.tsx +112 -0
  147. package/src/components/table.tsx +88 -71
  148. package/src/diagram-parser.tsx +17 -3
  149. package/src/examples/bar-graph-weekly.vitest.tsx +4 -4
  150. package/src/examples/detail-metadata-showcase.vitest.tsx +12 -12
  151. package/src/examples/form-basic.vitest.tsx +117 -16
  152. package/src/examples/graph-bar-chart.vitest.tsx +7 -7
  153. package/src/examples/graph-row.vitest.tsx +45 -45
  154. package/src/examples/graph-styles.vitest.tsx +19 -19
  155. package/src/examples/internal/descendants-rerender.vitest.tsx +94 -46
  156. package/src/examples/internal/simple-scrollbox.vitest.tsx +38 -14
  157. package/src/examples/list-dropdown-default.vitest.tsx +78 -58
  158. package/src/examples/list-slot.tsx +38 -0
  159. package/src/examples/list-with-detail.vitest.tsx +8 -8
  160. package/src/examples/list-with-dropdown.tsx +2 -2
  161. package/src/examples/list-with-dropdown.vitest.tsx +16 -16
  162. package/src/examples/list-with-sections.vitest.tsx +45 -32
  163. package/src/examples/simple-detail-table.vitest.tsx +2 -2
  164. package/src/examples/simple-file-picker.vitest.tsx +1 -1
  165. package/src/examples/simple-grid.vitest.tsx +27 -53
  166. package/src/examples/simple-heatmap.tsx +63 -0
  167. package/src/examples/simple-heatmap.vitest.tsx +88 -0
  168. package/src/examples/simple-progress-bar.tsx +82 -0
  169. package/src/examples/simple-progress-bar.vitest.tsx +72 -0
  170. package/src/examples/table-edge-cases.vitest.tsx +1 -1
  171. package/src/index.tsx +19 -0
  172. package/src/internal/date-picker-widget.tsx +23 -12
  173. package/src/internal/navigation.tsx +7 -2
  174. package/src/internal/providers.tsx +48 -3
  175. package/src/logger.tsx +6 -1
  176. package/src/state.tsx +38 -2
  177. package/src/theme.tsx +26 -2
  178. package/src/utils.tsx +6 -1
@@ -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 />)
@@ -0,0 +1,72 @@
1
+ // E2E tests for ProgressBar example.
2
+ // Verifies ProgressBar in List.Item.Detail.Metadata updates per selected item.
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-progress-bar.tsx'],
13
+ cols: 80,
14
+ rows: 24,
15
+ })
16
+ })
17
+
18
+ afterEach(() => {
19
+ session?.close()
20
+ })
21
+
22
+ test('progress bars render in detail metadata and update on selection', async () => {
23
+ const initial = await session.text({
24
+ waitFor: (text) => {
25
+ return text.includes('OpenAI account') && text.includes('37% used')
26
+ },
27
+ timeout: 10000,
28
+ })
29
+
30
+ expect(initial).toMatchInlineSnapshot(`
31
+ "
32
+
33
+
34
+ ProgressBar Metadata ─────────────────────────────────────────────────────
35
+
36
+ > Search...
37
+
38
+ ›OpenAI account default workspace │ Current session
39
+ Anthropic account research workspace │ █████████░░░░░░░░░░░░░░░░ 37% used
40
+ Google account sandbox workspace │ Resets 9pm (Asia/Bangkok)
41
+
42
+ │ Current week (all models)
43
+ │ ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ 7% used
44
+ │ Resets Feb 27, 1pm (Asia/Bangkok)
45
+
46
+ ↑↓ navigate ^k actions │
47
+
48
+
49
+
50
+
51
+
52
+
53
+
54
+
55
+ "
56
+ `)
57
+
58
+ expect(initial).toContain('37% used')
59
+ expect(initial).toContain('7% used')
60
+
61
+ await session.press('down')
62
+
63
+ const second = await session.text({
64
+ waitFor: (text) => {
65
+ return text.includes('›Anthropic account') && text.includes('82% used')
66
+ },
67
+ timeout: 10000,
68
+ })
69
+
70
+ expect(second).toContain('46% used')
71
+ expect(second).toContain('Europe/Rome')
72
+ }, 30000)
@@ -86,6 +86,7 @@ test('inline formatting table renders all rows', async () => {
86
86
  Done.
87
87
 
88
88
 
89
+ esc go back ^k actions powered by termcast.app
89
90
 
90
91
 
91
92
 
@@ -106,7 +107,6 @@ test('inline formatting table renders all rows', async () => {
106
107
 
107
108
 
108
109
 
109
- esc go back ^k actions powered by termcast.app
110
110
 
111
111
  "
112
112
  `)
package/src/index.tsx CHANGED
@@ -45,6 +45,10 @@ export type {
45
45
  ActionPanelSectionProps,
46
46
  } from 'termcast/src/components/actions'
47
47
 
48
+ // Core UI Components - Markdown
49
+ export { Markdown } from 'termcast/src/components/markdown'
50
+ export type { MarkdownProps } from 'termcast/src/components/markdown'
51
+
48
52
  // Core UI Components - Detail
49
53
  export { Detail } from 'termcast/src/components/detail'
50
54
  export type {
@@ -82,6 +86,21 @@ export type {
82
86
  BarGraphSeriesProps,
83
87
  } from 'termcast/src/components/bar-graph'
84
88
 
89
+ // Core UI Components - CalendarHeatmap
90
+ export { CalendarHeatmap, Heatmap } from 'termcast/src/components/heatmap'
91
+ export type {
92
+ CalendarHeatmapProps,
93
+ CalendarHeatmapData,
94
+ CalendarHeatmapCellChar,
95
+ HeatmapProps,
96
+ HeatmapData,
97
+ HeatmapCellChar,
98
+ } from 'termcast/src/components/heatmap'
99
+
100
+ // Core UI Components - ProgressBar
101
+ export { ProgressBar } from 'termcast/src/components/progress-bar'
102
+ export type { ProgressBarProps } from 'termcast/src/components/progress-bar'
103
+
85
104
  // Form Components
86
105
  import {
87
106
  Form as FormComponent,
@@ -377,7 +377,6 @@ export function DatePickerWidget({
377
377
  enableColors && focus === 'year' ? Theme.primary : undefined,
378
378
  marginBottom: 0,
379
379
  }}
380
- onMouseDown={() => setFocus('year')}
381
380
  >
382
381
  <box
383
382
  style={{
@@ -387,11 +386,17 @@ export function DatePickerWidget({
387
386
  width: headerWidth,
388
387
  }}
389
388
  >
390
- <text fg={focus === 'year' ? Theme.text : Theme.textMuted}>←</text>
391
- <text fg={focus === 'year' ? Theme.text : Theme.textMuted}>
392
- {String(y)}
393
- </text>
394
- <text fg={focus === 'year' ? Theme.text : Theme.textMuted}>→</text>
389
+ <box onMouseDown={() => { changeYear(-1); setFocus('year') }}>
390
+ <text fg={focus === 'year' ? Theme.text : Theme.textMuted}>←</text>
391
+ </box>
392
+ <box onMouseDown={() => { setFocus('year') }}>
393
+ <text fg={focus === 'year' ? Theme.text : Theme.textMuted}>
394
+ {String(y)}
395
+ </text>
396
+ </box>
397
+ <box onMouseDown={() => { changeYear(+1); setFocus('year') }}>
398
+ <text fg={focus === 'year' ? Theme.text : Theme.textMuted}>→</text>
399
+ </box>
395
400
  </box>
396
401
  </box>
397
402
 
@@ -404,7 +409,6 @@ export function DatePickerWidget({
404
409
  enableColors && focus === 'month' ? Theme.primary : undefined,
405
410
  marginBottom: 1,
406
411
  }}
407
- onMouseDown={() => setFocus('month')}
408
412
  >
409
413
  <box
410
414
  style={{
@@ -414,11 +418,17 @@ export function DatePickerWidget({
414
418
  width: headerWidth,
415
419
  }}
416
420
  >
417
- <text fg={focus === 'month' ? Theme.text : Theme.textMuted}>←</text>
418
- <text fg={focus === 'month' ? Theme.text : Theme.textMuted}>
419
- {MONTHS[m]}
420
- </text>
421
- <text fg={focus === 'month' ? Theme.text : Theme.textMuted}>→</text>
421
+ <box onMouseDown={() => { changeMonth(-1); setFocus('month') }}>
422
+ <text fg={focus === 'month' ? Theme.text : Theme.textMuted}>←</text>
423
+ </box>
424
+ <box onMouseDown={() => { setFocus('month') }}>
425
+ <text fg={focus === 'month' ? Theme.text : Theme.textMuted}>
426
+ {MONTHS[m]}
427
+ </text>
428
+ </box>
429
+ <box onMouseDown={() => { changeMonth(+1); setFocus('month') }}>
430
+ <text fg={focus === 'month' ? Theme.text : Theme.textMuted}>→</text>
431
+ </box>
422
432
  </box>
423
433
  </box>
424
434
 
@@ -485,6 +495,7 @@ export function DatePickerWidget({
485
495
  setSelected(d)
486
496
  setFocus('grid')
487
497
  ensureVisibleFor(d)
498
+ onChange?.(d)
488
499
  }}
489
500
  >
490
501
  <text
@@ -13,6 +13,7 @@ import { CommonProps } from 'termcast/src/utils'
13
13
  import { useStore, type NavigationStackItem } from 'termcast/src/state'
14
14
  import { useIsInFocus } from 'termcast/src/internal/focus-context'
15
15
  import { logger } from '../logger'
16
+ import { isAppMode } from '../apis/environment'
16
17
 
17
18
  interface Navigation {
18
19
  push: (element: ReactNode, onPop?: () => void) => void
@@ -172,8 +173,12 @@ export function NavigationProvider(props: NavigationProviderProps): any {
172
173
  activeSearchInputRef.setText('')
173
174
  return
174
175
  }
175
- // At root with no dialogs and no search text - exit the CLI
176
- renderer.destroy()
176
+ // At root with no dialogs and no search text - exit the CLI.
177
+ // In app mode (standalone desktop app), ESC at root is a no-op
178
+ // since destroying the renderer would kill the entire application.
179
+ if (!isAppMode()) {
180
+ renderer.destroy()
181
+ }
177
182
  }
178
183
  }
179
184
  })
@@ -86,6 +86,7 @@ class ErrorBoundaryClass extends Component<
86
86
  constructor(props: { children: ReactNode }) {
87
87
  super(props)
88
88
  this.state = { hasError: false, error: null }
89
+ this.reset = this.reset.bind(this)
89
90
  }
90
91
 
91
92
  static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
@@ -100,22 +101,45 @@ class ErrorBoundaryClass extends Component<
100
101
  })
101
102
  }
102
103
 
104
+ reset(): void {
105
+ // Clear navigation and dialog stacks so the app returns to the root view
106
+ // instead of re-rendering the same crashed component
107
+ useStore.setState({
108
+ navigationStack: [],
109
+ dialogStack: [],
110
+ toast: null,
111
+ toastWithPrimaryAction: false,
112
+ showActionsDialog: false,
113
+ })
114
+ this.setState({ hasError: false, error: null })
115
+ }
116
+
103
117
  render(): any {
104
118
  if (this.state.hasError) {
105
- return <ErrorDisplay error={this.state.error} />
119
+ return <ErrorDisplay error={this.state.error} onRetry={this.reset} />
106
120
  }
107
121
 
108
122
  return this.props.children
109
123
  }
110
124
  }
111
125
 
112
- function ErrorDisplay({ error }: { error: Error | null }): any {
126
+ function ErrorDisplay({ error, onRetry }: { error: Error | null; onRetry: () => void }): any {
113
127
  const theme = useTheme()
128
+
129
+ useKeyboard((evt) => {
130
+ if (evt.name === 'return') {
131
+ onRetry()
132
+ }
133
+ })
134
+
114
135
  return (
115
- <box padding={2}>
136
+ <box padding={2} flexDirection="column" gap={1}>
116
137
  <text fg={theme.error} wrapMode='none'>
117
138
  {error?.stack}
118
139
  </text>
140
+ <text fg={theme.textMuted}>
141
+ Press Enter to retry
142
+ </text>
119
143
  </box>
120
144
  )
121
145
  }
@@ -125,6 +149,7 @@ const ErrorBoundary = ErrorBoundaryClass as any
125
149
  export function TermcastProvider(props: ProvidersProps): any {
126
150
  const theme = useTheme()
127
151
  const renderer = useRenderer()
152
+
128
153
  useKeyboard((key) => {
129
154
  if (!renderer) return
130
155
  if (key.ctrl && key.name === 'd') {
@@ -136,6 +161,26 @@ export function TermcastProvider(props: ProvidersProps): any {
136
161
  }
137
162
  })
138
163
 
164
+ // Cmd+C (super+c): if there's an active selection, copy it to clipboard and clear.
165
+ // Otherwise let the key propagate to the TUI for other handlers.
166
+ // In standalone apps, WezTerm forwards Cmd+C via SendKey so it arrives as super modifier.
167
+ useKeyboard((key) => {
168
+ if (!renderer) return
169
+ if (key.super && key.name === 'c') {
170
+ if (renderer.hasSelection) {
171
+ const selection = renderer.getSelection()
172
+ if (selection) {
173
+ const text = selection.getSelectedText()
174
+ if (text) {
175
+ Clipboard.copy(text)
176
+ renderer.clearSelection()
177
+ key.stopPropagation()
178
+ }
179
+ }
180
+ }
181
+ }
182
+ })
183
+
139
184
  return (
140
185
  <ErrorBoundary>
141
186
  <Suspense fallback={<LoadingFallback />}>
package/src/logger.tsx CHANGED
@@ -66,7 +66,12 @@ process.on('uncaughtException', (error: Error) => {
66
66
  } else {
67
67
  logger.error('Uncaught Exception:', serialize(error))
68
68
  }
69
- process.exit(1)
69
+ // In app mode, don't exit on uncaught exceptions — the error boundary
70
+ // will catch React errors, and crashing the whole app is worse than
71
+ // a broken screen the user can recover from.
72
+ if (process.env.TERMCAST_APP_MODE !== '1') {
73
+ process.exit(1)
74
+ }
70
75
  })
71
76
 
72
77
  process.on('unhandledRejection', async (reason: any, promise: Promise<any>) => {