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
@@ -6,8 +6,8 @@ import React, {
6
6
  useMemo,
7
7
  useLayoutEffect,
8
8
  } from 'react'
9
- import { useKeyboard } from '@opentui/react'
10
- import { Theme } from 'termcast/src/theme'
9
+ import { createPortal, useRenderer } from '@opentui/react'
10
+ import { useTheme } from 'termcast/src/theme'
11
11
  import {
12
12
  copyToClipboard,
13
13
  openInBrowser,
@@ -22,15 +22,15 @@ import { Dropdown } from 'termcast/src/components/dropdown'
22
22
  import { ExtensionPreferences } from 'termcast/src/components/extension-preferences'
23
23
  import { ThemePicker } from 'termcast/src/components/theme-picker'
24
24
  import { useStore } from 'termcast/src/state'
25
- import { useIsInFocus } from 'termcast/src/internal/focus-context'
26
- import { useIsOffscreen } from 'termcast/src/internal/offscreen'
25
+ import { InFocus } from 'termcast/src/internal/focus-context'
26
+ import { Onscreen, useIsOffscreen } from 'termcast/src/internal/offscreen'
27
27
  import { CommonProps } from 'termcast/src/utils'
28
28
  import type {
29
- KeyboardShortcut,
30
29
  KeyboardKeyEquivalent,
31
30
  KeyboardKeyModifier,
32
31
  } from 'termcast/src/keyboard'
33
32
  import { showToast, Toast } from 'termcast/src/apis/toast'
33
+ import { Clipboard } from 'termcast/src/apis/clipboard'
34
34
  import { createDescendants } from 'termcast/src/descendants'
35
35
  import { useFormSubmit } from 'termcast/src/components/form/index'
36
36
  import { logger } from '../logger'
@@ -155,10 +155,13 @@ interface PickDateProps extends Omit<ActionProps, 'onAction'> {
155
155
  onPick?: (date: Date | null) => void
156
156
  }
157
157
 
158
- // Create descendants for Actions - minimal fields needed
158
+ // Create descendants for Actions - includes all data needed for display
159
159
  interface ActionDescendant {
160
160
  title: string
161
+ icon?: string | null
161
162
  shortcut?: { modifiers?: KeyboardKeyModifier[]; key: KeyboardKeyEquivalent } | null
163
+ style?: ActionStyle
164
+ sectionTitle?: string
162
165
  execute: () => void
163
166
  }
164
167
 
@@ -168,7 +171,7 @@ const {
168
171
  useDescendant: useActionDescendant,
169
172
  } = createDescendants<ActionDescendant>()
170
173
 
171
- // Context for ActionPanel
174
+ // Context for ActionPanel - provides section info to child actions
172
175
  interface ActionPanelContextValue {
173
176
  currentSection?: string
174
177
  }
@@ -176,10 +179,16 @@ interface ActionPanelContextValue {
176
179
  const ActionPanelContext = createContext<ActionPanelContextValue>({})
177
180
 
178
181
  const Action: ActionType = (props) => {
179
- // Register as descendant with execute function
182
+ const theme = useTheme()
183
+ const { currentSection } = useContext(ActionPanelContext)
184
+
185
+ // Register as descendant with execute function - captures all data including section
180
186
  useActionDescendant({
181
- title: props.title,
187
+ title: props.title || 'View',
188
+ icon: props.icon,
182
189
  shortcut: props.shortcut,
190
+ style: props.style,
191
+ sectionTitle: currentSection,
183
192
  execute: () => props.onAction?.(),
184
193
  })
185
194
 
@@ -192,7 +201,7 @@ const Action: ActionType = (props) => {
192
201
  value={props.title}
193
202
  icon={props.icon}
194
203
  label={formatShortcut(props.shortcut)}
195
- color={isDestructive ? Theme.error : undefined}
204
+ color={isDestructive ? theme.error : undefined}
196
205
  />
197
206
  )
198
207
  }
@@ -200,17 +209,20 @@ const Action: ActionType = (props) => {
200
209
  Action.Style = ActionStyle
201
210
 
202
211
  Action.Push = (props) => {
203
- const dialog = useDialog()
212
+ const { push } = useNavigation()
213
+ const { currentSection } = useContext(ActionPanelContext)
204
214
 
205
215
  // Register as descendant with execute function
206
216
  useActionDescendant({
207
- title: props.title,
217
+ title: props.title || 'Navigate',
218
+ icon: props.icon,
208
219
  shortcut: props.shortcut,
220
+ sectionTitle: currentSection,
209
221
  execute: () => {
210
222
  props.onPush?.()
211
- // Push the target to dialog if needed
223
+ // Push the target to navigation stack
212
224
  if (props.target) {
213
- dialog.push({ element: props.target, position: 'center' })
225
+ push(props.target)
214
226
  }
215
227
  },
216
228
  })
@@ -227,10 +239,14 @@ Action.Push = (props) => {
227
239
  }
228
240
 
229
241
  Action.CopyToClipboard = (props) => {
242
+ const { currentSection } = useContext(ActionPanelContext)
243
+
230
244
  // Register as descendant with execute function
231
245
  useActionDescendant({
232
- title: props.title,
246
+ title: props.title || 'Copy to clipboard',
247
+ icon: props.icon,
233
248
  shortcut: props.shortcut,
249
+ sectionTitle: currentSection,
234
250
  execute: () => {
235
251
  copyToClipboard(props.content, props.concealed)
236
252
  props.onCopy?.(props.content)
@@ -260,10 +276,14 @@ Action.CopyToClipboard = (props) => {
260
276
  }
261
277
 
262
278
  Action.OpenInBrowser = (props) => {
279
+ const { currentSection } = useContext(ActionPanelContext)
280
+
263
281
  // Register as descendant with execute function
264
282
  useActionDescendant({
265
- title: props.title,
283
+ title: props.title || 'Open in Browser',
284
+ icon: props.icon,
266
285
  shortcut: props.shortcut,
286
+ sectionTitle: currentSection,
267
287
  execute: () => {
268
288
  openInBrowser(props.url)
269
289
  props.onOpen?.(props.url)
@@ -278,8 +298,7 @@ Action.OpenInBrowser = (props) => {
278
298
  // Render as Dropdown.Item (handles offscreen check internally)
279
299
  return (
280
300
  <Dropdown.Item
281
- title={props.title}
282
- value={props.title}
301
+ title={props.title || 'Open in Browser'}
283
302
  icon={props.icon}
284
303
  label={formatShortcut(props.shortcut)}
285
304
  />
@@ -287,10 +306,14 @@ Action.OpenInBrowser = (props) => {
287
306
  }
288
307
 
289
308
  Action.Open = (props) => {
309
+ const { currentSection } = useContext(ActionPanelContext)
310
+
290
311
  // Register as descendant with execute function
291
312
  useActionDescendant({
292
- title: props.title,
313
+ title: props.title || 'Open',
314
+ icon: props.icon,
293
315
  shortcut: props.shortcut,
316
+ sectionTitle: currentSection,
294
317
  execute: () => {
295
318
  openFile(props.target, props.application)
296
319
  props.onOpen?.(props.target)
@@ -315,10 +338,14 @@ Action.Open = (props) => {
315
338
  }
316
339
 
317
340
  Action.Paste = (props) => {
341
+ const { currentSection } = useContext(ActionPanelContext)
342
+
318
343
  // Register as descendant with execute function
319
344
  useActionDescendant({
320
- title: props.title,
345
+ title: props.title || 'Paste',
346
+ icon: props.icon,
321
347
  shortcut: props.shortcut,
348
+ sectionTitle: currentSection,
322
349
  execute: () => {
323
350
  pasteContent(props.content)
324
351
  props.onPaste?.(props.content)
@@ -345,10 +372,14 @@ Action.Paste = (props) => {
345
372
  }
346
373
 
347
374
  Action.ShowInFinder = (props) => {
375
+ const { currentSection } = useContext(ActionPanelContext)
376
+
348
377
  // Register as descendant with execute function
349
378
  useActionDescendant({
350
379
  title: props.title || 'Show in Finder',
380
+ icon: props.icon,
351
381
  shortcut: props.shortcut,
382
+ sectionTitle: currentSection,
352
383
  execute: () => {
353
384
  showInFinder(props.path)
354
385
  props.onShow?.(props.path)
@@ -372,10 +403,14 @@ Action.ShowInFinder = (props) => {
372
403
  }
373
404
 
374
405
  Action.OpenWith = (props) => {
406
+ const { currentSection } = useContext(ActionPanelContext)
407
+
375
408
  // Register as descendant with execute function
376
409
  useActionDescendant({
377
410
  title: props.title || `Open with ${props.application}`,
411
+ icon: props.icon,
378
412
  shortcut: props.shortcut,
413
+ sectionTitle: currentSection,
379
414
  execute: () => {
380
415
  openFile(props.path, props.application)
381
416
  props.onOpen?.(props.path)
@@ -400,11 +435,15 @@ Action.OpenWith = (props) => {
400
435
 
401
436
  Action.Trash = (props) => {
402
437
  const paths = Array.isArray(props.paths) ? props.paths : [props.paths]
438
+ const { currentSection } = useContext(ActionPanelContext)
403
439
 
404
440
  // Register as descendant with execute function
405
441
  useActionDescendant({
406
442
  title: props.title || 'Move to Trash',
443
+ icon: props.icon,
407
444
  shortcut: props.shortcut,
445
+ style: ActionStyle.Destructive,
446
+ sectionTitle: currentSection,
408
447
  execute: async () => {
409
448
  for (const path of paths) {
410
449
  await moveToTrash(path)
@@ -431,6 +470,7 @@ Action.Trash = (props) => {
431
470
 
432
471
  Action.SubmitForm = (props) => {
433
472
  const dialog = useDialog()
473
+ const { currentSection } = useContext(ActionPanelContext)
434
474
 
435
475
  // Get form context - will be null if not in a form
436
476
  const formContext = useFormSubmit()
@@ -438,7 +478,9 @@ Action.SubmitForm = (props) => {
438
478
  // Register as descendant with execute function
439
479
  useActionDescendant({
440
480
  title: props.title || 'Submit',
481
+ icon: props.icon,
441
482
  shortcut: props.shortcut || { modifiers: ['cmd'], key: 'return' },
483
+ sectionTitle: currentSection,
442
484
  execute: () => {
443
485
  if (formContext) {
444
486
  // Also call the onSubmit if provided
@@ -467,10 +509,14 @@ Action.SubmitForm = (props) => {
467
509
  }
468
510
 
469
511
  Action.CreateSnippet = (props) => {
512
+ const { currentSection } = useContext(ActionPanelContext)
513
+
470
514
  // Register as descendant with execute function
471
515
  useActionDescendant({
472
516
  title: props.title || 'Create Snippet',
517
+ icon: props.icon,
473
518
  shortcut: props.shortcut,
519
+ sectionTitle: currentSection,
474
520
  execute: () => {
475
521
  // TODO: Navigate to Create Snippet command when extension system is implemented
476
522
  logger.log(
@@ -497,10 +543,14 @@ Action.CreateSnippet = (props) => {
497
543
  }
498
544
 
499
545
  Action.CreateQuicklink = (props) => {
546
+ const { currentSection } = useContext(ActionPanelContext)
547
+
500
548
  // Register as descendant with execute function
501
549
  useActionDescendant({
502
550
  title: props.title || 'Create Quicklink',
551
+ icon: props.icon,
503
552
  shortcut: props.shortcut,
553
+ sectionTitle: currentSection,
504
554
  execute: () => {
505
555
  // TODO: Navigate to Create Quicklink command when extension system is implemented
506
556
  logger.log(
@@ -527,10 +577,14 @@ Action.CreateQuicklink = (props) => {
527
577
  }
528
578
 
529
579
  Action.ToggleQuickLook = (props) => {
580
+ const { currentSection } = useContext(ActionPanelContext)
581
+
530
582
  // Register as descendant with execute function
531
583
  useActionDescendant({
532
584
  title: props.title || 'Quick Look',
585
+ icon: props.icon,
533
586
  shortcut: props.shortcut || { key: 'space' },
587
+ sectionTitle: currentSection,
534
588
  execute: async () => {
535
589
  if (!props.path) {
536
590
  props.onToggle?.()
@@ -569,10 +623,14 @@ Action.ToggleQuickLook = (props) => {
569
623
  }
570
624
 
571
625
  Action.PickDate = (props) => {
626
+ const { currentSection } = useContext(ActionPanelContext)
627
+
572
628
  // Register as descendant with execute function
573
629
  useActionDescendant({
574
630
  title: props.title || 'Pick Date',
631
+ icon: props.icon,
575
632
  shortcut: props.shortcut,
633
+ sectionTitle: currentSection,
576
634
  execute: () => {
577
635
  // TODO: Show date picker dialog when implemented
578
636
  logger.log(`Picking ${props.type || 'date'}`)
@@ -643,13 +701,32 @@ function formatShortcut(
643
701
  .join('')
644
702
  }
645
703
 
704
+ /**
705
+ * ActionPanel uses React portals to render its dialog content in the overlay
706
+ * area while staying in the original React tree. This preserves all React
707
+ * context (FormSubmitContext, NavigationContext, etc.) because portals inherit
708
+ * context from the source tree, not the portal target.
709
+ *
710
+ * Flow:
711
+ * 1. ActionPanel is rendered offscreen via <Offscreen> in List/Detail/Form
712
+ * 2. Action children register as descendants (title, icon, shortcut, execute)
713
+ * 3. A useLayoutEffect captures the first action title for footer display
714
+ * 4. When showActionsDialog is true, ActionPanel renders a Dropdown via
715
+ * createPortal into the overlay target (provided by DialogOverlay)
716
+ * 5. The Dropdown and all callbacks have fresh context from the original tree
717
+ */
646
718
  const ActionPanel: ActionPanelType = (props) => {
647
719
  const { children, title } = props
648
720
  const dialog = useDialog()
649
721
  const { push } = useNavigation()
650
- const inFocus = useIsInFocus()
651
- const isOffscreen = useIsOffscreen()
652
722
  const descendantsContext = useActionDescendants()
723
+ const renderer = useRenderer()
724
+ const isOffscreen = useIsOffscreen()
725
+
726
+ const showActionsDialog = useStore((state) => state.showActionsDialog)
727
+ const portalTarget = useStore((state) => state.actionsPortalTarget)
728
+ // Subscribe so a re-render + layout effect fires when Enter sets this flag
729
+ const shouldAutoExecute = useStore((state) => state.shouldAutoExecuteFirstAction)
653
730
 
654
731
  // Get extension and command info for configure actions
655
732
  const extensionPackageJson = useStore((state) => state.extensionPackageJson)
@@ -668,96 +745,150 @@ const ActionPanel: ActionPanelType = (props) => {
668
745
  [],
669
746
  )
670
747
 
671
- // Auto-execute first action if flag is set (triggered by enter/ctrl+enter)
672
- // Also report first action title when rendered offscreen
748
+ // Capture first action title for footer display, and handle auto-execute.
749
+ // Runs after every render so descendant props are always fresh.
673
750
  useLayoutEffect(() => {
674
751
  const allActions = Object.values(descendantsContext.map.current)
675
752
  .filter((item: any) => item.index !== -1)
676
- .map((item: any) => item.props as ActionDescendant)
753
+ .sort((a: any, b: any) => a.index - b.index)
677
754
 
678
- // When offscreen, just report first action title for footer display
679
- if (isOffscreen) {
680
- useStore.setState({ firstActionTitle: allActions[0]?.title ?? '' })
681
- return
755
+ const firstActionTitle = allActions[0]?.props?.title ?? ''
756
+ if (useStore.getState().firstActionTitle !== firstActionTitle) {
757
+ useStore.setState({ firstActionTitle })
682
758
  }
683
759
 
684
- const shouldExecute = useStore.getState().shouldAutoExecuteFirstAction
685
- useStore.setState({ shouldAutoExecuteFirstAction: false })
760
+ // Auto-execute first action when Enter was pressed (shouldAutoExecuteFirstAction flag)
761
+ if (shouldAutoExecute) {
762
+ useStore.setState({ shouldAutoExecuteFirstAction: false, showActionsDialog: false })
763
+ const firstAction = allActions[0]?.props as ActionDescendant | undefined
764
+ if (firstAction) {
765
+ logger.log(`Auto-executing first action: ${firstAction.title}`)
766
+ firstAction.execute()
767
+ }
768
+ }
769
+ })
686
770
 
687
- if (!shouldExecute) return
771
+ // Keep offscreen registration lightweight for fast initial command startup.
772
+ // We only need descendants for footer title + auto-execute; rendering the full
773
+ // Dropdown tree offscreen adds avoidable mount work before first paint.
774
+ const offscreenRegistrationTree = (
775
+ <ActionDescendantsProvider value={descendantsContext}>
776
+ <ActionPanelContext.Provider value={contextValue}>
777
+ {children}
778
+ </ActionPanelContext.Provider>
779
+ </ActionDescendantsProvider>
780
+ )
688
781
 
689
- if (allActions[0]) {
690
- logger.log(`Auto-executing first action: ${allActions[0].title}`)
691
- dialog.clear()
692
- allActions[0].execute()
693
- }
694
- }, [descendantsContext.map, dialog, isOffscreen])
782
+ // Always render the full Dropdown tree when actions are visible so Action
783
+ // children stay mounted and descendants are always registered.
784
+ // descendants are always registered. The Dropdown handles offscreen mode
785
+ // internally (returns null for visual output when isOffscreen is true).
786
+ // When showActionsDialog is true, we portal into the DialogOverlay's content
787
+ // target so the dialog shell and z-order are managed in one place.
788
+ const dropdownTree = (
789
+ <Dropdown
790
+ tooltip={title || 'Actions'}
791
+ placeholder='Search actions...'
792
+ filtering
793
+ onChange={(value) => {
794
+ logger.log(`actions dropdown onChange`, value)
795
+ // Find and execute the selected action from live descendants
796
+ const allActions = Object.values(descendantsContext.map.current)
797
+ .filter((item: any) => item.index !== -1)
798
+ .map((item: any) => item.props as ActionDescendant)
799
+
800
+ const action = allActions.find((a) => a.title === value)
801
+ if (action) {
802
+ useStore.setState({ showActionsDialog: false, dialogStack: [] })
803
+ action.execute()
804
+ }
805
+ }}
806
+ >
807
+ {children}
808
+ <ActionPanel.Section title="Settings">
809
+ {hasExtensionPrefs && (
810
+ <Action
811
+ title={`Configure ${extensionPackageJson!.title}...`}
812
+ shortcut={{ modifiers: ['cmd', 'shift'], key: ',' }}
813
+ onAction={() => {
814
+ useStore.setState({ showActionsDialog: false })
815
+ push(
816
+ <ExtensionPreferences
817
+ extensionName={extensionPackageJson!.name}
818
+ />,
819
+ )
820
+ }}
821
+ />
822
+ )}
823
+ {hasCommandPrefs && (
824
+ <Action
825
+ title="Configure Command..."
826
+ onAction={() => {
827
+ useStore.setState({ showActionsDialog: false })
828
+ push(
829
+ <ExtensionPreferences
830
+ extensionName={extensionPackageJson!.name}
831
+ commandName={currentCommandName!}
832
+ />,
833
+ )
834
+ }}
835
+ />
836
+ )}
837
+ <Action
838
+ title="Change Theme..."
839
+ onAction={() => {
840
+ useStore.setState({ showActionsDialog: false })
841
+ dialog.push({ element: <ThemePicker /> })
842
+ }}
843
+ />
844
+ <Action
845
+ title="See Console Logs"
846
+ onAction={() => {
847
+ useStore.setState({ showActionsDialog: false })
848
+ if (renderer) {
849
+ renderer.console.onCopySelection = (text: any) => {
850
+ Clipboard.copy(text)
851
+ }
852
+ renderer.toggleDebugOverlay()
853
+ renderer.console.toggle()
854
+ }
855
+ }}
856
+ />
857
+ </ActionPanel.Section>
858
+ </Dropdown>
859
+ )
695
860
 
696
- // prevent showing actions if we're not inside an actions dialog (must be after hooks)
697
- const lastStackItem = dialog.stack[dialog.stack.length - 1]
698
- if (lastStackItem?.type !== 'actions' && !isOffscreen) return null
861
+ // When dialog is active and portal target exists, render the Dropdown in
862
+ // a portal. The portal inherits React context from the source tree, which
863
+ // preserves FormSubmitContext, custom contexts, etc.
864
+ // Onscreen resets the OffscreenContext (since we're portaled from an offscreen
865
+ // tree into a visible overlay), so the Dropdown renders its items normally.
866
+ if (showActionsDialog && portalTarget) {
867
+ return createPortal(
868
+ <Onscreen>
869
+ <InFocus inFocus={true}>
870
+ <ActionDescendantsProvider value={descendantsContext}>
871
+ <ActionPanelContext.Provider value={contextValue}>
872
+ {dropdownTree}
873
+ </ActionPanelContext.Provider>
874
+ </ActionDescendantsProvider>
875
+ </InFocus>
876
+ </Onscreen>,
877
+ portalTarget,
878
+ null,
879
+ )
880
+ }
881
+
882
+ if (isOffscreen) {
883
+ return offscreenRegistrationTree
884
+ }
699
885
 
700
- // ActionPanel renders as Dropdown with children
886
+ // When not showing dialog, render the tree inline (offscreen) so descendants
887
+ // register and first action title is captured for the footer.
701
888
  return (
702
889
  <ActionDescendantsProvider value={descendantsContext}>
703
890
  <ActionPanelContext.Provider value={contextValue}>
704
- <Dropdown
705
- tooltip={title || 'Actions'}
706
- placeholder='Search actions...'
707
- filtering
708
- onChange={(value) => {
709
- logger.log(`actions dropdown onChange`, value)
710
- // Find and execute the selected action
711
- const allActions = Object.values(descendantsContext.map.current)
712
- .filter((item: any) => item.index !== -1)
713
- .map((item: any) => item.props as ActionDescendant)
714
-
715
- const action = allActions.find((a) => a.title === value)
716
- if (action) {
717
- dialog.clear()
718
- action.execute()
719
- }
720
- }}
721
- >
722
- {children}
723
- <ActionPanel.Section title="Settings">
724
- {hasExtensionPrefs && (
725
- <Action
726
- title={`Configure ${extensionPackageJson!.title}...`}
727
- shortcut={{ modifiers: ['cmd', 'shift'], key: ',' }}
728
- onAction={() => {
729
- dialog.clear()
730
- push(
731
- <ExtensionPreferences
732
- extensionName={extensionPackageJson!.name}
733
- />,
734
- )
735
- }}
736
- />
737
- )}
738
- {hasCommandPrefs && (
739
- <Action
740
- title="Configure Command..."
741
- onAction={() => {
742
- dialog.clear()
743
- push(
744
- <ExtensionPreferences
745
- extensionName={extensionPackageJson!.name}
746
- commandName={currentCommandName!}
747
- />,
748
- )
749
- }}
750
- />
751
- )}
752
- <Action
753
- title="Change Theme..."
754
- onAction={() => {
755
- dialog.clear()
756
- dialog.push({ element: <ThemePicker /> })
757
- }}
758
- />
759
- </ActionPanel.Section>
760
- </Dropdown>
891
+ {dropdownTree}
761
892
  </ActionPanelContext.Provider>
762
893
  </ActionDescendantsProvider>
763
894
  )
@@ -1,6 +1,6 @@
1
1
  import React, { useState } from 'react'
2
2
  import { useKeyboard } from '@opentui/react'
3
- import { Theme } from 'termcast/src/theme'
3
+ import { useTheme } from 'termcast/src/theme'
4
4
  import { TextAttributes } from '@opentui/core'
5
5
  import { LocalStorage } from 'termcast/src/apis/localstorage'
6
6
  import { useStore } from 'termcast/src/state'
@@ -44,6 +44,7 @@ function AlertComponent({
44
44
  onConfirm,
45
45
  onDismiss,
46
46
  }: AlertComponentProps): any {
47
+ const theme = useTheme()
47
48
  const [rememberChoice, setRememberChoice] = useState(false)
48
49
  const inFocus = useIsInFocus()
49
50
 
@@ -73,18 +74,18 @@ function AlertComponent({
73
74
  const getPrimaryColor = () => {
74
75
  switch (primaryStyle) {
75
76
  case Alert.ActionStyle.Destructive:
76
- return Theme.error
77
+ return theme.error
77
78
  case Alert.ActionStyle.Default:
78
79
  default:
79
- return Theme.primary
80
+ return theme.primary
80
81
  }
81
82
  }
82
83
 
83
84
  return (
84
85
  <box
85
86
  border
86
- borderColor={Theme.border}
87
- backgroundColor={Theme.backgroundPanel}
87
+ borderColor={theme.border}
88
+ backgroundColor={theme.backgroundPanel}
88
89
  paddingTop={2}
89
90
  paddingBottom={2}
90
91
  paddingLeft={3}
@@ -94,17 +95,17 @@ function AlertComponent({
94
95
  <box flexDirection='column' alignItems='center'>
95
96
  {options.icon && (
96
97
  <box marginBottom={1}>
97
- <text fg={Theme.accent}>⚠️</text>
98
+ <text fg={theme.accent}>⚠️</text>
98
99
  </box>
99
100
  )}
100
101
 
101
- <text fg={Theme.text} attributes={TextAttributes.BOLD}>
102
+ <text fg={theme.text} attributes={TextAttributes.BOLD}>
102
103
  {options.title}
103
104
  </text>
104
105
 
105
106
  {options.message && (
106
107
  <box marginTop={1} marginBottom={1}>
107
- <text fg={Theme.textMuted}>{options.message}</text>
108
+ <text fg={theme.textMuted}>{options.message}</text>
108
109
  </box>
109
110
  )}
110
111
 
@@ -114,7 +115,7 @@ function AlertComponent({
114
115
  marginBottom={1}
115
116
  onMouseDown={() => setRememberChoice(!rememberChoice)}
116
117
  >
117
- <text fg={Theme.textMuted} selectable={false}>
118
+ <text fg={theme.textMuted} selectable={false}>
118
119
  {rememberChoice ? '[x]' : '[ ]'} Do not show this message again
119
120
  (Space to toggle)
120
121
  </text>
@@ -149,7 +150,7 @@ function AlertComponent({
149
150
  onDismiss()
150
151
  }}
151
152
  >
152
- <text fg={Theme.textMuted} selectable={false}>
153
+ <text fg={theme.textMuted} selectable={false}>
153
154
  [{options.dismissAction?.title || 'Cancel'} (ESC)]
154
155
  </text>
155
156
  </box>
@@ -80,6 +80,6 @@ export function useAnimationTick(divisor: number = 1): number {
80
80
  // Waves share the same speed so they animate in sync
81
81
  export const TICK_DIVISORS = {
82
82
  LOADING_BAR: 2, // 40ms - wave animation
83
- LOADING_TEXT: 2, // 40ms - wave animation (same as bar)
83
+ LOADING_TEXT: 1, // 20ms - faster wave animation for text
84
84
  SPINNER: 10, // 200ms - pulses every 5 wave steps
85
85
  } as const