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
@@ -6,6 +6,7 @@ import { InFocus, useIsInFocus } from 'termcast/src/internal/focus-context'
6
6
  import { ActionPanel, Action } from 'termcast/src/components/actions'
7
7
  import { Image } from 'termcast/src/components/list'
8
8
  import { Color, resolveColor } from 'termcast/src/colors'
9
+ import { Footer } from 'termcast/src/components/footer'
9
10
 
10
11
  import { useDialog } from 'termcast/src/internal/dialog'
11
12
  import { ScrollBox } from 'termcast/src/internal/scrollbox'
@@ -198,37 +199,32 @@ function DetailFooter({
198
199
  firstActionTitle?: string
199
200
  }): any {
200
201
  return (
201
- <box
202
- border={false}
203
- style={{
204
- paddingLeft: 1,
205
- paddingRight: 1,
206
- paddingTop: 1,
207
- marginTop: 1,
208
- flexDirection: 'row',
209
- }}
210
- >
211
- <text fg={Theme.text} attributes={TextAttributes.BOLD}>
212
- esc
213
- </text>
214
- <text fg={Theme.textMuted}> go back</text>
215
- {hasActions && (
216
- <>
217
- <text fg={Theme.text} attributes={TextAttributes.BOLD}>
218
- {' '}^k
219
- </text>
220
- <text fg={Theme.textMuted}> actions</text>
221
- </>
222
- )}
223
- {hasActions && firstActionTitle && (
224
- <>
225
- <text fg={Theme.text} attributes={TextAttributes.BOLD}>
226
- {' '}↵
202
+ <Footer paddingLeft={0} paddingRight={0}>
203
+ <box style={{ flexDirection: 'row', gap: 3 }}>
204
+ <box style={{ flexDirection: 'row', gap: 1 }}>
205
+ <text flexShrink={0} fg={Theme.text} attributes={TextAttributes.BOLD}>
206
+ esc
227
207
  </text>
228
- <text fg={Theme.textMuted}> {firstActionTitle}</text>
229
- </>
230
- )}
231
- </box>
208
+ <text flexShrink={0} fg={Theme.textMuted}>go back</text>
209
+ </box>
210
+ {hasActions && (
211
+ <box style={{ flexDirection: 'row', gap: 1 }}>
212
+ <text flexShrink={0} fg={Theme.text} attributes={TextAttributes.BOLD}>
213
+ ^k
214
+ </text>
215
+ <text flexShrink={0} fg={Theme.textMuted}>actions</text>
216
+ </box>
217
+ )}
218
+ {hasActions && firstActionTitle && (
219
+ <box style={{ flexDirection: 'row', gap: 1 }}>
220
+ <text flexShrink={0} fg={Theme.text} attributes={TextAttributes.BOLD}>
221
+
222
+ </text>
223
+ <text flexShrink={0} fg={Theme.textMuted}>{firstActionTitle}</text>
224
+ </box>
225
+ )}
226
+ </box>
227
+ </Footer>
232
228
  )
233
229
  }
234
230
 
