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
@@ -1,8 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "@opentui/react/jsx-runtime";
2
2
  import { TextAttributes, } from '@opentui/core';
3
3
  import { useKeyboard, flushSync } from '@opentui/react';
4
- import React, { createContext, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
4
+ import React, { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
5
5
  import { LoadingBar } from 'termcast/src/components/loading-bar';
6
+ import { LoadingText } from 'termcast/src/components/loading-text';
6
7
  import { Footer } from 'termcast/src/components/footer';
7
8
  import { createDescendants } from 'termcast/src/descendants';
8
9
  import { useStore } from 'termcast/src/state';
@@ -12,9 +13,8 @@ import { useNavigationPending } from 'termcast/src/internal/navigation';
12
13
  import { Offscreen } from 'termcast/src/internal/offscreen';
13
14
  import { ScrollBox } from 'termcast/src/internal/scrollbox';
14
15
  import { Color, resolveColor } from 'termcast/src/colors';
15
- import { getIconEmoji } from 'termcast/src/components/icon';
16
- import { ActionPanel } from 'termcast/src/components/actions';
17
- import { Theme, markdownSyntaxStyle } from 'termcast/src/theme';
16
+ import { getIconEmoji, getIconValue } from 'termcast/src/components/icon';
17
+ import { useTheme, markdownSyntaxStyle } from 'termcast/src/theme';
18
18
  export { Color };
19
19
  function formatRelativeDate(date) {
20
20
  const now = new Date();
@@ -47,13 +47,47 @@ function formatRelativeDate(date) {
47
47
  return `${diffYear}y`;
48
48
  }
49
49
  function ListFooter() {
50
+ const theme = useTheme();
50
51
  const firstActionTitle = useStore((s) => s.firstActionTitle);
51
52
  const hasToast = useStore((s) => s.toast !== null);
52
53
  const listContext = useContext(ListContext);
53
54
  const isShowingDetail = listContext?.isShowingDetail ?? false;
54
- const content = hasToast ? null : (_jsxs("box", { style: { flexDirection: 'row', gap: 3 }, children: [firstActionTitle && (_jsxs("box", { style: { flexDirection: 'row', gap: 1 }, children: [_jsx("text", { flexShrink: 0, fg: Theme.text, attributes: TextAttributes.BOLD, children: "\u21B5" }), _jsx("text", { flexShrink: 0, fg: Theme.textMuted, children: firstActionTitle.toLowerCase() })] })), _jsxs("box", { style: { flexDirection: 'row', gap: 1 }, children: [_jsx("text", { flexShrink: 0, fg: Theme.text, attributes: TextAttributes.BOLD, children: "\u2191\u2193" }), _jsx("text", { flexShrink: 0, fg: Theme.textMuted, children: "navigate" })] }), _jsxs("box", { style: { flexDirection: 'row', gap: 1 }, children: [_jsx("text", { flexShrink: 0, fg: Theme.text, attributes: TextAttributes.BOLD, children: "^k" }), _jsx("text", { flexShrink: 0, fg: Theme.textMuted, children: "actions" })] })] }));
55
+ const hasDropdown = listContext?.hasDropdown ?? false;
56
+ const content = hasToast ? null : (_jsxs("box", { style: { flexDirection: 'row', gap: 3 }, children: [firstActionTitle && (_jsxs("box", { style: { flexDirection: 'row', gap: 1 }, children: [_jsx("text", { flexShrink: 0, fg: theme.text, attributes: TextAttributes.BOLD, children: "\u21B5" }), _jsx("text", { flexShrink: 0, fg: theme.textMuted, children: firstActionTitle.toLowerCase() })] })), _jsxs("box", { style: { flexDirection: 'row', gap: 1 }, children: [_jsx("text", { flexShrink: 0, fg: theme.text, attributes: TextAttributes.BOLD, children: "\u2191\u2193" }), _jsx("text", { flexShrink: 0, fg: theme.textMuted, children: "navigate" })] }), hasDropdown && (_jsxs("box", { style: { flexDirection: 'row', gap: 1 }, children: [_jsx("text", { flexShrink: 0, fg: theme.text, attributes: TextAttributes.BOLD, children: "^p" }), _jsx("text", { flexShrink: 0, fg: theme.textMuted, children: "dropdown" })] })), _jsxs("box", { style: { flexDirection: 'row', gap: 1 }, children: [_jsx("text", { flexShrink: 0, fg: theme.text, attributes: TextAttributes.BOLD, children: "^k" }), _jsx("text", { flexShrink: 0, fg: theme.textMuted, children: "actions" })] })] }));
55
57
  return _jsx(Footer, { hidePoweredBy: isShowingDetail, children: content });
56
58
  }
59
+ /**
60
+ * Component that subscribes to descendants changes and renders current item's
61
+ * actions offscreen. This ensures actions are captured even when items register
62
+ * after the initial render (preserving context via closures).
63
+ */
64
+ function CurrentItemActionsOffscreen(props) {
65
+ // Subscribe to descendants changes - this hook triggers re-render when items register
66
+ const descendantsMap = useListDescendantsRerender();
67
+ // Get current item's actions
68
+ const items = Object.values(descendantsMap)
69
+ .filter((item) => item.index !== -1)
70
+ .sort((a, b) => a.index - b.index);
71
+ const currentItem = items.find((item) => item.index === props.selectedIndex);
72
+ const actions = currentItem?.props?.actions ?? props.fallbackActions ?? null;
73
+ // Clear first action title when there are no actions
74
+ useLayoutEffect(() => {
75
+ if (!actions) {
76
+ useStore.setState({ firstActionTitle: '' });
77
+ }
78
+ }, [actions]);
79
+ if (!actions)
80
+ return null;
81
+ return _jsx(Offscreen, { children: actions });
82
+ }
83
+ export var Image;
84
+ (function (Image) {
85
+ let ImageMask;
86
+ (function (ImageMask) {
87
+ ImageMask["Circle"] = "circle";
88
+ ImageMask["RoundedRectangle"] = "roundedRectangle";
89
+ })(ImageMask = Image.ImageMask || (Image.ImageMask = {}));
90
+ })(Image || (Image = {}));
57
91
  const ListContext = createContext(undefined);
58
92
  // Helper function to determine if an item should be visible based on search
59
93
  function shouldItemBeVisible(searchQuery, props) {
@@ -75,6 +109,7 @@ const { DescendantsProvider: ListDescendantsProvider, useDescendants: useListDes
75
109
  const { DescendantsProvider: DropdownDescendantsProvider, useDescendants: useDropdownDescendants, useDescendant: useDropdownItemDescendant, } = createDescendants();
76
110
  const DropdownContext = createContext(undefined);
77
111
  function ListDropdownDialog(props) {
112
+ const theme = useTheme();
78
113
  const [searchText, setSearchTextRaw] = useState('');
79
114
  const [selectedIndex, setSelectedIndex] = useState(0);
80
115
  const inputRef = useRef(null);
@@ -143,13 +178,13 @@ function ListDropdownDialog(props) {
143
178
  return (_jsx(DropdownDescendantsProvider, { value: descendantsContext, children: _jsxs("box", { children: [_jsxs("box", { style: { paddingLeft: 2, paddingRight: 2 }, children: [_jsxs("box", { style: { paddingLeft: 1, paddingRight: 1 }, children: [_jsxs("box", { style: {
144
179
  flexDirection: 'row',
145
180
  justifyContent: 'space-between',
146
- }, children: [_jsx("text", { flexShrink: 0, fg: Theme.textMuted, children: props.tooltip }), _jsx("text", { flexShrink: 0, fg: Theme.textMuted, children: "esc" })] }), _jsxs("box", { style: { paddingTop: 1, paddingBottom: 1, flexDirection: 'row' }, children: [_jsx("text", { flexShrink: 0, fg: Theme.textMuted, children: "> " }), _jsx("textarea", { ref: inputRef, height: 1, flexGrow: 1, wrapMode: 'none', keyBindings: [
181
+ }, children: [_jsx("text", { flexShrink: 0, fg: theme.textMuted, children: props.tooltip }), _jsx("text", { flexShrink: 0, fg: theme.textMuted, children: "esc" })] }), _jsxs("box", { style: { paddingTop: 1, paddingBottom: 1, flexDirection: 'row' }, children: [_jsx("text", { flexShrink: 0, fg: theme.textMuted, children: "> " }), _jsx("textarea", { ref: inputRef, height: 1, flexGrow: 1, wrapMode: 'none', keyBindings: [
147
182
  { name: 'return', action: 'submit' },
148
183
  { name: 'linefeed', action: 'submit' },
149
184
  ], onContentChange: () => {
150
185
  const value = inputRef.current?.plainText || '';
151
186
  setSearchText(value);
152
- }, placeholder: props.placeholder || 'Search...', focused: inFocus, initialValue: searchText, focusedBackgroundColor: Theme.backgroundPanel, cursorColor: Theme.primary, focusedTextColor: Theme.textMuted })] })] }), _jsx("box", { style: { paddingBottom: 1 }, children: _jsx(DropdownContext.Provider, { value: {
187
+ }, placeholder: props.placeholder || 'Search...', focused: inFocus, initialValue: searchText, focusedBackgroundColor: theme.backgroundPanel, cursorColor: theme.primary, focusedTextColor: theme.textMuted })] })] }), _jsx("box", { style: { paddingBottom: 1 }, children: _jsx(DropdownContext.Provider, { value: {
153
188
  currentSection: undefined,
154
189
  selectedIndex,
155
190
  setSelectedIndex,
@@ -159,15 +194,17 @@ function ListDropdownDialog(props) {
159
194
  onChange: (value) => {
160
195
  props.onChange?.(value);
161
196
  },
162
- }, children: props.children }) }), props.isLoading && (_jsx("box", { style: { paddingLeft: 1 }, children: _jsx("text", { flexShrink: 0, fg: Theme.textMuted, children: "Loading..." }) }))] }), _jsx(DropdownFooter, {})] }) }));
197
+ }, children: props.children }) }), props.isLoading && (_jsx("box", { style: { paddingLeft: 1 }, children: _jsx("text", { flexShrink: 0, fg: theme.textMuted, children: "Loading..." }) }))] }), _jsx(DropdownFooter, {})] }) }));
163
198
  }
