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
@@ -7,7 +7,7 @@ import React, {
7
7
  useLayoutEffect,
8
8
  } from 'react'
9
9
  import { BoxRenderable, ScrollBoxRenderable } from '@opentui/core'
10
- import { useKeyboard } from '@opentui/react'
10
+ import { useKeyboard, flushSync } from '@opentui/react'
11
11
  import {
12
12
  useFormContext,
13
13
  Controller,
@@ -17,12 +17,12 @@ import {
17
17
  import { useFocusContext, useFormFieldDescendant } from './index'
18
18
  import { FormItemProps, FormItemRef } from './types'
19
19
  import { logger } from 'termcast/src/logger'
20
- import { Theme } from 'termcast/src/theme'
20
+ import { useTheme } from 'termcast/src/theme'
21
21
  import {
22
22
  createDescendants,
23
23
  DescendantContextType,
24
24
  } from 'termcast/src/descendants'
25
- import { WithLeftBorder } from './with-left-border'
25
+ import { WithLeftBorder, TitleIndicator } from './with-left-border'
26
26
  import { useIsInFocus } from 'termcast/src/internal/focus-context'
27
27
  import { useFormNavigationHelpers } from './use-form-navigation'
28
28
  import { LoadingText } from 'termcast/src/components/loading-text'
@@ -103,6 +103,7 @@ const FormDropdownContext = createContext<FormDropdownContextValue>({
103
103
  })
104
104
 
105
105
  const DropdownItem = (props: DropdownItemProps) => {
106
+ const theme = useTheme()
106
107
  const context = useContext(FormDropdownContext)
107
108
  const sectionContext = useContext(SectionContext)
108
109
  const elementRef = useRef<BoxRenderable | null>(null)
@@ -138,32 +139,27 @@ const DropdownItem = (props: DropdownItemProps) => {
138
139
 
139
140
  return (
140
141
  <box ref={elementRef} key={props.value}>
141
- <WithLeftBorder
142
- isFocused={context.isFocused}
143
- paddingLeft={0}
144
- paddingBottom={0}
142
+ <text
143
+ fg={
144
+ context.isFocused && isFocused
145
+ ? theme.accent
146
+ : context.isFocused
147
+ ? theme.text
148
+ : theme.textMuted
149
+ }
150
+ onMouseDown={() => {
151
+ context.handleSelect(descendant.descendantId)
152
+ }}
145
153
  >
146
- <text
147
- fg={
148
- context.isFocused && isFocused
149
- ? Theme.accent
150
- : context.isFocused
151
- ? Theme.text
152
- : Theme.textMuted
153
- }
154
- onMouseDown={() => {
155
- context.handleSelect(descendant.descendantId)
156
- }}
157
- >
158
- {context.isFocused && isFocused ? '› ' : ' '}
159
- {isSelected ? '●' : '○'} {props.title}
160
- </text>
161
- </WithLeftBorder>
154
+ {context.isFocused && isFocused ? '› ' : ' '}
155
+ {isSelected ? '●' : '○'} {props.title}
156
+ </text>
162
157
  </box>
163
158
  )
164
159
  }
165
160
 
166
161
  const DropdownSection = (props: DropdownSectionProps) => {
162
+ const theme = useTheme()
167
163
  const parentContext = useContext(FormDropdownContext)
168
164
 
169
165
  // Create section context value
@@ -178,13 +174,7 @@ const DropdownSection = (props: DropdownSectionProps) => {
178
174
  <SectionContext.Provider value={sectionContextValue}>
179
175
  <box flexDirection='column'>
180
176
  {props.title && (
181
- <WithLeftBorder
182
- paddingTop={0}
183
- paddingBottom={0}
184
- isFocused={parentContext.isFocused}
185
- >
186
- <text fg={Theme.textMuted}>{props.title}</text>
187
- </WithLeftBorder>
177
+ <text fg={theme.textMuted}> {props.title}</text>
188
178
  )}
189
179
  {props.children}
190
180
  </box>
@@ -203,6 +193,7 @@ const DropdownContent = ({
203
193
  fieldState,
204
194
  ...props
205
195
  }: DropdownContentProps) => {
196
+ const theme = useTheme()
206
197
  const descendantsContext = useFormDropdownDescendants()
207
198
  const isInFocus = useIsInFocus()
208
199
  const focusContext = useFocusContext()
@@ -224,10 +215,6 @@ const DropdownContent = ({
224
215
 
225
216
  const [selectedTitles, setSelectedTitles] = useState<string[]>([])
226
217
 
227
- const { navigateToPrevious, navigateToNext } = useFormNavigationHelpers(
228
- props.id,
229
- )
230
-
231
218
  const scrollToItem = (item: { props?: FormDropdownItemDescendant }) => {
232
219
  const scrollBox = scrollBoxRef.current
233
220
  const itemElementRef = item.props?.elementRef
@@ -244,11 +231,11 @@ const DropdownContent = ({
244
231
  scrollBox.scrollTo(Math.max(0, targetScrollTop))
245
232
  }
246
233
 
247
- // Helper to get value for a descendantId
234
+ // Helper to get value for a descendantId - use committedMap for stability
248
235
  const getValueForDescendantId = (
249
236
  descendantId: string,
250
237
  ): string | undefined => {
251
- const item = descendantsContext.map.current[descendantId]
238
+ const item = descendantsContext.committedMap[descendantId]
252
239
  return item?.props?.value
253
240
  }
254
241
 
@@ -256,7 +243,7 @@ const DropdownContent = ({
256
243
  const value = getValueForDescendantId(descendantId)
257
244
  if (!value) return
258
245
 
259
- const item = descendantsContext.map.current[descendantId]
246
+ const item = descendantsContext.committedMap[descendantId]
260
247
  const title = item?.props?.title || value
261
248
 
262
249
  if (props.hasMultipleSelection) {
@@ -290,7 +277,7 @@ const DropdownContent = ({
290
277
  useKeyboard((evt) => {
291
278
  if (!isFocused || !isInFocus) return
292
279
 
293
- const items = Object.values(descendantsContext.map.current)
280
+ const items = Object.values(descendantsContext.committedMap)
294
281
  .filter((item) => item.index !== -1)
295
282
  .sort((a, b) => a.index - b.index)
296
283
  const itemCount = items.length
@@ -298,21 +285,29 @@ const DropdownContent = ({
298
285
  if (itemCount > 0) {
299
286
  if (evt.name === 'down') {
300
287
  const nextIndex = (focusedIndex + 1) % itemCount
301
- setFocusedIndex(nextIndex)
302
288
  const nextItem = items[nextIndex]
303
289
  if (nextItem) {
290
+ flushSync(() => {
291
+ setFocusedIndex(nextIndex)
292
+ })
304
293
  scrollToItem(nextItem)
305
294
  }
306
295
  } else if (evt.name === 'up') {
307
296
  const nextIndex = (focusedIndex - 1 + itemCount) % itemCount
308
- setFocusedIndex(nextIndex)
309
297
  const nextItem = items[nextIndex]
310
298
  if (nextItem) {
299
+ flushSync(() => {
300
+ setFocusedIndex(nextIndex)
301
+ })
311
302
  scrollToItem(nextItem)
312
303
  }
313
- } else if (evt.name === 'return' || evt.name === 'space') {
314
- // Toggle selection of current focused item
315
- const entries = Object.entries(descendantsContext.map.current)
304
+ } else if (
305
+ (evt.name === 'return' || evt.name === 'space') &&
306
+ !evt.ctrl &&
307
+ !evt.meta
308
+ ) {
309
+ // Toggle selection of current focused item (but not when ctrl/meta is held for form submission)
310
+ const entries = Object.entries(descendantsContext.committedMap)
316
311
  const sortedEntries = entries
317
312
  .filter(([_, item]) => item.index !== -1)
318
313
  .sort((a, b) => a[1].index - b[1].index)
@@ -324,16 +319,6 @@ const DropdownContent = ({
324
319
  }
325
320
  }
326
321
 
327
- // Handle tab navigation
328
- if (evt.name === 'tab') {
329
- if (evt.shift) {
330
- navigateToPrevious()
331
- } else {
332
- navigateToNext()
333
- }
334
- return
335
- }
336
-
337
322
  // Type-ahead search
338
323
  if (
339
324
  evt.name.length === 1 &&
@@ -358,7 +343,9 @@ const DropdownContent = ({
358
343
  })
359
344
 
360
345
  if (matchingItem) {
361
- setFocusedIndex(matchingItem.index)
346
+ flushSync(() => {
347
+ setFocusedIndex(matchingItem.index)
348
+ })
362
349
  scrollToItem(matchingItem)
363
350
  }
364
351
 
@@ -370,9 +357,9 @@ const DropdownContent = ({
370
357
 
371
358
  // Initialize selected titles from field value only once when descendants are loaded
372
359
  useLayoutEffect(() => {
373
- if (field.value && Object.keys(descendantsContext.map.current).length > 0) {
360
+ if (field.value && Object.keys(descendantsContext.committedMap).length > 0) {
374
361
  const titles: string[] = []
375
- const entries = Object.entries(descendantsContext.map.current)
362
+ const entries = Object.entries(descendantsContext.committedMap)
376
363
 
377
364
  entries.forEach(([id, item]) => {
378
365
  if (item.props) {
@@ -410,23 +397,23 @@ const DropdownContent = ({
410
397
  <FormDropdownDescendantsProvider value={descendantsContext}>
411
398
  <FormDropdownContext.Provider value={contextValue}>
412
399
  <box ref={elementRef} flexDirection='column'>
413
- <WithLeftBorder withDiamond isFocused={isFocused} isLoading={focusContext.isLoading}>
414
- <box
415
- onMouseDown={() => {
416
- setFocusedField(props.id)
417
- }}
418
- >
419
- <LoadingText
420
- isLoading={isFocused && focusContext.isLoading}
421
- color={isFocused ? Theme.primary : Theme.text}
400
+ <WithLeftBorder isFocused={isFocused} paddingBottom={1}>
401
+ <TitleIndicator isFocused={isFocused} isLoading={focusContext.isLoading}>
402
+ <box
403
+ onMouseDown={() => {
404
+ setFocusedField(props.id)
405
+ }}
422
406
  >
423
- {props.title || ''}
424
- </LoadingText>
425
- </box>
426
- </WithLeftBorder>
427
- <WithLeftBorder isFocused={isFocused}>
407
+ <LoadingText
408
+ isLoading={isFocused && focusContext.isLoading}
409
+ color={isFocused ? theme.primary : theme.text}
410
+ >
411
+ {props.title || ''}
412
+ </LoadingText>
413
+ </box>
414
+ </TitleIndicator>
428
415
  <text
429
- fg={selectedTitles.length > 0 ? Theme.text : Theme.textMuted}
416
+ fg={selectedTitles.length > 0 ? theme.text : theme.textMuted}
430
417
  selectable={false}
431
418
  onMouseDown={() => {
432
419
  setFocusedField(props.id)
@@ -436,40 +423,45 @@ const DropdownContent = ({
436
423
  ? selectedTitles.join(', ')
437
424
  : props.placeholder || 'Select...'}
438
425
  </text>
439
- </WithLeftBorder>
440
-
441
- <scrollbox
442
- ref={scrollBoxRef}
443
- maxHeight={5}
444
- flexShrink={1}
445
- style={{
446
- rootOptions: {
447
- flexShrink: 1,
448
- },
449
- viewportOptions: {
450
- flexShrink: 1,
451
- },
452
- scrollbarOptions: {
453
- visible: false,
454
- },
455
- }}
456
- >
457
- {props.children}
458
- </scrollbox>
459
- <WithLeftBorder children={<box />} isFocused={isFocused} />
460
-
461
- {(fieldState.error || props.error) && (
462
- <WithLeftBorder isFocused={isFocused}>
463
- <text fg={Theme.error}>
426
+ {props.children && (
427
+ <>
428
+ <box height={1} />
429
+ <box marginLeft={-2}>
430
+ <scrollbox
431
+ ref={scrollBoxRef}
432
+ maxHeight={5}
433
+ flexShrink={1}
434
+ style={{
435
+ rootOptions: {
436
+ flexShrink: 1,
437
+ },
438
+ viewportOptions: {
439
+ flexShrink: 1,
440
+ },
441
+ contentOptions: {
442
+ flexShrink: 0,
443
+ minHeight: 0,
444
+ },
445
+ scrollbarOptions: {
446
+ visible: false,
447
+ },
448
+ }}
449
+ >
450
+ {props.children}
451
+ </scrollbox>
452
+ </box>
453
+ <box height={1} />
454
+ </>
455
+ )}
456
+ {(fieldState.error || props.error) && (
457
+ <text fg={theme.error}>
464
458
  {fieldState.error?.message || props.error}
465
459
  </text>
466
- </WithLeftBorder>
467
- )}
468
- {props.info && (
469
- <WithLeftBorder isFocused={isFocused}>
470
- <text fg={Theme.textMuted}>{props.info}</text>
471
- </WithLeftBorder>
472
- )}
460
+ )}
461
+ {props.info && (
462
+ <text fg={theme.textMuted}>{props.info}</text>
463
+ )}
464
+ </WithLeftBorder>
473
465
  </box>
474
466
  </FormDropdownContext.Provider>
475
467
  </FormDropdownDescendantsProvider>
@@ -1,6 +1,6 @@
1
1
  import React, { useState } from 'react'
2
2
  import { useQuery } from '@tanstack/react-query'
3
- import { Theme } from 'termcast/src/theme'
3
+ import { useTheme } from 'termcast/src/theme'
4
4
  import { searchFiles, parsePath } from '../../utils/file-system'
5
5
  import { useKeyboard } from '@opentui/react'
6
6
  import { useIsInFocus } from 'termcast/src/internal/focus-context'
@@ -50,6 +50,7 @@ export const FileAutocompleteDialog = ({
50
50
  canChooseDirectories = false,
51
51
  initialDirectory,
52
52
  }: FileAutocompleteDialogProps): any => {
53
+ const theme = useTheme()
53
54
  const [selectedIndex, setSelectedIndex] = useState(0)
54
55
  const isInFocus = useIsInFocus()
55
56
 
@@ -119,16 +120,16 @@ export const FileAutocompleteDialog = ({
119
120
  return (
120
121
  <box flexDirection='column' paddingLeft={1} paddingRight={1} overflow='hidden'>
121
122
  <box flexDirection='row'>
122
- <text fg={Theme.textMuted} wrapMode='none'>Filter: </text>
123
- <text fg={Theme.primary} wrapMode='none'>
123
+ <text fg={theme.textMuted} wrapMode='none'>Filter: </text>
124
+ <text fg={theme.primary} wrapMode='none'>
124
125
  {filter ? filter.replace(homedir, '~') : '(type to filter)'}
125
126
  </text>
126
127
  </box>
127
128
  <box height={1} />
128
129
  {isLoading ? (
129
- <text fg={Theme.textMuted}>Loading files...</text>
130
+ <text fg={theme.textMuted}>Loading files...</text>
130
131
  ) : visibleFiles.length === 0 ? (
131
- <text fg={Theme.textMuted}>No files found</text>
132
+ <text fg={theme.textMuted}>No files found</text>
132
133
  ) : (
133
134
  <>
134
135
  {visibleFiles.map((item, index) => {
@@ -136,8 +137,8 @@ export const FileAutocompleteDialog = ({
136
137
  return (
137
138
  <text
138
139
  key={item.path}
139
- fg={index === selectedIndex ? Theme.background : Theme.text}
140
- bg={index === selectedIndex ? Theme.primary : Theme.backgroundPanel}
140
+ fg={index === selectedIndex ? theme.background : theme.text}
141
+ bg={index === selectedIndex ? theme.primary : theme.backgroundPanel}
141
142
  wrapMode='none'
142
143
  >
143
144
  {' '}{icon}{item.name}{item.isDirectory ? '/' : ''}
@@ -147,7 +148,7 @@ export const FileAutocompleteDialog = ({
147
148
  </>
148
149
  )}
149
150
  <box height={1} />
150
- <text fg={Theme.textMuted} wrapMode='none'>{hintText}</text>
151
+ <text fg={theme.textMuted} wrapMode='none'>{hintText}</text>
151
152
  </box>
152
153
  )
153
154
  }
@@ -1,7 +1,7 @@
1
- import React, { useRef } from 'react'
2
- import { Theme } from 'termcast/src/theme'
1
+ import React, { useRef, useCallback } from 'react'
2
+ import { useTheme } from 'termcast/src/theme'
3
3
  import { BoxRenderable, TextareaRenderable } from '@opentui/core'
4
- import { WithLeftBorder } from './with-left-border'
4
+ import { WithLeftBorder, TitleIndicator } from './with-left-border'
5
5
  import { FormItemProps, FormItemRef } from './types'
6
6
  import { useFormContext, Controller } from 'react-hook-form'
7
7
  import { useFocusContext, useFormFieldDescendant } from './index'
@@ -11,6 +11,7 @@ import { FileAutocompleteDialog, createFileAutocompleteStore } from './file-auto
11
11
  import { useFormNavigationHelpers } from './use-form-navigation'
12
12
  import { useDialog } from 'termcast/src/internal/dialog'
13
13
  import { LoadingText } from 'termcast/src/components/loading-text'
14
+ import { useStore } from 'termcast/src/state'
14
15
 
15
16
  export interface FilePickerProps extends FormItemProps<string[]> {
16
17
  /**
@@ -64,8 +65,26 @@ const FilePickerField = ({
64
65
  setFocusedField: (id: string) => void
65
66
  isFormLoading: boolean
66
67
  }): any => {
68
+ const theme = useTheme()
67
69
  const isInFocus = useIsInFocus()
68
70
  const inputRef = React.useRef<TextareaRenderable>(null)
71
+
72
+ // Ref callback that registers the textarea in global state for ESC handling
73
+ const setInputRef = useCallback((node: TextareaRenderable | null) => {
74
+ if (!node) return
75
+
76
+ inputRef.current = node
77
+ useStore.setState({ activeSearchInputRef: node })
78
+
79
+ // React 19: return cleanup function for unmount
80
+ return () => {
81
+ if (useStore.getState().activeSearchInputRef === node) {
82
+ useStore.setState({ activeSearchInputRef: null })
83
+ }
84
+ inputRef.current = null
85
+ }
86
+ }, [])
87
+
69
88
  const dialog = useDialog()
70
89
 
71
90
  // Create store once for sharing state with dialog
@@ -150,24 +169,24 @@ const FilePickerField = ({
150
169
 
151
170
  return (
152
171
  <box flexDirection='column'>
153
- <WithLeftBorder withDiamond isFocused={isFocused} isLoading={isFormLoading}>
154
- <box
155
- onMouseDown={() => {
156
- setFocusedField(props.id)
157
- }}
158
- >
159
- <LoadingText
160
- isLoading={isFocused && isFormLoading}
161
- color={isFocused ? Theme.primary : Theme.text}
172
+ <WithLeftBorder isFocused={isFocused} paddingBottom={1}>
173
+ <TitleIndicator isFocused={isFocused} isLoading={isFormLoading}>
174
+ <box
175
+ onMouseDown={() => {
176
+ setFocusedField(props.id)
177
+ }}
162
178
  >
163
- {props.title || 'File Path'}
164
- </LoadingText>
165
- </box>
166
- </WithLeftBorder>
167
- <WithLeftBorder isFocused={isFocused}>
179
+ <LoadingText
180
+ isLoading={isFocused && isFormLoading}
181
+ color={isFocused ? theme.primary : theme.text}
182
+ >
183
+ {props.title || 'File Path'}
184
+ </LoadingText>
185
+ </box>
186
+ </TitleIndicator>
168
187
  <box flexDirection='column'>
169
188
  <textarea
170
- ref={inputRef}
189
+ ref={setInputRef}
171
190
  height={1}
172
191
  wrapMode='none'
173
192
  keyBindings={[
@@ -189,28 +208,24 @@ const FilePickerField = ({
189
208
  />
190
209
  {selectedFiles.length > 0 && (
191
210
  <box flexDirection='column' marginTop={1}>
192
- <text fg={Theme.textMuted}>Selected files:</text>
211
+ <text fg={theme.textMuted}>Selected files:</text>
193
212
  {selectedFiles.map((file: string, index: number) => (
194
- <text key={index} fg={Theme.text}>
213
+ <text key={index} fg={theme.text}>
195
214
  • {file}
196
215
  </text>
197
216
  ))}
198
217
  </box>
199
218
  )}
200
219
  </box>
201
- </WithLeftBorder>
202
- {(fieldState.error || props.error) && (
203
- <WithLeftBorder isFocused={isFocused}>
204
- <text fg={Theme.error}>
220
+ {(fieldState.error || props.error) && (
221
+ <text fg={theme.error}>
205
222
  {fieldState.error?.message || props.error}
206
223
  </text>
207
- </WithLeftBorder>
208
- )}
209
- {props.info && (
210
- <WithLeftBorder isFocused={isFocused}>
211
- <text fg={Theme.textMuted}>{props.info}</text>
212
- </WithLeftBorder>
213
- )}
224
+ )}
225
+ {props.info && (
226
+ <text fg={theme.textMuted}>{props.info}</text>
227
+ )}
228
+ </WithLeftBorder>
214
229
  </box>
215
230
  ) as React.ReactElement
216
231
  }
@@ -230,21 +245,6 @@ export const FilePicker = (props: FilePickerProps): any => {
230
245
  elementRef: elementRef.current,
231
246
  })
232
247
 
233
- const { navigateToPrevious, navigateToNext } = useFormNavigationHelpers(props.id)
234
-
235
- // Handle keyboard navigation
236
- useKeyboard((evt) => {
237
- if (!isFocused || !isInFocus) return
238
-
239
- if (evt.name === 'tab') {
240
- if (evt.shift) {
241
- navigateToPrevious()
242
- } else {
243
- navigateToNext()
244
- }
245
- }
246
- })
247
-
248
248
  return (
249
249
  <Controller
250
250
  name={props.id}
@@ -1,8 +1,9 @@
1
1
  import React, { useState, useLayoutEffect } from 'react'
2
- import { Theme } from 'termcast/src/theme'
2
+ import { useTheme } from 'termcast/src/theme'
3
3
  import { useFocusContext, useFormScrollContext } from './index'
4
4
 
5
5
  export const FormEnd = (): any => {
6
+ const theme = useTheme()
6
7
  const { focusedField } = useFocusContext()
7
8
  const scrollContext = useFormScrollContext()
8
9
  const [isLastFieldFocused, setIsLastFieldFocused] = useState(false)
@@ -12,7 +13,8 @@ export const FormEnd = (): any => {
12
13
  setIsLastFieldFocused(false)
13
14
  return
14
15
  }
15
- const descendants = Object.values(scrollContext.descendantsContext.map.current)
16
+ // Use committedMap for stability
17
+ const descendants = Object.values(scrollContext.descendantsContext.committedMap)
16
18
  .filter((item) => item.index !== -1 && item.props?.id)
17
19
  .sort((a, b) => a.index - b.index)
18
20
  if (descendants.length === 0) {
@@ -21,7 +23,7 @@ export const FormEnd = (): any => {
21
23
  }
22
24
  const lastField = descendants[descendants.length - 1]
23
25
  setIsLastFieldFocused(lastField.props?.id === focusedField)
24
- }, [focusedField])
26
+ }, [focusedField, scrollContext?.descendantsContext.committedMap])
25
27
 
26
- return <text fg={isLastFieldFocused ? Theme.accent : Theme.text}>└</text>
28
+ return <text fg={isLastFieldFocused ? theme.accent : theme.text}>└</text>
27
29
  }