termcast 1.3.32 → 1.3.34

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 (327) hide show
  1. package/dist/action-utils.d.ts.map +1 -1
  2. package/dist/action-utils.js +8 -0
  3. package/dist/action-utils.js.map +1 -1
  4. package/dist/apis/cache.d.ts +1 -2
  5. package/dist/apis/cache.d.ts.map +1 -1
  6. package/dist/apis/cache.js +138 -54
  7. package/dist/apis/cache.js.map +1 -1
  8. package/dist/apis/clipboard.d.ts.map +1 -1
  9. package/dist/apis/clipboard.js +4 -0
  10. package/dist/apis/clipboard.js.map +1 -1
  11. package/dist/apis/oauth.d.ts.map +1 -1
  12. package/dist/apis/oauth.js +31 -4
  13. package/dist/apis/oauth.js.map +1 -1
  14. package/dist/build.d.ts +0 -1
  15. package/dist/build.d.ts.map +1 -1
  16. package/dist/build.js +30 -51
  17. package/dist/build.js.map +1 -1
  18. package/dist/cli.js +31 -14
  19. package/dist/cli.js.map +1 -1
  20. package/dist/compile.d.ts.map +1 -1
  21. package/dist/compile.js +5 -1
  22. package/dist/compile.js.map +1 -1
  23. package/dist/components/actions.d.ts +14 -0
  24. package/dist/components/actions.d.ts.map +1 -1
  25. package/dist/components/actions.js +151 -59
  26. package/dist/components/actions.js.map +1 -1
  27. package/dist/components/alert.d.ts.map +1 -1
  28. package/dist/components/alert.js +6 -5
  29. package/dist/components/alert.js.map +1 -1
  30. package/dist/components/animation-tick.d.ts +1 -1
  31. package/dist/components/animation-tick.js +1 -1
  32. package/dist/components/animation-tick.js.map +1 -1
  33. package/dist/components/detail.d.ts +5 -31
  34. package/dist/components/detail.d.ts.map +1 -1
  35. package/dist/components/detail.js +36 -52
  36. package/dist/components/detail.js.map +1 -1
  37. package/dist/components/dropdown.d.ts +1 -1
  38. package/dist/components/dropdown.d.ts.map +1 -1
  39. package/dist/components/dropdown.js +50 -22
  40. package/dist/components/dropdown.js.map +1 -1
  41. package/dist/components/footer.d.ts.map +1 -1
  42. package/dist/components/footer.js +19 -18
  43. package/dist/components/footer.js.map +1 -1
  44. package/dist/components/form/checkbox.d.ts.map +1 -1
  45. package/dist/components/form/checkbox.js +12 -11
  46. package/dist/components/form/checkbox.js.map +1 -1
  47. package/dist/components/form/date-picker.d.ts.map +1 -1
  48. package/dist/components/form/date-picker.js +7 -22
  49. package/dist/components/form/date-picker.js.map +1 -1
  50. package/dist/components/form/description.d.ts +1 -1
  51. package/dist/components/form/description.d.ts.map +1 -1
  52. package/dist/components/form/description.js +6 -5
  53. package/dist/components/form/description.js.map +1 -1
  54. package/dist/components/form/dropdown.d.ts.map +1 -1
  55. package/dist/components/form/dropdown.js +53 -50
  56. package/dist/components/form/dropdown.js.map +1 -1
  57. package/dist/components/form/file-autocomplete.d.ts.map +1 -1
  58. package/dist/components/form/file-autocomplete.js +5 -4
  59. package/dist/components/form/file-autocomplete.js.map +1 -1
  60. package/dist/components/form/file-picker.d.ts.map +1 -1
  61. package/dist/components/form/file-picker.js +23 -22
  62. package/dist/components/form/file-picker.js.map +1 -1
  63. package/dist/components/form/form-end.d.ts.map +1 -1
  64. package/dist/components/form/form-end.js +6 -4
  65. package/dist/components/form/form-end.js.map +1 -1
  66. package/dist/components/form/form-field-wrapper.d.ts +15 -0
  67. package/dist/components/form/form-field-wrapper.d.ts.map +1 -0
  68. package/dist/components/form/form-field-wrapper.js +29 -0
  69. package/dist/components/form/form-field-wrapper.js.map +1 -0
  70. package/dist/components/form/index.d.ts.map +1 -1
  71. package/dist/components/form/index.js +31 -30
  72. package/dist/components/form/index.js.map +1 -1
  73. package/dist/components/form/password-field.d.ts.map +1 -1
  74. package/dist/components/form/password-field.js +7 -6
  75. package/dist/components/form/password-field.js.map +1 -1
  76. package/dist/components/form/separator.d.ts.map +1 -1
  77. package/dist/components/form/separator.js +3 -2
  78. package/dist/components/form/separator.js.map +1 -1
  79. package/dist/components/form/tagpicker.d.ts.map +1 -1
  80. package/dist/components/form/tagpicker.js +2 -1
  81. package/dist/components/form/tagpicker.js.map +1 -1
  82. package/dist/components/form/text-area.d.ts.map +1 -1
  83. package/dist/components/form/text-area.js +7 -6
  84. package/dist/components/form/text-area.js.map +1 -1
  85. package/dist/components/form/text-field.d.ts.map +1 -1
  86. package/dist/components/form/text-field.js +7 -6
  87. package/dist/components/form/text-field.js.map +1 -1
  88. package/dist/components/form/use-form-navigation.d.ts.map +1 -1
  89. package/dist/components/form/use-form-navigation.js +4 -4
  90. package/dist/components/form/use-form-navigation.js.map +1 -1
  91. package/dist/components/form/with-left-border.d.ts +15 -0
  92. package/dist/components/form/with-left-border.d.ts.map +1 -1
  93. package/dist/components/form/with-left-border.js +21 -9
  94. package/dist/components/form/with-left-border.js.map +1 -1
  95. package/dist/components/icon.d.ts +14 -0
  96. package/dist/components/icon.d.ts.map +1 -1
  97. package/dist/components/icon.js +60 -0
  98. package/dist/components/icon.js.map +1 -1
  99. package/dist/components/image.d.ts +47 -2
  100. package/dist/components/image.d.ts.map +1 -1
  101. package/dist/components/image.js +46 -7
  102. package/dist/components/image.js.map +1 -1
  103. package/dist/components/list.d.ts +5 -0
  104. package/dist/components/list.d.ts.map +1 -1
  105. package/dist/components/list.js +188 -132
  106. package/dist/components/list.js.map +1 -1
  107. package/dist/components/loading-bar.d.ts.map +1 -1
  108. package/dist/components/loading-bar.js +4 -3
  109. package/dist/components/loading-bar.js.map +1 -1
  110. package/dist/components/metadata.d.ts +70 -0
  111. package/dist/components/metadata.d.ts.map +1 -0
  112. package/dist/components/metadata.js +82 -0
  113. package/dist/components/metadata.js.map +1 -0
  114. package/dist/components/theme-picker.d.ts.map +1 -1
  115. package/dist/components/theme-picker.js +3 -2
  116. package/dist/components/theme-picker.js.map +1 -1
  117. package/dist/descendants-v2.d.ts +60 -0
  118. package/dist/descendants-v2.d.ts.map +1 -0
  119. package/dist/descendants-v2.js +144 -0
  120. package/dist/descendants-v2.js.map +1 -0
  121. package/dist/examples/actions-context.d.ts +2 -0
  122. package/dist/examples/actions-context.d.ts.map +1 -0
  123. package/dist/examples/actions-context.js +33 -0
  124. package/dist/examples/actions-context.js.map +1 -0
  125. package/dist/examples/form-basic.d.ts.map +1 -1
  126. package/dist/examples/form-basic.js +1 -1
  127. package/dist/examples/form-basic.js.map +1 -1
  128. package/dist/examples/form-dropdown.js +1 -1
  129. package/dist/examples/form-dropdown.js.map +1 -1
  130. package/dist/examples/internal/custom-action-renderables.d.ts +70 -0
  131. package/dist/examples/internal/custom-action-renderables.d.ts.map +1 -0
  132. package/dist/examples/internal/custom-action-renderables.js +163 -0
  133. package/dist/examples/internal/custom-action-renderables.js.map +1 -0
  134. package/dist/examples/internal/custom-dropdown.d.ts +99 -0
  135. package/dist/examples/internal/custom-dropdown.d.ts.map +1 -0
  136. package/dist/examples/internal/custom-dropdown.js +270 -0
  137. package/dist/examples/internal/custom-dropdown.js.map +1 -0
  138. package/dist/examples/internal/custom-renderable-form.d.ts +43 -0
  139. package/dist/examples/internal/custom-renderable-form.d.ts.map +1 -0
  140. package/dist/examples/internal/custom-renderable-form.js +284 -0
  141. package/dist/examples/internal/custom-renderable-form.js.map +1 -0
  142. package/dist/examples/internal/custom-renderable-list-default-search.d.ts +2 -0
  143. package/dist/examples/internal/custom-renderable-list-default-search.d.ts.map +1 -0
  144. package/dist/examples/internal/custom-renderable-list-default-search.js +16 -0
  145. package/dist/examples/internal/custom-renderable-list-default-search.js.map +1 -0
  146. package/dist/examples/internal/custom-renderable-list-v2-default-search.d.ts +2 -0
  147. package/dist/examples/internal/custom-renderable-list-v2-default-search.d.ts.map +1 -0
  148. package/dist/examples/internal/custom-renderable-list-v2-default-search.js +24 -0
  149. package/dist/examples/internal/custom-renderable-list-v2-default-search.js.map +1 -0
  150. package/dist/examples/internal/custom-renderable-list-v2.d.ts +189 -0
  151. package/dist/examples/internal/custom-renderable-list-v2.d.ts.map +1 -0
  152. package/dist/examples/internal/custom-renderable-list-v2.js +708 -0
  153. package/dist/examples/internal/custom-renderable-list-v2.js.map +1 -0
  154. package/dist/examples/internal/custom-renderable-list.d.ts +72 -0
  155. package/dist/examples/internal/custom-renderable-list.d.ts.map +1 -0
  156. package/dist/examples/internal/custom-renderable-list.js +544 -0
  157. package/dist/examples/internal/custom-renderable-list.js.map +1 -0
  158. package/dist/examples/internal/rhf-custom-ref.js +5 -4
  159. package/dist/examples/internal/rhf-custom-ref.js.map +1 -1
  160. package/dist/examples/internal/scrollbox-with-descendants.js +4 -2
  161. package/dist/examples/internal/scrollbox-with-descendants.js.map +1 -1
  162. package/dist/examples/list-controlled-search.d.ts +2 -0
  163. package/dist/examples/list-controlled-search.d.ts.map +1 -0
  164. package/dist/examples/list-controlled-search.js +12 -0
  165. package/dist/examples/list-controlled-search.js.map +1 -0
  166. package/dist/examples/list-detail-metadata.js +1 -1
  167. package/dist/examples/list-detail-metadata.js.map +1 -1
  168. package/dist/examples/simple-image-mask.d.ts +8 -0
  169. package/dist/examples/simple-image-mask.d.ts.map +1 -0
  170. package/dist/examples/simple-image-mask.js +12 -0
  171. package/dist/examples/simple-image-mask.js.map +1 -0
  172. package/dist/examples/toast-variations.js +1 -1
  173. package/dist/examples/toast-variations.js.map +1 -1
  174. package/dist/extensions/dev.d.ts.map +1 -1
  175. package/dist/extensions/dev.js +3 -2
  176. package/dist/extensions/dev.js.map +1 -1
  177. package/dist/extensions/react-refresh-init.d.ts.map +1 -1
  178. package/dist/extensions/react-refresh-init.js +4 -3
  179. package/dist/extensions/react-refresh-init.js.map +1 -1
  180. package/dist/index.d.ts +3 -2
  181. package/dist/index.d.ts.map +1 -1
  182. package/dist/index.js +1 -1
  183. package/dist/index.js.map +1 -1
  184. package/dist/internal/date-picker-widget.d.ts.map +1 -1
  185. package/dist/internal/date-picker-widget.js +2 -1
  186. package/dist/internal/date-picker-widget.js.map +1 -1
  187. package/dist/internal/dialog.d.ts +6 -0
  188. package/dist/internal/dialog.d.ts.map +1 -1
  189. package/dist/internal/dialog.js +59 -18
  190. package/dist/internal/dialog.js.map +1 -1
  191. package/dist/internal/navigation.d.ts.map +1 -1
  192. package/dist/internal/navigation.js +8 -1
  193. package/dist/internal/navigation.js.map +1 -1
  194. package/dist/internal/offscreen.d.ts +3 -0
  195. package/dist/internal/offscreen.d.ts.map +1 -1
  196. package/dist/internal/offscreen.js +5 -0
  197. package/dist/internal/offscreen.js.map +1 -1
  198. package/dist/internal/providers.d.ts.map +1 -1
  199. package/dist/internal/providers.js +20 -3
  200. package/dist/internal/providers.js.map +1 -1
  201. package/dist/internal/scrollbox.d.ts.map +1 -1
  202. package/dist/internal/scrollbox.js +3 -2
  203. package/dist/internal/scrollbox.js.map +1 -1
  204. package/dist/logger.d.ts.map +1 -1
  205. package/dist/logger.js +4 -0
  206. package/dist/logger.js.map +1 -1
  207. package/dist/preload.js +5 -17
  208. package/dist/preload.js.map +1 -1
  209. package/dist/state.d.ts +4 -0
  210. package/dist/state.d.ts.map +1 -1
  211. package/dist/state.js +4 -0
  212. package/dist/state.js.map +1 -1
  213. package/dist/test-border-overlay.d.ts +2 -0
  214. package/dist/test-border-overlay.d.ts.map +1 -0
  215. package/dist/test-border-overlay.js +7 -0
  216. package/dist/test-border-overlay.js.map +1 -0
  217. package/dist/test-layout-2.d.ts +2 -0
  218. package/dist/test-layout-2.d.ts.map +1 -0
  219. package/dist/test-layout-2.js +5 -0
  220. package/dist/test-layout-2.js.map +1 -0
  221. package/dist/test-layout.d.ts +2 -0
  222. package/dist/test-layout.d.ts.map +1 -0
  223. package/dist/test-layout.js +7 -0
  224. package/dist/test-layout.js.map +1 -0
  225. package/dist/theme.d.ts +1 -2
  226. package/dist/theme.d.ts.map +1 -1
  227. package/dist/theme.js +5 -9
  228. package/dist/theme.js.map +1 -1
  229. package/dist/utils/run-command.d.ts +1 -1
  230. package/dist/utils/run-command.d.ts.map +1 -1
  231. package/dist/utils/run-command.js +27 -7
  232. package/dist/utils/run-command.js.map +1 -1
  233. package/dist/utils.d.ts +1 -0
  234. package/dist/utils.d.ts.map +1 -1
  235. package/dist/utils.js +44 -23
  236. package/dist/utils.js.map +1 -1
  237. package/dist/watcher.d.ts.map +1 -1
  238. package/dist/watcher.js +24 -4
  239. package/dist/watcher.js.map +1 -1
  240. package/package.json +14 -12
  241. package/src/action-utils.tsx +10 -0
  242. package/src/apis/cache.test.ts +35 -3
  243. package/src/apis/cache.tsx +184 -59
  244. package/src/apis/clipboard.tsx +5 -0
  245. package/src/apis/oauth.tsx +33 -4
  246. package/src/build.tsx +35 -58
  247. package/src/cli.tsx +156 -134
  248. package/src/compile.tsx +6 -3
  249. package/src/compile.vitest.tsx +33 -15
  250. package/src/components/actions.tsx +230 -99
  251. package/src/components/alert.tsx +11 -10
  252. package/src/components/animation-tick.tsx +1 -1
  253. package/src/components/detail.tsx +56 -151
  254. package/src/components/dropdown.tsx +70 -36
  255. package/src/components/footer.tsx +58 -33
  256. package/src/components/form/checkbox.tsx +30 -32
  257. package/src/components/form/date-picker.tsx +27 -47
  258. package/src/components/form/description.tsx +19 -18
  259. package/src/components/form/dropdown.tsx +95 -103
  260. package/src/components/form/file-autocomplete.tsx +9 -8
  261. package/src/components/form/file-picker.tsx +46 -46
  262. package/src/components/form/form-end.tsx +6 -4
  263. package/src/components/form/index.tsx +38 -48
  264. package/src/components/form/password-field.tsx +25 -27
  265. package/src/components/form/separator.tsx +3 -2
  266. package/src/components/form/tagpicker.tsx +2 -1
  267. package/src/components/form/text-area.tsx +25 -30
  268. package/src/components/form/text-field.tsx +25 -27
  269. package/src/components/form/use-form-navigation.tsx +4 -5
  270. package/src/components/form/with-left-border.tsx +48 -10
  271. package/src/components/icon.tsx +69 -0
  272. package/src/components/image.tsx +60 -7
  273. package/src/components/list.tsx +270 -202
  274. package/src/components/loading-bar.tsx +4 -3
  275. package/src/components/metadata.tsx +217 -0
  276. package/src/components/theme-picker.tsx +3 -2
  277. package/src/examples/actions-context.tsx +63 -0
  278. package/src/examples/actions-context.vitest.tsx +110 -0
  279. package/src/examples/actions-dialog-layout.vitest.tsx +2 -1
  280. package/src/examples/file-autocomplete.vitest.tsx +15 -15
  281. package/src/examples/form-basic.tsx +12 -0
  282. package/src/examples/form-basic.vitest.tsx +74 -74
  283. package/src/examples/form-dropdown.tsx +8 -0
  284. package/src/examples/form-dropdown.vitest.tsx +364 -421
  285. package/src/examples/form-tagpicker.vitest.tsx +56 -54
  286. package/src/examples/github.vitest.tsx +252 -0
  287. package/src/examples/internal/rhf-custom-ref.tsx +16 -15
  288. package/src/examples/internal/scrollbox-with-descendants.tsx +4 -2
  289. package/src/examples/internal/simple-dialog.tsx +1 -1
  290. package/src/examples/internal/simple-scrollbox.vitest.tsx +14 -9
  291. package/src/examples/list-controlled-search.tsx +28 -0
  292. package/src/examples/list-controlled-search.vitest.tsx +49 -0
  293. package/src/examples/list-detail-metadata.tsx +8 -5
  294. package/src/examples/list-detail-metadata.vitest.tsx +22 -22
  295. package/src/examples/list-dropdown-default.vitest.tsx +12 -12
  296. package/src/examples/list-scrollbox.vitest.tsx +52 -38
  297. package/src/examples/list-with-detail.vitest.tsx +45 -41
  298. package/src/examples/list-with-dropdown.vitest.tsx +5 -5
  299. package/src/examples/list-with-sections.vitest.tsx +65 -12
  300. package/src/examples/list-with-toast.vitest.tsx +4 -4
  301. package/src/examples/simple-file-picker.vitest.tsx +12 -12
  302. package/src/examples/simple-grid.vitest.tsx +53 -53
  303. package/src/examples/simple-image-mask.tsx +58 -0
  304. package/src/examples/simple-navigation.vitest.tsx +19 -19
  305. package/src/examples/store.vitest.tsx +1 -1
  306. package/src/examples/swift-extension.vitest.tsx +4 -2
  307. package/src/examples/synonyms.vitest.tsx +31 -9
  308. package/src/examples/toast-action.vitest.tsx +8 -8
  309. package/src/examples/toast-variations.tsx +1 -1
  310. package/src/examples/toast-variations.vitest.tsx +69 -134
  311. package/src/extensions/dev.tsx +3 -2
  312. package/src/extensions/dev.vitest.tsx +65 -28
  313. package/src/extensions/react-refresh-init.tsx +4 -3
  314. package/src/index.tsx +3 -1
  315. package/src/internal/date-picker-widget.tsx +2 -1
  316. package/src/internal/dialog.tsx +100 -28
  317. package/src/internal/navigation.tsx +8 -1
  318. package/src/internal/offscreen.tsx +10 -0
  319. package/src/internal/providers.tsx +34 -8
  320. package/src/internal/scrollbox.tsx +4 -2
  321. package/src/logger.tsx +4 -0
  322. package/src/preload.tsx +5 -17
  323. package/src/state.tsx +12 -0
  324. package/src/theme.tsx +6 -9
  325. package/src/utils/run-command.tsx +32 -8
  326. package/src/utils.tsx +58 -23
  327. package/src/watcher.tsx +26 -6