164
199
  function DropdownFooter() {
200
+ const theme = useTheme();
165
201
  const hasToast = useStore((s) => s.toast !== null);
166
- const content = hasToast ? null : (_jsxs("box", { style: { flexDirection: 'row', gap: 3 }, children: [_jsxs("box", { style: { flexDirection: 'row', gap: 1 }, children: [_jsx("text", { flexShrink: 0, fg: Theme.text, attributes: TextAttributes.BOLD, children: "\u21B5" }), _jsx("text", { flexShrink: 0, fg: Theme.textMuted, children: "select" })] }), _jsxs("box", { style: { flexDirection: 'row', gap: 1 }, children: [_jsx("text", { flexShrink: 0, fg: Theme.text, attributes: TextAttributes.BOLD, children: "\u2191\u2193" }), _jsx("text", { flexShrink: 0, fg: Theme.textMuted, children: "navigate" })] })] }));
202
+ const content = hasToast ? null : (_jsxs("box", { style: { flexDirection: 'row', gap: 3 }, children: [_jsxs("box", { style: { flexDirection: 'row', gap: 1 }, children: [_jsx("text", { flexShrink: 0, fg: theme.text, attributes: TextAttributes.BOLD, children: "\u21B5" }), _jsx("text", { flexShrink: 0, fg: theme.textMuted, children: "select" })] }), _jsxs("box", { style: { flexDirection: 'row', gap: 1 }, children: [_jsx("text", { flexShrink: 0, fg: theme.text, attributes: TextAttributes.BOLD, children: "\u2191\u2193" }), _jsx("text", { flexShrink: 0, fg: theme.textMuted, children: "navigate" })] })] }));
167
203
  return (_jsx(Footer, { paddingLeft: 3, paddingRight: 2, paddingBottom: 1, marginTop: 0, children: content }));
168
204
  }
169
205
  // Render a single list item row
