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
@@ -3,7 +3,7 @@ import { TextareaRenderable } from '@opentui/core'
3
3
  import { useForm } from 'react-hook-form'
4
4
  import { useKeyboard } from '@opentui/react'
5
5
  import { renderWithProviders } from '../../utils'
6
- import { Theme } from 'termcast/src/theme'
6
+ import { useTheme } from 'termcast/src/theme'
7
7
  import { logger } from 'termcast/src/logger'
8
8
  import { createTextareaFormRef } from 'termcast/src/components/form/form-ref'
9
9
 
@@ -13,6 +13,7 @@ interface FormData {
13
13
  }
14
14
 
15
15
  function RHFCustomRefExample() {
16
+ const theme = useTheme()
16
17
  const textareaRef1 = useRef<TextareaRenderable>(null)
17
18
  const textareaRef2 = useRef<TextareaRenderable>(null)
18
19
  const [focusedField, setFocusedField] = useState<'username' | 'message'>('username')
@@ -73,14 +74,14 @@ function RHFCustomRefExample() {
73
74
 
74
75
  return (
75
76
  <box flexDirection="column" gap={1}>
76
- <text fg={Theme.accent}>React Hook Form with Custom Ref Adapter</text>
77
- <text fg={Theme.textMuted}>
77
+ <text fg={theme.accent}>React Hook Form with Custom Ref Adapter</text>
78
+ <text fg={theme.textMuted}>
78
79
  This example uses register() directly with opentui textarea
79
80
  </text>
80
81
 
81
82
  <box flexDirection="column">
82
83
  <text
83
- fg={Theme.text}
84
+ fg={theme.text}
84
85
  onMouseDown={() => {
85
86
  setFocusedField('username')
86
87
  }}
@@ -99,13 +100,13 @@ function RHFCustomRefExample() {
99
100
  focused={focusedField === 'username'}
100
101
  />
101
102
  {formState.errors.username && (
102
- <text fg={Theme.error}>{formState.errors.username.message}</text>
103
+ <text fg={theme.error}>{formState.errors.username.message}</text>
103
104
  )}
104
105
  </box>
105
106
 
106
107
  <box flexDirection="column">
107
108
  <text
108
- fg={Theme.text}
109
+ fg={theme.text}
109
110
  onMouseDown={() => {
110
111
  setFocusedField('message')
111
112
  }}
@@ -124,28 +125,28 @@ function RHFCustomRefExample() {
124
125
  focused={focusedField === 'message'}
125
126
  />
126
127
  {formState.errors.message && (
127
- <text fg={Theme.error}>{formState.errors.message.message}</text>
128
+ <text fg={theme.error}>{formState.errors.message.message}</text>
128
129
  )}
129
130
  </box>
130
131
 
131
132
  <box flexDirection="column" marginTop={1}>
132
- <text fg={Theme.textMuted}>Controls:</text>
133
- <text fg={Theme.textMuted}>• Ctrl+S: Submit form</text>
134
- <text fg={Theme.textMuted}>• Ctrl+V: Set values programmatically</text>
135
- <text fg={Theme.textMuted}>• Ctrl+G: Get current values</text>
136
- <text fg={Theme.textMuted}>• ESC: Reset form</text>
133
+ <text fg={theme.textMuted}>Controls:</text>
134
+ <text fg={theme.textMuted}>• Ctrl+S: Submit form</text>
135
+ <text fg={theme.textMuted}>• Ctrl+V: Set values programmatically</text>
136
+ <text fg={theme.textMuted}>• Ctrl+G: Get current values</text>
137
+ <text fg={theme.textMuted}>• ESC: Reset form</text>
137
138
  </box>
138
139
 
139
140
  <box marginTop={1}>
140
- <text fg={Theme.textMuted}>
141
+ <text fg={theme.textMuted}>
141
142
  isDirty: {String(formState.isDirty)} | isValid:{' '}
142
143
  {String(formState.isValid)} | submitCount: {formState.submitCount}
143
144
  </text>
144
145
  </box>
145
146
 
146
147
  <box marginTop={1} flexDirection="column">
147
- <text fg={Theme.accent}>Form values:</text>
148
- <text fg={Theme.text}>{JSON.stringify(formValues, null, 2)}</text>
148
+ <text fg={theme.accent}>Form values:</text>
149
+ <text fg={theme.text}>{JSON.stringify(formValues, null, 2)}</text>
149
150
  </box>
150
151
  </box>
151
152
  )
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react'
2
- import { useKeyboard } from '@opentui/react'
2
+ import { useKeyboard, flushSync } from '@opentui/react'
3
3
  import { createDescendants } from 'termcast/src/descendants'
4
4
  import { renderWithProviders } from 'termcast/src/utils'
5
5
 
@@ -45,7 +45,9 @@ function ScrollboxWithDescendants() {
45
45
 
46
46
  const nextItem = items[nextIndex]
47
47
  if (nextItem) {
48
- setSelectedIndex(nextIndex)
48
+ flushSync(() => {
49
+ setSelectedIndex(nextIndex)
50
+ })
49
51
  scrollToItem(nextItem)
50
52
  }
51
53
  }
@@ -5,7 +5,7 @@ import {
5
5
  useDialog,
6
6
  type DialogPosition,
7
7
  } from 'termcast/src/internal/dialog'
8
- import { Theme } from 'termcast'
8
+ import { useTheme } from 'termcast'
9
9
  import { List } from 'termcast'
10
10
  import { ActionPanel, Action } from 'termcast'
11
11
  import { Dropdown } from 'termcast'
@@ -7,10 +7,15 @@ test('simple scrollbox navigation and scrolling', async () => {
7
7
  args: ['src/examples/internal/simple-scrollbox.tsx'],
8
8
  })
9
9
 
10
+ // Wait for initial render with Item 1 visible
10
11
  await session.text({
11
- waitFor: (text) => text.includes('Simple ScrollBox Demo'),
12
+ waitFor: (text) =>
13
+ text.includes('Simple ScrollBox Demo') && text.includes('Item 1'),
12
14
  })
13
15
 
16
+ // Wait for render to stabilize
17
+ await session.waitIdle()
18
+
14
19
  const initialText = await session.text()
15
20
  expect(initialText).toMatchInlineSnapshot(`
16
21
  "
@@ -40,13 +45,13 @@ test('simple scrollbox navigation and scrolling', async () => {
40
45
  "
41
46
  `)
42
47
 
43
- // Scroll down to see more items
44
- await session.scrollDown(3)
48
+ // Scroll down to see more items - use more scroll events for reliability
49
+ await session.scrollDown(5)
45
50
 
46
51
  // Wait for Item 4 to appear (proves scroll happened)
47
52
  const afterScrollDownSnapshot = await session.text({
48
53
  waitFor: (text) => text.includes('Item 4'),
49
- timeout: 5000,
54
+ timeout: 10000,
50
55
  })
51
56
  expect(afterScrollDownSnapshot).toMatchInlineSnapshot(`
52
57
  "
@@ -76,13 +81,13 @@ test('simple scrollbox navigation and scrolling', async () => {
76
81
  "
77
82
  `)
78
83
 
79
- // Scroll back up
80
- await session.scrollUp(2)
84
+ // Scroll back up - use more scroll events for reliability
85
+ await session.scrollUp(5)
81
86
 
82
- // Wait for scrollbar to move back up (proves scroll happened)
87
+ // Wait for scroll to take effect - Item 4 should no longer be visible
83
88
  const afterScrollUpSnapshot = await session.text({
84
- waitFor: (text) => text !== afterScrollDownSnapshot,
85
- timeout: 5000,
89
+ waitFor: (text) => !text.includes('Item 4'),
90
+ timeout: 10000,
86
91
  })
87
92
  expect(afterScrollUpSnapshot).toMatchInlineSnapshot(`
88
93
  "
@@ -0,0 +1,28 @@
1
+ // Example: List with controlled searchText (parent manages search state)
2
+ // Used to demonstrate that selection must reset to first visible item
3
+ // when search text changes, even in controlled mode.
4
+ import React, { useState } from 'react'
5
+ import { List, renderWithProviders } from 'termcast'
6
+
7
+ function ControlledSearchExample() {
8
+ const [searchText, setSearchText] = useState('')
9
+
10
+ return (
11
+ <List
12
+ navigationTitle='Controlled Search'
13
+ searchBarPlaceholder='Search items...'
14
+ filtering={true}
15
+ searchText={searchText}
16
+ onSearchTextChange={setSearchText}
17
+ >
18
+ <List.Item id='apple' title='Apple' subtitle='Red fruit' />
19
+ <List.Item id='banana' title='Banana' subtitle='Yellow fruit' />
20
+ <List.Item id='cherry' title='Cherry' subtitle='Small fruit' />
21
+ <List.Item id='grape' title='Grape' subtitle='Purple fruit' />
22
+ <List.Item id='lettuce' title='Lettuce' subtitle='Green veggie' />
23
+ <List.Item id='mango' title='Mango' subtitle='Tropical fruit' />
24
+ </List>
25
+ )
26
+ }
27
+
28
+ await renderWithProviders(<ControlledSearchExample />)
@@ -0,0 +1,49 @@
1
+ // Repro: controlled searchText + filtering must reset selection to first visible item.
2
+ // Bug: when parent manages searchText, List never calls setSelectedIndex on search change,
3
+ // so the › marker stays on a stale (now-hidden) item or disappears entirely.
4
+ import { test, expect, afterEach, beforeEach } from 'vitest'
5
+ import { launchTerminal, Session } from 'tuistory/src'
6
+
7
+ let session: Session
8
+
9
+ beforeEach(async () => {
10
+ session = await launchTerminal({
11
+ command: 'bun',
12
+ args: ['src/examples/list-controlled-search.tsx'],
13
+ cols: 60,
14
+ rows: 16,
15
+ })
16
+ })
17
+
18
+ afterEach(() => {
19
+ session?.close()
20
+ })
21
+
22
+ test('controlled search resets selection to first visible item', async () => {
23
+ await session.text({
24
+ waitFor: (text) => {
25
+ return /search items/i.test(text) && text.includes('Apple')
26
+ },
27
+ })
28
+
29
+ // Navigate down to select Grape (4th item)
30
+ await session.press('down')
31
+ await session.press('down')
32
+ await session.press('down')
33
+
34
+ const beforeSearch = await session.text()
35
+ expect(beforeSearch).toContain('›Grape')
36
+
37
+ // Type "let" — only Lettuce should match.
38
+ // Selection MUST move to Lettuce (first visible item).
39
+ await session.type('let')
40
+
41
+ const afterSearch = await session.text({
42
+ waitFor: (text) => {
43
+ return text.includes('let') && text.includes('Lettuce')
44
+ },
45
+ })
46
+
47
+ // BUG REPRO: without the fix, › is missing or on a stale item
48
+ expect(afterSearch).toContain('›Lettuce')
49
+ }, 10000)
@@ -11,7 +11,7 @@ const ListDetailMetadataExample = () => {
11
11
  >
12
12
  <List.Item
13
13
  id="item1"
14
- title="Item with Metadata"
14
+ title="Short Values"
15
15
  detail={
16
16
  <List.Item.Detail
17
17
  markdown="# Details"
@@ -29,14 +29,17 @@ const ListDetailMetadataExample = () => {
29
29
  />
30
30
  <List.Item
31
31
  id="item2"
32
- title="Another Item"
32
+ title="Long Values"
33
33
  detail={
34
34
  <List.Item.Detail
35
- markdown="# Info"
35
+ markdown="# Info with Long Values"
36
36
  metadata={
37
37
  <List.Item.Detail.Metadata>
38
- <List.Item.Detail.Metadata.Label title="Count" text="42" />
39
- <List.Item.Detail.Metadata.Label title="Price" text="$99.99" />
38
+ <List.Item.Detail.Metadata.Label title="Description" text="This is a very long description that would be truncated if shown inline" />
39
+ <List.Item.Detail.Metadata.Label title="Path" text="/Users/username/Documents/Projects/my-project/src/components" />
40
+ <List.Item.Detail.Metadata.Separator />
41
+ <List.Item.Detail.Metadata.Label title="Short" text="OK" />
42
+ <List.Item.Detail.Metadata.Link title="URL" target="https://example.com/very/long/path/that/exceeds/limit" text="example.com/very/long/path" />
40
43
  </List.Item.Detail.Metadata>
41
44
  }
42
45
  />
@@ -16,7 +16,7 @@ afterEach(() => {
16
16
  session?.close()
17
17
  })
18
18
 
19
- test('list detail metadata label renders title and text in column layout', async () => {
19
+ test('list detail metadata label renders short values in row layout (key: value)', async () => {
20
20
  const snapshot = await session.text({
21
21
  waitFor: (text) => {
22
22
  return (
@@ -36,24 +36,24 @@ test('list detail metadata label renders title and text in column layout', async
36
36
 
37
37
  > Search...
38
38
 
39
- Item with Metadata
40
- Another Item
41
- │ ───────────────────────────────── █
42
- │ ▀
43
- │ Name:
44
- │ John Doe
39
+ Short Values
40
+ Long Values
45
41
 
46
- Email:
47
- john@example.com
42
+
43
+ Name: John Doe
44
+
45
+ │ Email: john@example.com
48
46
  │ ─────────────────
49
47
 
50
- ↑↓ navigate ^k actions │ Status:
48
+ │ Status: Active
49
+ │ Website: example.com
50
+ ↑↓ navigate ^k actions │
51
51
 
52
52
  "
53
53
  `)
54
54
  }, 10000)
55
55
 
56
- test('list detail metadata navigation shows different metadata', async () => {
56
+ test('list detail metadata renders long values in column layout (key on one line, value below)', async () => {
57
57
  await session.text({
58
58
  waitFor: (text) => text.includes('Metadata Test'),
59
59
  })
@@ -62,7 +62,7 @@ test('list detail metadata navigation shows different metadata', async () => {
62
62
 
63
63
  const snapshot = await session.text({
64
64
  waitFor: (text) => {
65
- return text.includes('Count') && text.includes('42')
65
+ return text.includes('Description') && text.includes('very long')
66
66
  },
67
67
  })
68
68
 
@@ -74,18 +74,18 @@ test('list detail metadata navigation shows different metadata', async () => {
74
74
 
75
75
  > Search...
76
76
 
77
- Item with Metadata
78
- Another Item │ Info
79
- ──────────────────────────────────
77
+ Short Values
78
+ Long Values │ Info with Long Values ▲
79
+
80
80
 
81
- Count:
82
- 42
81
+ Description:
82
+ This is a very long description
83
+ │ that would be truncated if shown
84
+ │ inline
83
85
 
84
- Price:
85
- ↑↓ navigate ^k actions $99.99
86
-
87
-
88
-
86
+ Path:
87
+ /Users/username/Documents/
88
+ ↑↓ navigate ^k actions │ Projects/my-project/src/components▼
89
89
 
90
90
  "
91
91
  `)
@@ -43,9 +43,9 @@ test('dropdown defaults to first item when no value is provided', async () => {
43
43
 
44
44
 
45
45
 
46
- ↵ show selected fruit ↑↓ navigate ^k actions powered by termcast
47
-
48
46
 
47
+ ↵ show selected ↑↓ navigate ^p dropdown ^k actionpowered by termcast
48
+ fruit
49
49
 
50
50
 
51
51
 
@@ -77,6 +77,7 @@ test('dropdown opens and shows items', async () => {
77
77
 
78
78
 
79
79
  Dropdown Default Value Example ───────────────────────────────────────────
80
+
80
81
  ╭──────────────────────────────────────────────────────────────────────────╮
81
82
  │ │
82
83
  │ Filter by category esc │
@@ -88,7 +89,7 @@ test('dropdown opens and shows items', async () => {
88
89
  │ Orange │
89
90
  │ Grape │
90
91
  │ │
91
- │ │
92
+ │ │t
92
93
  │ ↵ select ↑↓ navigate powered by termcast │
93
94
  │ │
94
95
  ╰──────────────────────────────────────────────────────────────────────────╯
@@ -102,7 +103,6 @@ test('dropdown opens and shows items', async () => {
102
103
 
103
104
 
104
105
 
105
-
106
106
  "
107
107
  `)
108
108
 
@@ -114,6 +114,7 @@ test('dropdown opens and shows items', async () => {
114
114
 
115
115
 
116
116
  Dropdown Default Value Example ───────────────────────────────────────────
117
+
117
118
  ╭──────────────────────────────────────────────────────────────────────────╮
118
119
  │ │
119
120
  │ Filter by category esc │
@@ -125,7 +126,7 @@ test('dropdown opens and shows items', async () => {
125
126
  │ Orange │
126
127
  │ Grape │
127
128
  │ │
128
- │ │
129
+ │ │t
129
130
  │ ↵ select ↑↓ navigate powered by termcast │
130
131
  │ │
131
132
  ╰──────────────────────────────────────────────────────────────────────────╯
@@ -139,7 +140,6 @@ test('dropdown opens and shows items', async () => {
139
140
 
140
141
 
141
142
 
142
-
143
143
  "
144
144
  `)
145
145
 
@@ -162,9 +162,9 @@ test('dropdown opens and shows items', async () => {
162
162
 
163
163
 
164
164
 
165
- ↵ show selected fruit ↑↓ navigate ^k actions powered by termcast
166
-
167
165
 
166
+ ↵ show selected ↑↓ navigate ^p dropdown ^k actionpowered by termcast
167
+ fruit
168
168
 
169
169
 
170
170
 
@@ -196,6 +196,7 @@ test('clicking dropdown opens it', async () => {
196
196
 
197
197
 
198
198
  Dropdown Default Value Example ───────────────────────────────────────────
199
+
199
200
  ╭──────────────────────────────────────────────────────────────────────────╮
200
201
  │ │
201
202
  │ Filter by category esc │
@@ -207,7 +208,7 @@ test('clicking dropdown opens it', async () => {
207
208
  │ Orange │
208
209
  │ Grape │
209
210
  │ │
210
- │ │
211
+ │ │t
211
212
  │ ↵ select ↑↓ navigate powered by termcast │
212
213
  │ │
213
214
  ╰──────────────────────────────────────────────────────────────────────────╯
@@ -221,7 +222,6 @@ test('clicking dropdown opens it', async () => {
221
222
 
222
223
 
223
224
 
224
-
225
225
  "
226
226
  `)
227
227
 
@@ -244,9 +244,9 @@ test('clicking dropdown opens it', async () => {
244
244
 
245
245
 
246
246
 
247
- ↵ show selected fruit ↑↓ navigate ^k actions powered by termcast
248
-
249
247
 
248
+ ↵ show selected ↑↓ navigate ^p dropdown ^k actionpowered by termcast
249
+ fruit
250
250
 
251
251
 
252
252
 
@@ -2,8 +2,10 @@ import { test, expect, afterEach, beforeEach } from 'vitest'
2
2
  import { launchTerminal, Session } from 'tuistory/src'
3
3
 
4
4
  let session: Session
5
+ let sessionStartMs = 0
5
6
 
6
7
  beforeEach(async () => {
8
+ sessionStartMs = Date.now()
7
9
  session = await launchTerminal({
8
10
  command: 'bun',
9
11
  args: ['src/examples/list-scrollbox.tsx'],
@@ -16,6 +18,18 @@ afterEach(() => {
16
18
  session?.close()
17
19
  })
18
20
 
21
+ test('list scrollbox startup appears in under one second', async () => {
22
+ await session.text({
23
+ waitFor: (text) => {
24
+ return /Search items\.\.\./i.test(text)
25
+ },
26
+ timeout: 5000,
27
+ })
28
+
29
+ const startupMs = Date.now() - sessionStartMs
30
+ expect(startupMs).toBeLessThan(1000)
31
+ }, 10000)
32
+
19
33
  test('list scrollbox auto-scrolls when navigating down', async () => {
20
34
  await session.text({
21
35
  waitFor: (text) => {
@@ -33,11 +47,11 @@ test('list scrollbox auto-scrolls when navigating down', async () => {
33
47
  > Search items...
34
48
 
35
49
  ›▲ Item 1 Description for item 1 ▲
36
-
37
-
38
- ↑↓ navigate ^k actions
39
-
40
- "
50
+ ■ Item 2 Description for item 2 ▀
51
+ ▲ Item 3 Description for item 3
52
+ Item 4 Description for item 4
53
+ ▼ Item 5 Description for item 5
54
+ ● Item 6 Description for item 6 ▼"
41
55
  `)
42
56
 
43
57
  await session.press('down')
@@ -55,12 +69,12 @@ test('list scrollbox auto-scrolls when navigating down', async () => {
55
69
 
56
70
  > Search items...
57
71
 
58
- ›● Item 6 Description for item 6
59
-
60
-
61
- ↑↓ navigate ^k actions
62
-
63
- "
72
+ Item 3 Description for item 3
73
+ ■ Item 4 Description for item 4 ▄
74
+ ▼ Item 5 Description for item 5
75
+ ›● Item 6 Description for item 6
76
+ ■ Item 7 Description for item 7
77
+ ■ Item 8 Description for item 8 ▼"
64
78
  `)
65
79
 
66
80
  await session.press('down')
@@ -76,12 +90,12 @@ test('list scrollbox auto-scrolls when navigating down', async () => {
76
90
 
77
91
  > Search items...
78
92
 
79
- ›■ Item 9 Description for item 9
80
-
81
-
82
- ↑↓ navigate ^k actions
83
-
84
- "
93
+ Item 6 Description for item 6
94
+ ■ Item 7 Description for item 7
95
+ ■ Item 8 Description for item 8 ▄
96
+ ›■ Item 9 Description for item 9
97
+ Item 10 Description for item 10
98
+ ▲ Item 11 Description for item 11 ▼"
85
99
  `)
86
100
 
87
101
  await session.press('up')
@@ -101,12 +115,12 @@ test('list scrollbox auto-scrolls when navigating down', async () => {
101
115
 
102
116
  > Search items...
103
117
 
104
- ›■ Item 2 Description for item 2
105
-
106
-
107
- ↑↓ navigate ^k actions
108
-
109
- "
118
+ Item 1 Description for item 1
119
+ ›■ Item 2 Description for item 2 ▀
120
+ ▲ Item 3 Description for item 3
121
+ Item 4 Description for item 4
122
+ ▼ Item 5 Description for item 5
123
+ ● Item 6 Description for item 6 ▼"
110
124
  `)
111
125
  }, 15000)
112
126
 
@@ -127,11 +141,11 @@ test('list scrollbox scrolls with mouse wheel', async () => {
127
141
  > Search items...
128
142
 
129
143
  ›▲ Item 1 Description for item 1 ▲
130
-
131
-
132
- ↑↓ navigate ^k actions
133
-
134
- "
144
+ ■ Item 2 Description for item 2 ▀
145
+ ▲ Item 3 Description for item 3
146
+ Item 4 Description for item 4
147
+ ▼ Item 5 Description for item 5
148
+ ● Item 6 Description for item 6 ▼"
135
149
  `)
136
150
 
137
151
  await session.scrollDown(3)
@@ -150,11 +164,11 @@ test('list scrollbox scrolls with mouse wheel', async () => {
150
164
  > Search items...
151
165
 
152
166
  ■ Item 2 Description for item 2 ▲
153
-
154
-
155
- ↑↓ navigate ^k actions
156
-
157
- "
167
+ ▲ Item 3 Description for item 3 ▄
168
+ ■ Item 4 Description for item 4
169
+ Item 5 Description for item 5
170
+ ● Item 6 Description for item 6
171
+ ■ Item 7 Description for item 7 ▼"
158
172
  `)
159
173
 
160
174
  await session.scrollUp(2)
@@ -173,10 +187,10 @@ test('list scrollbox scrolls with mouse wheel', async () => {
173
187
  > Search items...
174
188
 
175
189
  ›▲ Item 1 Description for item 1 ▲
176
-
177
-
178
- ↑↓ navigate ^k actions
179
-
180
- "
190
+ ■ Item 2 Description for item 2 ▀
191
+ ▲ Item 3 Description for item 3
192
+ Item 4 Description for item 4
193
+ ▼ Item 5 Description for item 5
194
+ ● Item 6 Description for item 6 ▼"
181
195
  `)
182
196
  }, 15000)