termcast 1.3.30 → 1.3.32

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 (294) hide show
  1. package/dist/apis/cache.d.ts.map +1 -1
  2. package/dist/apis/cache.js +4 -39
  3. package/dist/apis/cache.js.map +1 -1
  4. package/dist/apis/hud.d.ts.map +1 -1
  5. package/dist/apis/hud.js +13 -31
  6. package/dist/apis/hud.js.map +1 -1
  7. package/dist/apis/localstorage.d.ts.map +1 -1
  8. package/dist/apis/localstorage.js +3 -27
  9. package/dist/apis/localstorage.js.map +1 -1
  10. package/dist/apis/toast.d.ts +16 -43
  11. package/dist/apis/toast.d.ts.map +1 -1
  12. package/dist/apis/toast.js +78 -177
  13. package/dist/apis/toast.js.map +1 -1
  14. package/dist/build.d.ts +3 -1
  15. package/dist/build.d.ts.map +1 -1
  16. package/dist/build.js +52 -2
  17. package/dist/build.js.map +1 -1
  18. package/dist/cli.d.ts +1 -0
  19. package/dist/cli.d.ts.map +1 -1
  20. package/dist/cli.js +206 -25
  21. package/dist/cli.js.map +1 -1
  22. package/dist/colors.d.ts.map +1 -1
  23. package/dist/colors.js +1 -0
  24. package/dist/colors.js.map +1 -1
  25. package/dist/compile.d.ts +0 -1
  26. package/dist/compile.d.ts.map +1 -1
  27. package/dist/compile.js +18 -23
  28. package/dist/compile.js.map +1 -1
  29. package/dist/components/actions.d.ts.map +1 -1
  30. package/dist/components/actions.js +30 -15
  31. package/dist/components/actions.js.map +1 -1
  32. package/dist/components/animation-tick.d.ts +12 -0
  33. package/dist/components/animation-tick.d.ts.map +1 -0
  34. package/dist/components/animation-tick.js +63 -0
  35. package/dist/components/animation-tick.js.map +1 -0
  36. package/dist/components/detail.d.ts.map +1 -1
  37. package/dist/components/detail.js +10 -13
  38. package/dist/components/detail.js.map +1 -1
  39. package/dist/components/dropdown.d.ts +1 -0
  40. package/dist/components/dropdown.d.ts.map +1 -1
  41. package/dist/components/dropdown.js +27 -26
  42. package/dist/components/dropdown.js.map +1 -1
  43. package/dist/components/extension-preferences.d.ts.map +1 -1
  44. package/dist/components/extension-preferences.js +15 -10
  45. package/dist/components/extension-preferences.js.map +1 -1
  46. package/dist/components/footer.d.ts +13 -0
  47. package/dist/components/footer.d.ts.map +1 -0
  48. package/dist/components/footer.js +106 -0
  49. package/dist/components/footer.js.map +1 -0
  50. package/dist/components/form/file-autocomplete.d.ts +19 -4
  51. package/dist/components/form/file-autocomplete.d.ts.map +1 -1
  52. package/dist/components/form/file-autocomplete.js +56 -55
  53. package/dist/components/form/file-autocomplete.js.map +1 -1
  54. package/dist/components/form/file-picker.d.ts.map +1 -1
  55. package/dist/components/form/file-picker.js +26 -15
  56. package/dist/components/form/file-picker.js.map +1 -1
  57. package/dist/components/form/index.d.ts.map +1 -1
  58. package/dist/components/form/index.js +17 -15
  59. package/dist/components/form/index.js.map +1 -1
  60. package/dist/components/form/with-left-border.d.ts.map +1 -1
  61. package/dist/components/form/with-left-border.js +4 -12
  62. package/dist/components/form/with-left-border.js.map +1 -1
  63. package/dist/components/list.d.ts.map +1 -1
  64. package/dist/components/list.js +126 -86
  65. package/dist/components/list.js.map +1 -1
  66. package/dist/components/loading-bar.d.ts.map +1 -1
  67. package/dist/components/loading-bar.js +5 -22
  68. package/dist/components/loading-bar.js.map +1 -1
  69. package/dist/components/loading-text.d.ts.map +1 -1
  70. package/dist/components/loading-text.js +3 -22
  71. package/dist/components/loading-text.js.map +1 -1
  72. package/dist/components/theme-picker.d.ts +2 -0
  73. package/dist/components/theme-picker.d.ts.map +1 -0
  74. package/dist/components/theme-picker.js +37 -0
  75. package/dist/components/theme-picker.js.map +1 -0
  76. package/dist/descendants.d.ts +6 -0
  77. package/dist/descendants.d.ts.map +1 -1
  78. package/dist/descendants.js +74 -8
  79. package/dist/descendants.js.map +1 -1
  80. package/dist/examples/internal/descendants-rerender.d.ts +14 -0
  81. package/dist/examples/internal/descendants-rerender.d.ts.map +1 -0
  82. package/dist/examples/internal/descendants-rerender.js +145 -0
  83. package/dist/examples/internal/descendants-rerender.js.map +1 -0
  84. package/dist/examples/internal/simple-dialog.js +4 -1
  85. package/dist/examples/internal/simple-dialog.js.map +1 -1
  86. package/dist/examples/internal/simple-scrollbox.js +1 -1
  87. package/dist/examples/internal/simple-scrollbox.js.map +1 -1
  88. package/dist/examples/list-with-dropdown.js +1 -1
  89. package/dist/examples/list-with-dropdown.js.map +1 -1
  90. package/dist/examples/miscellaneous.js +1 -1
  91. package/dist/examples/miscellaneous.js.map +1 -1
  92. package/dist/examples/toast-action.d.ts +2 -0
  93. package/dist/examples/toast-action.d.ts.map +1 -0
  94. package/dist/examples/toast-action.js +76 -0
  95. package/dist/examples/toast-action.js.map +1 -0
  96. package/dist/examples/toast-variations.js +38 -36
  97. package/dist/examples/toast-variations.js.map +1 -1
  98. package/dist/extensions/dev.d.ts +1 -1
  99. package/dist/extensions/dev.d.ts.map +1 -1
  100. package/dist/extensions/dev.js +62 -30
  101. package/dist/extensions/dev.js.map +1 -1
  102. package/dist/extensions/home.d.ts.map +1 -1
  103. package/dist/extensions/home.js +4 -3
  104. package/dist/extensions/home.js.map +1 -1
  105. package/dist/extensions/react-refresh-init.d.ts +5 -0
  106. package/dist/extensions/react-refresh-init.d.ts.map +1 -0
  107. package/dist/extensions/react-refresh-init.js +52 -0
  108. package/dist/extensions/react-refresh-init.js.map +1 -0
  109. package/dist/internal/date-picker-widget.js +1 -1
  110. package/dist/internal/date-picker-widget.js.map +1 -1
  111. package/dist/internal/dialog.d.ts +8 -3
  112. package/dist/internal/dialog.d.ts.map +1 -1
  113. package/dist/internal/dialog.js +37 -53
  114. package/dist/internal/dialog.js.map +1 -1
  115. package/dist/internal/navigation.d.ts +1 -0
  116. package/dist/internal/navigation.d.ts.map +1 -1
  117. package/dist/internal/navigation.js +25 -1
  118. package/dist/internal/navigation.js.map +1 -1
  119. package/dist/internal/providers.d.ts.map +1 -1
  120. package/dist/internal/providers.js +9 -197
  121. package/dist/internal/providers.js.map +1 -1
  122. package/dist/internal/scrollbox.d.ts.map +1 -1
  123. package/dist/internal/scrollbox.js +1 -0
  124. package/dist/internal/scrollbox.js.map +1 -1
  125. package/dist/release.d.ts +1 -0
  126. package/dist/release.d.ts.map +1 -1
  127. package/dist/release.js +16 -9
  128. package/dist/release.js.map +1 -1
  129. package/dist/state.d.ts +27 -1
  130. package/dist/state.d.ts.map +1 -1
  131. package/dist/state.js +6 -0
  132. package/dist/state.js.map +1 -1
  133. package/dist/theme.d.ts +6 -19
  134. package/dist/theme.d.ts.map +1 -1
  135. package/dist/theme.js +76 -45
  136. package/dist/theme.js.map +1 -1
  137. package/dist/themes/aura.json +69 -0
  138. package/dist/themes/ayu.json +80 -0
  139. package/dist/themes/catppuccin-frappe.json +233 -0
  140. package/dist/themes/catppuccin-macchiato.json +233 -0
  141. package/dist/themes/catppuccin.json +112 -0
  142. package/dist/themes/cobalt2.json +228 -0
  143. package/dist/themes/cursor.json +249 -0
  144. package/dist/themes/dracula.json +219 -0
  145. package/dist/themes/everforest.json +241 -0
  146. package/dist/themes/flexoki.json +237 -0
  147. package/dist/themes/github-light.json +56 -0
  148. package/dist/themes/github.json +241 -0
  149. package/dist/themes/gruvbox.json +95 -0
  150. package/dist/themes/kanagawa.json +77 -0
  151. package/dist/themes/lucent-orng.json +227 -0
  152. package/dist/themes/material.json +235 -0
  153. package/dist/themes/matrix.json +77 -0
  154. package/dist/themes/mercury.json +245 -0
  155. package/dist/themes/monokai.json +221 -0
  156. package/dist/themes/nightowl.json +221 -0
  157. package/dist/themes/nord.json +223 -0
  158. package/dist/themes/one-dark.json +84 -0
  159. package/dist/themes/opencode-light.json +62 -0
  160. package/dist/themes/opencode.json +245 -0
  161. package/dist/themes/orng.json +245 -0
  162. package/dist/themes/palenight.json +222 -0
  163. package/dist/themes/rosepine.json +234 -0
  164. package/dist/themes/solarized.json +223 -0
  165. package/dist/themes/synthwave84.json +226 -0
  166. package/dist/themes/termcast.json +226 -0
  167. package/dist/themes/tokyonight.json +243 -0
  168. package/dist/themes/vercel.json +255 -0
  169. package/dist/themes/vesper.json +218 -0
  170. package/dist/themes/zenburn.json +223 -0
  171. package/dist/themes.d.ts +57 -0
  172. package/dist/themes.d.ts.map +1 -0
  173. package/dist/themes.js +181 -0
  174. package/dist/themes.js.map +1 -0
  175. package/dist/utils/run-command.d.ts +2 -1
  176. package/dist/utils/run-command.d.ts.map +1 -1
  177. package/dist/utils/run-command.js +20 -10
  178. package/dist/utils/run-command.js.map +1 -1
  179. package/dist/utils.d.ts +2 -1
  180. package/dist/utils.d.ts.map +1 -1
  181. package/dist/utils.js +90 -17
  182. package/dist/utils.js.map +1 -1
  183. package/dist/watcher.d.ts +3 -0
  184. package/dist/watcher.d.ts.map +1 -0
  185. package/dist/watcher.js +16 -0
  186. package/dist/watcher.js.map +1 -0
  187. package/package.json +16 -10
  188. package/src/apis/cache.tsx +5 -44
  189. package/src/apis/hud.tsx +17 -62
  190. package/src/apis/localstorage.tsx +3 -32
  191. package/src/apis/toast.tsx +91 -275
  192. package/src/build.test.tsx +10 -0
  193. package/src/build.tsx +61 -1
  194. package/src/cli.tsx +365 -103
  195. package/src/colors.tsx +1 -0
  196. package/src/compile.tsx +21 -29
  197. package/src/compile.vitest.tsx +300 -0
  198. package/src/components/actions.tsx +64 -45
  199. package/src/components/animation-tick.tsx +85 -0
  200. package/src/components/detail.tsx +31 -35
  201. package/src/components/dropdown.tsx +32 -21
  202. package/src/components/extension-preferences.tsx +14 -10
  203. package/src/components/footer.tsx +241 -0
  204. package/src/components/form/file-autocomplete.tsx +80 -60
  205. package/src/components/form/file-picker.tsx +37 -25
  206. package/src/components/form/index.tsx +45 -41
  207. package/src/components/form/with-left-border.tsx +4 -14
  208. package/src/components/list.tsx +181 -121
  209. package/src/components/loading-bar.tsx +5 -25
  210. package/src/components/loading-text.tsx +4 -23
  211. package/src/components/theme-picker.tsx +57 -0
  212. package/src/descendants.tsx +98 -9
  213. package/src/examples/actions-dialog-layout.vitest.tsx +112 -0
  214. package/src/examples/file-autocomplete.vitest.tsx +131 -122
  215. package/src/examples/form-basic.vitest.tsx +463 -644
  216. package/src/examples/form-dropdown.vitest.tsx +553 -571
  217. package/src/examples/form-scroll.vitest.tsx +112 -102
  218. package/src/examples/form-tagpicker.vitest.tsx +364 -338
  219. package/src/examples/internal/descendants-rerender.tsx +273 -0
  220. package/src/examples/internal/descendants-rerender.vitest.tsx +194 -0
  221. package/src/examples/internal/simple-dialog.tsx +4 -4
  222. package/src/examples/internal/simple-scrollbox.tsx +2 -2
  223. package/src/examples/internal/simple-scrollbox.vitest.tsx +43 -31
  224. package/src/examples/list-detail-metadata.vitest.tsx +34 -30
  225. package/src/examples/list-dropdown-default.vitest.tsx +84 -72
  226. package/src/examples/list-empty-view.vitest.tsx +93 -0
  227. package/src/examples/list-fetch-data.vitest.tsx +36 -30
  228. package/src/examples/list-scrollbox.vitest.tsx +59 -39
  229. package/src/examples/list-with-detail.vitest.tsx +339 -314
  230. package/src/examples/list-with-dropdown.tsx +1 -0
  231. package/src/examples/list-with-dropdown.vitest.tsx +176 -150
  232. package/src/examples/list-with-sections.vitest.tsx +289 -270
  233. package/src/examples/list-with-toast.vitest.tsx +44 -44
  234. package/src/examples/miscellaneous.tsx +10 -0
  235. package/src/examples/simple-file-picker.vitest.tsx +90 -86
  236. package/src/examples/simple-grid.vitest.tsx +275 -249
  237. package/src/examples/simple-navigation.vitest.tsx +192 -168
  238. package/src/examples/store.vitest.tsx +6 -4
  239. package/src/examples/swift-extension.vitest.tsx +31 -19
  240. package/src/examples/synonyms.vitest.tsx +93 -83
  241. package/src/examples/toast-action.tsx +160 -0
  242. package/src/examples/toast-action.vitest.tsx +404 -0
  243. package/src/examples/toast-variations.tsx +58 -57
  244. package/src/examples/toast-variations.vitest.tsx +186 -166
  245. package/src/extensions/dev.tsx +74 -33
  246. package/src/extensions/dev.vitest.tsx +162 -69
  247. package/src/extensions/home.tsx +5 -6
  248. package/src/extensions/react-refresh-init.tsx +59 -0
  249. package/src/internal/date-picker-widget.tsx +1 -1
  250. package/src/internal/dialog.tsx +59 -83
  251. package/src/internal/navigation.tsx +37 -4
  252. package/src/internal/providers.tsx +27 -315
  253. package/src/internal/scrollbox.tsx +1 -0
  254. package/src/release.tsx +16 -10
  255. package/src/state.tsx +36 -3
  256. package/src/theme.tsx +82 -51
  257. package/src/themes/aura.json +69 -0
  258. package/src/themes/ayu.json +80 -0
  259. package/src/themes/catppuccin-frappe.json +233 -0
  260. package/src/themes/catppuccin-macchiato.json +233 -0
  261. package/src/themes/catppuccin.json +112 -0
  262. package/src/themes/cobalt2.json +228 -0
  263. package/src/themes/cursor.json +249 -0
  264. package/src/themes/dracula.json +219 -0
  265. package/src/themes/everforest.json +241 -0
  266. package/src/themes/flexoki.json +237 -0
  267. package/src/themes/github-light.json +56 -0
  268. package/src/themes/github.json +241 -0
  269. package/src/themes/gruvbox.json +95 -0
  270. package/src/themes/kanagawa.json +77 -0
  271. package/src/themes/lucent-orng.json +227 -0
  272. package/src/themes/material.json +235 -0
  273. package/src/themes/matrix.json +77 -0
  274. package/src/themes/mercury.json +252 -0
  275. package/src/themes/monokai.json +221 -0
  276. package/src/themes/nightowl.json +221 -0
  277. package/src/themes/nord.json +223 -0
  278. package/src/themes/one-dark.json +84 -0
  279. package/src/themes/opencode-light.json +62 -0
  280. package/src/themes/opencode.json +245 -0
  281. package/src/themes/orng.json +245 -0
  282. package/src/themes/palenight.json +222 -0
  283. package/src/themes/rosepine.json +234 -0
  284. package/src/themes/solarized.json +223 -0
  285. package/src/themes/synthwave84.json +226 -0
  286. package/src/themes/termcast.json +227 -0
  287. package/src/themes/tokyonight.json +243 -0
  288. package/src/themes/vercel.json +255 -0
  289. package/src/themes/vesper.json +218 -0
  290. package/src/themes/zenburn.json +223 -0
  291. package/src/themes.ts +291 -0
  292. package/src/utils/run-command.tsx +23 -12
  293. package/src/utils.tsx +115 -18
  294. package/src/watcher.tsx +19 -0