170
206
  function ListItemRow(props) {
207
+ const theme = useTheme();
171
208
  const { title, subtitle, icon, iconColor, accessories, active, ref } = props;
172
209
  const [isHovered, setIsHovered] = useState(false);
173
210
  const accessoryElements = [];
@@ -179,7 +216,7 @@ function ListItemRow(props) {
179
216
  : accessory.text?.value;
180
217
  const textColor = typeof accessory.text === 'object' ? accessory.text?.color : undefined;
181
218
  if (textValue) {
182
- accessoryElements.push(_jsx("text", { flexShrink: 0, fg: active ? Theme.background : resolveColor(textColor) || Theme.info, wrapMode: "none", children: textValue }, `text-${textValue}`));
219
+ accessoryElements.push(_jsx("text", { flexShrink: 0, fg: active ? theme.background : resolveColor(textColor) || theme.info, wrapMode: "none", children: textValue }, `text-${textValue}`));
183
220
  }
184
221
  }
185
222
  if ('tag' in accessory && accessory.tag) {
@@ -188,7 +225,7 @@ function ListItemRow(props) {
188
225
  : accessory.tag?.value;
189
226
  const tagColor = typeof accessory.tag === 'object' ? accessory.tag?.color : undefined;
190
227
  if (tagValue) {
191
- accessoryElements.push(_jsxs("text", { flexShrink: 0, fg: active ? Theme.background : resolveColor(tagColor) || Theme.warning, wrapMode: "none", children: ["[", tagValue, "]"] }, `tag-${tagValue}`));
228
+ accessoryElements.push(_jsxs("text", { flexShrink: 0, fg: active ? theme.background : resolveColor(tagColor) || theme.warning, wrapMode: "none", children: ["[", tagValue, "]"] }, `tag-${tagValue}`));
192
229
  }
193
230
  }
194
231
  if ('date' in accessory && accessory.date) {
@@ -200,7 +237,7 @@ function ListItemRow(props) {
200
237
  : undefined;
201
238
  if (dateValue) {
202
239
  const formatted = formatRelativeDate(dateValue);
203
- accessoryElements.push(_jsx("text", { flexShrink: 0, fg: active ? Theme.background : resolveColor(dateColor) || Theme.success, wrapMode: "none", children: formatted }, `date-${dateValue.getTime()}`));
240
+ accessoryElements.push(_jsx("text", { flexShrink: 0, fg: active ? theme.background : resolveColor(dateColor) || theme.success, wrapMode: "none", children: formatted }, `date-${dateValue.getTime()}`));
204
241
  }
205
242
  }
206
243
  });
@@ -209,9 +246,9 @@ function ListItemRow(props) {
209
246
  flexDirection: 'row',
210
247
  justifyContent: 'space-between',
211
248
  backgroundColor: active
212
- ? Theme.primary
249
+ ? theme.primary
213
250
  : isHovered
214
- ? Theme.backgroundPanel
251
+ ? theme.backgroundPanel
215
252
  : undefined,
216
253
  paddingLeft: 0,
217
254
  paddingRight: 1,
@@ -220,16 +257,31 @@ function ListItemRow(props) {
220
257
  setIsHovered(true);
221
258
  }, onMouseOut: () => {
222
259
  setIsHovered(false);
223
- }, onMouseDown: props.onMouseDown, children: [_jsxs("box", { style: { flexDirection: 'row', flexGrow: 1, flexShrink: 1, overflow: 'hidden', gap: 1 }, children: [_jsxs("box", { style: { flexDirection: 'row', flexShrink: 0 }, children: [_jsx("text", { flexShrink: 0, fg: active ? Theme.background : Theme.text, attributes: active ? TextAttributes.BOLD : undefined, selectable: false, wrapMode: "none", children: active ? '›' : ' ' }), icon && _jsxs("text", { flexShrink: 0, fg: active ? Theme.background : iconColor || Theme.text, selectable: false, wrapMode: "none", children: [getIconEmoji(icon), " "] }), _jsx("text", { flexShrink: 0, fg: active ? Theme.background : Theme.text, attributes: active ? TextAttributes.BOLD : undefined, selectable: false, wrapMode: "none", children: title })] }), subtitle && (_jsx("text", { flexShrink: 0, fg: active ? Theme.background : Theme.textMuted, selectable: false, wrapMode: "none", children: subtitle }))] }), accessoryElements.length > 0 && (_jsx("box", { style: { flexDirection: 'row', flexShrink: 0 }, children: accessoryElements.map((elem, i) => (_jsxs("box", { style: { flexDirection: 'row' }, children: [i > 0 && _jsx("text", { flexShrink: 0, children: " " }), elem] }, i))) }))] }));
260
+ }, onMouseDown: props.onMouseDown, children: [_jsxs("box", { style: { flexDirection: 'row', flexGrow: 1, flexShrink: 1, overflow: 'hidden', gap: 1 }, children: [_jsxs("box", { style: { flexDirection: 'row', flexShrink: 0 }, children: [_jsx("text", { flexShrink: 0, fg: active ? theme.background : theme.text, attributes: active ? TextAttributes.BOLD : undefined, selectable: false, wrapMode: "none", children: active ? '›' : ' ' }), icon && _jsxs("text", { flexShrink: 0, fg: active ? theme.background : iconColor || theme.text, selectable: false, wrapMode: "none", children: [getIconEmoji(icon), " "] }), _jsx("text", { flexShrink: 0, fg: active ? theme.background : theme.text, attributes: active ? TextAttributes.BOLD : undefined, selectable: false, wrapMode: "none", children: title })] }), subtitle && (_jsx("text", { flexShrink: 0, fg: active ? theme.background : theme.textMuted, selectable: false, wrapMode: "none", children: subtitle }))] }), accessoryElements.length > 0 && (_jsx("box", { style: { flexDirection: 'row', flexShrink: 0 }, children: accessoryElements.map((elem, i) => (_jsxs("box", { style: { flexDirection: 'row' }, children: [i > 0 && _jsx("text", { flexShrink: 0, children: " " }), elem] }, i))) }))] }));
224
261
  }
