termcast 1.3.48 → 1.3.50

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 (255) hide show
  1. package/dist/build.d.ts.map +1 -1
  2. package/dist/build.js +12 -0
  3. package/dist/build.js.map +1 -1
  4. package/dist/cli.js +5 -40
  5. package/dist/cli.js.map +1 -1
  6. package/dist/colors.d.ts +7 -7
  7. package/dist/colors.js +7 -7
  8. package/dist/compile.d.ts +6 -1
  9. package/dist/compile.d.ts.map +1 -1
  10. package/dist/compile.js +45 -26
  11. package/dist/compile.js.map +1 -1
  12. package/dist/components/actions.js +1 -1
  13. package/dist/components/actions.js.map +1 -1
  14. package/dist/components/bar-chart.d.ts +38 -0
  15. package/dist/components/bar-chart.d.ts.map +1 -0
  16. package/dist/components/bar-chart.js +158 -0
  17. package/dist/components/bar-chart.js.map +1 -0
  18. package/dist/components/bar-graph.d.ts +41 -0
  19. package/dist/components/bar-graph.d.ts.map +1 -0
  20. package/dist/components/bar-graph.js +95 -0
  21. package/dist/components/bar-graph.js.map +1 -0
  22. package/dist/components/detail.d.ts.map +1 -1
  23. package/dist/components/detail.js +5 -7
  24. package/dist/components/detail.js.map +1 -1
  25. package/dist/components/footer.d.ts.map +1 -1
  26. package/dist/components/footer.js +8 -9
  27. package/dist/components/footer.js.map +1 -1
  28. package/dist/components/form/date-picker.d.ts.map +1 -1
  29. package/dist/components/form/date-picker.js +7 -1
  30. package/dist/components/form/date-picker.js.map +1 -1
  31. package/dist/components/form/dropdown.d.ts.map +1 -1
  32. package/dist/components/form/dropdown.js +10 -2
  33. package/dist/components/form/dropdown.js.map +1 -1
  34. package/dist/components/form/index.d.ts.map +1 -1
  35. package/dist/components/form/index.js +4 -5
  36. package/dist/components/form/index.js.map +1 -1
  37. package/dist/components/form/use-form-navigation.d.ts.map +1 -1
  38. package/dist/components/form/use-form-navigation.js +6 -0
  39. package/dist/components/form/use-form-navigation.js.map +1 -1
  40. package/dist/components/graph.d.ts +111 -0
  41. package/dist/components/graph.d.ts.map +1 -0
  42. package/dist/components/graph.js +392 -0
  43. package/dist/components/graph.js.map +1 -0
  44. package/dist/components/icon.js +5 -5
  45. package/dist/components/icon.js.map +1 -1
  46. package/dist/components/list.d.ts +53 -5
  47. package/dist/components/list.d.ts.map +1 -1
  48. package/dist/components/list.js +125 -71
  49. package/dist/components/list.js.map +1 -1
  50. package/dist/components/loading-bar.js +3 -3
  51. package/dist/components/loading-bar.js.map +1 -1
  52. package/dist/components/loading-text.d.ts +1 -1
  53. package/dist/components/loading-text.d.ts.map +1 -1
  54. package/dist/components/loading-text.js +3 -1
  55. package/dist/components/loading-text.js.map +1 -1
  56. package/dist/components/metadata.js +2 -2
  57. package/dist/components/metadata.js.map +1 -1
  58. package/dist/components/row.d.ts +10 -0
  59. package/dist/components/row.d.ts.map +1 -0
  60. package/dist/components/row.js +12 -0
  61. package/dist/components/row.js.map +1 -0
  62. package/dist/components/table.d.ts +57 -0
  63. package/dist/components/table.d.ts.map +1 -0
  64. package/dist/components/table.js +365 -0
  65. package/dist/components/table.js.map +1 -0
  66. package/dist/descendants.js +13 -13
  67. package/dist/descendants.js.map +1 -1
  68. package/dist/examples/bar-graph-weekly.d.ts +2 -0
  69. package/dist/examples/bar-graph-weekly.d.ts.map +1 -0
  70. package/dist/examples/bar-graph-weekly.js +95 -0
  71. package/dist/examples/bar-graph-weekly.js.map +1 -0
  72. package/dist/examples/components-weird-places.d.ts +2 -0
  73. package/dist/examples/components-weird-places.d.ts.map +1 -0
  74. package/dist/examples/components-weird-places.js +46 -0
  75. package/dist/examples/components-weird-places.js.map +1 -0
  76. package/dist/examples/graph-bar-chart.d.ts +2 -0
  77. package/dist/examples/graph-bar-chart.d.ts.map +1 -0
  78. package/dist/examples/graph-bar-chart.js +270 -0
  79. package/dist/examples/graph-bar-chart.js.map +1 -0
  80. package/dist/examples/graph-multi-series.d.ts +2 -0
  81. package/dist/examples/graph-multi-series.d.ts.map +1 -0
  82. package/dist/examples/graph-multi-series.js +23 -0
  83. package/dist/examples/graph-multi-series.js.map +1 -0
  84. package/dist/examples/graph-polymarket.d.ts +2 -0
  85. package/dist/examples/graph-polymarket.d.ts.map +1 -0
  86. package/dist/examples/graph-polymarket.js +109 -0
  87. package/dist/examples/graph-polymarket.js.map +1 -0
  88. package/dist/examples/graph-row.d.ts +2 -0
  89. package/dist/examples/graph-row.d.ts.map +1 -0
  90. package/dist/examples/graph-row.js +226 -0
  91. package/dist/examples/graph-row.js.map +1 -0
  92. package/dist/examples/graph-styles.d.ts +2 -0
  93. package/dist/examples/graph-styles.d.ts.map +1 -0
  94. package/dist/examples/graph-styles.js +316 -0
  95. package/dist/examples/graph-styles.js.map +1 -0
  96. package/dist/examples/list-accessory-table.d.ts +2 -0
  97. package/dist/examples/list-accessory-table.d.ts.map +1 -0
  98. package/dist/examples/list-accessory-table.js +46 -0
  99. package/dist/examples/list-accessory-table.js.map +1 -0
  100. package/dist/examples/list-item-accessories.d.ts +2 -0
  101. package/dist/examples/list-item-accessories.d.ts.map +1 -0
  102. package/dist/examples/list-item-accessories.js +27 -0
  103. package/dist/examples/list-item-accessories.js.map +1 -0
  104. package/dist/examples/list-no-actions.d.ts +2 -0
  105. package/dist/examples/list-no-actions.d.ts.map +1 -0
  106. package/dist/examples/list-no-actions.js +7 -0
  107. package/dist/examples/list-no-actions.js.map +1 -0
  108. package/dist/examples/simple-detail-table.d.ts +2 -0
  109. package/dist/examples/simple-detail-table.d.ts.map +1 -0
  110. package/dist/examples/simple-detail-table.js +45 -0
  111. package/dist/examples/simple-detail-table.js.map +1 -0
  112. package/dist/examples/simple-graph.d.ts +2 -0
  113. package/dist/examples/simple-graph.d.ts.map +1 -0
  114. package/dist/examples/simple-graph.js +32 -0
  115. package/dist/examples/simple-graph.js.map +1 -0
  116. package/dist/examples/simple-table-wrap.d.ts +2 -0
  117. package/dist/examples/simple-table-wrap.d.ts.map +1 -0
  118. package/dist/examples/simple-table-wrap.js +37 -0
  119. package/dist/examples/simple-table-wrap.js.map +1 -0
  120. package/dist/examples/table-edge-cases.d.ts +2 -0
  121. package/dist/examples/table-edge-cases.d.ts.map +1 -0
  122. package/dist/examples/table-edge-cases.js +70 -0
  123. package/dist/examples/table-edge-cases.js.map +1 -0
  124. package/dist/examples/table-flex-grow.d.ts +2 -0
  125. package/dist/examples/table-flex-grow.d.ts.map +1 -0
  126. package/dist/examples/table-flex-grow.js +18 -0
  127. package/dist/examples/table-flex-grow.js.map +1 -0
  128. package/dist/extensions/dev.d.ts.map +1 -1
  129. package/dist/extensions/dev.js +5 -1
  130. package/dist/extensions/dev.js.map +1 -1
  131. package/dist/globals.d.ts +1 -0
  132. package/dist/globals.d.ts.map +1 -1
  133. package/dist/globals.js +2 -0
  134. package/dist/globals.js.map +1 -1
  135. package/dist/index.d.ts +10 -0
  136. package/dist/index.d.ts.map +1 -1
  137. package/dist/index.js +10 -0
  138. package/dist/index.js.map +1 -1
  139. package/dist/internal/date-picker-widget.d.ts.map +1 -1
  140. package/dist/internal/date-picker-widget.js +4 -0
  141. package/dist/internal/date-picker-widget.js.map +1 -1
  142. package/dist/internal/providers.d.ts.map +1 -1
  143. package/dist/internal/providers.js +1 -3
  144. package/dist/internal/providers.js.map +1 -1
  145. package/dist/markdown-utils.d.ts +22 -1
  146. package/dist/markdown-utils.d.ts.map +1 -1
  147. package/dist/markdown-utils.js +66 -1
  148. package/dist/markdown-utils.js.map +1 -1
  149. package/dist/opentui.d.ts +4 -0
  150. package/dist/opentui.d.ts.map +1 -0
  151. package/dist/opentui.js +3 -0
  152. package/dist/opentui.js.map +1 -0
  153. package/dist/release.d.ts +2 -1
  154. package/dist/release.d.ts.map +1 -1
  155. package/dist/release.js +2 -1
  156. package/dist/release.js.map +1 -1
  157. package/dist/state.d.ts +1 -0
  158. package/dist/state.d.ts.map +1 -1
  159. package/dist/state.js +1 -1
  160. package/dist/state.js.map +1 -1
  161. package/dist/theme.d.ts +1 -0
  162. package/dist/theme.d.ts.map +1 -1
  163. package/dist/theme.js +13 -0
  164. package/dist/theme.js.map +1 -1
  165. package/dist/themes/nerv.json +227 -0
  166. package/dist/themes/termcast.json +72 -71
  167. package/dist/themes.d.ts +2 -1
  168. package/dist/themes.d.ts.map +1 -1
  169. package/dist/themes.js +7 -5
  170. package/dist/themes.js.map +1 -1
  171. package/dist/utils.d.ts.map +1 -1
  172. package/dist/utils.js +3 -0
  173. package/dist/utils.js.map +1 -1
  174. package/package.json +13 -5
  175. package/src/build.tsx +13 -0
  176. package/src/cli.tsx +5 -49
  177. package/src/colors.tsx +7 -7
  178. package/src/compile.tsx +52 -29
  179. package/src/components/actions.tsx +1 -1
  180. package/src/components/bar-chart.tsx +271 -0
  181. package/src/components/bar-graph.tsx +214 -0
  182. package/src/components/detail.tsx +7 -8
  183. package/src/components/footer.tsx +14 -15
  184. package/src/components/form/date-picker.tsx +9 -0
  185. package/src/components/form/dropdown.tsx +13 -3
  186. package/src/components/form/index.tsx +4 -6
  187. package/src/components/form/use-form-navigation.tsx +6 -0
  188. package/src/components/graph.tsx +506 -0
  189. package/src/components/icon.tsx +5 -5
  190. package/src/components/list.tsx +210 -102
  191. package/src/components/loading-bar.tsx +3 -3
  192. package/src/components/loading-text.tsx +4 -2
  193. package/src/components/metadata.tsx +2 -2
  194. package/src/components/row.tsx +31 -0
  195. package/src/components/table.tsx +511 -0
  196. package/src/descendants.tsx +13 -13
  197. package/src/examples/action-shortcut.vitest.tsx +1 -1
  198. package/src/examples/actions-context.vitest.tsx +1 -1
  199. package/src/examples/bar-graph-weekly.tsx +264 -0
  200. package/src/examples/bar-graph-weekly.vitest.tsx +275 -0
  201. package/src/examples/detail-metadata-showcase.vitest.tsx +8 -8
  202. package/src/examples/form-basic.vitest.tsx +239 -0
  203. package/src/examples/form-dropdown.vitest.tsx +29 -29
  204. package/src/examples/form-tagpicker.vitest.tsx +27 -27
  205. package/src/examples/github.vitest.tsx +4 -4
  206. package/src/examples/graph-bar-chart.tsx +408 -0
  207. package/src/examples/graph-bar-chart.vitest.tsx +283 -0
  208. package/src/examples/graph-multi-series.tsx +36 -0
  209. package/src/examples/graph-multi-series.vitest.tsx +89 -0
  210. package/src/examples/graph-polymarket.tsx +182 -0
  211. package/src/examples/graph-polymarket.vitest.tsx +130 -0
  212. package/src/examples/graph-row.tsx +347 -0
  213. package/src/examples/graph-row.vitest.tsx +295 -0
  214. package/src/examples/graph-styles.tsx +457 -0
  215. package/src/examples/graph-styles.vitest.tsx +322 -0
  216. package/src/examples/list-accessory-table.tsx +77 -0
  217. package/src/examples/list-detail-metadata.vitest.tsx +21 -21
  218. package/src/examples/list-dropdown-default.vitest.tsx +12 -12
  219. package/src/examples/list-item-accessories.tsx +106 -0
  220. package/src/examples/list-item-accessories.vitest.tsx +115 -0
  221. package/src/examples/list-no-actions.tsx +18 -0
  222. package/src/examples/list-no-actions.vitest.tsx +97 -0
  223. package/src/examples/list-spacing-mode.vitest.tsx +6 -6
  224. package/src/examples/list-with-detail.vitest.tsx +92 -92
  225. package/src/examples/list-with-dropdown.vitest.tsx +49 -6
  226. package/src/examples/list-with-sections.vitest.tsx +61 -56
  227. package/src/examples/simple-detail-markdown.vitest.tsx +21 -17
  228. package/src/examples/simple-detail-table.tsx +65 -0
  229. package/src/examples/simple-detail-table.vitest.tsx +200 -0
  230. package/src/examples/simple-graph.tsx +51 -0
  231. package/src/examples/simple-graph.vitest.tsx +124 -0
  232. package/src/examples/simple-grid.vitest.tsx +3 -3
  233. package/src/examples/simple-list-search.vitest.tsx +65 -0
  234. package/src/examples/simple-navigation.vitest.tsx +3 -3
  235. package/src/examples/simple-table-wrap.tsx +55 -0
  236. package/src/examples/simple-table-wrap.vitest.tsx +91 -0
  237. package/src/examples/store.vitest.tsx +1 -1
  238. package/src/examples/table-edge-cases.tsx +72 -0
  239. package/src/examples/table-edge-cases.vitest.tsx +307 -0
  240. package/src/examples/table-flex-grow.tsx +53 -0
  241. package/src/examples/table-flex-grow.vitest.tsx +124 -0
  242. package/src/extensions/dev.tsx +7 -1
  243. package/src/globals.ts +3 -0
  244. package/src/index.tsx +31 -0
  245. package/src/internal/date-picker-widget.tsx +4 -0
  246. package/src/internal/providers.tsx +1 -4
  247. package/src/markdown-utils.tsx +82 -1
  248. package/src/opentui.tsx +5 -0
  249. package/src/release.tsx +3 -0
  250. package/src/state.tsx +2 -1
  251. package/src/theme.tsx +14 -0
  252. package/src/themes/nerv.json +231 -0
  253. package/src/themes/termcast.json +75 -71
  254. package/src/themes.ts +8 -5
  255. package/src/utils.tsx +4 -0