@@ -1,6 +1,6 @@
1
1
  import React, { useState, useRef, useLayoutEffect } from 'react'
2
2
  import { BoxRenderable } from '@opentui/core'
3
- import { Theme } from 'termcast/src/theme'
3
+ import { useTheme } from 'termcast/src/theme'
4
4
  import { useAnimationTick, TICK_DIVISORS } from 'termcast/src/components/animation-tick'
5
5
 
6
6
  interface LoadingBarProps {
@@ -11,6 +11,7 @@ interface LoadingBarProps {
11
11
 
12
12
  export function LoadingBar(props: LoadingBarProps): any {
13
13
  let { title, isLoading = false, barLength: propBarLength } = props
14
+ const theme = useTheme()
14
15
  const [calculatedBarLength, setCalculatedBarLength] = useState(
15
16
  propBarLength || 0,
16
17
  )
@@ -78,12 +79,12 @@ export function LoadingBar(props: LoadingBarProps): any {
78
79
  const getCharacterColor = (index: number): string => {
79
80
  if (!isLoading) {
80
81
  // When not loading, use default theme colors
81
- return index < title.length ? Theme.text : '#626262'
82
+ return index < title.length ? theme.text : '#626262'
82
83
  }
83
84
 
84
85
  // Title text stays static when loading, only animate the bar
85
86
  if (index < title.length) {
86
- return Theme.textMuted // Keep title text muted during loading
87
+ return theme.textMuted // Keep title text muted during loading
87
88
  }
88
89
 
89
90
  // Only animate the bar part
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Shared Metadata components used by both Detail.Metadata and List.Item.Detail.Metadata
3
+ *
4
+ * Provides Label, Separator, Link, and TagList components with configurable styling
5
+ * via MetadataContext for different use cases (standalone Detail vs List detail panel).
6
+ */
7
+
8
+ import React, { createContext, useContext, ReactNode } from 'react'
9
+ import { TextAttributes } from '@opentui/core'
10
+ import { useTheme } from 'termcast/src/theme'
11
+ import { Color, resolveColor } from 'termcast/src/colors'
12
+ import type { ImageLike } from 'termcast/src/components/image'
13
+
14
+ interface MetadataConfig {
15
+ /**
16
+ * Maximum text length before switching to column layout.
17
+ * Can be a number or a function that takes title length and returns max value length.
18
+ * This allows dynamic calculation based on terminal width and title length.
19
+ */
20
+ maxValueLen: number | ((titleLen: number) => number)
21
+ /** Minimum width for title column in row layout (default: 12) */
22
+ titleMinWidth: number
23
+ /** Padding below each metadata item (default: 0.5) */
24
+ paddingBottom: number
25
+ /** Width of separator line (default: 17) */
26
+ separatorWidth: number
27
+ }
28
+
29
+ const defaultConfig: MetadataConfig = {
30
+ maxValueLen: 20,
31
+ titleMinWidth: 12,
32
+ paddingBottom: 0.5,
33
+ separatorWidth: 17,
34
+ }
35
+
36
+ /** Helper to resolve maxValueLen - handles both number and function */
37
+ const resolveMaxValueLen = (config: MetadataConfig, titleLen: number): number => {
38
+ if (typeof config.maxValueLen === 'function') {
39
+ return config.maxValueLen(titleLen)
40
+ }
41
+ return config.maxValueLen
42
+ }
43
+
44
+ const MetadataContext = createContext<MetadataConfig>(defaultConfig)
45
+
46
+ // Props types
47
+ interface MetadataProps {
48
+ children: ReactNode
49
+ /** Configuration for metadata display */
50
+ config?: Partial<MetadataConfig>
51
+ }
52
+
53
+ interface LabelProps {
54
+ title: string
55
+ icon?: ImageLike | undefined | null
56
+ text?:
57
+ | string
58
+ | {
59
+ value: string
60
+ color?: Color.ColorLike | null
61
+ }
62
+ }
63
+
64
+ interface SeparatorProps {}
65
+
66
+ interface LinkProps {
67
+ title: string
68
+ target: string
69
+ text: string
70
+ }
71
+
72
+ interface TagListProps {
73
+ title: string
74
+ children: ReactNode
75
+ }
76
+
77
+ interface TagListItemProps {
78
+ icon?: ImageLike | undefined | null
79
+ text?: string
80
+ color?: Color.ColorLike | undefined | null
81
+ onAction?: () => void
82
+ }
83
+
84
+ // Components
85
+ const MetadataLabel = (props: LabelProps): any => {
86
+ const theme = useTheme()
87
+ const config = useContext(MetadataContext)
88
+ const textValue = typeof props.text === 'string' ? props.text : props.text?.value
89
+ const textColor = typeof props.text === 'object' ? props.text?.color : undefined
90
+
91
+ // No text = header label (just title, no colon or dash)
92
+ if (!textValue) {
93
+ return (
94
+ <box style={{ paddingBottom: config.paddingBottom }}>
95
+ <text flexShrink={0} fg={theme.textMuted}>{props.title}</text>
96
+ </box>
97
+ )
98
+ }
99
+
100
+ const maxLen = resolveMaxValueLen(config, props.title.length)
101
+
102
+ // Long value = column layout (title on one line, value below)
103
+ if (textValue.length > maxLen) {
104
+ return (
105
+ <box style={{ flexDirection: 'column', paddingBottom: config.paddingBottom }}>
106
+ <text flexShrink={0} fg={theme.textMuted}>{props.title}:</text>
107
+ <text flexShrink={0} fg={resolveColor(textColor) || theme.text}>{textValue}</text>
108
+ </box>
109
+ )
110
+ }
111
+
112
+ // Short value = row layout (title: value on same line)
113
+ return (
114
+ <box style={{ flexDirection: 'row', paddingBottom: config.paddingBottom }}>
115
+ <text flexShrink={0} fg={theme.textMuted} style={{ minWidth: config.titleMinWidth }}>{props.title}:</text>
116
+ <text flexShrink={0} fg={resolveColor(textColor) || theme.text}>{textValue}</text>
117
+ </box>
118
+ )
119
+ }
120
+
121
+ const MetadataSeparator = (_props: SeparatorProps): any => {
122
+ const theme = useTheme()
123
+ const config = useContext(MetadataContext)
124
+ return (
125
+ <box style={{ paddingBottom: config.paddingBottom }}>
126
+ <text flexShrink={0} fg={theme.border}>{'─'.repeat(config.separatorWidth)}</text>
127
+ </box>
128
+ )
129
+ }
130
+
131
+ const MetadataLink = (props: LinkProps): any => {
132
+ const theme = useTheme()
133
+ const config = useContext(MetadataContext)
134
+ const maxLen = resolveMaxValueLen(config, props.title.length)
135
+ const isLongValue = props.text.length > maxLen
136
+
137
+ if (isLongValue) {
138
+ return (
139
+ <box style={{ flexDirection: 'column', paddingBottom: config.paddingBottom }}>
140
+ <text flexShrink={0} fg={theme.textMuted}>{props.title}:</text>
141
+ <text flexShrink={0} fg={theme.accent} attributes={TextAttributes.UNDERLINE}>{props.text}</text>
142
+ </box>
143
+ )
144
+ }
145
+
146
+ return (
147
+ <box style={{ flexDirection: 'row', paddingBottom: config.paddingBottom }}>
148
+ <text flexShrink={0} fg={theme.textMuted} style={{ minWidth: config.titleMinWidth }}>{props.title}:</text>
149
+ <text flexShrink={0} fg={theme.accent} attributes={TextAttributes.UNDERLINE}>{props.text}</text>
150
+ </box>
151
+ )
152
+ }
153
+
154
+ const MetadataTagListItem = (props: TagListItemProps): any => {
155
+ const theme = useTheme()
156
+ const displayText = props.text || ''
157
+
158
+ return (
159
+ <text
160
+ fg={resolveColor(props.color) || theme.text}
161
+ style={{
162
+ paddingRight: 1,
163
+ paddingLeft: props.icon ? 1 : 0,
164
+ }}
165
+ >
166
+ {displayText}
167
+ </text>
168
+ )
169
+ }
170
+
171
+ interface MetadataTagListType {
172
+ (props: TagListProps): any
173
+ Item: (props: TagListItemProps) => any
174
+ }
175
+
176
+ const MetadataTagList: MetadataTagListType = (props) => {
177
+ const theme = useTheme()
178
+ const config = useContext(MetadataContext)
179
+
180
+ return (
181
+ <box style={{ flexDirection: 'row', paddingBottom: config.paddingBottom }}>
182
+ <text flexShrink={0} fg={theme.textMuted} style={{ minWidth: config.titleMinWidth }}>{props.title}:</text>
183
+ <box style={{ flexDirection: 'row' }}>{props.children}</box>
184
+ </box>
185
+ )
186
+ }
187
+
188
+ MetadataTagList.Item = MetadataTagListItem
189
+
190
+ // Main Metadata component with compound components
191
+ interface MetadataType {
192
+ (props: MetadataProps): any
193
+ Label: (props: LabelProps) => any
194
+ Separator: (props: SeparatorProps) => any
195
+ Link: (props: LinkProps) => any
196
+ TagList: MetadataTagListType
197
+ }
198
+
199
+ const Metadata: MetadataType = (props) => {
200
+ const config = { ...defaultConfig, ...props.config }
201
+
202
+ return (
203
+ <MetadataContext.Provider value={config}>
204
+ <box style={{ flexDirection: 'column' }}>
205
+ {props.children}
206
+ </box>
207
+ </MetadataContext.Provider>
208
+ )
209
+ }
210
+
211
+ Metadata.Label = MetadataLabel
212
+ Metadata.Separator = MetadataSeparator
213
+ Metadata.Link = MetadataLink
214
+ Metadata.TagList = MetadataTagList
215
+
216
+ export { Metadata, MetadataContext, defaultConfig }
217
+ export type { MetadataProps, MetadataConfig, LabelProps, SeparatorProps, LinkProps, TagListProps, TagListItemProps }
@@ -1,6 +1,6 @@
1
1
  import React, { useState } from 'react'
2
2
  import { useKeyboard } from '@opentui/react'
3
- import { Theme, persistTheme } from 'termcast/src/theme'
3
+ import { useTheme, persistTheme } from 'termcast/src/theme'
4
4
  import { themeNames } from '../themes'
5
5
  import { useStore } from 'termcast/src/state'
6
6
  import { useDialog } from 'termcast/src/internal/dialog'
@@ -8,6 +8,7 @@ import { useIsInFocus } from 'termcast/src/internal/focus-context'
8
8
  import { Dropdown } from 'termcast/src/components/dropdown'
9
9
 
10
10
  export function ThemePicker(): any {
11
+ const theme = useTheme()
11
12
  const dialog = useDialog()
12
13
  const inFocus = useIsInFocus()
13
14
  const currentThemeName = useStore((state) => state.currentThemeName)
@@ -49,7 +50,7 @@ export function ThemePicker(): any {
49
50
  key={name}
50
51
  title={name}
51
52
  value={name}
52
- color={name === previousTheme ? Theme.primary : undefined}
53
+ color={name === previousTheme ? theme.primary : undefined}
53
54
  />
54
55
  ))}
55
56
  </Dropdown>
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Example validating that ActionPanel preserves React context through portals.
3
+ *
4
+ * A custom React context (CounterContext) provides a counter value. The
5
+ * CounterAction component reads from this context at render time and closes
6
+ * over it in its onAction callback. If the portal preserves context correctly,
7
+ * the toast will show the matching prop and context values.
8
+ */
9
+ import React, { createContext, useContext, useState } from 'react'
10
+ import { List, ActionPanel, Action, showToast, Toast, renderWithProviders } from 'termcast'
11
+
12
+ const CounterContext = createContext(0)
13
+
14
+ // Action component that reads from context at render time
15
+ function CounterAction({ counter }: { counter: number }) {
16
+ // Read from context - this works because the portal preserves the
17
+ // React tree context from the source component
18
+ const contextValue = useContext(CounterContext)
19
+
20
+ return (
21
+ <Action
22
+ title="Show Counter"
23
+ onAction={() => {
24
+ // Both the prop and context value should match
25
+ showToast({
26
+ title: `prop=${counter} ctx=${contextValue}`,
27
+ style: Toast.Style.Success,
28
+ })
29
+ }}
30
+ />
31
+ )
32
+ }
33
+
34
+ function ActionsContextExample() {
35
+ const [counter, setCounter] = useState(42)
36
+
37
+ return (
38
+ <CounterContext.Provider value={counter}>
39
+ <List
40
+ navigationTitle="Context Test"
41
+ searchBarPlaceholder="Search..."
42
+ >
43
+ <List.Item
44
+ title={`Counter: ${counter}`}
45
+ subtitle="Press enter to show counter via action"
46
+ actions={
47
+ <ActionPanel>
48
+ <CounterAction counter={counter} />
49
+ <Action
50
+ title="Increment"
51
+ onAction={() => {
52
+ setCounter((c) => c + 1)
53
+ }}
54
+ />
55
+ </ActionPanel>
56
+ }
57
+ />
58
+ </List>
59
+ </CounterContext.Provider>
60
+ )
61
+ }
62
+
63
+ await renderWithProviders(<ActionsContextExample />)
@@ -0,0 +1,110 @@
1
+ import { test, expect, afterEach, beforeEach } from 'vitest'
2
+ import { launchTerminal, Session } from 'tuistory/src'
3
+
4
+ let session: Session
5
+
6
+ beforeEach(async () => {
7
+ session = await launchTerminal({
8
+ command: 'bun',
9
+ args: ['src/examples/actions-context.tsx'],
10
+ cols: 70,
11
+ rows: 20,
12
+ })
13
+ })
14
+
15
+ afterEach(() => {
16
+ session?.close()
17
+ })
18
+
19
+ test('actions preserve React context through portal', async () => {
20
+ // Wait for list to render
21
+ await session.text({
22
+ waitFor: (text) => /Counter: 42/.test(text),
23
+ })
24
+
25
+ const initial = await session.text()
26
+ expect(initial).toContain('Counter:')
27
+
28
+ // Open actions panel with Ctrl+K
29
+ await session.press(['ctrl', 'k'])
30
+
31
+ const actionsPanel = await session.text({
32
+ waitFor: (text) => /Show Counter/.test(text),
33
+ timeout: 5000,
34
+ })
35
+ // Verify the actions dialog is shown with our custom action
36
+ expect(actionsPanel).toContain('Show Counter')
37
+ expect(actionsPanel).toContain('Increment')
38
+ expect(actionsPanel).toMatchInlineSnapshot(`
39
+ "
40
+
41
+
42
+ ╭────────────────────────────────────────────────────────────────╮
43
+ │ │
44
+ │ Actions esc │
45
+ │ │
46
+ │ > Search actions... │
47
+ │ │
48
+ │ ›Show Counter │
49
+ │ Increment │
50
+ │ │
51
+ │ Settings │
52
+ │ Change Theme... │
53
+ │ See Console Logs │
54
+ │ │
55
+ │ │
56
+ │ ↵ select ↑↓ navigate │
57
+ │ │
58
+ ╰────────────────────────────────────────────────────────────────╯
59
+ "
60
+ `)
61
+
62
+ // Select "Show Counter" (first action, already selected)
63
+ await session.press('return')
64
+
65
+ // Verify toast shows correct context value (prop=42 ctx=42)
66
+ const afterAction = await session.text({
67
+ waitFor: (text) => /prop=42 ctx=42/.test(text),
68
+ timeout: 5000,
69
+ })
70
+ expect(afterAction).toContain('prop=42')
71
+ }, 30000)
72
+
73
+ test('actions context stays fresh after state updates', async () => {
74
+ // Wait for list to render
75
+ await session.text({
76
+ waitFor: (text) => /Counter: 42/.test(text),
77
+ })
78
+
79
+ // Open actions, select "Increment" to change counter
80
+ await session.press(['ctrl', 'k'])
81
+ await session.text({
82
+ waitFor: (text) => /Increment/.test(text),
83
+ timeout: 5000,
84
+ })
85
+
86
+ // Navigate down to "Increment" action
87
+ await session.press('down')
88
+ await session.press('return')
89
+
90
+ // Wait for counter to update to 43
91
+ await session.text({
92
+ waitFor: (text) => /Counter: 43/.test(text),
93
+ timeout: 5000,
94
+ })
95
+
96
+ // Now open actions again and run "Show Counter"
97
+ await session.press(['ctrl', 'k'])
98
+ await session.text({
99
+ waitFor: (text) => /Show Counter/.test(text),
100
+ timeout: 5000,
101
+ })
102
+ await session.press('return')
103
+
104
+ // Verify toast shows UPDATED context value (43, not stale 42)
105
+ const afterAction = await session.text({
106
+ waitFor: (text) => /prop=43 ctx=43/.test(text),
107
+ timeout: 5000,
108
+ })
109
+ expect(afterAction).toContain('prop=43')
110
+ }, 30000)
@@ -17,7 +17,8 @@ afterEach(() => {
17
17
  session?.close()
18
18
  })
19
19
 
20
- test('actions dialog layout shift when opening with ctrl+k', async () => {
20
+ // this was a temporary test to find out if there was any layout shift in dialogs
21
+ test.skip('actions dialog layout shift when opening with ctrl+k', async () => {
21
22
  // Wait for list to fully render
22
23
  await session.text({
23
24
  waitFor: (text) => /search/i.test(text) && /Apple/.test(text),
@@ -41,7 +41,6 @@ test('autocomplete shows flat file list in dialog', async () => {
41
41
 
42
42
  ◇ Select Files
43
43
  │ Enter file path...
44
-
45
44
  │ Choose one or more files to upload
46
45
 
47
46
  ╭────────────────────────────────────────────────────────────────╮
@@ -52,8 +51,6 @@ test('autocomplete shows flat file list in dialog', async () => {
52
51
  │ │
53
52
  │ ↑↓ navigate ⏎/tab select esc close │
54
53
  ╰────────────────────────────────────────────────────────────────╯
55
- │ Choose exactly one file
56
-
57
54
 
58
55
 
59
56
 
@@ -61,6 +58,9 @@ test('autocomplete shows flat file list in dialog', async () => {
61
58
 
62
59
 
63
60
 
61
+
62
+
63
+
64
64
  "
65
65
  `)
66
66
  }, 10000)
@@ -93,7 +93,6 @@ test('autocomplete navigation with down/up keys', async () => {
93
93
 
94
94
  ◇ Select Files
95
95
  │ Enter file path...
96
-
97
96
  │ Choose one or more files to upload
98
97
 
99
98
  ╭────────────────────────────────────────────────────────────────╮
@@ -104,8 +103,6 @@ test('autocomplete navigation with down/up keys', async () => {
104
103
  │ │
105
104
  │ ↑↓ navigate ⏎/tab select esc close │
106
105
  ╰────────────────────────────────────────────────────────────────╯
107
- │ Choose exactly one file
108
-
109
106
 
110
107
 
111
108
 
@@ -113,6 +110,9 @@ test('autocomplete navigation with down/up keys', async () => {
113
110
 
114
111
 
115
112
 
113
+
114
+
115
+
116
116
  "
117
117
  `)
118
118
 
@@ -130,7 +130,6 @@ test('autocomplete navigation with down/up keys', async () => {
130
130
 
131
131
  ◇ Select Files
132
132
  │ Enter file path...
133
-
134
133
  │ Choose one or more files to upload
135
134
 
136
135
  ╭────────────────────────────────────────────────────────────────╮
@@ -141,8 +140,6 @@ test('autocomplete navigation with down/up keys', async () => {
141
140
  │ │
142
141
  │ ↑↓ navigate ⏎/tab select esc close │
143
142
  ╰────────────────────────────────────────────────────────────────╯
144
- │ Choose exactly one file
145
-
146
143
 
147
144
 
148
145
 
@@ -150,6 +147,9 @@ test('autocomplete navigation with down/up keys', async () => {
150
147
 
151
148
 
152
149
 
150
+
151
+
152
+
153
153
  "
154
154
  `)
155
155
  }, 10000)
@@ -179,8 +179,6 @@ test('file picker shows only files, not folders', async () => {
179
179
  │ John Doe
180
180
 
181
181
  ◆ Select Files
182
- │ t
183
-
184
182
  ╭────────────────────────────────────────────────────────────────╮
185
183
  │ │
186
184
  │ Filter: t │
@@ -195,11 +193,13 @@ test('file picker shows only files, not folders', async () => {
195
193
  │ ↑↓ navigate ⏎/tab select esc close │
196
194
  ╰────────────────────────────────────────────────────────────────╯
197
195
 
198
-
199
196
  ctrl ↵ submit tab navigate ^k actions
200
197
 
201
198
 
202
199
 
200
+
201
+
202
+
203
203
  "
204
204
  `)
205
205
  expect(snapshot).toContain('▪')
@@ -234,17 +234,14 @@ test('escape closes autocomplete and form stays visible', async () => {
234
234
 
235
235
  ◇ Select Files
236
236
  │ Enter file path...
237
-
238
237
  │ Choose one or more files to upload
239
238
 
240
239
  ◆ Select Folder
241
240
  │ s
242
-
243
241
  │ Choose a folder for output
244
242
 
245
243
  ◇ Select Single File
246
244
  │ Enter file path...
247
-
248
245
  │ Choose exactly one file
249
246
 
250
247
 
@@ -254,6 +251,9 @@ test('escape closes autocomplete and form stays visible', async () => {
254
251
 
255
252
 
256
253
 
254
+
255
+
256
+
257
257
  "
258
258
  `)
259
259
  }, 10000)
@@ -82,6 +82,18 @@ export function FormBasicExample(): any {
82
82
  </Form.Dropdown.Section>
83
83
  </Form.Dropdown>
84
84
 
85
+ <Form.Dropdown
86
+ id='emptyDropdown'
87
+ title='Empty Dropdown'
88
+ placeholder='No items available'
89
+ />
90
+
91
+ <Form.TextField
92
+ id='minimalField'
93
+ title='Minimal Field'
94
+ placeholder='No info text'
95
+ />
96
+
85
97
  <Form.DatePicker
86
98
  id='birthdate'
87
99
  title='Date of Birth'