225
262
  export const List = (props) => {
226
263
  const { children, onSelectionChange, filtering, searchText: controlledSearchText, onSearchTextChange, searchBarPlaceholder = 'Search...', isLoading, navigationTitle, isShowingDetail, selectedItemId, searchBarAccessory, ...otherProps } = props;
227
- const [internalSearchText, setInternalSearchTextRaw] = useState('');
264
+ const theme = useTheme();
265
+ const [internalSearchText, setInternalSearchText] = useState('');
228
266
  const [selectedIndex, setSelectedIndex] = useState(0);
229
267
  const [isDropdownOpen, setIsDropdownOpen] = useState(false);
230
268
  const [currentDetail, setCurrentDetail] = useState(null);
231
- const [currentItemActions, setCurrentItemActions] = useState(null);
232
269
  const inputRef = useRef(null);
270
+ const customEmptyViewRef = useRef(false);
271
+ // Ref callback that registers the textarea in global state for ESC handling
272
+ const setInputRef = useCallback((node) => {
273
+ if (!node)
274
+ return;
275
+ inputRef.current = node;
276
+ useStore.setState({ activeSearchInputRef: node });
277
+ // React 19: return cleanup function for unmount
278
+ return () => {
279
+ if (useStore.getState().activeSearchInputRef === node) {
280
+ useStore.setState({ activeSearchInputRef: null });
281
+ }
282
+ inputRef.current = null;
283
+ };
284
+ }, []);
233
285
  const scrollBoxRef = useRef(null);
234
286
  const descendantsContext = useListDescendants();
235
287
  const navigationPending = useNavigationPending();
@@ -275,19 +327,24 @@ export const List = (props) => {
275
327
  const openDropdown = () => {
276
328
  setIsDropdownOpen(true);
277
329
  };
278
- // Wrapper function that updates search text
279
- const setInternalSearchText = (value) => {
280
- // Using flushSync to force descendants to update visibility before querying
281
- flushSync(() => {
282
- setInternalSearchTextRaw(value);
283
- });
330
+ // Sync selection to the first visible item whenever searchText changes.
331
+ // Runs after children's useLayoutEffects (descendants registered) but before paint,
332
+ // so there is no intermediate frame with stale selection.
333
+ // Works for both controlled and uncontrolled searchText.
334
+ const prevSearchTextRef = useRef(searchText);
335
+ useLayoutEffect(() => {
336
+ if (prevSearchTextRef.current === searchText)
337
+ return;
338
+ prevSearchTextRef.current = searchText;
339
+ if (!isFilteringEnabled)
340
+ return;
284
341
  const items = Object.values(descendantsContext.map.current)
285
342
  .filter((item) => item.index !== -1 && item.props?.visible !== false)
286
343
  .sort((a, b) => a.index - b.index);
287
344
  if (items.length > 0 && items[0]) {
288
345
  setSelectedIndex(items[0].index);
289
346
  }
290
- };
347
+ });
291
348
  const listContextValue = useMemo(() => ({
292
349
  isDropdownOpen,
293
350
  setIsDropdownOpen,
@@ -298,15 +355,18 @@ export const List = (props) => {
298
355
  isFiltering: isFilteringEnabled,
299
356
  setCurrentDetail,
300
357
  isShowingDetail,
301
- }), [isDropdownOpen, selectedIndex, searchText, isFilteringEnabled, isShowingDetail]);
302
- // Clear detail when detail view is hidden
303
- useEffect(() => {
358
+ customEmptyViewRef,
359
+ isLoading,
360
+ hasDropdown: !!searchBarAccessory,
361
+ }), [isDropdownOpen, selectedIndex, searchText, isFilteringEnabled, isShowingDetail, isLoading, searchBarAccessory]);
362
+ // Clear detail when detail view is hidden (before paint to avoid flash)
363
+ useLayoutEffect(() => {
304
364
  if (!isShowingDetail) {
305
365
  setCurrentDetail(null);
306
366
  }
307
367
  }, [isShowingDetail]);
308
- // Handle selectedItemId prop changes
309
- useEffect(() => {
368
+ // Handle selectedItemId prop changes (before paint to avoid flash)
369
+ useLayoutEffect(() => {
310
370
  // Only update selection if selectedItemId is explicitly provided
311
371
  if (selectedItemId !== undefined) {
312
372
  const items = Object.values(descendantsContext.map.current)
@@ -317,25 +377,18 @@ export const List = (props) => {
317
377
  }
318
378
  }
319
379
  }, [selectedItemId]);
320
- // Call onSelectionChange when selection changes and track current item's actions
380
+ // Call onSelectionChange when selection changes
321
381
  useEffect(() => {
322
382
  const items = Object.values(descendantsContext.map.current)
323
383
  .filter((item) => item.index !== -1)
324
384
  .sort((a, b) => a.index - b.index);
325
385
  const currentItem = items.find((item) => item.index === selectedIndex);
326
- // Track current item's actions for footer display
327
- const actions = currentItem?.props?.actions ?? props.actions ?? null;
328
- setCurrentItemActions(actions);
329
- // Clear first action title when there are no actions
330
- if (!actions) {
331
- useStore.setState({ firstActionTitle: '' });
332
- }
333
386
  // Call onSelectionChange callback if provided
334
387
  if (onSelectionChange) {
335
388
  const selectedId = currentItem?.props?.id ?? null;
336
389
  onSelectionChange(selectedId);
337
390
  }
338
- }, [selectedIndex, props.actions]);
391
+ }, [selectedIndex]);
339
392
  const scrollToItem = (item) => {
340
393
  const scrollBox = scrollBoxRef.current;
341
394
  const elementRef = item.props?.elementRef;
@@ -373,8 +426,10 @@ export const List = (props) => {
373
426
  nextVisibleIndex = 0;
374
427
  const nextItem = items[nextVisibleIndex];
375
428
  if (nextItem) {
429
+ flushSync(() => {
430
+ setSelectedIndex(nextItem.index);
431
+ });
376
432
  scrollToItem(nextItem);
377
- setSelectedIndex(nextItem.index);
378
433
  }
379
434
  };
380
435
  const inFocus = useIsInFocus();
@@ -392,19 +447,11 @@ export const List = (props) => {
392
447
  .filter((item) => item.index !== -1)
393
448
  .sort((a, b) => a.index - b.index);
394
449
  const currentItem = items.find((item) => item.index === selectedIndex);
395
- // Handle Ctrl+K to show actions (always show sheet)
450
+ // Handle Ctrl+K to show actions dialog via portal
396
451
  if (evt.name === 'k' && evt.ctrl) {
397
- // Show current item's actions if available
398
- if (currentItem?.props?.actions) {
399
- dialog.pushActions(currentItem.props.actions);
400
- }
401
- // Otherwise show List's own actions
402
- else if (props.actions) {
403
- dialog.pushActions(props.actions);
404
- }
405
- // Otherwise show empty ActionPanel (still has Settings section with Configure Extension, etc.)
406
- else {
407
- dialog.pushActions(_jsx(ActionPanel, {}));
452
+ const hasActions = currentItem?.props?.actions || props.actions;
453
+ if (hasActions) {
454
+ useStore.setState({ showActionsDialog: true });
408
455
  }
409
456
  return;
410
457
  }
@@ -412,13 +459,12 @@ export const List = (props) => {
412
459
  move(-1);
413
460
  if (evt.name === 'down')
414
461
  move(1);
415
- // Handle Enter to execute first action directly
462
+ // Handle Enter to auto-execute first action via ActionPanel
416
463
  if (evt.name === 'return') {
417
464
  if (!currentItem?.props)
418
465
  return;
419
466
  if (currentItem.props.actions) {
420
467
  useStore.setState({ shouldAutoExecuteFirstAction: true });
421
- dialog.pushActions(currentItem.props.actions);
422
468
  }
423
469
  }
424
470
  });
@@ -451,31 +497,33 @@ export const List = (props) => {
451
497
  flexGrow: 1,
452
498
  flexDirection: 'row',
453
499
  flexShrink: 1,
454
- }, children: [_jsx("text", { flexShrink: 0, fg: Theme.textMuted, children: "> " }), _jsx("textarea", { ref: inputRef, height: 1, flexGrow: 1, wrapMode: 'none', keyBindings: [
500
+ }, children: [_jsx("text", { flexShrink: 0, fg: theme.textMuted, children: "> " }), _jsx("textarea", { ref: setInputRef, height: 1, flexGrow: 1, wrapMode: 'none', keyBindings: [
455
501
  { name: 'return', action: 'submit' },
456
502
  { name: 'linefeed', action: 'submit' },
457
503
  ], placeholder: searchBarPlaceholder, focused: inFocus && !isDropdownOpen, initialValue: searchText, onContentChange: () => {
458
504
  const value = inputRef.current?.plainText || '';
459
505
  handleSearchChange(value);
460
- }, focusedBackgroundColor: Theme.backgroundPanel, cursorColor: Theme.primary, focusedTextColor: Theme.text })] }), searchBarAccessory] }) }), _jsxs("box", { style: { flexDirection: 'row', flexGrow: 1, flexShrink: 1 }, children: [_jsxs("box", { style: { width: isShowingDetail ? '50%' : '100%', flexGrow: 1, flexShrink: 1, flexDirection: 'column' }, children: [_jsx(ScrollBox, { ref: scrollBoxRef, focused: false, flexGrow: 1, flexShrink: 1, style: {
506
+ }, focusedBackgroundColor: theme.backgroundPanel, cursorColor: theme.primary, focusedTextColor: theme.text })] }), searchBarAccessory] }) }), _jsxs("box", { style: { flexDirection: 'row', flexGrow: 1, flexShrink: 1 }, children: [_jsxs("box", { style: { width: isShowingDetail ? '50%' : '100%', flexGrow: 1, flexShrink: 1, flexDirection: 'column' }, children: [_jsx(ScrollBox, { ref: scrollBoxRef, focused: false, flexGrow: 1, flexShrink: 1, minHeight: 6, style: {
461
507
  rootOptions: {
462
508
  backgroundColor: undefined,
463
509
  },
464
510
  scrollbarOptions: {
465
511
  showArrows: true,
466
512
  },
467
- }, children: _jsx(ListItemsRenderer, { children: children }) }), _jsx(ListFooter, {}), currentItemActions && (_jsx(Offscreen, { children: currentItemActions }))] }), isShowingDetail && currentDetail && (_jsx("box", { style: {
513
+ }, children: _jsx(ListItemsRenderer, { children: children }) }), _jsx(ListFooter, {}), _jsx(CurrentItemActionsOffscreen, { selectedIndex: selectedIndex, fallbackActions: props.actions })] }), isShowingDetail && currentDetail && (_jsx("box", { style: {
468
514
  marginTop: 1,
469
515
  width: '50%',
470
516
  paddingLeft: 1,
471
517
  paddingRight: 1,
472
- }, border: ['left'], borderStyle: 'single', borderColor: Theme.border, children: currentDetail }))] })] }) }) }));
518
+ }, border: ['left'], borderStyle: 'single', borderColor: theme.border, children: currentDetail }))] })] }) }) }));
473
519
  };