@@ -0,0 +1,214 @@
1
+ /**
2
+ * BarGraph component for rendering vertical stacked bar charts in the terminal.
3
+ *
4
+ * Pure React/opentui implementation using <box> elements with justifyContent
5
+ * "space-evenly" for bar distribution. Each bar is a column of stacked colored
6
+ * segments sized via flexGrow. Labels sit below each bar, truncated with
7
+ * overflow="hidden" when the bar is narrower than the label text.
8
+ *
9
+ * Legend is a compact row of ■ Title pairs, no border.
10
+ *
11
+ * Color palette (same as Graph and BarChart):
12
+ * accent, info, success, warning, error, secondary, primary (cycles with %)
13
+ */
14
+
15
+ import React, { ReactNode, useMemo } from 'react'
16
+ import { BoxProps } from '@opentui/react'
17
+ import { useTheme, getThemePalette } from 'termcast/src/theme'
18
+ import { Color, resolveColor } from 'termcast/src/colors'
19
+
20
+ // ── Types ────────────────────────────────────────────────────────────
21
+
22
+ export interface BarGraphSeriesProps {
23
+ /** One value per bar position */
24
+ data: number[]
25
+ /** Override the auto-assigned color */
26
+ color?: Color.ColorLike
27
+ /** Series label shown in legend */
28
+ title?: string
29
+ }
30
+
31
+ export interface BarGraphProps extends BoxProps {
32
+ /** Height of the bar area in terminal rows (default: 15) */
33
+ height?: number
34
+ /** X-axis labels, one per bar position */
35
+ labels?: string[]
36
+ /** Show compact legend below the chart (default: true when any series has a title) */
37
+ showLegend?: boolean
38
+ /** BarGraph.Series children */
39
+ children: ReactNode
40
+ }
41
+
42
+ interface BarGraphType {
43
+ (props: BarGraphProps): any
44
+ Series: (props: BarGraphSeriesProps) => any
45
+ }
46
+
47
+ // ── Internal types ───────────────────────────────────────────────────
48
+
49
+ interface CollectedSeries {
50
+ data: number[]
51
+ color: string
52
+ title?: string
53
+ }
54
+
55
+ // ── BarGraph.Series (data-only, renders null like Graph.Line) ────────
56
+
57
+ const BarGraphSeries = (_props: BarGraphSeriesProps): any => {
58
+ return null
59
+ }
60
+
61
+ // ── Main BarGraph component ──────────────────────────────────────────
62
+
63
+ const BarGraph: BarGraphType = (props) => {
64
+ const theme = useTheme()
65
+ const { height = 15, labels = [], showLegend, children, ...rest } = props
66
+
67
+ const palette = getThemePalette(theme)
68
+
69
+ // Collect series from children
70
+ const seriesList = useMemo<CollectedSeries[]>(() => {
71
+ const result: CollectedSeries[] = []
72
+ let colorIndex = 0
73
+ React.Children.forEach(children, (child) => {
74
+ if (!React.isValidElement(child)) {
75
+ return
76
+ }
77
+ const childProps = child.props as BarGraphSeriesProps
78
+ if (!childProps.data) {
79
+ return
80
+ }
81
+ const color = resolveColor(childProps.color) || palette[colorIndex % palette.length]!
82
+ result.push({
83
+ data: childProps.data,
84
+ color,
85
+ title: childProps.title,
86
+ })
87
+ colorIndex++
88
+ })
89
+ return result
90
+ }, [children, palette])
91
+
92
+ // Compute number of bars from max data length across series
93
+ const numBars = useMemo(() => {
94
+ return Math.max(0, ...seriesList.map((s) => s.data.length))
95
+ }, [seriesList])
96
+
97
+ // Compute stacked totals per bar position and the global max
98
+ const { stackedTotals, maxTotal } = useMemo(() => {
99
+ const totals: number[] = []
100
+ for (let i = 0; i < numBars; i++) {
101
+ let sum = 0
102
+ for (const series of seriesList) {
103
+ sum += series.data[i] || 0
104
+ }
105
+ totals.push(sum)
106
+ }
107
+ const max = Math.max(0, ...totals)
108
+ return { stackedTotals: totals, maxTotal: max }
109
+ }, [seriesList, numBars])
110
+
111
+ // Whether to show legend: explicit prop, or auto when any series has a title
112
+ const legendVisible = showLegend ?? seriesList.some((s) => s.title)
113
+
114
+ if (numBars === 0 || maxTotal === 0) {
115
+ return null
116
+ }
117
+
118
+ return (
119
+ <box flexDirection="column" {...rest}>
120
+ {/* Bars area: overflow="hidden" clips excess bars when there are too many.
121
+ alignItems="flex-start" left-aligns bars in wide containers. */}
122
+ <box flexDirection="row" height={height} width="100%" alignItems="flex-start" overflow="hidden">
123
+ {Array.from({ length: numBars }, (_, barIdx) => {
124
+ const barTotal = stackedTotals[barIdx]!
125
+ const emptyGrow = maxTotal - barTotal
126
+ const label = labels[barIdx]
127
+
128
+ const barElements: any[] = []
129
+
130
+ // Min 1-col gap between bars (not before the first bar)
131
+ if (barIdx > 0) {
132
+ barElements.push(
133
+ <box key={`gap-${barIdx}`} width={1} flexShrink={0} />
134
+ )
135
+ }
136
+
137
+ barElements.push(
138
+ <box
139
+ key={barIdx}
140
+ flexDirection="column"
141
+ height="100%"
142
+ flexGrow={0}
143
+ flexShrink={0}
144
+ width={3}
145
+ >
146
+ {/* Plot area: spacer on top pushes colored segments to the bottom
147
+ so all bars are bottom-aligned regardless of total value. */}
148
+ <box flexDirection="column" flexGrow={1} width="100%">
149
+ {emptyGrow > 0 && (
150
+ <box flexGrow={emptyGrow} />
151
+ )}
152
+ {/* Segments: last series at top, first at bottom.
153
+ Each segment uses backgroundColor for the visual fill, plus
154
+ a single █ with matching fg so bars appear in text snapshots.
155
+ wrapMode="none" prevents text from expanding the segment height. */}
156
+ {[...seriesList].reverse().map((series, reverseIdx) => {
157
+ const value = series.data[barIdx] || 0
158
+ if (value <= 0) {
159
+ return null
160
+ }
161
+ return (
162
+ <box
163
+ key={reverseIdx}
164
+ flexGrow={value}
165
+ backgroundColor={series.color}
166
+ width="100%"
167
+ minHeight={1}
168
+ overflow="hidden"
169
+ >
170
+ {/* Absolute-positioned text doesn't affect flex layout.
171
+ The parent height is purely from flexGrow. The text
172
+ wraps to fill the area and gets clipped. */}
173
+ <box position="absolute" width="100%" height="100%" overflow="hidden">
174
+ <text fg={series.color}>{'█'.repeat(200)}</text>
175
+ </box>
176
+ </box>
177
+ )
178
+ })}
179
+ </box>
180
+ {/* X-axis label */}
181
+ {label !== undefined && (
182
+ <box height={1} width="100%" overflow="hidden" flexShrink={0}>
183
+ <text wrapMode="none" fg={theme.textMuted}>{label}</text>
184
+ </box>
185
+ )}
186
+ </box>
187
+ )
188
+
189
+ return barElements
190
+ })}
191
+ </box>
192
+ {/* Legend: single line, no wrap, clips when too many series */}
193
+ {legendVisible && (
194
+ <box height={1} width="100%" flexShrink={0} overflow="hidden">
195
+ <text wrapMode="none">
196
+ {seriesList.filter((s) => s.title).map((series, i, arr) => {
197
+ const sep = i < arr.length - 1 ? ' ' : ''
198
+ return (
199
+ <React.Fragment key={i}>
200
+ <span fg={series.color}>■ </span>
201
+ <span fg={theme.textMuted}>{series.title}{sep}</span>
202
+ </React.Fragment>
203
+ )
204
+ })}
205
+ </text>
206
+ </box>
207
+ )}
208
+ </box>
209
+ )
210
+ }
211
+
212
+ BarGraph.Series = BarGraphSeries
213
+
214
+ export { BarGraph }
@@ -72,7 +72,7 @@ const DetailMetadata: DetailMetadataType = (props) => {
72
72
  const config: MetadataConfig = {
73
73
  maxValueLen: 9999, // No limit - let text wrap naturally
74
74
  titleMinWidth: computedTitleWidth,
75
- paddingBottom: 1,
75
+ paddingBottom: 0,
76
76
  // Keep separators deterministic by rendering a fixed number of chars.
77
77
  // The content area has paddingRight=2 in <Detail>, so leave some margin.
78
78
  separatorWidth: Math.max(20, width - 6),
@@ -81,6 +81,7 @@ const DetailMetadata: DetailMetadataType = (props) => {
81
81
  return (
82
82
  <MetadataContext.Provider value={config}>
83
83
  <box
84
+ gap={1}
84
85
  style={{
85
86
  flexDirection: 'column',
86
87
  paddingTop: 1,
@@ -195,10 +196,8 @@ const Detail: DetailType = (props) => {
195
196
  if (!inFocus) return
196
197
 
197
198
  if (evt.name === 'k' && evt.ctrl) {
198
- // Ctrl+K shows actions dialog via portal
199
- if (actions) {
200
- useStore.setState({ showActionsDialog: true })
201
- }
199
+ // Always open — built-in actions (Change Theme, etc.) are always available
200
+ useStore.setState({ showActionsDialog: true })
202
201
  } else if (evt.name === 'return' && actions) {
203
202
  // Enter auto-executes first action via ActionPanel's layout effect
204
203
  useStore.setState({ shouldAutoExecuteFirstAction: true })
@@ -236,11 +235,11 @@ const Detail: DetailType = (props) => {
236
235
  <box style={{ flexDirection: 'column', height: '100%', flexGrow: 1 }}>
237
236
  {content}
238
237
  <DetailFooter
239
- hasActions={!!actions}
238
+ hasActions={true}
240
239
  firstActionTitle={firstActionTitle}
241
240
  />
242
- {/* Render actions offscreen to capture them */}
243
- {actions && <Offscreen>{actions}</Offscreen>}
241
+ {/* Always mount ActionPanel offscreen so built-in actions are available */}
242
+ <Offscreen>{actions || <ActionPanel />}</Offscreen>
244
243
  </box>
245
244
  )
246
245
  }
@@ -1,7 +1,7 @@
1
1
  import React, { ReactNode, useState, useEffect } from 'react'
2
2
  import { TextAttributes } from '@opentui/core'
3
3
  import { useTerminalDimensions, useKeyboard } from '@opentui/react'
4
- import { colord } from 'colord'
4
+
5
5
  import { useTheme } from 'termcast/src/theme'
6
6
  import { openInBrowser } from 'termcast/src/action-utils'
7
7
  import { termcastMaxContentWidth } from 'termcast/src/utils'
@@ -88,7 +88,8 @@ function ToastInline({ toast }: { toast: ToastData }): any {
88
88
  }
89
89
  }
90
90
 
91
- const getIconColor = () => {
91
+ // All toast styles use a colored background with contrasting text
92
+ const toastBg = (() => {
92
93
  switch (toast.style) {
93
94
  case 'SUCCESS':
94
95
  return theme.success
@@ -99,10 +100,8 @@ function ToastInline({ toast }: { toast: ToastData }): any {
99
100
  default:
100
101
  return theme.success
101
102
  }
102
- }
103
-
104
- const primaryColor = theme.primary
105
- const mutedColor = colord(primaryColor).darken(0.06).toHex()
103
+ })()
104
+ const toastFg = theme.background
106
105
 
107
106
  const maxToastWidth = Math.min(terminalWidth, termcastMaxContentWidth)
108
107
  const toastWidth = maxToastWidth - TOAST_MARGIN * 2
@@ -117,6 +116,7 @@ function ToastInline({ toast }: { toast: ToastData }): any {
117
116
  flexShrink={0}
118
117
  overflow='hidden'
119
118
  height={1}
119
+ backgroundColor={toastBg}
120
120
  >
121
121
  {/* Title box */}
122
122
  <box
@@ -126,12 +126,12 @@ function ToastInline({ toast }: { toast: ToastData }): any {
126
126
  overflow='hidden'
127
127
  height={1}
128
128
  >
129
- <text flexShrink={0} fg={getIconColor()} wrapMode='none'>
129
+ <text flexShrink={0} fg={toastFg} wrapMode='none'>
130
130
  {getIcon()}{' '}
131
131
  </text>
132
132
  <text
133
133
  flexShrink={1}
134
- fg={primaryColor}
134
+ fg={toastFg}
135
135
  attributes={TextAttributes.BOLD}
136
136
  wrapMode='none'
137
137
  >
@@ -148,14 +148,13 @@ function ToastInline({ toast }: { toast: ToastData }): any {
148
148
  overflow='hidden'
149
149
  height={1}
150
150
  >
151
- <text fg={mutedColor} flexShrink={1} wrapMode='none'>
151
+ <text fg={toastFg} flexShrink={1} wrapMode='none'>
152
152
  {toast.message || ''}
153
153
  </text>
154
154
  </box>
155
155
  {/* Keys box (right aligned, no grow) */}
156
156
  <box
157
157
  paddingLeft={1}
158
-
159
158
  gap={1}
160
159
  flexDirection='row'
161
160
  flexShrink={0}
@@ -171,13 +170,13 @@ function ToastInline({ toast }: { toast: ToastData }): any {
171
170
  }}
172
171
  >
173
172
  <text
174
- fg={mutedColor}
173
+ fg={toastFg}
175
174
  attributes={TextAttributes.BOLD}
176
175
  wrapMode='none'
177
176
  >
178
177
  {toast.primaryAction.title}
179
178
  </text>
180
- <text fg={mutedColor} wrapMode='none'>
179
+ <text fg={toastFg} wrapMode='none'>
181
180
  {' '}
182
181
  ctrl t
183
182
  </text>
@@ -192,13 +191,13 @@ function ToastInline({ toast }: { toast: ToastData }): any {
192
191
  }}
193
192
  >
194
193
  <text
195
- fg={mutedColor}
194
+ fg={toastFg}
196
195
  attributes={TextAttributes.BOLD}
197
196
  wrapMode='none'
198
197
  >
199
198
  {toast.secondaryAction.title}
200
199
  </text>
201
- <text fg={mutedColor} wrapMode='none'>
200
+ <text fg={toastFg} wrapMode='none'>
202
201
  {' '}
203
202
  ctrl g
204
203
  </text>
@@ -257,7 +256,7 @@ export function Footer({
257
256
  fg={theme.textMuted}
258
257
  attributes={TextAttributes.BOLD}
259
258
  >
260
- termcast
259
+ termcast.app
261
260
  </text>
262
261
  </box>
263
262
  )}
@@ -34,6 +34,9 @@ const DatePickerComponent = (props: DatePickerProps): any => {
34
34
  const { control } = useFormContext()
35
35
  const focusContext = useFocusContext()
36
36
  const { focusedField, setFocusedField } = focusContext
37
+ const { navigateToPrevious, navigateToNext } = useFormNavigationHelpers(
38
+ props.id,
39
+ )
37
40
  const isFocused = focusedField === props.id
38
41
  const isInFocus = useIsInFocus()
39
42
 
@@ -71,6 +74,12 @@ const DatePickerComponent = (props: DatePickerProps): any => {
71
74
  <DatePickerWidget
72
75
  enableColors={isFocused}
73
76
  initialValue={field.value || undefined}
77
+ onFirstRowUpKey={() => {
78
+ navigateToPrevious()
79
+ }}
80
+ onLastRowDownKey={() => {
81
+ navigateToNext()
82
+ }}
74
83
  onChange={(date) => {
75
84
  field.onChange(date)
76
85
  if (props.onChange) {
@@ -198,6 +198,9 @@ const DropdownContent = ({
198
198
  const isInFocus = useIsInFocus()
199
199
  const focusContext = useFocusContext()
200
200
  const { focusedField, setFocusedField } = focusContext
201
+ const { navigateToPrevious, navigateToNext } = useFormNavigationHelpers(
202
+ props.id,
203
+ )
201
204
  const isFocused = focusedField === props.id
202
205
  const [focusedIndex, setFocusedIndex] = useState(0)
203
206
 
@@ -295,7 +298,6 @@ const DropdownContent = ({
295
298
  // Handle keyboard navigation when focused
296
299
  useKeyboard((evt) => {
297
300
  if (!isFocused || !isInFocus) return
298
-
299
301
  const items = Object.values(descendantsContext.committedMap)
300
302
  .filter((item) => item.index !== -1)
301
303
  .sort((a, b) => a.index - b.index)
@@ -303,7 +305,11 @@ const DropdownContent = ({
303
305
 
304
306
  if (itemCount > 0) {
305
307
  if (evt.name === 'down') {
306
- if (focusedIndex >= itemCount - 1) return
308
+ if (focusedIndex >= itemCount - 1) {
309
+ navigateToNext()
310
+ evt.stopPropagation()
311
+ return
312
+ }
307
313
  const nextIndex = focusedIndex + 1
308
314
  const nextItem = items[nextIndex]
309
315
  if (nextItem) {
@@ -313,7 +319,11 @@ const DropdownContent = ({
313
319
  scrollToItemIfNeeded({ item: nextItem, direction: 1 })
314
320
  }
315
321
  } else if (evt.name === 'up') {
316
- if (focusedIndex <= 0) return
322
+ if (focusedIndex <= 0) {
323
+ navigateToPrevious()
324
+ evt.stopPropagation()
325
+ return
326
+ }
317
327
  const nextIndex = focusedIndex - 1
318
328
  const nextItem = items[nextIndex]
319
329
  if (nextItem) {
@@ -331,10 +331,8 @@ export const Form: FormType = ((props) => {
331
331
  }
332
332
 
333
333
  if (evt.name === 'k' && evt.ctrl) {
334
- // Ctrl+K shows actions dialog via portal
335
- if (props.actions) {
336
- useStore.setState({ showActionsDialog: true })
337
- }
334
+ // Always open — built-in actions (Change Theme, etc.) are always available
335
+ useStore.setState({ showActionsDialog: true })
338
336
  } else if ((evt.name === 'return' && evt.ctrl) || (evt.name === 'return' && evt.meta)) {
339
337
  // Ctrl+Return or Cmd+Return auto-executes first action via ActionPanel
340
338
  if (props.actions) {
@@ -413,8 +411,8 @@ export const Form: FormType = ((props) => {
413
411
  </box>
414
412
  </ScrollBox>
415
413
  <FormFooter />
416
- {/* Render actions offscreen to capture them with FormSubmitContext */}
417
- {props.actions && <Offscreen>{props.actions}</Offscreen>}
414
+ {/* Always mount ActionPanel offscreen so built-in actions are available */}
415
+ <Offscreen>{props.actions || <ActionPanel />}</Offscreen>
418
416
  </box>
419
417
  </box>
420
418
  </FocusContext.Provider>
@@ -68,11 +68,17 @@ export function useFormNavigation(
68
68
  } else {
69
69
  navigateToNext()
70
70
  }
71
+ evt.stopPropagation()
71
72
  } else if (handleArrows) {
73
+ // Prevent the newly-focused field from also processing this arrow.
74
+ // setFocusedField uses flushSync which updates all useKeyboard handler
75
+ // refs via useEffectEvent before the next handler in the dispatch loop runs.
72
76
  if (evt.name === 'up') {
73
77
  navigateToPrevious()
78
+ evt.stopPropagation()
74
79
  } else if (evt.name === 'down') {
75
80
  navigateToNext()
81
+ evt.stopPropagation()
76
82
  }
77
83
  }
78
84
  })