@@ -4,7 +4,7 @@ import {
4
4
  TextAttributes,
5
5
  TextareaRenderable,
6
6
  } from '@opentui/core'
7
- import { useKeyboard } from '@opentui/react'
7
+ import { useKeyboard, flushSync } from '@opentui/react'
8
8
  import React, {
9
9
  ReactElement,
10
10
  ReactNode,
@@ -17,6 +17,7 @@ import React, {
17
17
  useState
18
18
  } from 'react'
19
19
  import { LoadingBar } from 'termcast/src/components/loading-bar'
20
+ import { Footer } from 'termcast/src/components/footer'
20
21
  import { createDescendants } from 'termcast/src/descendants'
21
22
  import { useStore } from 'termcast/src/state'
22
23
  import { useDialog } from 'termcast/src/internal/dialog'
@@ -27,6 +28,7 @@ import { ScrollBox } from 'termcast/src/internal/scrollbox'
27
28
 
28
29
  import { Color, resolveColor } from 'termcast/src/colors'
29
30
  import { getIconEmoji } from 'termcast/src/components/icon'
31
+ import { ActionPanel } from 'termcast/src/components/actions'
30
32
  import { Theme, markdownSyntaxStyle } from 'termcast/src/theme'
31
33
  import { CommonProps } from 'termcast/src/utils'
32
34
 
@@ -71,44 +73,35 @@ interface ActionsInterface {
71
73
  function ListFooter(): any {
72
74
  const firstActionTitle = useStore((s) => s.firstActionTitle)
73
75
  const hasToast = useStore((s) => s.toast !== null)
76
+ const listContext = useContext(ListContext)
77
+ const isShowingDetail = listContext?.isShowingDetail ?? false
74
78
 
75
79
  const content = hasToast ? null : (
76
- <>
80
+ <box style={{ flexDirection: 'row', gap: 3 }}>
77
81
  {firstActionTitle && (
78
- <>
79
- <text fg={Theme.text} attributes={TextAttributes.BOLD}>
82
+ <box style={{ flexDirection: 'row', gap: 1 }}>
83
+ <text flexShrink={0} fg={Theme.text} attributes={TextAttributes.BOLD}>
80
84
 
81
85
  </text>
82
- <text fg={Theme.textMuted}> {firstActionTitle.toLowerCase()}</text>
83
- </>
86
+ <text flexShrink={0} fg={Theme.textMuted}>{firstActionTitle.toLowerCase()}</text>
87
+ </box>
84
88
  )}
85
- <text fg={Theme.text} attributes={TextAttributes.BOLD}>
86
- {' '}↑↓
87
- </text>
88
- <text fg={Theme.textMuted}> navigate</text>
89
- <text fg={Theme.text} attributes={TextAttributes.BOLD}>
90
- {' '}^k
91
- </text>
92
- <text fg={Theme.textMuted}> actions</text>
93
- </>
94
- )
95
-
96
- return (
97
- <box
98
- border={false}
99
- height={1}
100
- style={{
101
- paddingLeft: 1,
102
- flexShrink: 0,
103
- paddingRight: 1,
104
- paddingTop: 1,
105
- marginTop: 1,
106
- flexDirection: 'row',
107
- }}
108
- >
109
- {content}
89
+ <box style={{ flexDirection: 'row', gap: 1 }}>
90
+ <text flexShrink={0} fg={Theme.text} attributes={TextAttributes.BOLD}>
91
+ ↑↓
92
+ </text>
93
+ <text flexShrink={0} fg={Theme.textMuted}>navigate</text>
94
+ </box>
95
+ <box style={{ flexDirection: 'row', gap: 1 }}>
96
+ <text flexShrink={0} fg={Theme.text} attributes={TextAttributes.BOLD}>
97
+ ^k
98
+ </text>
99
+ <text flexShrink={0} fg={Theme.textMuted}>actions</text>
100
+ </box>
110
101
  </box>
111
102
  )
103
+
104
+ return <Footer hidePoweredBy={isShowingDetail}>{content}</Footer>
112
105
  }
113
106
 
114
107
  interface NavigationChildInterface {
@@ -350,6 +343,8 @@ const {
350
343
  DescendantsProvider: ListDescendantsProvider,
351
344
  useDescendants: useListDescendants,
352
345
  useDescendant: useListItemDescendant,
346
+ useDescendantsRerender: useListDescendantsRerender,
347
+ useDescendantsMap: useListDescendantsMap,
353
348
  } = createDescendants<ListItemDescendant>()
354
349
 
355
350
  // Create descendants for Dropdown items
@@ -395,8 +390,10 @@ function ListDropdownDialog(props: ListDropdownDialogProps): any {
395
390
 
396
391
  // Wrapper function that updates search text
397
392
  const setSearchText = (value: string) => {
398
- setSearchTextRaw(value)
399
- // TODO: use flushSync when available to force descendants to update visibility
393
+ // Using flushSync to force descendants to update visibility before querying
394
+ flushSync(() => {
395
+ setSearchTextRaw(value)
396
+ })
400
397
  const items = Object.values(descendantsContext.map.current)
401
398
  .filter((item) => item.index !== -1 && item.props?.visible !== false)
402
399
  .sort((a, b) => a.index - b.index)
@@ -472,13 +469,15 @@ function ListDropdownDialog(props: ListDropdownDialogProps): any {
472
469
  justifyContent: 'space-between',
473
470
  }}
474
471
  >
475
- <text attributes={TextAttributes.BOLD}>{props.tooltip}</text>
476
- <text fg={Theme.textMuted}>esc</text>
472
+ <text flexShrink={0} fg={Theme.textMuted}>{props.tooltip}</text>
473
+ <text flexShrink={0} fg={Theme.textMuted}>esc</text>
477
474
  </box>
478
- <box style={{ paddingTop: 1, paddingBottom: 1 }}>
475
+ <box style={{ paddingTop: 1, paddingBottom: 1, flexDirection: 'row' }}>
476
+ <text flexShrink={0} fg={Theme.textMuted}>&gt; </text>
479
477
  <textarea
480
478
  ref={inputRef}
481
479
  height={1}
480
+ flexGrow={1}
482
481
  wrapMode='none'
483
482
  keyBindings={[
484
483
  { name: 'return', action: 'submit' },
@@ -518,7 +517,7 @@ function ListDropdownDialog(props: ListDropdownDialogProps): any {
518
517
  </box>
519
518
  {props.isLoading && (
520
519
  <box style={{ paddingLeft: 1 }}>
521
- <text fg={Theme.textMuted}>Loading...</text>
520
+ <text flexShrink={0} fg={Theme.textMuted}>Loading...</text>
522
521
  </box>
523
522
  )}
524
523
  </box>
@@ -533,32 +532,26 @@ function DropdownFooter(): any {
533
532
  const hasToast = useStore((s) => s.toast !== null)
534
533
 
535
534
  const content = hasToast ? null : (
536
- <>
537
- <text fg={Theme.text} attributes={TextAttributes.BOLD}>
538
-
539
- </text>
540
- <text fg={Theme.textMuted}> select</text>
541
- <text fg={Theme.text} attributes={TextAttributes.BOLD}>
542
- {' '}↑↓
543
- </text>
544
- <text fg={Theme.textMuted}> navigate</text>
545
- </>
535
+ <box style={{ flexDirection: 'row', gap: 3 }}>
536
+ <box style={{ flexDirection: 'row', gap: 1 }}>
537
+ <text flexShrink={0} fg={Theme.text} attributes={TextAttributes.BOLD}>
538
+
539
+ </text>
540
+ <text flexShrink={0} fg={Theme.textMuted}>select</text>
541
+ </box>
542
+ <box style={{ flexDirection: 'row', gap: 1 }}>
543
+ <text flexShrink={0} fg={Theme.text} attributes={TextAttributes.BOLD}>
544
+ ↑↓
545
+ </text>
546
+ <text flexShrink={0} fg={Theme.textMuted}>navigate</text>
547
+ </box>
548
+ </box>
546
549
  )
547
550
 
548
551
  return (
549
- <box
550
- border={false}
551
- height={1}
552
- style={{
553
- paddingRight: 2,
554
- paddingLeft: 3,
555
- paddingBottom: 1,
556
- paddingTop: 1,
557
- flexDirection: 'row',
558
- }}
559
- >
552
+ <Footer paddingLeft={3} paddingRight={2} paddingBottom={1} marginTop={0}>
560
553
  {content}
561
- </box>
554
+ </Footer>
562
555
  )
563
556
  }
564
557
 
@@ -592,6 +585,7 @@ function ListItemRow(props: {
592
585
  accessoryElements.push(
593
586
  <text
594
587
  key={`text-${textValue}`}
588
+ flexShrink={0}
595
589
  fg={active ? Theme.background : resolveColor(textColor) || Theme.info}
596
590
  wrapMode="none"
597
591
  >
@@ -611,6 +605,7 @@ function ListItemRow(props: {
611
605
  accessoryElements.push(
612
606
  <text
613
607
  key={`tag-${tagValue}`}
608
+ flexShrink={0}
614
609
  fg={active ? Theme.background : resolveColor(tagColor) || Theme.warning}
615
610
  wrapMode="none"
616
611
  >
@@ -633,6 +628,7 @@ function ListItemRow(props: {
633
628
  accessoryElements.push(
634
629
  <text
635
630
  key={`date-${dateValue.getTime()}`}
631
+ flexShrink={0}
636
632
  fg={active ? Theme.background : resolveColor(dateColor) || Theme.success}
637
633
  wrapMode="none"
638
634
  >
@@ -670,9 +666,10 @@ function ListItemRow(props: {
670
666
  >
671
667
  <box style={{ flexDirection: 'row', flexGrow: 1, flexShrink: 1, overflow: 'hidden', gap: 1 }}>
672
668
  <box style={{ flexDirection: 'row', flexShrink: 0 }}>
673
- <text fg={active ? Theme.background : Theme.text} attributes={active ? TextAttributes.BOLD : undefined} selectable={false} wrapMode="none">{active ? '›' : ' '}</text>
674
- {icon && <text fg={active ? Theme.background : iconColor || Theme.text} selectable={false} wrapMode="none">{getIconEmoji(icon)} </text>}
669
+ <text flexShrink={0} fg={active ? Theme.background : Theme.text} attributes={active ? TextAttributes.BOLD : undefined} selectable={false} wrapMode="none">{active ? '›' : ' '}</text>
670
+ {icon && <text flexShrink={0} fg={active ? Theme.background : iconColor || Theme.text} selectable={false} wrapMode="none">{getIconEmoji(icon)} </text>}
675
671
  <text
672
+ flexShrink={0}
676
673
  fg={active ? Theme.background : Theme.text}
677
674
  attributes={active ? TextAttributes.BOLD : undefined}
678
675
  selectable={false}
@@ -683,6 +680,7 @@ function ListItemRow(props: {
683
680
  </box>
684
681
  {subtitle && (
685
682
  <text
683
+ flexShrink={0}
686
684
  fg={active ? Theme.background : Theme.textMuted}
687
685
  selectable={false}
688
686
  wrapMode="none"
@@ -695,7 +693,7 @@ function ListItemRow(props: {
695
693
  <box style={{ flexDirection: 'row', flexShrink: 0 }}>
696
694
  {accessoryElements.map((elem, i) => (
697
695
  <box key={i} style={{ flexDirection: 'row' }}>
698
- {i > 0 && <text> </text>}
696
+ {i > 0 && <text flexShrink={0}> </text>}
699
697
  {elem}
700
698
  </box>
701
699
  ))}
@@ -777,9 +775,10 @@ export const List: ListType = (props) => {
777
775
 
778
776
  // Wrapper function that updates search text
779
777
  const setInternalSearchText = (value: string) => {
780
- setInternalSearchTextRaw(value)
781
- // TODO: use flushSync when available to force descendants to update visibility
782
- // before querying. For now, we compute visibility inline with the new search value.
778
+ // Using flushSync to force descendants to update visibility before querying
779
+ flushSync(() => {
780
+ setInternalSearchTextRaw(value)
781
+ })
783
782
  const items = Object.values(descendantsContext.map.current)
784
783
  .filter((item) => item.index !== -1 && item.props?.visible !== false)
785
784
  .sort((a, b) => a.index - b.index)
@@ -919,11 +918,15 @@ export const List: ListType = (props) => {
919
918
  if (evt.name === 'k' && evt.ctrl) {
920
919
  // Show current item's actions if available
921
920
  if (currentItem?.props?.actions) {
922
- dialog.push(currentItem.props.actions, 'bottom-right')
921
+ dialog.pushActions(currentItem.props.actions)
923
922
  }
924
923
  // Otherwise show List's own actions
925
924
  else if (props.actions) {
926
- dialog.push(props.actions, 'bottom-right')
925
+ dialog.pushActions(props.actions)
926
+ }
927
+ // Otherwise show empty ActionPanel (still has Settings section with Configure Extension, etc.)
928
+ else {
929
+ dialog.pushActions(<ActionPanel />)
927
930
  }
928
931
  return
929
932
  }
@@ -936,7 +939,7 @@ export const List: ListType = (props) => {
936
939
 
937
940
  if (currentItem.props.actions) {
938
941
  useStore.setState({ shouldAutoExecuteFirstAction: true })
939
- dialog.push(currentItem.props.actions, 'bottom-right')
942
+ dialog.pushActions(currentItem.props.actions)
940
943
  }
941
944
  }
942
945
  })
@@ -995,13 +998,15 @@ export const List: ListType = (props) => {
995
998
  <box
996
999
  style={{
997
1000
  flexGrow: 1,
998
- flexDirection: 'column',
1001
+ flexDirection: 'row',
999
1002
  flexShrink: 1,
1000
1003
  }}
1001
1004
  >
1005
+ <text flexShrink={0} fg={Theme.textMuted}>&gt; </text>
1002
1006
  <textarea
1003
1007
  ref={inputRef}
1004
1008
  height={1}
1009
+ flexGrow={1}
1005
1010
  wrapMode='none'
1006
1011
  keyBindings={[
1007
1012
  { name: 'return', action: 'submit' },
@@ -1026,7 +1031,7 @@ export const List: ListType = (props) => {
1026
1031
  {/* Main content area with optional detail view */}
1027
1032
  <box style={{ flexDirection: 'row', flexGrow: 1, flexShrink: 1 }}>
1028
1033
  {/* List content - render children which will register themselves */}
1029
- <box style={{ width: isShowingDetail ? '50%' : '100%', flexGrow: isShowingDetail ? 0 : 1, flexShrink: 1, flexDirection: 'column' }}>
1034
+ <box style={{ width: isShowingDetail ? '50%' : '100%', flexGrow: 1, flexShrink: 1, flexDirection: 'column' }}>
1030
1035
  {/* Scrollable list items */}
1031
1036
  <ScrollBox
1032
1037
  ref={scrollBoxRef}
@@ -1080,6 +1085,47 @@ export const List: ListType = (props) => {
1080
1085
  )
1081
1086
  }
1082
1087
 
1088
+
1089
+ function DefaultEmptyView(): any {
1090
+ // Subscribe to re-render when items are added/removed
1091
+ void useListDescendantsRerender()
1092
+ // Get live map ref for reading in useLayoutEffect
1093
+ const map = useListDescendantsMap()
1094
+ const [hasVisibleItems, setHasVisibleItems] = useState(true)
1095
+
1096
+ // We must check visibility in useLayoutEffect because:
1097
+ // 1. map.current is cleared by reset() during render, so it's empty if read during render
1098
+ // 2. committedMap is stale - it's a snapshot from the previous render cycle and doesn't
1099
+ // reflect prop changes like 'visible' (only tracks which items exist, not their props)
1100
+ // 3. Items register in their own useLayoutEffect, so map.current is only populated after
1101
+ // all items' layout effects have run
1102
+ useLayoutEffect(() => {
1103
+ const items = Object.values(map.current)
1104
+ .filter((item) => item.index !== -1 && item.props?.visible !== false)
1105
+ setHasVisibleItems(items.length > 0)
1106
+ })
1107
+
1108
+ if (hasVisibleItems) return null
1109
+
1110
+ return (
1111
+ <box
1112
+ style={{
1113
+ flexDirection: 'column',
1114
+ alignItems: 'center',
1115
+ justifyContent: 'center',
1116
+ paddingTop: 2,
1117
+ paddingBottom: 2,
1118
+ paddingLeft: 2,
1119
+ paddingRight: 2,
1120
+ }}
1121
+ >
1122
+ <text flexShrink={0} fg={Theme.textMuted}>
1123
+ No items found
1124
+ </text>
1125
+ </box>
1126
+ )
1127
+ }
1128
+
1083
1129
  // Component to render list items and sections
1084
1130
  function ListItemsRenderer(props: { children?: ReactNode }): any {
1085
1131
  const { children } = props
@@ -1090,6 +1136,7 @@ function ListItemsRenderer(props: { children?: ReactNode }): any {
1090
1136
  return (
1091
1137
  <ListSectionContext.Provider value={{ searchText }}>
1092
1138
  {children}
1139
+ <DefaultEmptyView />
1093
1140
  </ListSectionContext.Provider>
1094
1141
  )
1095
1142
  }
@@ -1164,8 +1211,8 @@ const ListItem: ListItemType = (props) => {
1164
1211
  const handleMouseDown = () => {
1165
1212
  if (listContext && index !== -1) {
1166
1213
  // If clicking on already selected item, show actions (like pressing Enter)
1167
- if (isActive && props.actions) {
1168
- dialog.push(props.actions, 'bottom-right')
1214
+ if (isActive) {
1215
+ dialog.pushActions(props.actions || <ActionPanel />)
1169
1216
  } else if (listContext.setSelectedIndex) {
1170
1217
  // Otherwise just select the item
1171
1218
  listContext.setSelectedIndex(index)
@@ -1221,13 +1268,13 @@ const ListItemDetail: ListItemDetailType = (props) => {
1221
1268
  <box style={{ flexDirection: 'column', flexGrow: 1 }}>
1222
1269
  {isLoading && (
1223
1270
  <box style={{ paddingBottom: 1 }}>
1224
- <text fg={Theme.textMuted}>Loading...</text>
1271
+ <text flexShrink={0} fg={Theme.textMuted}>Loading...</text>
1225
1272
  </box>
1226
1273
  )}
1227
1274
 
1228
1275
  <ScrollBox
1229
1276
  focused={false}
1230
- flexGrow={1}
1277
+ // flexGrow={1}
1231
1278
  flexShrink={1}
1232
1279
  style={{
1233
1280
  rootOptions: {
@@ -1271,8 +1318,8 @@ const ListItemDetailMetadata = (props: MetadataProps) => {
1271
1318
  const ListItemDetailMetadataLabel = (props: { title: string; text?: string; icon?: Image.ImageLike }) => {
1272
1319
  return (
1273
1320
  <box style={{ flexDirection: 'column', paddingBottom: 0.5 }}>
1274
- <text fg={Theme.textMuted}>{props.title}:</text>
1275
- {props.text && <text fg={Theme.text}>{props.text}</text>}
1321
+ <text flexShrink={0} fg={Theme.textMuted}>{props.title}:</text>
1322
+ {props.text && <text flexShrink={0} fg={Theme.text}>{props.text}</text>}
1276
1323
  </box>
1277
1324
  )
1278
1325
  }
@@ -1280,7 +1327,7 @@ const ListItemDetailMetadataLabel = (props: { title: string; text?: string; icon
1280
1327
  const ListItemDetailMetadataSeparator = () => {
1281
1328
  return (
1282
1329
  <box style={{ paddingBottom: 0.5 }}>
1283
- <text fg={Theme.border}>─────────────────</text>
1330
+ <text flexShrink={0} fg={Theme.border}>─────────────────</text>
1284
1331
  </box>
1285
1332
  )
1286
1333
  }
@@ -1288,8 +1335,8 @@ const ListItemDetailMetadataSeparator = () => {
1288
1335
  const ListItemDetailMetadataLink = (props: { title: string; target: string; text: string }) => {
1289
1336
  return (
1290
1337
  <box style={{ flexDirection: 'column', paddingBottom: 0.5 }}>
1291
- <text fg={Theme.textMuted}>{props.title}:</text>
1292
- <text fg={Theme.link}>{props.text}</text>
1338
+ <text flexShrink={0} fg={Theme.textMuted}>{props.title}:</text>
1339
+ <text flexShrink={0} fg={Theme.markdownLink}>{props.text}</text>
1293
1340
  </box>
1294
1341
  )
1295
1342
  }
@@ -1297,7 +1344,7 @@ const ListItemDetailMetadataLink = (props: { title: string; target: string; text
1297
1344
  const ListItemDetailMetadataTagList = (props: { title: string; children: ReactNode }) => {
1298
1345
  return (
1299
1346
  <box style={{ flexDirection: 'column', paddingBottom: 0.5 }}>
1300
- <text fg={Theme.textMuted}>{props.title}:</text>
1347
+ <text flexShrink={0} fg={Theme.textMuted}>{props.title}:</text>
1301
1348
  <box style={{ flexDirection: 'row', paddingLeft: 1 }}>
1302
1349
  {props.children}
1303
1350
  </box>
@@ -1308,7 +1355,7 @@ const ListItemDetailMetadataTagList = (props: { title: string; children: ReactNo
1308
1355
  const ListItemDetailMetadataTagListItem = (props: { text?: string; color?: Color.ColorLike; icon?: Image.ImageLike; onAction?: () => void }) => {
1309
1356
  return (
1310
1357
  <box style={{ paddingRight: 1 }}>
1311
- <text fg={resolveColor(props.color) || Theme.accent}>[{props.text}]</text>
1358
+ <text flexShrink={0} fg={resolveColor(props.color) || Theme.accent}>[{props.text}]</text>
1312
1359
  </box>
1313
1360
  )
1314
1361
  }
@@ -1322,6 +1369,14 @@ ListItemDetailMetadataTagList.Item = ListItemDetailMetadataTagListItem as any
1322
1369
 
1323
1370
  ListItem.Detail = ListItemDetail
1324
1371
 
1372
+ /**
1373
+ * A dropdown menu shown in the right-hand-side of the search bar.
1374
+ * Open it with Ctrl+P or by clicking on it.
1375
+ *
1376
+ * Note: There is no built-in "All" or reset option. If you want users to be
1377
+ * able to reset the filter, add a `List.Dropdown.Item` with title="All" and
1378
+ * value="" (or your preferred reset value) at the top of your dropdown items.
1379
+ */
1325
1380
  const ListDropdown: ListDropdownType = (props) => {
1326
1381
  const listContext = useContext(ListContext)
1327
1382
  const [isHovered, setIsHovered] = useState(false)
@@ -1391,37 +1446,39 @@ const ListDropdown: ListDropdownType = (props) => {
1391
1446
  useEffect(() => {
1392
1447
  if (isDropdownOpen && !dialog.stack.length) {
1393
1448
  // Pass the children to the dialog to render them there
1394
- dialog.push(
1395
- <ListDropdownDialog
1396
- {...props}
1397
- value={dropdownState.value}
1398
- onChange={(newValue) => {
1399
- // Find the title for this value
1400
- let title = newValue
1401
- for (const item of Object.values(descendantsContext.map.current)) {
1402
- const itemProps = item.props as DropdownItemDescendant
1403
- if (itemProps.value === newValue) {
1404
- title = itemProps.title
1405
- break
1449
+ dialog.push({
1450
+ element: (
1451
+ <ListDropdownDialog
1452
+ {...props}
1453
+ value={dropdownState.value}
1454
+ onChange={(newValue) => {
1455
+ // Find the title for this value
1456
+ let title = newValue
1457
+ for (const item of Object.values(descendantsContext.map.current)) {
1458
+ const itemProps = item.props as DropdownItemDescendant
1459
+ if (itemProps.value === newValue) {
1460
+ title = itemProps.title
1461
+ break
1462
+ }
1406
1463
  }
1407
- }
1408
- setDropdownState({ value: newValue, title })
1409
- setIsDropdownOpen(false)
1410
- dialog.clear()
1411
- if (props.onChange) {
1412
- props.onChange(newValue)
1413
- }
1414
- // TODO: Handle storeValue to persist the value
1415
- }}
1416
- onCancel={() => {
1417
- setIsDropdownOpen(false)
1418
- dialog.clear()
1419
- }}
1420
- >
1421
- {props.children}
1422
- </ListDropdownDialog>,
1423
- 'top-right',
1424
- )
1464
+ setDropdownState({ value: newValue, title })
1465
+ setIsDropdownOpen(false)
1466
+ dialog.clear()
1467
+ if (props.onChange) {
1468
+ props.onChange(newValue)
1469
+ }
1470
+ // TODO: Handle storeValue to persist the value
1471
+ }}
1472
+ onCancel={() => {
1473
+ setIsDropdownOpen(false)
1474
+ dialog.clear()
1475
+ }}
1476
+ >
1477
+ {props.children}
1478
+ </ListDropdownDialog>
1479
+ ),
1480
+ position: 'center',
1481
+ })
1425
1482
  }
1426
1483
  }, [isDropdownOpen, props.children])
1427
1484
 
@@ -1455,12 +1512,14 @@ const ListDropdown: ListDropdownType = (props) => {
1455
1512
  >
1456
1513
  {/*<text >^p </text>*/}
1457
1514
  <text
1515
+ flexShrink={0}
1458
1516
  fg={isHovered ? Theme.text : Theme.textMuted}
1459
1517
  selectable={false}
1460
1518
  >
1461
1519
  {displayValue}
1462
1520
  </text>
1463
1521
  <text
1522
+ flexShrink={0}
1464
1523
  fg={isHovered ? Theme.text : Theme.textMuted}
1465
1524
  selectable={false}
1466
1525
  >
@@ -1552,11 +1611,12 @@ ListDropdown.Item = (props) => {
1552
1611
  >
1553
1612
  <box style={{ flexDirection: 'row' }}>
1554
1613
  {isActive && (
1555
- <text fg={Theme.background} selectable={false}>
1614
+ <text flexShrink={0} fg={Theme.background} selectable={false}>
1556
1615
  ›{''}
1557
1616
  </text>
1558
1617
  )}
1559
1618
  <text
1619
+ flexShrink={0}
1560
1620
  fg={
1561
1621
  isActive
1562
1622
  ? Theme.background
@@ -1605,7 +1665,7 @@ ListDropdown.Section = (props) => {
1605
1665
  {/* Render section title if we're in the dialog and not searching */}
1606
1666
  {showTitle && (
1607
1667
  <box style={{ paddingTop: 1, paddingLeft: 1 }}>
1608
- <text fg={Theme.accent} attributes={TextAttributes.BOLD}>
1668
+ <text flexShrink={0} fg={Theme.accent} attributes={TextAttributes.BOLD}>
1609
1669
  {props.title}
1610
1670
  </text>
1611
1671
  </box>
@@ -1659,7 +1719,7 @@ const ListSection = (props: SectionProps) => {
1659
1719
  paddingLeft: 1,
1660
1720
  }}
1661
1721
  >
1662
- <text fg={Theme.accent} attributes={TextAttributes.BOLD}>
1722
+ <text flexShrink={0} fg={Theme.accent} attributes={TextAttributes.BOLD}>
1663
1723
  {props.title}
1664
1724
  </text>
1665
1725
  </box>
@@ -1679,16 +1739,16 @@ List.EmptyView = (props: EmptyViewProps) => {
1679
1739
  useKeyboard((evt) => {
1680
1740
  if (!inFocus) return
1681
1741
 
1682
- // Handle Ctrl+K to show actions
1683
- if (evt.name === 'k' && evt.ctrl && props.actions) {
1684
- dialog.push(props.actions, 'bottom-right')
1742
+ // Handle Ctrl+K to show actions (always show panel, even without actions)
1743
+ if (evt.name === 'k' && evt.ctrl) {
1744
+ dialog.pushActions(props.actions || <ActionPanel />)
1685
1745
  return
1686
1746
  }
1687
1747
 
1688
1748
  // Handle Enter to execute first action
1689
1749
  if (evt.name === 'return' && props.actions) {
1690
1750
  useStore.setState({ shouldAutoExecuteFirstAction: true })
1691
- dialog.push(props.actions, 'bottom-right')
1751
+ dialog.pushActions(props.actions)
1692
1752
  }
1693
1753
  })
1694
1754
 
@@ -1724,17 +1784,17 @@ List.EmptyView = (props: EmptyViewProps) => {
1724
1784
  }}
1725
1785
  >
1726
1786
  {iconEmoji && (
1727
- <text fg={Theme.textMuted} style={{ marginBottom: 1 }}>
1787
+ <text flexShrink={0} fg={Theme.textMuted} style={{ marginBottom: 1 }}>
1728
1788
  {iconEmoji}
1729
1789
  </text>
1730
1790
  )}
1731
1791
  {props.title && (
1732
- <text fg={Theme.text} attributes={TextAttributes.BOLD}>
1792
+ <text flexShrink={0} fg={Theme.text} attributes={TextAttributes.BOLD}>
1733
1793
  {props.title?.replace(/\bRaycast\b/g, 'Termcast').replace(/\braycast\b/g, 'termcast') || ''}
1734
1794
  </text>
1735
1795
  )}
1736
1796
  {props.description && (
1737
- <text fg={Theme.textMuted} wrapMode='word'>
1797
+ <text flexShrink={0} fg={Theme.textMuted} wrapMode='word'>
1738
1798
  {props.description?.replace(/\bRaycast\b/g, 'Termcast').replace(/\braycast\b/g, 'termcast') || ''}
1739
1799
  </text>
1740
1800
  )}
@@ -1,8 +1,7 @@
1
- import React, { useState, useEffect, useRef, useLayoutEffect } from 'react'
1
+ import React, { useState, useRef, useLayoutEffect } from 'react'
2
2
  import { BoxRenderable } from '@opentui/core'
3
- import {} from '@opentui/react'
4
3
  import { Theme } from 'termcast/src/theme'
5
- import { logger } from 'termcast/src/logger'
4
+ import { useAnimationTick, TICK_DIVISORS } from 'termcast/src/components/animation-tick'
6
5
 
7
6
  interface LoadingBarProps {
8
7
  title: string
@@ -12,12 +11,11 @@ interface LoadingBarProps {
12
11
 
13
12
  export function LoadingBar(props: LoadingBarProps): any {
14
13
  let { title, isLoading = false, barLength: propBarLength } = props
15
- const [position, setPosition] = useState(0)
16
14
  const [calculatedBarLength, setCalculatedBarLength] = useState(
17
15
  propBarLength || 0,
18
16
  )
19
- const intervalRef = useRef<NodeJS.Timeout | null>(null)
20
17
  const containerRef = useRef<BoxRenderable>(null)
18
+ const tick = useAnimationTick(isLoading ? TICK_DIVISORS.LOADING_BAR : 0)
21
19
 
22
20
  // Calculate bar length based on container width
23
21
  useLayoutEffect(() => {
@@ -73,26 +71,8 @@ export function LoadingBar(props: LoadingBarProps): any {
73
71
  ]
74
72
 
75
73
  const waveWidth = waveColors.length
76
-
77
- useEffect(() => {
78
- if (isLoading) {
79
- intervalRef.current = setInterval(() => {
80
- setPosition((prev) => (prev + 1) % (characters.length + waveWidth))
81
- }, 10)
82
- } else {
83
- if (intervalRef.current) {
84
- clearInterval(intervalRef.current)
85
- intervalRef.current = null
86
- }
87
- setPosition(0)
88
- }
89
-
90
- return () => {
91
- if (intervalRef.current) {
92
- clearInterval(intervalRef.current)
93
- }
94
- }
95
- }, [isLoading, characters.length, waveWidth])
74
+ const totalAnimLength = characters.length + waveWidth
75
+ const position = isLoading ? tick % totalAnimLength : 0
96
76
 
97
77
  // Calculate color for each character
98
78
  const getCharacterColor = (index: number): string => {