474
- function DefaultEmptyView() {
520
+ // Wrapper component that only renders children when no visible items exist
521
+ function ShowOnNoItems(props) {
475
522
  // Subscribe to re-render when items are added/removed
476
523
  void useListDescendantsRerender();
477
524
  // Get live map ref for reading in useLayoutEffect
478
525
  const map = useListDescendantsMap();
526
+ const listContext = useContext(ListContext);
479
527
  const [hasVisibleItems, setHasVisibleItems] = useState(true);
480
528
  // We must check visibility in useLayoutEffect because:
481
529
  // 1. map.current is cleared by reset() during render, so it's empty if read during render
@@ -486,19 +534,25 @@ function DefaultEmptyView() {
486
534
  useLayoutEffect(() => {
487
535
  const items = Object.values(map.current)
488
536
  .filter((item) => item.index !== -1 && item.props?.visible !== false);
489
- setHasVisibleItems(items.length > 0);
537
+ // For default empty view, also check if custom empty view exists
538
+ const hasCustomEmptyView = !props.isCustomEmptyView && (listContext?.customEmptyViewRef.current ?? false);
539
+ setHasVisibleItems(items.length > 0 || hasCustomEmptyView);
490
540
  });
491
541
  if (hasVisibleItems)
492
542
  return null;
493
- return (_jsx("box", { style: {
494
- flexDirection: 'column',
495
- alignItems: 'center',
496
- justifyContent: 'center',
497
- paddingTop: 2,
498
- paddingBottom: 2,
499
- paddingLeft: 2,
500
- paddingRight: 2,
501
- }, children: _jsx("text", { flexShrink: 0, fg: Theme.textMuted, children: "No items found" }) }));
543
+ return props.children;
544
+ }
545
+ function DefaultEmptyView() {
546
+ const theme = useTheme();
547
+ return (_jsx(ShowOnNoItems, { children: _jsx("box", { style: {
548
+ flexDirection: 'column',
549
+ alignItems: 'center',
550
+ justifyContent: 'center',
551
+ paddingTop: 2,
552
+ paddingBottom: 2,
553
+ paddingLeft: 2,
554
+ paddingRight: 2,
555
+ }, children: _jsx("text", { flexShrink: 0, fg: theme.textMuted, children: "No items found" }) }) }));
502
556
  }
503
557
  // Component to render list items and sections
504
558
  function ListItemsRenderer(props) {
@@ -545,8 +599,8 @@ const ListItem = (props) => {
545
599
  // Check if this item is selected
546
600
  const selectedIndex = listContext?.selectedIndex ?? 0;
547
601
  const isActive = index === selectedIndex;
548
- // Update detail when this item becomes active or detail prop changes
549
- useEffect(() => {
602
+ // Update detail when this item becomes active or detail prop changes (before paint)
603
+ useLayoutEffect(() => {
550
604
  if (isActive && listContext?.isShowingDetail && listContext?.setCurrentDetail) {
551
605
  listContext.setCurrentDetail(props.detail || null);
552
606
  }
@@ -559,7 +613,10 @@ const ListItem = (props) => {
559
613
  if (listContext && index !== -1) {
560
614
  // If clicking on already selected item, show actions (like pressing Enter)
561
615
  if (isActive) {
562
- dialog.pushActions(props.actions || _jsx(ActionPanel, {}));
616
+ // Show actions dialog via portal
617
+ if (props.actions) {
618
+ useStore.setState({ showActionsDialog: true });
619
+ }
563
620
  }
564
621
  else if (listContext.setSelectedIndex) {
565
622
  // Otherwise just select the item
@@ -596,8 +653,9 @@ const ListItem = (props) => {
596
653
  return (_jsx(ListItemRow, { title: titleText, subtitle: subtitleText, icon: iconValue, iconColor: iconColor, accessories: showAccessories ? props.accessories : undefined, active: isActive, isShowingDetail: props.detail !== undefined, onMouseDown: handleMouseDown, index: index, ref: elementRef }));
597
654
  };
598
655
  const ListItemDetail = (props) => {
656
+ const theme = useTheme();
599
657
  const { isLoading, markdown, metadata } = props;
600
- return (_jsxs("box", { style: { flexDirection: 'column', flexGrow: 1 }, children: [isLoading && (_jsx("box", { style: { paddingBottom: 1 }, children: _jsx("text", { flexShrink: 0, fg: Theme.textMuted, children: "Loading..." }) })), _jsx(ScrollBox, { focused: false,
658
+ return (_jsxs("box", { style: { flexDirection: 'column', flexGrow: 1 }, children: [isLoading && (_jsx("box", { style: { paddingBottom: 1 }, children: _jsx("text", { flexShrink: 0, fg: theme.textMuted, children: "Loading..." }) })), _jsx(ScrollBox, { focused: false,
601
659
  // flexGrow={1}
602
660
  flexShrink: 1, style: {
603
661
  rootOptions: {
@@ -606,32 +664,24 @@ const ListItemDetail = (props) => {
606
664
  scrollbarOptions: {
607
665
  showArrows: true,
608
666
  },
609
- }, children: _jsxs("box", { style: { flexDirection: 'column' }, children: [markdown && (_jsx("code", { content: markdown, filetype: "markdown", syntaxStyle: markdownSyntaxStyle, drawUnstyledText: false })), metadata && (_jsx("box", { style: { paddingTop: 1 }, border: ['top'], borderStyle: 'single', borderColor: Theme.border, children: metadata }))] }) })] }));
610
- };
611
- const ListItemDetailMetadata = (props) => {
612
- return (_jsx("box", { style: { flexDirection: 'column' }, children: props.children }));
667
+ }, children: _jsxs("box", { gap: 1, style: { flexDirection: 'column' }, children: [markdown && (_jsx("code", { content: markdown, filetype: "markdown", syntaxStyle: markdownSyntaxStyle, drawUnstyledText: false })), metadata && (_jsx("box", { style: { paddingTop: 1 }, children: metadata }))] }) })] }));
613
668
  };
614
- const ListItemDetailMetadataLabel = (props) => {
615
- return (_jsxs("box", { style: { flexDirection: 'column', paddingBottom: 0.5 }, children: [_jsxs("text", { flexShrink: 0, fg: Theme.textMuted, children: [props.title, ":"] }), props.text && _jsx("text", { flexShrink: 0, fg: Theme.text, children: props.text })] }));
669
+ import { Metadata, MetadataContext } from 'termcast/src/components/metadata';
670
+ // List.Item.Detail.Metadata config: smaller padding for compact list detail panel
671
+ const listDetailMetadataConfig = {
672
+ maxValueLen: 20,
673
+ titleMinWidth: 12,
674
+ paddingBottom: 0.5,
675
+ separatorWidth: 17,
616
676
  };
617
- const ListItemDetailMetadataSeparator = () => {
618
- return (_jsx("box", { style: { paddingBottom: 0.5 }, children: _jsx("text", { flexShrink: 0, fg: Theme.border, children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }) }));
619
- };
620
- const ListItemDetailMetadataLink = (props) => {
621
- return (_jsxs("box", { style: { flexDirection: 'column', paddingBottom: 0.5 }, children: [_jsxs("text", { flexShrink: 0, fg: Theme.textMuted, children: [props.title, ":"] }), _jsx("text", { flexShrink: 0, fg: Theme.markdownLink, children: props.text })] }));
622
- };
623
- const ListItemDetailMetadataTagList = (props) => {
624
- return (_jsxs("box", { style: { flexDirection: 'column', paddingBottom: 0.5 }, children: [_jsxs("text", { flexShrink: 0, fg: Theme.textMuted, children: [props.title, ":"] }), _jsx("box", { style: { flexDirection: 'row', paddingLeft: 1 }, children: props.children })] }));
625
- };
626
- const ListItemDetailMetadataTagListItem = (props) => {
627
- return (_jsx("box", { style: { paddingRight: 1 }, children: _jsxs("text", { flexShrink: 0, fg: resolveColor(props.color) || Theme.accent, children: ["[", props.text, "]"] }) }));
677
+ const ListItemDetailMetadata = (props) => {
678
+ return (_jsx(MetadataContext.Provider, { value: listDetailMetadataConfig, children: _jsx("box", { style: { flexDirection: 'column' }, children: props.children }) }));
628
679
  };
629
680
  ListItemDetail.Metadata = ListItemDetailMetadata;
630
- ListItemDetailMetadata.Label = ListItemDetailMetadataLabel;
631
- ListItemDetailMetadata.Separator = ListItemDetailMetadataSeparator;
632
- ListItemDetailMetadata.Link = ListItemDetailMetadataLink;
633
- ListItemDetailMetadata.TagList = ListItemDetailMetadataTagList;
634
- ListItemDetailMetadataTagList.Item = ListItemDetailMetadataTagListItem;
681
+ ListItemDetailMetadata.Label = Metadata.Label;
682
+ ListItemDetailMetadata.Separator = Metadata.Separator;
683
+ ListItemDetailMetadata.Link = Metadata.Link;
684
+ ListItemDetailMetadata.TagList = Metadata.TagList;
635
685
  ListItem.Detail = ListItemDetail;
636
686
  /**
637
687
  * A dropdown menu shown in the right-hand-side of the search bar.
@@ -642,6 +692,7 @@ ListItem.Detail = ListItemDetail;
642
692
  * value="" (or your preferred reset value) at the top of your dropdown items.
643
693
  */
644
694
  const ListDropdown = (props) => {
695
+ const theme = useTheme();
645
696
  const listContext = useContext(ListContext);
646
697
  const [isHovered, setIsHovered] = useState(false);
647
698
  // If not inside a List, just render nothing (for type safety)
@@ -690,8 +741,8 @@ const ListDropdown = (props) => {
690
741
  const dropdownContextValue = useMemo(() => ({
691
742
  currentSection: undefined,
692
743
  }), []);
693
- // Open dropdown dialog when triggered
694
- useEffect(() => {
744
+ // Open dropdown dialog when triggered (before paint to avoid flash)
745
+ useLayoutEffect(() => {
695
746
  if (isDropdownOpen && !dialog.stack.length) {
696
747
  // Pass the children to the dialog to render them there
697
748
  dialog.push({
@@ -728,15 +779,16 @@ const ListDropdown = (props) => {
728
779
  // minWidth: value.length + 4,
729
780
  flexDirection: 'row',
730
781
  flexShrink: 0,
731
- backgroundColor: isHovered ? Theme.backgroundPanel : undefined,
782
+ backgroundColor: isHovered ? theme.backgroundPanel : undefined,
732
783
  }, onMouseMove: () => setIsHovered(true), onMouseOut: () => setIsHovered(false), onMouseDown: () => {
733
784
  // Open dropdown when clicked
734
785
  if (!isDropdownOpen) {
735
786
  listContext.openDropdown();
736
787
  }
737
- }, children: [_jsx("text", { flexShrink: 0, fg: isHovered ? Theme.text : Theme.textMuted, selectable: false, children: displayValue }), _jsxs("text", { flexShrink: 0, fg: isHovered ? Theme.text : Theme.textMuted, selectable: false, children: [' ', "\u25BE"] })] }, dropdownState.value)] }) }));
788
+ }, children: [listContext.isLoading ? (_jsx(LoadingText, { isLoading: true, color: isHovered ? theme.text : theme.textMuted, children: displayValue || 'Loading...' })) : (_jsx("text", { flexShrink: 0, fg: isHovered ? theme.text : theme.textMuted, selectable: false, children: displayValue })), _jsxs("text", { flexShrink: 0, fg: isHovered ? theme.text : theme.textMuted, selectable: false, children: [' ', "\u25BE"] })] }, dropdownState.value)] }) }));
738
789
  };
739
790
  ListDropdown.Item = (props) => {
791
+ const theme = useTheme();
740
792
  const dropdownContext = useContext(DropdownContext);
741
793
  const [isHovered, setIsHovered] = useState(false);
742
794
  // If not inside a Dropdown, just render nothing
@@ -781,22 +833,23 @@ ListDropdown.Item = (props) => {
781
833
  return (_jsx("box", { style: {
782
834
  flexDirection: 'row',
783
835
  backgroundColor: isActive
784
- ? Theme.primary
836
+ ? theme.primary
785
837
  : isHovered
786
- ? Theme.backgroundPanel
838
+ ? theme.backgroundPanel
787
839
  : undefined,
788
840
  paddingLeft: isActive ? 0 : 1,
789
841
  paddingRight: 1,
790
842
  justifyContent: 'space-between',
791
- }, border: false, onMouseMove: handleMouseMove, onMouseOut: () => setIsHovered(false), onMouseDown: handleMouseDown, children: _jsxs("box", { style: { flexDirection: 'row' }, children: [isActive && (_jsxs("text", { flexShrink: 0, fg: Theme.background, selectable: false, children: ["\u203A", ''] })), _jsx("text", { flexShrink: 0, fg: isActive
792
- ? Theme.background
843
+ }, border: false, onMouseMove: handleMouseMove, onMouseOut: () => setIsHovered(false), onMouseDown: handleMouseDown, children: _jsxs("box", { style: { flexDirection: 'row' }, children: [isActive && (_jsxs("text", { flexShrink: 0, fg: theme.background, selectable: false, children: ["\u203A", ''] })), _jsx("text", { flexShrink: 0, fg: isActive
844
+ ? theme.background
793
845
  : isCurrent
794
- ? Theme.primary
795
- : Theme.text, attributes: isActive ? TextAttributes.BOLD : undefined, selectable: false, children: props.title })] }) }));
846
+ ? theme.primary
847
+ : theme.text, attributes: isActive ? TextAttributes.BOLD : undefined, selectable: false, children: props.title })] }) }));
796
848
  }
797
849
  return null;
798
850
  };
799
851
  ListDropdown.Section = (props) => {
852
+ const theme = useTheme();
800
853
  const parentContext = useContext(DropdownContext);
801
854
  // If not inside a Dropdown, just render nothing
802
855
  if (!parentContext) {
@@ -811,23 +864,25 @@ ListDropdown.Section = (props) => {
811
864
  const showTitle = parentContext.selectedIndex !== undefined &&
812
865
  props.title &&
813
866
  !parentContext.searchText?.trim();
814
- return (_jsxs(_Fragment, { children: [showTitle && (_jsx("box", { style: { paddingTop: 1, paddingLeft: 1 }, children: _jsx("text", { flexShrink: 0, fg: Theme.accent, attributes: TextAttributes.BOLD, children: props.title }) })), _jsx(DropdownContext.Provider, { value: sectionContextValue, children: props.children })] }));
867
+ return (_jsxs(_Fragment, { children: [showTitle && (_jsx("box", { style: { paddingTop: 1, paddingLeft: 1 }, children: _jsx("text", { flexShrink: 0, fg: theme.accent, attributes: TextAttributes.BOLD, children: props.title }) })), _jsx(DropdownContext.Provider, { value: sectionContextValue, children: props.children })] }));
815
868
  };
816
869
  List.Item = ListItem;
817
870
  const ListSection = (props) => {
871
+ const theme = useTheme();
818
872
  const parentContext = useContext(ListSectionContext);
819
873
  const listContext = useContext(ListContext);
820
874
  const searchText = listContext?.searchText || '';
821
- // Don't render empty sections
822
- if (React.Children.count(props.children) === 0) {
823
- return null;
824
- }
825
875
  // Create new context with section title and search text
876
+ // NOTE: Must be called before any early returns to satisfy React hooks rules
826
877
  const sectionContextValue = useMemo(() => ({
827
878
  ...parentContext,
828
879
  sectionTitle: props.title,
829
880
  searchText,
830
881
  }), [parentContext, props.title, searchText]);
882
+ // Don't render empty sections
883
+ if (React.Children.count(props.children) === 0) {
884
+ return null;
885
+ }
831
886
  const isSearching = searchText.trim().length > 0;
832
887
  const children = (_jsx(ListSectionContext.Provider, { value: sectionContextValue, children: props.children }));
833
888
  if (isSearching) {
@@ -835,43 +890,31 @@ const ListSection = (props) => {
835
890
  }
836
891
  return (_jsxs("box", { style: { marginBottom: 1 }, children: [props.title && (_jsx("box", { border: false, style: {
837
892
  paddingLeft: 1,
838
- }, children: _jsx("text", { flexShrink: 0, fg: Theme.accent, attributes: TextAttributes.BOLD, children: props.title }) })), children] }));
893
+ }, children: _jsx("text", { flexShrink: 0, fg: theme.accent, attributes: TextAttributes.BOLD, children: props.title }) })), children] }));
839
894
  };
