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
@@ -0,0 +1,708 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
2
+ /**
3
+ * Custom Renderable List V2 - Wrapper Pattern
4
+ *
5
+ * This example uses opentui's extend() to register thin wrapper renderables
6
+ * that handle tracking and visibility, while React handles all UI rendering.
7
+ *
8
+ * ## Architecture
9
+ *
10
+ * CustomListRenderable (custom renderable)
11
+ * ├── owns: filtering logic, navigation, scrolling
12
+ * ├── children redirected to scrollBox
13
+ * │
14
+ * └── CustomListItemWrapperRenderable (thin wrapper)
15
+ * ├── tracks: keywords, onAction, section, visibleIndex, itemId
16
+ * ├── handles: visibility (height: 0 when hidden)
17
+ * └── React children render all UI (indicator, title, subtitle)
18
+ *
19
+ * ## Key Patterns
20
+ *
21
+ * 1. Wrapper renderables self-register via onLifecyclePass (SYNC)
22
+ * 2. All UI in JSX - wrappers only track/hide
23
+ * 3. Zustand store for React state (selectedIndex, visibleCount, searchQuery)
24
+ * 4. Renderable calls zustand.setState to trigger React re-renders
25
+ * 5. Items compare visibleIndex to selectedIndex from zustand
26
+ *
27
+ * ## Phase 1: Bidirectional Selection Sync
28
+ *
29
+ * - `selectedItemId` prop on List controls selection by item id
30
+ * - `onSelectionChange` callback fires when selection changes
31
+ * - Supports both controlled (via prop) and uncontrolled (internal) selection
32
+ *
33
+ * ## Phase 2: Detail Panel Support
34
+ *
35
+ * - `isShowingDetail` prop on List enables detail panel on the right
36
+ * - `detail` prop on Item provides React node to render in detail panel
37
+ * - Detail updates automatically when selection changes
38
+ * - List splits into two columns when detail is shown
39
+ *
40
+ * ## How React Props Work with opentui Renderables
41
+ *
42
+ * React props are applied via direct property assignment, NOT just constructor:
43
+ *
44
+ * 1. `createInstance()` - constructor called (props passed in options)
45
+ * 2. `setInitialProperties()` - iterates props, does `instance[propKey] = propValue`
46
+ * 3. `commitUpdate()` - on re-render, applies changed props via `instance[propKey] = propValue`
47
+ *
48
+ * This means:
49
+ * - Props don't need to be read from constructor options - React sets them after
50
+ * - Simple props (just stored/read later) can be public fields
51
+ * - Props that need side effects on change require setters
52
+ * - Constructor should just create the renderable structure
53
+ *
54
+ * ─────────────────────────────────────────────────────────────────────────────
55
+ * ## Migration Notes (Lessons from Form conversion)
56
+ * ─────────────────────────────────────────────────────────────────────────────
57
+ *
58
+ * ### 1. Registration Order ≠ Visual Order
59
+ *
60
+ * IMPORTANT: `onLifecyclePass` is called in tree traversal order, which may NOT
61
+ * match the visual/React render order. For Form, we solved this by sorting
62
+ * fields by y-position instead of registration order:
63
+ *
64
+ * ```typescript
65
+ * private getFieldOrder(): string[] {
66
+ * return Array.from(this.fields.values())
67
+ * .sort((a, b) => (a.elementRef?.y ?? 0) - (b.elementRef?.y ?? 0))
68
+ * .map((f) => f.id)
69
+ * }
70
+ * ```
71
+ *
72
+ * For List, this is less critical since items use visibleIndex from filtering,
73
+ * but be aware if you need stable ordering based on visual position.
74
+ *
75
+ * ### 2. Auto-Focus Pattern
76
+ *
77
+ * Don't auto-focus in registerField() - registration order is unpredictable.
78
+ * Instead, use React useEffect that runs until focus is set:
79
+ *
80
+ * ```typescript
81
+ * useEffect(() => {
82
+ * if (focusedField) return
83
+ * const firstId = formRef.current?.getFirstFieldId()
84
+ * if (firstId) formRef.current?.focusField(firstId)
85
+ * })
86
+ * ```
87
+ *
88
+ * ### 3. Hybrid Approach - Keep React for Styling
89
+ *
90
+ * The renderable should only handle STATE (registry, focus, scroll).
91
+ * Keep existing React components for UI (e.g., WithLeftBorder for Form).
92
+ * This ensures visual output stays identical - only the wiring changes.
93
+ *
94
+ * ### 4. Focus State Sync to React
95
+ *
96
+ * Form uses `onFocusChange` callback to sync focus to React state:
97
+ *
98
+ * ```typescript
99
+ * // In FormRenderable
100
+ * public onFocusChange?: (fieldId: string | null) => void
101
+ *
102
+ * focusField(id: string) {
103
+ * this._focusedFieldId = id
104
+ * this.onFocusChange?.(id) // Notify React
105
+ * }
106
+ *
107
+ * // In Form component
108
+ * const handleFormRef = (ref: FormRenderable | null) => {
109
+ * formRef.current = ref
110
+ * if (ref) ref.onFocusChange = setFocusedField
111
+ * }
112
+ * ```
113
+ *
114
+ * This example uses zustand instead (setState triggers re-render).
115
+ * Both patterns work - zustand is simpler for shared state.
116
+ *
117
+ * ### 5. Legacy Compatibility
118
+ *
119
+ * When migrating, keep old descendants/context during transition:
120
+ *
121
+ * ```typescript
122
+ * // Keep old system for components not yet migrated
123
+ * const { DescendantsProvider, useDescendant } = createDescendants<...>()
124
+ * export { useDescendant } // Still exported for old components
125
+ * ```
126
+ *
127
+ * ### 6. Field Component Changes
128
+ *
129
+ * Each field component needs minimal changes:
130
+ * - Remove: useFormFieldDescendant() call, elementRef
131
+ * - Add: wrap return in <termcast-form-field-wrapper fieldId={id}>
132
+ * - Keep: all UI code (WithLeftBorder, etc.) unchanged
133
+ *
134
+ * ### 7. Testing Strategy
135
+ *
136
+ * Run existing tests with -u to update snapshots. Visual output should be
137
+ * identical - if tests fail, the wiring is wrong, not the rendering.
138
+ * ─────────────────────────────────────────────────────────────────────────────
139
+ */
140
+ import { BoxRenderable, TextRenderable, ScrollBoxRenderable, TextareaRenderable, } from '@opentui/core';
141
+ import { extend, useKeyboard } from '@opentui/react';
142
+ import { useIsInFocus } from 'termcast/src/internal/focus-context';
143
+ import { useStore } from 'termcast/src/state';
144
+ import { useRef, useState } from 'react';
145
+ import { renderWithProviders } from '../../utils';
146
+ import { create } from 'zustand';
147
+ import { useTheme } from 'termcast/src/theme';
148
+ const useCustomListStore = create(() => ({
149
+ selectedIndex: 0,
150
+ visibleCount: 0,
151
+ totalCount: 0,
152
+ searchQuery: '',
153
+ itemStates: {},
154
+ selectedItemId: null,
155
+ isShowingDetail: false,
156
+ currentDetailNode: null,
157
+ }));
158
+ // ─────────────────────────────────────────────────────────────────────────────
159
+ // Helper: Find parent of specific type by traversing up
160
+ // ─────────────────────────────────────────────────────────────────────────────
161
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
162
+ function findParent(node, type) {
163
+ let current = node.parent;
164
+ while (current) {
165
+ if (current instanceof type) {
166
+ return current;
167
+ }
168
+ current = current.parent;
169
+ }
170
+ return undefined;
171
+ }
172
+ // ─────────────────────────────────────────────────────────────────────────────
173
+ // CustomListItemWrapperRenderable - thin wrapper for tracking/hiding
174
+ // ─────────────────────────────────────────────────────────────────────────────
175
+ class CustomListItemWrapperRenderable extends BoxRenderable {
176
+ parentList;
177
+ // Props set by React - used for filtering
178
+ keywords;
179
+ onAction;
180
+ itemTitle = '';
181
+ itemSubtitle;
182
+ itemId; // Phase 1: unique id for controlled selection
183
+ detail; // Phase 2: detail panel content
184
+ // Set by parent during refilter
185
+ visibleIndex = -1;
186
+ section;
187
+ constructor(ctx, options) {
188
+ super(ctx, { ...options, flexDirection: 'row', width: '100%' });
189
+ // NO UI creation - React children provide that
190
+ // Self-register with parent list after being added to tree
191
+ this.onLifecyclePass = () => {
192
+ if (!this.parentList) {
193
+ this.parentList = findParent(this, CustomListRenderable);
194
+ this.section = findParent(this, CustomListSectionWrapperRenderable);
195
+ this.parentList?.registerItem(this);
196
+ }
197
+ };
198
+ // Example: register click handler directly on renderable
199
+ this.onMouseDown = (event) => {
200
+ console.log('CustomListItemWrapperRenderable clicked:', this.itemTitle, event);
201
+ };
202
+ }
203
+ }
204
+ // ─────────────────────────────────────────────────────────────────────────────
205
+ // CustomListSectionWrapperRenderable - thin wrapper for sections
206
+ // ─────────────────────────────────────────────────────────────────────────────
207
+ class CustomListSectionWrapperRenderable extends BoxRenderable {
208
+ parentList;
209
+ // Props set by React
210
+ sectionTitle;
211
+ constructor(ctx, options) {
212
+ super(ctx, { ...options, flexDirection: 'column', width: '100%' });
213
+ // NO UI creation - React children provide that
214
+ // Self-register with parent list after being added to tree
215
+ this.onLifecyclePass = () => {
216
+ if (!this.parentList) {
217
+ this.parentList = findParent(this, CustomListRenderable);
218
+ this.parentList?.registerSection(this);
219
+ }
220
+ };
221
+ }
222
+ }
223
+ // ─────────────────────────────────────────────────────────────────────────────
224
+ // CustomListEmptyViewWrapperRenderable - data holder for empty state
225
+ // ─────────────────────────────────────────────────────────────────────────────
226
+ class CustomListEmptyViewWrapperRenderable extends BoxRenderable {
227
+ parentList;
228
+ // Props set by React - read by parent for empty state
229
+ emptyTitle = 'No items';
230
+ emptyDescription;
231
+ constructor(ctx, options) {
232
+ // Hidden by default - this is just a data holder
233
+ super(ctx, { ...options, visible: false });
234
+ // Self-register with parent list after being added to tree
235
+ this.onLifecyclePass = () => {
236
+ if (!this.parentList) {
237
+ this.parentList = findParent(this, CustomListRenderable);
238
+ this.parentList?.registerEmptyView(this);
239
+ }
240
+ };
241
+ }
242
+ }
243
+ // ─────────────────────────────────────────────────────────────────────────────
244
+ // CustomListRenderable - parent container with filtering/navigation logic
245
+ // ─────────────────────────────────────────────────────────────────────────────
246
+ class CustomListRenderable extends BoxRenderable {
247
+ // Registered children (they register themselves via onLifecyclePass)
248
+ registeredItems = new Set();
249
+ registeredSections = new Set();
250
+ emptyView;
251
+ // Internal state
252
+ searchQuery = '';
253
+ // UI components owned by renderable
254
+ scrollBox;
255
+ searchInput;
256
+ statusText;
257
+ // Prop with setter - updates search input placeholder
258
+ _placeholder = 'Search...';
259
+ get placeholder() {
260
+ return this._placeholder;
261
+ }
262
+ set placeholder(value) {
263
+ this._placeholder = value;
264
+ this.searchInput.placeholder = value;
265
+ }
266
+ // Prop with setter - sets initial search query
267
+ _defaultSearchQuery = '';
268
+ get defaultSearchQuery() {
269
+ return this._defaultSearchQuery;
270
+ }
271
+ set defaultSearchQuery(value) {
272
+ if (this._defaultSearchQuery === value)
273
+ return;
274
+ this._defaultSearchQuery = value;
275
+ // Set search input text
276
+ this.searchInput.editBuffer?.setText(value);
277
+ this.searchQuery = value;
278
+ // Refilter will be called after items register
279
+ }
280
+ // ─────────────────────────────────────────────────────────────────────────
281
+ // Phase 1: Bidirectional Selection Sync
282
+ // ─────────────────────────────────────────────────────────────────────────
283
+ // Callback when selection changes - set by React
284
+ onSelectionChange;
285
+ // Controlled selection by item id
286
+ _selectedItemId = null;
287
+ get selectedItemId() {
288
+ return this._selectedItemId;
289
+ }
290
+ set selectedItemId(value) {
291
+ if (this._selectedItemId === value)
292
+ return;
293
+ this._selectedItemId = value;
294
+ // Find item with this id and update selectedIndex
295
+ if (value !== null) {
296
+ const item = this.getAllItems().find((i) => i.itemId === value);
297
+ if (item && item.visibleIndex !== -1) {
298
+ useCustomListStore.setState({
299
+ selectedIndex: item.visibleIndex,
300
+ selectedItemId: value,
301
+ });
302
+ this.scrollToIndex(item.visibleIndex);
303
+ }
304
+ }
305
+ }
306
+ // Helper to notify selection change
307
+ notifySelectionChange(index) {
308
+ const item = this.getAllItems().find((i) => i.visibleIndex === index);
309
+ const itemId = item?.itemId ?? null;
310
+ useCustomListStore.setState({ selectedItemId: itemId });
311
+ this.onSelectionChange?.(itemId);
312
+ }
313
+ // ─────────────────────────────────────────────────────────────────────────
314
+ // Phase 2: Detail Panel Support
315
+ // ─────────────────────────────────────────────────────────────────────────
316
+ // Prop with setter - controls whether detail panel is shown
317
+ _isShowingDetail = false;
318
+ get isShowingDetail() {
319
+ return this._isShowingDetail;
320
+ }
321
+ set isShowingDetail(value) {
322
+ if (this._isShowingDetail === value)
323
+ return;
324
+ this._isShowingDetail = value;
325
+ useCustomListStore.setState({ isShowingDetail: value });
326
+ this.updateCurrentDetail();
327
+ }
328
+ // Updates the current detail node in zustand based on selected item
329
+ updateCurrentDetail() {
330
+ if (!this._isShowingDetail) {
331
+ useCustomListStore.setState({ currentDetailNode: null });
332
+ return;
333
+ }
334
+ const { selectedIndex } = useCustomListStore.getState();
335
+ const item = this.getAllItems().find((i) => i.visibleIndex === selectedIndex);
336
+ useCustomListStore.setState({ currentDetailNode: item?.detail ?? null });
337
+ }
338
+ constructor(ctx, options) {
339
+ super(ctx, { ...options, flexDirection: 'column' });
340
+ // Create search input - React will set placeholder/defaultSearchQuery props after constructor
341
+ this.searchInput = new TextareaRenderable(ctx, {
342
+ placeholder: 'Search...',
343
+ height: 1,
344
+ width: '100%',
345
+ keyBindings: [
346
+ { name: 'return', action: 'submit' },
347
+ { name: 'linefeed', action: 'submit' },
348
+ ],
349
+ });
350
+ this.searchInput.onContentChange = () => {
351
+ const value = this.searchInput.editBuffer?.getText() || '';
352
+ this.setSearchQuery(value);
353
+ };
354
+ this.searchInput.focus();
355
+ this.scrollBox = new ScrollBoxRenderable(ctx, {
356
+ flexGrow: 1,
357
+ flexDirection: 'column',
358
+ });
359
+ this.statusText = new TextRenderable(ctx, {
360
+ content: '0 items',
361
+ });
362
+ super.add(this.searchInput);
363
+ super.add(this.scrollBox);
364
+ super.add(this.statusText);
365
+ // Example: register key handler directly on renderable (logs all key presses)
366
+ this.onKeyDown = (key) => {
367
+ console.log('CustomListRenderable received key:', key.name, key);
368
+ };
369
+ // Subscribe to zustand store for defaultSearchQuery sync
370
+ // When React sets defaultSearchQuery prop, we need to refilter
371
+ }
372
+ // ─────────────────────────────────────────────────────────────────────────
373
+ // Child Management - redirect to scrollBox
374
+ // ─────────────────────────────────────────────────────────────────────────
375
+ add(child, index) {
376
+ return this.scrollBox.add(child, index);
377
+ }
378
+ insertBefore(child, anchor) {
379
+ return this.scrollBox.insertBefore(child, anchor);
380
+ }
381
+ remove(id) {
382
+ this.scrollBox.remove(id);
383
+ }
384
+ // ─────────────────────────────────────────────────────────────────────────
385
+ // Registration - children call these via onLifecyclePass
386
+ // ─────────────────────────────────────────────────────────────────────────
387
+ registerItem(item) {
388
+ this.registeredItems.add(item);
389
+ this.refilter();
390
+ }
391
+ registerSection(section) {
392
+ this.registeredSections.add(section);
393
+ }
394
+ registerEmptyView(emptyView) {
395
+ this.emptyView = emptyView;
396
+ }
397
+ // ─────────────────────────────────────────────────────────────────────────
398
+ // Filtering
399
+ // ─────────────────────────────────────────────────────────────────────────
400
+ setSearchQuery(query) {
401
+ if (this.searchQuery === query)
402
+ return;
403
+ this.searchQuery = query;
404
+ this.refilter();
405
+ }
406
+ refilter() {
407
+ const query = this.searchQuery.toLowerCase();
408
+ const allItems = this.getAllItems();
409
+ let visibleIndex = 0;
410
+ // Update item visibility and visible indices
411
+ // We use the `visible` prop instead of conditional rendering to keep elements
412
+ // in the tree - this preserves registration order and visibleIndex for navigation
413
+ const itemStates = {};
414
+ for (const item of allItems) {
415
+ const matches = !query || this.scoreItem(item, query) > 0;
416
+ item.visible = matches;
417
+ item.visibleIndex = matches ? visibleIndex++ : -1;
418
+ // Store in itemStates for React to subscribe to
419
+ if (item.itemId) {
420
+ itemStates[item.itemId] = { visibleIndex: item.visibleIndex };
421
+ }
422
+ }
423
+ // Update section visibility based on their items
424
+ for (const section of this.registeredSections) {
425
+ const sectionItems = allItems.filter((item) => item.section === section);
426
+ const hasVisibleItems = sectionItems.some((item) => item.visible);
427
+ section.visible = hasVisibleItems;
428
+ }
429
+ // Get current selection and clamp it
430
+ const { selectedIndex } = useCustomListStore.getState();
431
+ let newSelectedIndex = Math.max(0, Math.min(selectedIndex, Math.max(0, visibleIndex - 1)));
432
+ // Phase 1: Apply controlled selection if set
433
+ // This handles the case where selectedItemId is set before items register
434
+ if (this._selectedItemId !== null) {
435
+ const targetItem = allItems.find((i) => i.itemId === this._selectedItemId);
436
+ if (targetItem && targetItem.visibleIndex !== -1) {
437
+ newSelectedIndex = targetItem.visibleIndex;
438
+ }
439
+ }
440
+ // Update zustand store - triggers React re-render
441
+ // itemStates allows each item to subscribe to its own visibleIndex
442
+ const finalSelectedIndex = visibleIndex > 0 ? newSelectedIndex : 0;
443
+ useCustomListStore.setState({
444
+ searchQuery: this.searchQuery,
445
+ visibleCount: visibleIndex,
446
+ totalCount: allItems.length,
447
+ selectedIndex: finalSelectedIndex,
448
+ selectedItemId: this._selectedItemId,
449
+ itemStates,
450
+ });
451
+ // Update status text (owned by renderable, not React)
452
+ this.updateStatusText(visibleIndex, allItems.length, this.searchQuery);
453
+ // Phase 2: Update detail panel after filtering (handles initial render and filter changes)
454
+ this.updateCurrentDetail();
455
+ }
456
+ scoreItem(item, query) {
457
+ let score = 0;
458
+ const title = item.itemTitle.toLowerCase();
459
+ if (title.includes(query)) {
460
+ score += title.startsWith(query) ? 2 : 1;
461
+ }
462
+ if (item.itemSubtitle?.toLowerCase().includes(query)) {
463
+ score += 0.6;
464
+ }
465
+ for (const kw of item.keywords || []) {
466
+ if (kw.toLowerCase().includes(query)) {
467
+ score += 0.3;
468
+ }
469
+ }
470
+ return score;
471
+ }
472
+ // ─────────────────────────────────────────────────────────────────────────
473
+ // Helpers - clean stale refs
474
+ // ─────────────────────────────────────────────────────────────────────────
475
+ isConnected(node) {
476
+ let current = node.parent;
477
+ while (current) {
478
+ if (current === this.scrollBox || current === this) {
479
+ return true;
480
+ }
481
+ current = current.parent;
482
+ }
483
+ return false;
484
+ }
485
+ getAllItems() {
486
+ // Clean stale refs (items no longer in tree)
487
+ for (const item of this.registeredItems) {
488
+ if (!this.isConnected(item)) {
489
+ this.registeredItems.delete(item);
490
+ }
491
+ }
492
+ return Array.from(this.registeredItems);
493
+ }
494
+ updateStatusText(visibleCount, totalCount, searchQuery) {
495
+ this.statusText.content = searchQuery
496
+ ? `${visibleCount} of ${totalCount} items • Searching: "${searchQuery}"`
497
+ : `${visibleCount} of ${totalCount} items`;
498
+ }
499
+ // ─────────────────────────────────────────────────────────────────────────
500
+ // Navigation - called by React via ref
501
+ // ─────────────────────────────────────────────────────────────────────────
502
+ moveSelection(delta) {
503
+ const { selectedIndex, visibleCount } = useCustomListStore.getState();
504
+ if (visibleCount === 0)
505
+ return;
506
+ const newIndex = (selectedIndex + delta + visibleCount) % visibleCount;
507
+ useCustomListStore.setState({ selectedIndex: newIndex });
508
+ this.scrollToIndex(newIndex);
509
+ // Phase 1: Notify selection change
510
+ this.notifySelectionChange(newIndex);
511
+ // Phase 2: Update detail panel
512
+ this.updateCurrentDetail();
513
+ }
514
+ scrollToIndex(index) {
515
+ const item = this.getAllItems().find((i) => i.visibleIndex === index);
516
+ if (!item)
517
+ return;
518
+ const itemY = item.y;
519
+ const scrollBoxY = this.scrollBox.content.y;
520
+ const viewportHeight = this.scrollBox.viewport?.height || 10;
521
+ const relativeY = itemY - scrollBoxY;
522
+ const targetScrollTop = relativeY - Math.floor(viewportHeight / 2);
523
+ this.scrollBox.scrollTop = Math.max(0, targetScrollTop);
524
+ }
525
+ activateSelected() {
526
+ const { selectedIndex } = useCustomListStore.getState();
527
+ const item = this.getAllItems().find((i) => i.visibleIndex === selectedIndex);
528
+ item?.onAction?.();
529
+ }
530
+ // Get selected item's title - for dialog display
531
+ getSelectedItemTitle() {
532
+ const { selectedIndex } = useCustomListStore.getState();
533
+ const item = this.getAllItems().find((i) => i.visibleIndex === selectedIndex);
534
+ return item?.itemTitle;
535
+ }
536
+ // Get empty view data - for React to render
537
+ getEmptyViewData() {
538
+ if (!this.emptyView)
539
+ return undefined;
540
+ return {
541
+ title: this.emptyView.emptyTitle,
542
+ description: this.emptyView.emptyDescription,
543
+ };
544
+ }
545
+ }
546
+ // ─────────────────────────────────────────────────────────────────────────────
547
+ // Register with opentui
548
+ // ─────────────────────────────────────────────────────────────────────────────
549
+ extend({
550
+ 'custom-list-v2': CustomListRenderable,
551
+ 'custom-list-item-wrapper-v2': CustomListItemWrapperRenderable,
552
+ 'custom-list-section-wrapper-v2': CustomListSectionWrapperRenderable,
553
+ 'custom-list-empty-view-wrapper-v2': CustomListEmptyViewWrapperRenderable,
554
+ });
555
+ // ─────────────────────────────────────────────────────────────────────────────
556
+ // React Components
557
+ // ─────────────────────────────────────────────────────────────────────────────
558
+ // Action Dialog - same as v1
559
+ function ActionDialog({ itemTitle }) {
560
+ const inFocus = useIsInFocus();
561
+ useKeyboard((evt) => {
562
+ if (!inFocus)
563
+ return;
564
+ if (evt.name === 'escape') {
565
+ const state = useStore.getState();
566
+ useStore.setState({
567
+ dialogStack: state.dialogStack.slice(0, -1),
568
+ });
569
+ }
570
+ });
571
+ return (_jsxs("box", { flexDirection: "column", padding: 1, children: [_jsxs("text", { children: ["Actions for: ", itemTitle || 'No item selected'] }), _jsx("text", { marginTop: 1, children: "Press ESC to close" })] }));
572
+ }
573
+ function CustomList({ children, placeholder, defaultSearchQuery, selectedItemId, onSelectionChange, isShowingDetail }) {
574
+ const listRef = useRef(null);
575
+ const inFocus = useIsInFocus();
576
+ const theme = useTheme();
577
+ // Subscribe to zustand for UI updates
578
+ const visibleCount = useCustomListStore((s) => s.visibleCount);
579
+ const totalCount = useCustomListStore((s) => s.totalCount);
580
+ const searchQuery = useCustomListStore((s) => s.searchQuery);
581
+ // Phase 2: Subscribe to detail panel state
582
+ const currentDetailNode = useCustomListStore((s) => s.currentDetailNode);
583
+ const showingDetail = useCustomListStore((s) => s.isShowingDetail);
584
+ // Get empty view data from renderable
585
+ const emptyViewData = listRef.current?.getEmptyViewData();
586
+ // Keyboard navigation
587
+ useKeyboard((evt) => {
588
+ if (!inFocus || !listRef.current)
589
+ return;
590
+ if (evt.name === 'up')
591
+ listRef.current.moveSelection(-1);
592
+ if (evt.name === 'down')
593
+ listRef.current.moveSelection(1);
594
+ if (evt.name === 'return')
595
+ listRef.current.activateSelected();
596
+ if (evt.name === 'k' && evt.ctrl) {
597
+ const selectedItemTitle = listRef.current.getSelectedItemTitle();
598
+ const state = useStore.getState();
599
+ useStore.setState({
600
+ dialogStack: [
601
+ ...state.dialogStack,
602
+ {
603
+ element: _jsx(ActionDialog, { itemTitle: selectedItemTitle }),
604
+ position: 'center',
605
+ },
606
+ ],
607
+ });
608
+ }
609
+ });
610
+ // Phase 2: Wrap list and detail panel in a row layout when detail is shown
611
+ const listContent = (_jsxs("custom-list-v2", { ref: listRef, flexGrow: 1, placeholder: placeholder, defaultSearchQuery: defaultSearchQuery, selectedItemId: selectedItemId, onSelectionChange: onSelectionChange, isShowingDetail: isShowingDetail, children: [children, visibleCount === 0 && totalCount > 0 && (_jsxs("box", { padding: 1, flexDirection: "column", children: [_jsx("text", { children: emptyViewData?.title || `No results for "${searchQuery}"` }), emptyViewData?.description && _jsx("text", { children: emptyViewData.description })] }))] }));
612
+ // Phase 2: When detail panel is enabled, show list on left, detail on right
613
+ if (showingDetail) {
614
+ return (_jsxs("box", { flexDirection: "row", flexGrow: 1, children: [_jsx("box", { width: "50%", flexDirection: "column", children: listContent }), _jsx("text", { flexShrink: 0, children: "\u2502" }), _jsx("box", { width: "50%", flexDirection: "column", paddingLeft: 1, children: currentDetailNode || _jsx("text", { fg: theme.textMuted, children: "No detail available" }) })] }));
615
+ }
616
+ return listContent;
617
+ }
618
+ function CustomListItem({ id, title, subtitle, keywords, onAction, detail }) {
619
+ const wrapperRef = useRef(null);
620
+ const selectedIndex = useCustomListStore((s) => s.selectedIndex);
621
+ // Subscribe to THIS item's state - React re-renders when it changes
622
+ const itemState = useCustomListStore((s) => (id ? s.itemStates[id] : undefined));
623
+ const theme = useTheme();
624
+ // Compare visibleIndex to selectedIndex - using zustand state, not ref
625
+ const isSelected = itemState?.visibleIndex === selectedIndex;
626
+ return (_jsxs("custom-list-item-wrapper-v2", { ref: wrapperRef, keywords: keywords, onAction: onAction, itemTitle: title, itemSubtitle: subtitle, itemId: id, detail: detail, backgroundColor: isSelected ? '#0066cc' : undefined, flexShrink: 0, children: [_jsx("text", { flexShrink: 0, children: isSelected ? '› ' : ' ' }), _jsx("text", { flexShrink: 0, children: title }), subtitle && _jsxs("text", { flexShrink: 0, fg: theme.textMuted, children: [" ", subtitle] })] }));
627
+ }
628
+ function CustomListSection({ title, children }) {
629
+ return (_jsxs("custom-list-section-wrapper-v2", { sectionTitle: title, flexShrink: 0, children: [title && (_jsxs("text", { paddingTop: 1, paddingLeft: 1, flexShrink: 0, children: ["\u2500\u2500 ", title, " \u2500\u2500"] })), children] }));
630
+ }
631
+ function CustomListEmptyView({ title, description }) {
632
+ return _jsx("custom-list-empty-view-wrapper-v2", { emptyTitle: title, emptyDescription: description });
633
+ }
634
+ // ─────────────────────────────────────────────────────────────────────────────
635
+ // Compound Component
636
+ // ─────────────────────────────────────────────────────────────────────────────
637
+ CustomList.Item = CustomListItem;
638
+ CustomList.Section = CustomListSection;
639
+ CustomList.EmptyView = CustomListEmptyView;
640
+ // ─────────────────────────────────────────────────────────────────────────────
641
+ // Example
642
+ // ─────────────────────────────────────────────────────────────────────────────
643
+ const FRUITS = [
644
+ { id: 'apple', title: 'Apple', subtitle: 'A red fruit', keywords: ['red'] },
645
+ { id: 'banana', title: 'Banana', subtitle: 'A yellow fruit', keywords: ['yellow'] },
646
+ { id: 'date', title: 'Date', subtitle: 'A sweet fruit', keywords: ['sweet'] },
647
+ { id: 'fig', title: 'Fig', subtitle: 'A small fruit', keywords: ['small'] },
648
+ { id: 'grape', title: 'Grape', subtitle: 'A vine fruit', keywords: ['vine'] },
649
+ { id: 'lemon', title: 'Lemon', subtitle: 'A citrus fruit', keywords: ['citrus'] },
650
+ ];
651
+ const VEGETABLES = [
652
+ { id: 'carrot', title: 'Carrot', subtitle: 'An orange vegetable', keywords: ['orange'] },
653
+ { id: 'eggplant', title: 'Eggplant', subtitle: 'A purple vegetable', keywords: ['purple'] },
654
+ { id: 'jalapeno', title: 'Jalapeno', subtitle: 'A spicy pepper', keywords: ['spicy'] },
655
+ { id: 'kale', title: 'Kale', subtitle: 'A superfood', keywords: ['healthy'] },
656
+ ];
657
+ // Wrapper component to test tree traversal (items nested in other components)
658
+ function ItemWrapper({ children }) {
659
+ return _jsx("box", { children: children });
660
+ }
661
+ function Example() {
662
+ // Phase 1: Track selected item id in React state
663
+ const [selectedId, setSelectedId] = useState(null);
664
+ const inFocus = useIsInFocus();
665
+ const theme = useTheme();
666
+ // Phase 1: Demonstrate programmatic selection change (bidirectional sync)
667
+ // Press ctrl+1-4 to jump to specific items (ctrl to avoid typing in search)
668
+ useKeyboard((evt) => {
669
+ if (!inFocus)
670
+ return;
671
+ if (evt.ctrl && evt.name === '1') {
672
+ setSelectedId('apple');
673
+ }
674
+ if (evt.ctrl && evt.name === '2') {
675
+ setSelectedId('banana');
676
+ }
677
+ if (evt.ctrl && evt.name === '3') {
678
+ setSelectedId('carrot');
679
+ }
680
+ if (evt.ctrl && evt.name === '4') {
681
+ setSelectedId('kale');
682
+ }
683
+ });
684
+ // Phase 2: Helper to create detail content for an item
685
+ const createDetail = (item) => (_jsxs("box", { flexDirection: "column", padding: 1, children: [_jsxs("text", { children: ["Details for ", item.title] }), _jsx("text", { fg: theme.textMuted, marginTop: 1, children: item.subtitle }), _jsxs("text", { fg: theme.textMuted, marginTop: 1, children: ["Keywords: ", item.keywords.join(', ')] }), _jsxs("text", { fg: theme.textMuted, marginTop: 1, children: ["ID: ", item.id] })] }));
686
+ return (_jsxs("box", { flexDirection: "column", padding: 1, flexGrow: 1, children: [_jsx("text", { marginBottom: 1, children: "Custom Renderable List V2 - Detail Panel" }), _jsxs("text", { fg: theme.textMuted, children: ["Selected: ", selectedId || '(none)'] }), _jsx("text", { fg: theme.textMuted, marginBottom: 1, children: "Press ^1=apple, ^2=banana, ^3=carrot, ^4=kale to jump" }), _jsxs(CustomList, { placeholder: "Search items...", selectedItemId: selectedId, onSelectionChange: (id) => {
687
+ setSelectedId(id);
688
+ }, isShowingDetail: true, children: [_jsx(CustomList.EmptyView, { title: "Nothing found", description: "Try a different search term" }), _jsx(CustomList.Section, { title: "Fruits", children: FRUITS.map((item) => (_jsx(ItemWrapper, { children: _jsx(CustomList.Item, { id: item.id, title: item.title, subtitle: item.subtitle, keywords: item.keywords, detail: createDetail(item), onAction: () => {
689
+ console.log(`Activated: ${item.title} (id: ${item.id})`);
690
+ } }) }, item.id))) }), _jsx(CustomList.Section, { title: "Vegetables", children: VEGETABLES.map((item) => (_jsx(CustomList.Item, { id: item.id, title: item.title, subtitle: item.subtitle, keywords: item.keywords, detail: createDetail(item), onAction: () => {
691
+ console.log(`Activated: ${item.title} (id: ${item.id})`);
692
+ } }, item.id))) })] })] }));
693
+ }
694
+ // Reset store when running as main (for testing isolation)
695
+ if (import.meta.main) {
696
+ useCustomListStore.setState({
697
+ selectedIndex: 0,
698
+ visibleCount: 0,
699
+ totalCount: 0,
700
+ searchQuery: '',
701
+ selectedItemId: null,
702
+ isShowingDetail: false,
703
+ currentDetailNode: null,
704
+ });
705
+ renderWithProviders(_jsx(Example, {}));
706
+ }
707
+ export { CustomList, CustomListItem, CustomListSection, CustomListEmptyView, Example, useCustomListStore };
708
+ //# sourceMappingURL=custom-renderable-list-v2.js.map