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,16 +1,17 @@
1
1
  import React, { ReactNode, useMemo, ReactElement } from 'react'
2
2
  import { TextAttributes } from '@opentui/core'
3
- import { useKeyboard } from '@opentui/react'
4
- import { Theme, markdownSyntaxStyle } from 'termcast/src/theme'
3
+ import { useKeyboard, useTerminalDimensions } from '@opentui/react'
4
+ import { useTheme, markdownSyntaxStyle } from 'termcast/src/theme'
5
5
  import { InFocus, useIsInFocus } from 'termcast/src/internal/focus-context'
6
6
  import { ActionPanel, Action } from 'termcast/src/components/actions'
7
- import { Image } from 'termcast/src/components/list'
8
- import { Color, resolveColor } from 'termcast/src/colors'
9
7
  import { Footer } from 'termcast/src/components/footer'
10
8
 
11
9
  import { useDialog } from 'termcast/src/internal/dialog'
12
10
  import { ScrollBox } from 'termcast/src/internal/scrollbox'
13
11
  import { useStore } from 'termcast/src/state'
12
+ import { Offscreen } from 'termcast/src/internal/offscreen'
13
+ import { Metadata, MetadataContext } from 'termcast/src/components/metadata'
14
+ import type { LabelProps, SeparatorProps, LinkProps, TagListProps, TagListItemProps, MetadataConfig } from 'termcast/src/components/metadata'
14
15
 