840
895
  List.Section = ListSection;
841
896
  List.Dropdown = ListDropdown;
842
- List.EmptyView = (props) => {
843
- const dialog = useDialog();
897
+ // Inner component for EmptyView content (needs hooks at top level)
898
+ function EmptyViewContent(props) {
899
+ const theme = useTheme();
844
900
  const inFocus = useIsInFocus();
845
901
  // Handle keyboard for actions
846
902
  useKeyboard((evt) => {
847
903
  if (!inFocus)
848
904
  return;
849
- // Handle Ctrl+K to show actions (always show panel, even without actions)
905
+ // Handle Ctrl+K to show actions dialog via portal
850
906
  if (evt.name === 'k' && evt.ctrl) {
851
- dialog.pushActions(props.actions || _jsx(ActionPanel, {}));
907
+ if (props.actions) {
908
+ useStore.setState({ showActionsDialog: true });
909
+ }
852
910
  return;
853
911
  }
854
- // Handle Enter to execute first action
912
+ // Handle Enter to auto-execute first action via ActionPanel
855
913
  if (evt.name === 'return' && props.actions) {
856
914
  useStore.setState({ shouldAutoExecuteFirstAction: true });
857
- dialog.pushActions(props.actions);
858
915
  }
859
916
  });
860
- // Get icon string from ImageLike
861
- const getIconString = (icon) => {
862
- if (typeof icon === 'string') {
863
- return getIconEmoji(icon);
864
- }
865
- if (icon && typeof icon === 'object' && 'source' in icon) {
866
- // For { source: string } or { source: { light, dark } } objects
867
- const source = icon.source;
868
- if (typeof source === 'string') {
869
- return getIconEmoji(source);
870
- }
871
- }
872
- return null;
873
- };
874
- const iconEmoji = props.icon ? getIconString(props.icon) : null;
917
+ const iconEmoji = props.icon ? getIconValue(props.icon) || null : null;
875
918
  return (_jsxs("box", { style: {
876
919
  flexDirection: 'column',
877
920
  alignItems: 'center',
@@ -882,7 +925,20 @@ List.EmptyView = (props) => {
882
925
  paddingLeft: 2,
883
926
  paddingRight: 2,
884
927
  gap: 1,
885
- }, children: [iconEmoji && (_jsx("text", { flexShrink: 0, fg: Theme.textMuted, style: { marginBottom: 1 }, children: iconEmoji })), props.title && (_jsx("text", { flexShrink: 0, fg: Theme.text, attributes: TextAttributes.BOLD, children: props.title?.replace(/\bRaycast\b/g, 'Termcast').replace(/\braycast\b/g, 'termcast') || '' })), props.description && (_jsx("text", { flexShrink: 0, fg: Theme.textMuted, wrapMode: 'word', children: props.description?.replace(/\bRaycast\b/g, 'Termcast').replace(/\braycast\b/g, 'termcast') || '' }))] }));
928
+ }, children: [iconEmoji && (_jsx("text", { flexShrink: 0, fg: theme.textMuted, style: { marginBottom: 1 }, children: iconEmoji })), props.title && (_jsx("text", { flexShrink: 0, fg: theme.text, attributes: TextAttributes.BOLD, children: props.title?.replace(/\bRaycast\b/g, 'Termcast').replace(/\braycast\b/g, 'termcast') || '' })), props.description && (_jsx("text", { flexShrink: 0, fg: theme.textMuted, wrapMode: 'word', children: props.description?.replace(/\bRaycast\b/g, 'Termcast').replace(/\braycast\b/g, 'termcast') || '' })), props.actions && _jsx(Offscreen, { children: props.actions })] }));
929
+ }
930
+ List.EmptyView = (props) => {
931
+ const listContext = useContext(ListContext);
932
+ // Register that a custom empty view exists
933
+ useLayoutEffect(() => {
934
+ if (listContext?.customEmptyViewRef) {
935
+ listContext.customEmptyViewRef.current = true;
936
+ return () => {
937
+ listContext.customEmptyViewRef.current = false;
938
+ };
939
+ }
940
+ }, [listContext]);
941
+ return (_jsx(ShowOnNoItems, { isCustomEmptyView: true, children: _jsx(EmptyViewContent, { ...props }) }));
886
942
  };
887
943
  export default List;
888
944
  // Grid uses List internally with a different visual representation