@@ -277,20 +273,20 @@ const Detail: DetailType = (props) => {
277
273
  useKeyboard((evt) => {
278
274
  if (!inFocus) return
279
275
 
280
- if (evt.name === 'k' && evt.ctrl && actions) {
281
- // Ctrl+K shows actions (always show sheet)
282
- dialog.push(actions, 'bottom-right')
276
+ if (evt.name === 'k' && evt.ctrl) {
277
+ // Ctrl+K shows actions (always show panel, even without actions)
278
+ dialog.pushActions(actions || <ActionPanel />)
283
279
  } else if (evt.name === 'return' && actions) {
284
280
  // Enter executes first action directly
285
281
  useStore.setState({ shouldAutoExecuteFirstAction: true })
286
- dialog.push(actions, 'bottom-right')
282
+ dialog.pushActions(actions)
287
283
  }
288
284
  })
289
285
 
290
286
  const content = (
291
287
  <ScrollBox
292
288
  focused={true}
293
- flexGrow={1}
289
+ // flexGrow={1}
294
290
  flexShrink={1}
295
291
  style={{
296
292
  rootOptions: {
@@ -7,7 +7,7 @@ import React, {
7
7
  createContext,
8
8
  useContext,
9
9
  } from 'react'
10
- import { useKeyboard } from '@opentui/react'
10
+ import { useKeyboard, flushSync } from '@opentui/react'
11
11
  import {
12
12
  TextAttributes,
13
13
  ScrollBoxRenderable,
@@ -38,6 +38,7 @@ export interface DropdownProps extends SearchBarInterface, CommonProps {
38
38
  defaultValue?: string
39
39
  children?: ReactNode
40
40
  onChange?: (newValue: string) => void
41
+ onSelectionChange?: (value: string) => void
41
42
  }
42
43
 
43
44
  export interface DropdownItemProps extends CommonProps {
@@ -78,6 +79,7 @@ interface DropdownContextValue {
78
79
  setSelectedIndex?: (index: number) => void
79
80
  currentValue?: string
80
81
  onChange?: (value: string) => void
82
+ onSelectionChange?: (value: string) => void
81
83
  scrollBoxRef?: React.RefObject<ScrollBoxRenderable | null>
82
84
  }
83
85
 
@@ -97,6 +99,7 @@ const Dropdown: DropdownType = (props) => {
97
99
  const {
98
100
  tooltip,
99
101
  onChange,
102
+ onSelectionChange,
100
103
  value,
101
104
  defaultValue,
102
105
  children,
@@ -120,19 +123,7 @@ const Dropdown: DropdownType = (props) => {
120
123
  const scrollBoxRef = useRef<ScrollBoxRenderable>(null)
121
124
  const descendantsContext = useDropdownDescendants()
122
125
 
123
- // Update textarea and reset selection - single source of truth is the ref
124
- const setSearchText = (text: string) => {
125
- inputRef.current?.setText(text)
126
- setSearchTextState(text)
127
- // TODO: use flushSync when available to force descendants to update visibility
128
- const items = Object.values(descendantsContext.map.current)
129
- .filter((item: any) => item.index !== -1 && !item.props?.hidden)
130
- .sort((a: any, b: any) => a.index - b.index)
131
126
 
132
- if (items.length > 0 && items[0]) {
133
- setSelected(items[0].index)
134
- }
135
- }
136
127
 
137
128
  const scrollToItem = (item: { props?: DropdownItemDescendant }) => {
138
129
  const scrollBox = scrollBoxRef.current
@@ -160,9 +151,10 @@ const Dropdown: DropdownType = (props) => {
160
151
  setSelectedIndex: setSelected,
161
152
  currentValue,
162
153
  onChange: (value: string) => selectItem(value),
154
+ onSelectionChange,
163
155
  scrollBoxRef,
164
156
  }),
165
- [searchText, filtering, selected, currentValue],
157
+ [searchText, filtering, selected, currentValue, onSelectionChange],
166
158
  )
167
159
 
168
160
  // Update controlled value
@@ -176,9 +168,10 @@ const Dropdown: DropdownType = (props) => {
176
168
  const handleSearchTextChange = (text: string) => {
177
169
  if (!inFocus) return
178
170
 
179
- // Update state for context
180
- setSearchTextState(text)
181
- // TODO: use flushSync when available to force descendants to update visibility
171
+ // Update state for context, using flushSync to force descendants to update visibility
172
+ flushSync(() => {
173
+ setSearchTextState(text)
174
+ })
182
175
  const items = Object.values(descendantsContext.map.current)
183
176
  .filter((item: any) => item.index !== -1 && !item.props?.hidden)
184
177
  .sort((a: any, b: any) => a.index - b.index)
@@ -227,6 +220,9 @@ const Dropdown: DropdownType = (props) => {
227
220
  if (nextItem) {
228
221
  setSelected(nextItem.index)
229
222
  scrollToItem(nextItem)
223
+ if (onSelectionChange && nextItem.props) {
224
+ onSelectionChange((nextItem.props as DropdownItemDescendant).value)
225
+ }
230
226
  }
231
227
  }
232
228
 
@@ -296,13 +292,15 @@ const Dropdown: DropdownType = (props) => {
296
292
  justifyContent: 'space-between',
297
293
  }}
298
294
  >
299
- <text attributes={TextAttributes.BOLD}>{tooltip}</text>
295
+ <text fg={Theme.textMuted}>{tooltip}</text>
300
296
  <text fg={Theme.textMuted}>esc</text>
301
297
  </box>
302
- <box style={{ paddingTop: 1, paddingBottom: 1 }}>
298
+ <box style={{ paddingTop: 1, paddingBottom: 1, flexDirection: 'row' }}>
299
+ <text flexShrink={0} fg={Theme.primary}>&gt; </text>
303
300
  <textarea
304
301
  ref={inputRef}
305
302
  height={1}
303
+ flexGrow={1}
306
304
  wrapMode='none'
307
305
  keyBindings={[
308
306
  { name: 'return', action: 'submit' },
@@ -329,7 +327,7 @@ const Dropdown: DropdownType = (props) => {
329
327
  style={{
330
328
  rootOptions: {
331
329
  backgroundColor: undefined,
332
- maxHeight: 10,
330
+ height: 7, // Fixed height prevents layout shift when content mounts incrementally
333
331
  },
334
332
  scrollbarOptions: {
335
333
 
@@ -450,7 +448,7 @@ const DropdownItem: (props: DropdownItemProps) => any = (props) => {
450
448
  const elementRef = useRef<{ y: number; height: number } | null>(null)
451
449
  const isOffscreen = useIsOffscreen()
452
450
  if (!context) return null
453
-
451
+
454
452
  // Don't render UI when offscreen (used for collecting action descendants)
455
453
  if (isOffscreen) return null
456
454
 
@@ -492,6 +490,9 @@ const DropdownItem: (props: DropdownItemProps) => any = (props) => {
492
490
  index !== -1
493
491
  ) {
494
492
  context.setSelectedIndex(index)
493
+ if (context.onSelectionChange) {
494
+ context.onSelectionChange(props.value)
495
+ }
495
496
  }
496
497
  }
497
498
 
@@ -520,6 +521,7 @@ const DropdownItem: (props: DropdownItemProps) => any = (props) => {
520
521
 
521
522
  const DropdownSection: (props: DropdownSectionProps) => any = (props) => {
522
523
  const parentContext = useContext(DropdownContext)
524
+ const isOffscreen = useIsOffscreen()
523
525
  if (!parentContext) return null
524
526
 
525
527
  // Create new context with section title
@@ -531,6 +533,15 @@ const DropdownSection: (props: DropdownSectionProps) => any = (props) => {
531
533
  [parentContext, props.title],
532
534
  )
533
535
 
536
+ // When offscreen, just render children without section title UI
537
+ if (isOffscreen) {
538
+ return (
539
+ <DropdownContext.Provider value={sectionContextValue}>
540
+ {props.children}
541
+ </DropdownContext.Provider>
542
+ )
543
+ }
544
+
534
545
  return (
535
546
  <>
536
547
  {/* Render section title if provided */}
@@ -52,9 +52,12 @@ export function ExtensionPreferences({
52
52
 
53
53
  let packageJson: RaycastPackageJson
54
54
 
55
- if (extensionPath && extensionPackageJson?.name === extensionName) {
56
- // Dev mode - use package.json from state or read from extensionPath
55
+ if (extensionPackageJson?.name === extensionName) {
56
+ // Dev mode or compiled extension - use package.json from state
57
57
  packageJson = extensionPackageJson
58
+ } else if (extensionPath && fs.existsSync(path.join(extensionPath, 'package.json'))) {
59
+ // Dev mode with extensionPath - read from disk
60
+ packageJson = JSON.parse(fs.readFileSync(path.join(extensionPath, 'package.json'), 'utf-8'))
58
61
  } else {
59
62
  // Store extension - read from store directory
60
63
  const storeDir = getStoreDirectory()
@@ -95,7 +98,7 @@ export function ExtensionPreferences({
95
98
  }
96
99
  }
97
100
 
98
- return { preferences: prefsToUse, savedValues }
101
+ return { preferences: prefsToUse, savedValues, extensionTitle: packageJson.title || extensionName }
99
102
  } catch (error) {
100
103
  logger.error(`Failed to load preferences for ${extensionName}:`, error)
101
104
  showToast({
@@ -103,13 +106,14 @@ export function ExtensionPreferences({
103
106
  title: 'Failed to load preferences',
104
107
  message: String(error),
105
108
  })
106
- return { preferences: [], savedValues: {} }
109
+ return { preferences: [], savedValues: {}, extensionTitle: extensionName }
107
110
  }
108
111
  },
109
112
  })
110
113
 
111
114
  const preferences = data?.preferences ?? []
112
115
  const savedValues = data?.savedValues ?? {}
116
+ const extensionTitle = data?.extensionTitle ?? extensionName
113
117
 
114
118
  const handleSubmit = async (values: Record<string, any>) => {
115
119
  try {
@@ -141,8 +145,8 @@ export function ExtensionPreferences({
141
145
  style: Toast.Style.Success,
142
146
  title: 'Preferences saved',
143
147
  message: commandName
144
- ? `Preferences for ${extensionName}/${commandName} have been saved`
145
- : `Preferences for ${extensionName} have been saved`,
148
+ ? `Preferences for ${extensionTitle}/${commandName} have been saved`
149
+ : `Preferences for ${extensionTitle} have been saved`,
146
150
  })
147
151
 
148
152
  if (onSubmit) {
@@ -181,8 +185,8 @@ export function ExtensionPreferences({
181
185
  <Form.Description
182
186
  text={
183
187
  commandName
184
- ? `No preferences available for ${extensionName}/${commandName}`
185
- : `No preferences available for ${extensionName}`
188
+ ? `No preferences available for ${extensionTitle}/${commandName}`
189
+ : `No preferences available for ${extensionTitle}`
186
190
  }
187
191
  />
188
192
  </Form>
@@ -198,8 +202,8 @@ export function ExtensionPreferences({
198
202
  }
199
203
  navigationTitle={
200
204
  commandName
201
- ? `${extensionName}/${commandName} Preferences`
202
- : `${extensionName} Preferences`
205
+ ? `${extensionTitle}/${commandName} Preferences`
206
+ : `${extensionTitle} Preferences`
203
207
  }
204
208
  >
205
209
  {preferences.map((pref) => {
@@ -0,0 +1,241 @@
1
+ import React, { ReactNode, useState, useEffect } from 'react'
2
+ import { TextAttributes } from '@opentui/core'
3
+ import { useTerminalDimensions, useKeyboard } from '@opentui/react'
4
+ import { colord } from 'colord'
5
+ import { Theme } from 'termcast/src/theme'
6
+ import { openInBrowser } from 'termcast/src/action-utils'
7
+ import {
8
+ useStore,
9
+ toastPrimaryActionKey,
10
+ toastSecondaryActionKey,
11
+ ToastData,
12
+ } from 'termcast/src/state'
13
+ import { useIsInFocus } from 'termcast/src/internal/focus-context'
14
+
15
+ /** Returns white or black foreground color based on background lightness */
16
+ function getFgForBg(bgColor: string): string {
17
+ return colord(bgColor).isLight() ? '#000000' : '#ffffff'
18
+ }
19
+
20
+ interface FooterProps {
21
+ children?: ReactNode
22
+ paddingLeft?: number
23
+ paddingRight?: number
24
+ paddingTop?: number
25
+ paddingBottom?: number
26
+ marginTop?: number
27
+ hidePoweredBy?: boolean
28
+ }
29
+
30
+ const MIN_WIDTH_FOR_POWERED_BY = 75
31
+
32
+ function ToastInline({ toast }: { toast: ToastData }): any {
33
+ const inFocus = useIsInFocus()
34
+ const [animationFrame, setAnimationFrame] = useState(0)
35
+
36
+ // Keyboard handling for toast actions
37
+ useKeyboard((evt) => {
38
+ if (!inFocus) return
39
+
40
+ if (evt.name === 'escape') {
41
+ toast.onHide()
42
+ } else if (
43
+ toast.primaryAction &&
44
+ evt.ctrl &&
45
+ evt.name === toastPrimaryActionKey.name
46
+ ) {
47
+ toast.primaryAction.onAction()
48
+ } else if (
49
+ toast.secondaryAction &&
50
+ evt.ctrl &&
51
+ evt.name === toastSecondaryActionKey.name
52
+ ) {
53
+ toast.secondaryAction.onAction()
54
+ }
55
+ })
56
+
57
+ // Animation for animated toasts
58
+ useEffect(() => {
59
+ if (toast.style === 'ANIMATED') {
60
+ const interval = setInterval(() => {
61
+ setAnimationFrame((prev) => (prev + 1) % 8)
62
+ }, 100)
63
+ return () => clearInterval(interval)
64
+ }
65
+ }, [toast.style])
66
+
67
+ // Auto-dismiss for non-animated toasts
68
+ useEffect(() => {
69
+ if (toast.style !== 'ANIMATED') {
70
+ const duration = toast.style === 'FAILURE' ? 8000 : 5000
71
+ const timer = setTimeout(() => {
72
+ toast.onHide()
73
+ }, duration)
74
+ return () => clearTimeout(timer)
75
+ }
76
+ }, [toast.style, toast.onHide])
77
+
78
+ const getIcon = () => {
79
+ switch (toast.style) {
80
+ case 'SUCCESS':
81
+ return '✓'
82
+ case 'FAILURE':
83
+ return '✗'
84
+ case 'ANIMATED':
85
+ return '⣾⣽⣻⢿⡿⣟⣯⣷'[animationFrame]
86
+ default:
87
+ return '✓'
88
+ }
89
+ }
90
+
91
+ const getIconColor = () => {
92
+ switch (toast.style) {
93
+ case 'SUCCESS':
94
+ return Theme.success
95
+ case 'FAILURE':
96
+ return Theme.error
97
+ case 'ANIMATED':
98
+ return Theme.primary
99
+ default:
100
+ return Theme.success
101
+ }
102
+ }
103
+
104
+ const primaryBg = Theme.primary
105
+ const primaryFg = getFgForBg(primaryBg)
106
+ const keysBg = colord(primaryBg).darken(0.06).toHex()
107
+
108
+ const hasKeys = !!toast.primaryAction?.title || !!toast.secondaryAction?.title
109
+ return (
110
+ <box
111
+ flexDirection='row'
112
+ marginLeft={-3}
113
+ marginRight={-3}
114
+ flexGrow={1}
115
+ overflow='hidden'
116
+ >
117
+ {/* Title box */}
118
+ <box
119
+ flexDirection='row'
120
+ flexShrink={0}
121
+ backgroundColor={colord(primaryBg).lighten(0.1).toHex()}
122
+ paddingLeft={3}
123
+ paddingRight={1}
124
+ >
125
+ <text flexShrink={0} fg={getIconColor()}>
126
+ {getIcon()}{' '}
127
+ </text>
128
+ <text flexShrink={0} fg={primaryFg} attributes={TextAttributes.BOLD}>
129
+ {toast.title}
130
+ </text>
131
+ </box>
132
+ {/* Message/description box (in the middle with keys background) */}
133
+ <box
134
+ flexGrow={1}
135
+ backgroundColor={keysBg}
136
+ paddingLeft={1}
137
+ paddingRight={1}
138
+ flexDirection='row'
139
+ overflow='hidden'
140
+ >
141
+ <text fg={primaryFg} wrapMode='none'>
142
+ {toast.message || ''}
143
+ </text>
144
+ </box>
145
+ {/* Keys box (right aligned, no grow) */}
146
+
147
+ <box
148
+ backgroundColor={keysBg}
149
+ paddingLeft={1}
150
+ paddingRight={3}
151
+ gap={1}
152
+ flexDirection='row'
153
+ flexShrink={0}
154
+ >
155
+ {toast.primaryAction?.title && (
156
+ <box
157
+ flexShrink={0}
158
+ flexDirection='row'
159
+ onMouseDown={() => {
160
+ toast.primaryAction?.onAction()
161
+ }}
162
+ >
163
+ <text fg={primaryFg} attributes={TextAttributes.BOLD}>
164
+ {toast.primaryAction.title}
165
+ </text>
166
+ <text fg={primaryFg}> ctrl t</text>
167
+ </box>
168
+ )}
169
+ {toast.secondaryAction?.title && (
170
+ <box
171
+ flexShrink={0}
172
+ flexDirection='row'
173
+ onMouseDown={() => {
174
+ toast.secondaryAction?.onAction()
175
+ }}
176
+ >
177
+ <text fg={primaryFg} attributes={TextAttributes.BOLD}>
178
+ {toast.secondaryAction.title}
179
+ </text>
180
+ <text fg={primaryFg}> ctrl g</text>
181
+ </box>
182
+ )}
183
+ </box>
184
+ </box>
185
+ )
186
+ }
187
+
188
+ export function Footer({
189
+ children,
190
+ paddingLeft = 1,
191
+ paddingRight = 1,
192
+ paddingTop = 1,
193
+ paddingBottom,
194
+ marginTop = 1,
195
+ hidePoweredBy = false,
196
+ }: FooterProps): any {
197
+ const { width } = useTerminalDimensions()
198
+ const showPoweredBy = !hidePoweredBy && width >= MIN_WIDTH_FOR_POWERED_BY
199
+ const toast = useStore((state) => state.toast)
200
+
201
+ return (
202
+ <box
203
+ border={false}
204
+ style={{
205
+ paddingLeft,
206
+ paddingRight,
207
+ paddingTop,
208
+ paddingBottom,
209
+ marginTop,
210
+ flexShrink: 0,
211
+ flexDirection: 'row',
212
+ justifyContent: 'space-between',
213
+ }}
214
+ >
215
+ {toast ? (
216
+ <ToastInline toast={toast} />
217
+ ) : (
218
+ <>
219
+ {children}
220
+ {showPoweredBy && (
221
+ <box flexDirection='row' gap={1}>
222
+ <text flexShrink={0} fg={Theme.textMuted}>
223
+ powered by
224
+ </text>
225
+ <text
226
+ flexShrink={0}
227
+ onMouseDown={() => {
228
+ openInBrowser('https://termcast.app')
229
+ }}
230
+ fg={Theme.textMuted}
231
+ attributes={TextAttributes.BOLD}
232
+ >
233
+ termcast
234
+ </text>
235
+ </box>
236
+ )}
237
+ </>
238
+ )}
239
+ </box>
240
+ )
241
+ }