15
16
  interface ActionsInterface {
16
17
  actions?: ReactNode
@@ -35,133 +36,11 @@ interface MetadataProps {
35
36
  children: ReactNode
36
37
  }
37
38
 
38
- interface LabelProps {
39
- title: string
40
- icon?: Image.ImageLike | undefined | null
41
- text?:
42
- | string
43
- | {
44
- value: string
45
- color?: Color.ColorLike | null
46
- }
47
- }
48
-
49
- interface SeparatorProps {}
50
-
51
- interface LinkProps {
52
- title: string
53
- target: string
54
- text: string
55
- }
56
-
57
- interface TagListProps {
58
- title: string
59
- children: ReactNode
60
- }
61
-
62
- interface TagListItemProps {
63
- icon?: Image.ImageLike | undefined | null
64
- text?: string
65
- color?: Color.ColorLike | undefined | null
66
- onAction?: () => void
67
- }
68
-
69
- interface DetailType {
70
- (props: DetailProps): any
71
- Metadata: DetailMetadataType
72
- }
73
-
74
- const DetailMetadataLabel = (props: LabelProps): any => {
75
- const textValue =
76
- typeof props.text === 'string' ? props.text : props.text?.value
77
- const textColor =
78
- typeof props.text === 'object' ? props.text?.color : undefined
79
-
80
- return (
81
- <box
82
- style={{
83
- flexDirection: 'row',
84
- paddingBottom: 1,
85
- }}
86
- >
87
- <text fg={Theme.textMuted} style={{ minWidth: 15 }}>
88
- {props.title}:
89
- </text>
90
- <text fg={resolveColor(textColor) || Theme.text}>{textValue || '—'}</text>
91
- </box>
92
- )
93
- }
94
-
95
- const DetailMetadataSeparator = (props: SeparatorProps): any => {
96
- return (
97
- <box
98
- style={{
99
- paddingTop: 1,
100
- paddingBottom: 1,
101
- }}
102
- >
103
- <text fg={Theme.textMuted}>{'─'.repeat(30)}</text>
104
- </box>
105
- )
106
- }
107
-
108
- const DetailMetadataLink = (props: LinkProps): any => {
109
- return (
110
- <box
111
- style={{
112
- flexDirection: 'row',
113
- paddingBottom: 1,
114
- }}
115
- >
116
- <text fg={Theme.textMuted} style={{ minWidth: 15 }}>
117
- {props.title}:
118
- </text>
119
- <text fg={Theme.accent} attributes={TextAttributes.UNDERLINE}>
120
- {props.text}
121
- </text>
122
- </box>
123
- )
124
- }
125
-
126
- const DetailMetadataTagListItem = (props: TagListItemProps): any => {
127
- const displayText = props.text || ''
128
-
129
- return (
130
- <text
131
- fg={resolveColor(props.color) || Theme.text}
132
- style={{
133
- paddingRight: 1,
134
- paddingLeft: props.icon ? 1 : 0,
135
- }}
136
- >
137
- {displayText}
138
- </text>
139
- )
140
- }
141
-
142
39
  interface DetailMetadataTagListType {
143
40
  (props: TagListProps): any
144
41
  Item: (props: TagListItemProps) => any
145
42
  }
146
43
 
147
- const DetailMetadataTagList: DetailMetadataTagListType = (props) => {
148
- return (
149
- <box
150
- style={{
151
- flexDirection: 'column',
152
- paddingBottom: 1,
153
- }}
154
- >
155
- <text fg={Theme.textMuted} style={{ minWidth: 15 }}>
156
- {props.title}:
157
- </text>
158
- <box style={{ flexDirection: 'row' }}>{props.children}</box>
159
- </box>
160
- )
161
- }
162
-
163
- DetailMetadataTagList.Item = DetailMetadataTagListItem
164
-
165
44
  interface DetailMetadataType {
166
45
  (props: MetadataProps): any
167
46
  Label: (props: LabelProps) => any
@@ -170,26 +49,47 @@ interface DetailMetadataType {
170
49
  TagList: DetailMetadataTagListType
171
50
  }
172
51
 
52
+ interface DetailType {
53
+ (props: DetailProps): any
54
+ Metadata: DetailMetadataType
55
+ }
56
+
173
57
  const DetailMetadata: DetailMetadataType = (props) => {
58
+ const { width } = useTerminalDimensions()
59
+
60
+ // Dynamic config based on terminal width
61
+ // Calculate maxValueLen as a function of title length:
62
+ // availableWidth = terminalWidth - padding(~4) - titleWidth - colon+space(2)
63
+ const config: MetadataConfig = {
64
+ maxValueLen: (titleLen: number) => {
65
+ const padding = 4
66
+ const colonSpace = 2
67
+ const titleWidth = Math.max(titleLen, 15) // minimum title width
68
+ return Math.max(10, width - padding - titleWidth - colonSpace)
69
+ },
70
+ titleMinWidth: 15,
71
+ paddingBottom: 1,
72
+ separatorWidth: Math.min(30, width - 4),
73
+ }
74
+
174
75
  return (
175
- <box
176
- style={{
177
- flexDirection: 'column',
178
- paddingTop: 1,
179
- }}
180
- border={['top']}
181
- borderStyle='single'
182
- borderColor={Theme.border}
183
- >
184
- {props.children}
185
- </box>
76
+ <MetadataContext.Provider value={config}>
77
+ <box
78
+ style={{
79
+ flexDirection: 'column',
80
+ paddingTop: 1,
81
+ }}
82
+ >
83
+ {props.children}
84
+ </box>
85
+ </MetadataContext.Provider>
186
86
  )
187
87
  }
188
88
 
189
- DetailMetadata.Label = DetailMetadataLabel
190
- DetailMetadata.Separator = DetailMetadataSeparator
191
- DetailMetadata.Link = DetailMetadataLink
192
- DetailMetadata.TagList = DetailMetadataTagList
89
+ DetailMetadata.Label = Metadata.Label
90
+ DetailMetadata.Separator = Metadata.Separator
91
+ DetailMetadata.Link = Metadata.Link
92
+ DetailMetadata.TagList = Metadata.TagList
193
93
 
194
94
  function DetailFooter({
195
95
  hasActions,
@@ -198,29 +98,31 @@ function DetailFooter({
198
98
  hasActions?: boolean
199
99
  firstActionTitle?: string
200
100
  }): any {
101
+ const theme = useTheme()
102
+
201
103
  return (
202
104
  <Footer paddingLeft={0} paddingRight={0}>
203
105
  <box style={{ flexDirection: 'row', gap: 3 }}>
204
106
  <box style={{ flexDirection: 'row', gap: 1 }}>
205
- <text flexShrink={0} fg={Theme.text} attributes={TextAttributes.BOLD}>
107
+ <text flexShrink={0} fg={theme.text} attributes={TextAttributes.BOLD}>
206
108
  esc
207
109
  </text>
208
- <text flexShrink={0} fg={Theme.textMuted}>go back</text>
110
+ <text flexShrink={0} fg={theme.textMuted}>go back</text>
209
111
  </box>
210
112
  {hasActions && (
211
113
  <box style={{ flexDirection: 'row', gap: 1 }}>
212
- <text flexShrink={0} fg={Theme.text} attributes={TextAttributes.BOLD}>
114
+ <text flexShrink={0} fg={theme.text} attributes={TextAttributes.BOLD}>
213
115
  ^k
214
116
  </text>
215
- <text flexShrink={0} fg={Theme.textMuted}>actions</text>
117
+ <text flexShrink={0} fg={theme.textMuted}>actions</text>
216
118
  </box>
217
119
  )}
218
120
  {hasActions && firstActionTitle && (
219
121
  <box style={{ flexDirection: 'row', gap: 1 }}>
220
- <text flexShrink={0} fg={Theme.text} attributes={TextAttributes.BOLD}>
122
+ <text flexShrink={0} fg={theme.text} attributes={TextAttributes.BOLD}>
221
123
 
222
124
  </text>
223
- <text flexShrink={0} fg={Theme.textMuted}>{firstActionTitle}</text>
125
+ <text flexShrink={0} fg={theme.textMuted}>{firstActionTitle}</text>
224
126
  </box>
225
127
  )}
226
128
  </box>
@@ -274,12 +176,13 @@ const Detail: DetailType = (props) => {
274
176
  if (!inFocus) return
275
177
 
276
178
  if (evt.name === 'k' && evt.ctrl) {
277
- // Ctrl+K shows actions (always show panel, even without actions)
278
- dialog.pushActions(actions || <ActionPanel />)
179
+ // Ctrl+K shows actions dialog via portal
180
+ if (actions) {
181
+ useStore.setState({ showActionsDialog: true })
182
+ }
279
183
  } else if (evt.name === 'return' && actions) {
280
- // Enter executes first action directly
184
+ // Enter auto-executes first action via ActionPanel's layout effect
281
185
  useStore.setState({ shouldAutoExecuteFirstAction: true })
282
- dialog.pushActions(actions)
283
186
  }
284
187
  })
285
188
 
@@ -317,6 +220,8 @@ const Detail: DetailType = (props) => {
317
220
  hasActions={!!actions}
318
221
  firstActionTitle={firstActionTitle}
319
222
  />
223
+ {/* Render actions offscreen to capture them */}
224
+ {actions && <Offscreen>{actions}</Offscreen>}
320
225
  </box>
321
226
  )
322
227
  }
@@ -1,9 +1,10 @@
1
1
  import React, {
2
2
  ReactNode,
3
3
  useState,
4
- useEffect,
4
+ useLayoutEffect,
5
5
  useMemo,
6
6
  useRef,
7
+ useCallback,
7
8
  createContext,
8
9
  useContext,
9
10
  } from 'react'
@@ -13,8 +14,10 @@ import {
13
14
  ScrollBoxRenderable,
14
15
  TextareaRenderable,
15
16
  } from '@opentui/core'
16
- import { Theme } from 'termcast/src/theme'
17
+ import { useTheme } from 'termcast/src/theme'
18
+ import { getIconValue } from 'termcast/src/components/icon'
17
19
  import { logger } from 'termcast/src/logger'
20
+ import { useStore } from 'termcast/src/state'
18
21
  import { useIsInFocus } from 'termcast/src/internal/focus-context'
19
22
  import { useIsOffscreen } from 'termcast/src/internal/offscreen'
20
23
  import { CommonProps } from 'termcast/src/utils'
@@ -43,7 +46,7 @@ export interface DropdownProps extends SearchBarInterface, CommonProps {
43
46
 
44
47
  export interface DropdownItemProps extends CommonProps {
45
48
  title: string
46
- value: string
49
+ value?: string
47
50
  icon?: ReactNode
48
51
 
49
52
  keywords?: string[]
@@ -111,6 +114,7 @@ const Dropdown: DropdownType = (props) => {
111
114
  throttle,
112
115
  } = props
113
116
 
117
+ const theme = useTheme()
114
118
  const isOffscreen = useIsOffscreen()
115
119
  const [selected, setSelected] = useState(0)
116
120
  const [searchText, setSearchTextState] = useState('')
@@ -118,6 +122,23 @@ const Dropdown: DropdownType = (props) => {
118
122
  value || defaultValue,
119
123
  )
120
124
  const inputRef = useRef<TextareaRenderable>(null)
125
+
126
+ // Ref callback that registers the textarea in global state for ESC handling
127
+ const setInputRef = useCallback((node: TextareaRenderable | null) => {
128
+ if (!node) return
129
+
130
+ inputRef.current = node
131
+ useStore.setState({ activeSearchInputRef: node })
132
+
133
+ // React 19: return cleanup function for unmount
134
+ return () => {
135
+ if (useStore.getState().activeSearchInputRef === node) {
136
+ useStore.setState({ activeSearchInputRef: null })
137
+ }
138
+ inputRef.current = null
139
+ }
140
+ }, [])
141
+
121
142
  const lastSearchTextRef = useRef('')
122
143
  const throttleTimeoutRef = useRef<NodeJS.Timeout | undefined>(undefined)
123
144
  const scrollBoxRef = useRef<ScrollBoxRenderable>(null)
@@ -157,8 +178,8 @@ const Dropdown: DropdownType = (props) => {
157
178
  [searchText, filtering, selected, currentValue, onSelectionChange],
158
179
  )
159
180
 
160
- // Update controlled value
161
- useEffect(() => {
181
+ // Update controlled value (before paint to avoid flash)
182
+ useLayoutEffect(() => {
162
183
  if (value !== undefined) {
163
184
  setCurrentValue(value)
164
185
  }
@@ -177,7 +198,10 @@ const Dropdown: DropdownType = (props) => {
177
198
  .sort((a: any, b: any) => a.index - b.index)
178
199
 
179
200
  if (items.length > 0 && items[0]) {
180
- setSelected(items[0].index)
201
+ flushSync(() => {
202
+ setSelected(items[0].index)
203
+ })
204
+ scrollToItem(items[0])
181
205
  }
182
206
 
183
207
  if (onSearchTextChange) {
@@ -218,7 +242,9 @@ const Dropdown: DropdownType = (props) => {
218
242
 
219
243
  const nextItem = items[nextVisibleIndex]
220
244
  if (nextItem) {
221
- setSelected(nextItem.index)
245
+ flushSync(() => {
246
+ setSelected(nextItem.index)
247
+ })
222
248
  scrollToItem(nextItem)
223
249
  if (onSelectionChange && nextItem.props) {
224
250
  onSelectionChange((nextItem.props as DropdownItemDescendant).value)
@@ -292,13 +318,13 @@ const Dropdown: DropdownType = (props) => {
292
318
  justifyContent: 'space-between',
293
319
  }}
294
320
  >
295
- <text fg={Theme.textMuted}>{tooltip}</text>
296
- <text fg={Theme.textMuted}>esc</text>
321
+ <text fg={theme.textMuted}>{tooltip}</text>
322
+ <text fg={theme.textMuted}>esc</text>
297
323
  </box>
298
324
  <box style={{ paddingTop: 1, paddingBottom: 1, flexDirection: 'row' }}>
299
- <text flexShrink={0} fg={Theme.primary}>&gt; </text>
325
+ <text flexShrink={0} fg={theme.primary}>&gt; </text>
300
326
  <textarea
301
- ref={inputRef}
327
+ ref={setInputRef}
302
328
  height={1}
303
329
  flexGrow={1}
304
330
  wrapMode='none'
@@ -313,9 +339,9 @@ const Dropdown: DropdownType = (props) => {
313
339
  placeholder={placeholder}
314
340
  focused={inFocus}
315
341
  initialValue=""
316
- focusedBackgroundColor={Theme.backgroundPanel}
317
- cursorColor={Theme.primary}
318
- focusedTextColor={Theme.textMuted}
342
+ focusedBackgroundColor={theme.backgroundPanel}
343
+ cursorColor={theme.primary}
344
+ focusedTextColor={theme.textMuted}
319
345
  />
320
346
  </box>
321
347
  </box>
@@ -350,14 +376,14 @@ const Dropdown: DropdownType = (props) => {
350
376
  flexDirection: 'row',
351
377
  }}
352
378
  >
353
- <text fg={Theme.text} attributes={TextAttributes.BOLD}>
379
+ <text fg={theme.text} attributes={TextAttributes.BOLD}>
354
380
 
355
381
  </text>
356
- <text fg={Theme.textMuted}> select</text>
357
- <text fg={Theme.text} attributes={TextAttributes.BOLD}>
382
+ <text fg={theme.textMuted}> select</text>
383
+ <text fg={theme.text} attributes={TextAttributes.BOLD}>
358
384
  {' '}↑↓
359
385
  </text>
360
- <text fg={Theme.textMuted}> navigate</text>
386
+ <text fg={theme.textMuted}> navigate</text>
361
387
  </box>
362
388
  </box>
363
389
  </DropdownContext.Provider>
@@ -376,6 +402,7 @@ function ItemOption(props: {
376
402
  onMouseMove?: () => void
377
403
  elementRef?: React.Ref<any>
378
404
  }) {
405
+ const theme = useTheme()
379
406
  const [isHovered, setIsHovered] = useState(false)
380
407
 
381
408
  return (
@@ -384,9 +411,9 @@ function ItemOption(props: {
384
411
  style={{
385
412
  flexDirection: 'row',
386
413
  backgroundColor: props.active
387
- ? Theme.primary
414
+ ? theme.primary
388
415
  : isHovered
389
- ? Theme.backgroundPanel
416
+ ? theme.backgroundPanel
390
417
  : undefined,
391
418
  paddingLeft: props.active ? 0 : 1,
392
419
  paddingRight: 1,
@@ -402,27 +429,27 @@ function ItemOption(props: {
402
429
  >
403
430
  <box style={{ flexDirection: 'row' }}>
404
431
  {props.active && (
405
- <text fg={Theme.background} selectable={false}>
432
+ <text fg={theme.background} selectable={false}>
406
433
  ›{''}
407
434
  </text>
408
435
  )}
409
436
  {props.icon && (
410
437
  <text
411
- fg={props.active ? Theme.background : Theme.text}
438
+ fg={props.active ? theme.background : theme.text}
412
439
  selectable={false}
413
440
  >
414
- {String(props.icon)}{' '}
441
+ {getIconValue(props.icon)}{' '}
415
442
  </text>
416
443
  )}
417
444
  <text
418
445
  fg={
419
446
  props.active
420
- ? Theme.background
447
+ ? theme.background
421
448
  : props.color
422
449
  ? props.color
423
450
  : props.current
424
- ? Theme.primary
425
- : Theme.text
451
+ ? theme.primary
452
+ : theme.text
426
453
  }
427
454
  attributes={props.active ? TextAttributes.BOLD : undefined}
428
455
  selectable={false}
@@ -432,7 +459,7 @@ function ItemOption(props: {
432
459
  </box>
433
460
  {props.label && (
434
461
  <text
435
- fg={props.active ? Theme.background : Theme.textMuted}
462
+ fg={props.active ? theme.background : theme.textMuted}
436
463
  attributes={props.active ? TextAttributes.BOLD : undefined}
437
464
  selectable={false}
438
465
  >
@@ -466,9 +493,10 @@ const DropdownItem: (props: DropdownItemProps) => any = (props) => {
466
493
  return !searchableText.includes(needle)
467
494
  })()
468
495
 
469
- // Register as descendant
496
+ // Register as descendant - use title as fallback for value
497
+ const value = props.value ?? props.title
470
498
  const { index } = useDropdownItemDescendant({
471
- value: props.value,
499
+ value,
472
500
  title: props.title,
473
501
  hidden: shouldHide,
474
502
  elementRef: elementRef.current,
@@ -479,7 +507,7 @@ const DropdownItem: (props: DropdownItemProps) => any = (props) => {
479
507
 
480
508
  // Determine if active (index will be -1 if hidden)
481
509
  const isActive = index === selectedIndex && index !== -1
482
- const isCurrent = props.value === currentValue
510
+ const isCurrent = value === currentValue
483
511
 
484
512
  // Handle mouse events
485
513
  const handleMouseMove = () => {
@@ -491,15 +519,15 @@ const DropdownItem: (props: DropdownItemProps) => any = (props) => {
491
519
  ) {
492
520
  context.setSelectedIndex(index)
493
521
  if (context.onSelectionChange) {
494
- context.onSelectionChange(props.value)
522
+ context.onSelectionChange(value)
495
523
  }
496
524
  }
497
525
  }
498
526
 
499
527
  const handleMouseDown = () => {
500
528
  // Trigger selection on click
501
- if (context.onChange && props.value) {
502
- context.onChange(props.value)
529
+ if (context.onChange && value) {
530
+ context.onChange(value)
503
531
  }
504
532
  }
505
533
 
@@ -520,6 +548,7 @@ const DropdownItem: (props: DropdownItemProps) => any = (props) => {
520
548
  }
521
549
 
522
550
  const DropdownSection: (props: DropdownSectionProps) => any = (props) => {
551
+ const theme = useTheme()
523
552
  const parentContext = useContext(DropdownContext)
524
553
  const isOffscreen = useIsOffscreen()
525
554
  if (!parentContext) return null
@@ -542,12 +571,17 @@ const DropdownSection: (props: DropdownSectionProps) => any = (props) => {
542
571
  )
543
572
  }
544
573
 
574
+ // Hide section titles when filtering is active and there's search text
575
+ // This prevents showing empty section headers when all items are filtered out
576
+ const hideTitle =
577
+ parentContext.filtering && parentContext.searchText.trim().length > 0
578
+
545
579
  return (
546
580
  <>
547
- {/* Render section title if provided */}
548
- {props.title && (
581
+ {/* Render section title if provided and not hidden by filtering */}
582
+ {props.title && !hideTitle && (
549
583
  <box style={{ paddingTop: 1, paddingLeft: 1 }}>
550
- <text fg={Theme.accent} attributes={TextAttributes.BOLD}>
584
+ <text fg={theme.accent} attributes={TextAttributes.BOLD}>
551
585
  {props.title}
552
586
  </text>
553
587
  </box>