termcast 1.3.30 → 1.3.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (294) hide show
  1. package/dist/apis/cache.d.ts.map +1 -1
  2. package/dist/apis/cache.js +4 -39
  3. package/dist/apis/cache.js.map +1 -1
  4. package/dist/apis/hud.d.ts.map +1 -1
  5. package/dist/apis/hud.js +13 -31
  6. package/dist/apis/hud.js.map +1 -1
  7. package/dist/apis/localstorage.d.ts.map +1 -1
  8. package/dist/apis/localstorage.js +3 -27
  9. package/dist/apis/localstorage.js.map +1 -1
  10. package/dist/apis/toast.d.ts +16 -43
  11. package/dist/apis/toast.d.ts.map +1 -1
  12. package/dist/apis/toast.js +78 -177
  13. package/dist/apis/toast.js.map +1 -1
  14. package/dist/build.d.ts +3 -1
  15. package/dist/build.d.ts.map +1 -1
  16. package/dist/build.js +52 -2
  17. package/dist/build.js.map +1 -1
  18. package/dist/cli.d.ts +1 -0
  19. package/dist/cli.d.ts.map +1 -1
  20. package/dist/cli.js +206 -25
  21. package/dist/cli.js.map +1 -1
  22. package/dist/colors.d.ts.map +1 -1
  23. package/dist/colors.js +1 -0
  24. package/dist/colors.js.map +1 -1
  25. package/dist/compile.d.ts +0 -1
  26. package/dist/compile.d.ts.map +1 -1
  27. package/dist/compile.js +18 -23
  28. package/dist/compile.js.map +1 -1
  29. package/dist/components/actions.d.ts.map +1 -1
  30. package/dist/components/actions.js +30 -15
  31. package/dist/components/actions.js.map +1 -1
  32. package/dist/components/animation-tick.d.ts +12 -0
  33. package/dist/components/animation-tick.d.ts.map +1 -0
  34. package/dist/components/animation-tick.js +63 -0
  35. package/dist/components/animation-tick.js.map +1 -0
  36. package/dist/components/detail.d.ts.map +1 -1
  37. package/dist/components/detail.js +10 -13
  38. package/dist/components/detail.js.map +1 -1
  39. package/dist/components/dropdown.d.ts +1 -0
  40. package/dist/components/dropdown.d.ts.map +1 -1
  41. package/dist/components/dropdown.js +27 -26
  42. package/dist/components/dropdown.js.map +1 -1
  43. package/dist/components/extension-preferences.d.ts.map +1 -1
  44. package/dist/components/extension-preferences.js +15 -10
  45. package/dist/components/extension-preferences.js.map +1 -1
  46. package/dist/components/footer.d.ts +13 -0
  47. package/dist/components/footer.d.ts.map +1 -0
  48. package/dist/components/footer.js +106 -0
  49. package/dist/components/footer.js.map +1 -0
  50. package/dist/components/form/file-autocomplete.d.ts +19 -4
  51. package/dist/components/form/file-autocomplete.d.ts.map +1 -1
  52. package/dist/components/form/file-autocomplete.js +56 -55
  53. package/dist/components/form/file-autocomplete.js.map +1 -1
  54. package/dist/components/form/file-picker.d.ts.map +1 -1
  55. package/dist/components/form/file-picker.js +26 -15
  56. package/dist/components/form/file-picker.js.map +1 -1
  57. package/dist/components/form/index.d.ts.map +1 -1
  58. package/dist/components/form/index.js +17 -15
  59. package/dist/components/form/index.js.map +1 -1
  60. package/dist/components/form/with-left-border.d.ts.map +1 -1
  61. package/dist/components/form/with-left-border.js +4 -12
  62. package/dist/components/form/with-left-border.js.map +1 -1
  63. package/dist/components/list.d.ts.map +1 -1
  64. package/dist/components/list.js +126 -86
  65. package/dist/components/list.js.map +1 -1
  66. package/dist/components/loading-bar.d.ts.map +1 -1
  67. package/dist/components/loading-bar.js +5 -22
  68. package/dist/components/loading-bar.js.map +1 -1
  69. package/dist/components/loading-text.d.ts.map +1 -1
  70. package/dist/components/loading-text.js +3 -22
  71. package/dist/components/loading-text.js.map +1 -1
  72. package/dist/components/theme-picker.d.ts +2 -0
  73. package/dist/components/theme-picker.d.ts.map +1 -0
  74. package/dist/components/theme-picker.js +37 -0
  75. package/dist/components/theme-picker.js.map +1 -0
  76. package/dist/descendants.d.ts +6 -0
  77. package/dist/descendants.d.ts.map +1 -1
  78. package/dist/descendants.js +74 -8
  79. package/dist/descendants.js.map +1 -1
  80. package/dist/examples/internal/descendants-rerender.d.ts +14 -0
  81. package/dist/examples/internal/descendants-rerender.d.ts.map +1 -0
  82. package/dist/examples/internal/descendants-rerender.js +145 -0
  83. package/dist/examples/internal/descendants-rerender.js.map +1 -0
  84. package/dist/examples/internal/simple-dialog.js +4 -1
  85. package/dist/examples/internal/simple-dialog.js.map +1 -1
  86. package/dist/examples/internal/simple-scrollbox.js +1 -1
  87. package/dist/examples/internal/simple-scrollbox.js.map +1 -1
  88. package/dist/examples/list-with-dropdown.js +1 -1
  89. package/dist/examples/list-with-dropdown.js.map +1 -1
  90. package/dist/examples/miscellaneous.js +1 -1
  91. package/dist/examples/miscellaneous.js.map +1 -1
  92. package/dist/examples/toast-action.d.ts +2 -0
  93. package/dist/examples/toast-action.d.ts.map +1 -0
  94. package/dist/examples/toast-action.js +76 -0
  95. package/dist/examples/toast-action.js.map +1 -0
  96. package/dist/examples/toast-variations.js +38 -36
  97. package/dist/examples/toast-variations.js.map +1 -1
  98. package/dist/extensions/dev.d.ts +1 -1
  99. package/dist/extensions/dev.d.ts.map +1 -1
  100. package/dist/extensions/dev.js +62 -30
  101. package/dist/extensions/dev.js.map +1 -1
  102. package/dist/extensions/home.d.ts.map +1 -1
  103. package/dist/extensions/home.js +4 -3
  104. package/dist/extensions/home.js.map +1 -1
  105. package/dist/extensions/react-refresh-init.d.ts +5 -0
  106. package/dist/extensions/react-refresh-init.d.ts.map +1 -0
  107. package/dist/extensions/react-refresh-init.js +52 -0
  108. package/dist/extensions/react-refresh-init.js.map +1 -0
  109. package/dist/internal/date-picker-widget.js +1 -1
  110. package/dist/internal/date-picker-widget.js.map +1 -1
  111. package/dist/internal/dialog.d.ts +8 -3
  112. package/dist/internal/dialog.d.ts.map +1 -1
  113. package/dist/internal/dialog.js +37 -53
  114. package/dist/internal/dialog.js.map +1 -1
  115. package/dist/internal/navigation.d.ts +1 -0
  116. package/dist/internal/navigation.d.ts.map +1 -1
  117. package/dist/internal/navigation.js +25 -1
  118. package/dist/internal/navigation.js.map +1 -1
  119. package/dist/internal/providers.d.ts.map +1 -1
  120. package/dist/internal/providers.js +9 -197
  121. package/dist/internal/providers.js.map +1 -1
  122. package/dist/internal/scrollbox.d.ts.map +1 -1
  123. package/dist/internal/scrollbox.js +1 -0
  124. package/dist/internal/scrollbox.js.map +1 -1
  125. package/dist/release.d.ts +1 -0
  126. package/dist/release.d.ts.map +1 -1
  127. package/dist/release.js +16 -9
  128. package/dist/release.js.map +1 -1
  129. package/dist/state.d.ts +27 -1
  130. package/dist/state.d.ts.map +1 -1
  131. package/dist/state.js +6 -0
  132. package/dist/state.js.map +1 -1
  133. package/dist/theme.d.ts +6 -19
  134. package/dist/theme.d.ts.map +1 -1
  135. package/dist/theme.js +76 -45
  136. package/dist/theme.js.map +1 -1
  137. package/dist/themes/aura.json +69 -0
  138. package/dist/themes/ayu.json +80 -0
  139. package/dist/themes/catppuccin-frappe.json +233 -0
  140. package/dist/themes/catppuccin-macchiato.json +233 -0
  141. package/dist/themes/catppuccin.json +112 -0
  142. package/dist/themes/cobalt2.json +228 -0
  143. package/dist/themes/cursor.json +249 -0
  144. package/dist/themes/dracula.json +219 -0
  145. package/dist/themes/everforest.json +241 -0
  146. package/dist/themes/flexoki.json +237 -0
  147. package/dist/themes/github-light.json +56 -0
  148. package/dist/themes/github.json +241 -0
  149. package/dist/themes/gruvbox.json +95 -0
  150. package/dist/themes/kanagawa.json +77 -0
  151. package/dist/themes/lucent-orng.json +227 -0
  152. package/dist/themes/material.json +235 -0
  153. package/dist/themes/matrix.json +77 -0
  154. package/dist/themes/mercury.json +245 -0
  155. package/dist/themes/monokai.json +221 -0
  156. package/dist/themes/nightowl.json +221 -0
  157. package/dist/themes/nord.json +223 -0
  158. package/dist/themes/one-dark.json +84 -0
  159. package/dist/themes/opencode-light.json +62 -0
  160. package/dist/themes/opencode.json +245 -0
  161. package/dist/themes/orng.json +245 -0
  162. package/dist/themes/palenight.json +222 -0
  163. package/dist/themes/rosepine.json +234 -0
  164. package/dist/themes/solarized.json +223 -0
  165. package/dist/themes/synthwave84.json +226 -0
  166. package/dist/themes/termcast.json +226 -0
  167. package/dist/themes/tokyonight.json +243 -0
  168. package/dist/themes/vercel.json +255 -0
  169. package/dist/themes/vesper.json +218 -0
  170. package/dist/themes/zenburn.json +223 -0
  171. package/dist/themes.d.ts +57 -0
  172. package/dist/themes.d.ts.map +1 -0
  173. package/dist/themes.js +181 -0
  174. package/dist/themes.js.map +1 -0
  175. package/dist/utils/run-command.d.ts +2 -1
  176. package/dist/utils/run-command.d.ts.map +1 -1
  177. package/dist/utils/run-command.js +20 -10
  178. package/dist/utils/run-command.js.map +1 -1
  179. package/dist/utils.d.ts +2 -1
  180. package/dist/utils.d.ts.map +1 -1
  181. package/dist/utils.js +90 -17
  182. package/dist/utils.js.map +1 -1
  183. package/dist/watcher.d.ts +3 -0
  184. package/dist/watcher.d.ts.map +1 -0
  185. package/dist/watcher.js +16 -0
  186. package/dist/watcher.js.map +1 -0
  187. package/package.json +16 -10
  188. package/src/apis/cache.tsx +5 -44
  189. package/src/apis/hud.tsx +17 -62
  190. package/src/apis/localstorage.tsx +3 -32
  191. package/src/apis/toast.tsx +91 -275
  192. package/src/build.test.tsx +10 -0
  193. package/src/build.tsx +61 -1
  194. package/src/cli.tsx +365 -103
  195. package/src/colors.tsx +1 -0
  196. package/src/compile.tsx +21 -29
  197. package/src/compile.vitest.tsx +300 -0
  198. package/src/components/actions.tsx +64 -45
  199. package/src/components/animation-tick.tsx +85 -0
  200. package/src/components/detail.tsx +31 -35
  201. package/src/components/dropdown.tsx +32 -21
  202. package/src/components/extension-preferences.tsx +14 -10
  203. package/src/components/footer.tsx +241 -0
  204. package/src/components/form/file-autocomplete.tsx +80 -60
  205. package/src/components/form/file-picker.tsx +37 -25
  206. package/src/components/form/index.tsx +45 -41
  207. package/src/components/form/with-left-border.tsx +4 -14
  208. package/src/components/list.tsx +181 -121
  209. package/src/components/loading-bar.tsx +5 -25
  210. package/src/components/loading-text.tsx +4 -23
  211. package/src/components/theme-picker.tsx +57 -0
  212. package/src/descendants.tsx +98 -9
  213. package/src/examples/actions-dialog-layout.vitest.tsx +112 -0
  214. package/src/examples/file-autocomplete.vitest.tsx +131 -122
  215. package/src/examples/form-basic.vitest.tsx +463 -644
  216. package/src/examples/form-dropdown.vitest.tsx +553 -571
  217. package/src/examples/form-scroll.vitest.tsx +112 -102
  218. package/src/examples/form-tagpicker.vitest.tsx +364 -338
  219. package/src/examples/internal/descendants-rerender.tsx +273 -0
  220. package/src/examples/internal/descendants-rerender.vitest.tsx +194 -0
  221. package/src/examples/internal/simple-dialog.tsx +4 -4
  222. package/src/examples/internal/simple-scrollbox.tsx +2 -2
  223. package/src/examples/internal/simple-scrollbox.vitest.tsx +43 -31
  224. package/src/examples/list-detail-metadata.vitest.tsx +34 -30
  225. package/src/examples/list-dropdown-default.vitest.tsx +84 -72
  226. package/src/examples/list-empty-view.vitest.tsx +93 -0
  227. package/src/examples/list-fetch-data.vitest.tsx +36 -30
  228. package/src/examples/list-scrollbox.vitest.tsx +59 -39
  229. package/src/examples/list-with-detail.vitest.tsx +339 -314
  230. package/src/examples/list-with-dropdown.tsx +1 -0
  231. package/src/examples/list-with-dropdown.vitest.tsx +176 -150
  232. package/src/examples/list-with-sections.vitest.tsx +289 -270
  233. package/src/examples/list-with-toast.vitest.tsx +44 -44
  234. package/src/examples/miscellaneous.tsx +10 -0
  235. package/src/examples/simple-file-picker.vitest.tsx +90 -86
  236. package/src/examples/simple-grid.vitest.tsx +275 -249
  237. package/src/examples/simple-navigation.vitest.tsx +192 -168
  238. package/src/examples/store.vitest.tsx +6 -4
  239. package/src/examples/swift-extension.vitest.tsx +31 -19
  240. package/src/examples/synonyms.vitest.tsx +93 -83
  241. package/src/examples/toast-action.tsx +160 -0
  242. package/src/examples/toast-action.vitest.tsx +404 -0
  243. package/src/examples/toast-variations.tsx +58 -57
  244. package/src/examples/toast-variations.vitest.tsx +186 -166
  245. package/src/extensions/dev.tsx +74 -33
  246. package/src/extensions/dev.vitest.tsx +162 -69
  247. package/src/extensions/home.tsx +5 -6
  248. package/src/extensions/react-refresh-init.tsx +59 -0
  249. package/src/internal/date-picker-widget.tsx +1 -1
  250. package/src/internal/dialog.tsx +59 -83
  251. package/src/internal/navigation.tsx +37 -4
  252. package/src/internal/providers.tsx +27 -315
  253. package/src/internal/scrollbox.tsx +1 -0
  254. package/src/release.tsx +16 -10
  255. package/src/state.tsx +36 -3
  256. package/src/theme.tsx +82 -51
  257. package/src/themes/aura.json +69 -0
  258. package/src/themes/ayu.json +80 -0
  259. package/src/themes/catppuccin-frappe.json +233 -0
  260. package/src/themes/catppuccin-macchiato.json +233 -0
  261. package/src/themes/catppuccin.json +112 -0
  262. package/src/themes/cobalt2.json +228 -0
  263. package/src/themes/cursor.json +249 -0
  264. package/src/themes/dracula.json +219 -0
  265. package/src/themes/everforest.json +241 -0
  266. package/src/themes/flexoki.json +237 -0
  267. package/src/themes/github-light.json +56 -0
  268. package/src/themes/github.json +241 -0
  269. package/src/themes/gruvbox.json +95 -0
  270. package/src/themes/kanagawa.json +77 -0
  271. package/src/themes/lucent-orng.json +227 -0
  272. package/src/themes/material.json +235 -0
  273. package/src/themes/matrix.json +77 -0
  274. package/src/themes/mercury.json +252 -0
  275. package/src/themes/monokai.json +221 -0
  276. package/src/themes/nightowl.json +221 -0
  277. package/src/themes/nord.json +223 -0
  278. package/src/themes/one-dark.json +84 -0
  279. package/src/themes/opencode-light.json +62 -0
  280. package/src/themes/opencode.json +245 -0
  281. package/src/themes/orng.json +245 -0
  282. package/src/themes/palenight.json +222 -0
  283. package/src/themes/rosepine.json +234 -0
  284. package/src/themes/solarized.json +223 -0
  285. package/src/themes/synthwave84.json +226 -0
  286. package/src/themes/termcast.json +227 -0
  287. package/src/themes/tokyonight.json +243 -0
  288. package/src/themes/vercel.json +255 -0
  289. package/src/themes/vesper.json +218 -0
  290. package/src/themes/zenburn.json +223 -0
  291. package/src/themes.ts +291 -0
  292. package/src/utils/run-command.tsx +23 -12
  293. package/src/utils.tsx +115 -18
  294. package/src/watcher.tsx +19 -0
@@ -0,0 +1,273 @@
1
+ // Test useDescendantsRerender() behavior in different component positions
2
+ // This example tracks render counts to verify:
3
+ // 1. Hook in parent of descendants
4
+ // 2. Hook in independent child component
5
+ // 3. Hook in a descendant item component
6
+ //
7
+ import { createDescendants } from 'termcast/src/descendants'
8
+ import { createContext, useContext, useState, useRef } from 'react'
9
+ import { renderWithProviders } from '../../utils'
10
+ import { useKeyboard } from '@opentui/react'
11
+ import { useIsInFocus } from 'termcast/src/internal/focus-context'
12
+
13
+ // Create descendants with the new useDescendantsRerender hook
14
+ const {
15
+ DescendantsProvider,
16
+ useDescendants,
17
+ useDescendant,
18
+ useDescendantsRerender,
19
+ } = createDescendants<{
20
+ title: string
21
+ }>()
22
+
23
+ // Global render counters for testing
24
+ const renderCounts = {
25
+ parent: 0,
26
+ parentWithHook: 0,
27
+ independentChild: 0,
28
+ independentChildWithHook: 0,
29
+ descendantItem: 0,
30
+ descendantItemWithHook: 0,
31
+ }
32
+
33
+ // Reset counters - call between test scenarios
34
+ export function resetRenderCounts() {
35
+ renderCounts.parent = 0
36
+ renderCounts.parentWithHook = 0
37
+ renderCounts.independentChild = 0
38
+ renderCounts.independentChildWithHook = 0
39
+ renderCounts.descendantItem = 0
40
+ renderCounts.descendantItemWithHook = 0
41
+ }
42
+
43
+ // Get current render counts
44
+ export function getRenderCounts() {
45
+ return { ...renderCounts }
46
+ }
47
+
48
+ // Context to share item count for display
49
+ const ItemCountContext = createContext<number>(0)
50
+
51
+ // ============================================
52
+ // SCENARIO 1: Hook in parent of descendants
53
+ // The hook must be called INSIDE the provider, so we need a wrapper
54
+ // ============================================
55
+ function ParentWithHookInner({ children }: { children: React.ReactNode }) {
56
+ renderCounts.parentWithHook++
57
+
58
+ // Using the hook inside provider - will this cause infinite loop?
59
+ const descendants = useDescendantsRerender()
60
+ const itemCount = Object.keys(descendants).length
61
+
62
+ return (
63
+ <ItemCountContext.Provider value={itemCount}>
64
+ <box flexDirection="column">
65
+ <text>Parent with hook - renders: {renderCounts.parentWithHook}</text>
66
+ <text>Item count (from hook): {itemCount}</text>
67
+ {children}
68
+ </box>
69
+ </ItemCountContext.Provider>
70
+ )
71
+ }
72
+
73
+ function ParentWithHook({ children }: { children: React.ReactNode }) {
74
+ const descendantsContext = useDescendants()
75
+
76
+ return (
77
+ <DescendantsProvider value={descendantsContext}>
78
+ <ParentWithHookInner>{children}</ParentWithHookInner>
79
+ </DescendantsProvider>
80
+ )
81
+ }
82
+
83
+ // ============================================
84
+ // SCENARIO 2: Parent without hook (baseline)
85
+ // ============================================
86
+ function ParentWithoutHook({ children }: { children: React.ReactNode }) {
87
+ renderCounts.parent++
88
+ const descendantsContext = useDescendants()
89
+
90
+ return (
91
+ <DescendantsProvider value={descendantsContext}>
92
+ <box flexDirection="column">
93
+ <text>Parent without hook - renders: {renderCounts.parent}</text>
94
+ {children}
95
+ </box>
96
+ </DescendantsProvider>
97
+ )
98
+ }
99
+
100
+ // ============================================
101
+ // SCENARIO 3: Independent child with hook
102
+ // ============================================
103
+ function IndependentChildWithHook() {
104
+ renderCounts.independentChildWithHook++
105
+ const descendants = useDescendantsRerender()
106
+ const itemCount = Object.keys(descendants).length
107
+
108
+ return (
109
+ <box flexShrink={0}>
110
+ <text>
111
+ Independent child with hook - renders:{' '}
112
+ {renderCounts.independentChildWithHook}, items: {itemCount}
113
+ </text>
114
+ </box>
115
+ )
116
+ }
117
+
118
+ // ============================================
119
+ // SCENARIO 4: Independent child without hook
120
+ // ============================================
121
+ function IndependentChildWithoutHook() {
122
+ renderCounts.independentChild++
123
+
124
+ return (
125
+ <box flexShrink={0}>
126
+ <text>
127
+ Independent child without hook - renders:{' '}
128
+ {renderCounts.independentChild}
129
+ </text>
130
+ </box>
131
+ )
132
+ }
133
+
134
+ // ============================================
135
+ // SCENARIO 5: Descendant item with hook
136
+ // ============================================
137
+ function DescendantItemWithHook({ title }: { title: string }) {
138
+ renderCounts.descendantItemWithHook++
139
+ const { index } = useDescendant({ title })
140
+ const descendants = useDescendantsRerender()
141
+ const totalItems = Object.keys(descendants).length
142
+
143
+ return (
144
+ <text>
145
+ [{index}] {title} (with hook, renders: {renderCounts.descendantItemWithHook}, total: {totalItems})
146
+ </text>
147
+ )
148
+ }
149
+
150
+ // ============================================
151
+ // SCENARIO 6: Descendant item without hook
152
+ // ============================================
153
+ function DescendantItemWithoutHook({ title }: { title: string }) {
154
+ renderCounts.descendantItem++
155
+ const { index } = useDescendant({ title })
156
+
157
+ return (
158
+ <text>
159
+ [{index}] {title} (without hook, renders: {renderCounts.descendantItem})
160
+ </text>
161
+ )
162
+ }
163
+
164
+ // ============================================
165
+ // Main example component
166
+ // ============================================
167
+ type Scenario =
168
+ | 'parent-with-hook'
169
+ | 'parent-without-hook'
170
+ | 'independent-child-with-hook'
171
+ | 'descendant-item-with-hook'
172
+
173
+ function Example({ scenario }: { scenario: Scenario }) {
174
+ const [items, setItems] = useState(['Apple', 'Banana', 'Cherry'])
175
+ const inFocus = useIsInFocus()
176
+
177
+ useKeyboard((evt) => {
178
+ if (!inFocus) return
179
+
180
+ if (evt.name === 'a') {
181
+ // Add item with stable key
182
+ setItems((prev) => [...prev, `Item ${prev.length + 1}`])
183
+ } else if (evt.name === 'd' && items.length > 0) {
184
+ // Delete last item
185
+ setItems((prev) => prev.slice(0, -1))
186
+ } else if (evt.name === 'r') {
187
+ // Reset counts
188
+ resetRenderCounts()
189
+ }
190
+ })
191
+
192
+ const renderScenario = () => {
193
+ switch (scenario) {
194
+ case 'parent-with-hook':
195
+ return (
196
+ <ParentWithHook>
197
+ {items.map((item) => (
198
+ <DescendantItemWithoutHook key={item} title={item} />
199
+ ))}
200
+ </ParentWithHook>
201
+ )
202
+
203
+ case 'parent-without-hook':
204
+ return (
205
+ <ParentWithoutHook>
206
+ {items.map((item) => (
207
+ <DescendantItemWithoutHook key={item} title={item} />
208
+ ))}
209
+ </ParentWithoutHook>
210
+ )
211
+
212
+ case 'independent-child-with-hook':
213
+ return (
214
+ <ParentWithoutHook>
215
+ <IndependentChildWithHook />
216
+ <IndependentChildWithoutHook />
217
+ {items.map((item) => (
218
+ <DescendantItemWithoutHook key={item} title={item} />
219
+ ))}
220
+ </ParentWithoutHook>
221
+ )
222
+
223
+ case 'descendant-item-with-hook':
224
+ return (
225
+ <ParentWithoutHook>
226
+ {items.map((item) => (
227
+ <DescendantItemWithHook key={item} title={item} />
228
+ ))}
229
+ </ParentWithoutHook>
230
+ )
231
+ }
232
+ }
233
+
234
+ return (
235
+ <box flexDirection="column">
236
+ <text>Scenario: {scenario}</text>
237
+ <text>Items: {items.length} | [a] add | [d] delete | [r] reset counts</text>
238
+ <box marginTop={1}>{renderScenario()}</box>
239
+ <box marginTop={1} borderStyle="single" padding={1}>
240
+ <box flexDirection="column">
241
+ <text>Render Counts:</text>
242
+ <text> parent: {renderCounts.parent}</text>
243
+ <text> parentWithHook: {renderCounts.parentWithHook}</text>
244
+ <text> independentChild: {renderCounts.independentChild}</text>
245
+ <text> independentChildWithHook: {renderCounts.independentChildWithHook}</text>
246
+ <text> descendantItem: {renderCounts.descendantItem}</text>
247
+ <text> descendantItemWithHook: {renderCounts.descendantItemWithHook}</text>
248
+ </box>
249
+ </box>
250
+ </box>
251
+ )
252
+ }
253
+
254
+ // Export for different scenarios
255
+ export function ParentWithHookExample() {
256
+ return <Example scenario="parent-with-hook" />
257
+ }
258
+
259
+ export function ParentWithoutHookExample() {
260
+ return <Example scenario="parent-without-hook" />
261
+ }
262
+
263
+ export function IndependentChildWithHookExample() {
264
+ return <Example scenario="independent-child-with-hook" />
265
+ }
266
+
267
+ export function DescendantItemWithHookExample() {
268
+ return <Example scenario="descendant-item-with-hook" />
269
+ }
270
+
271
+ // Default export runs parent-with-hook scenario
272
+ const scenario = (process.argv[2] as Scenario) || 'parent-with-hook'
273
+ await renderWithProviders(<Example scenario={scenario} />)
@@ -0,0 +1,194 @@
1
+ import { expect, test } from 'vitest'
2
+ import { launchTerminal } from 'tuistory/src'
3
+
4
+ test(
5
+ 'useDescendantsRerender - compare render counts with and without hook',
6
+ async () => {
7
+ // First test WITHOUT hook (baseline)
8
+ const sessionWithout = await launchTerminal({
9
+ command: 'bun',
10
+ args: [
11
+ 'src/examples/internal/descendants-rerender.tsx',
12
+ 'parent-without-hook',
13
+ ],
14
+ })
15
+
16
+ const withoutHook = await sessionWithout.text({
17
+ waitFor: (text) =>
18
+ text.includes('Parent without hook') && text.includes('Render Counts'),
19
+ })
20
+
21
+ expect(withoutHook).toMatchInlineSnapshot(`
22
+ "
23
+
24
+
25
+ Scenario: parent-without-hook
26
+ Items: 3 | [a] add | [d] delete | [r] reset counts
27
+
28
+ Parent without hook - renders: 2
29
+ [0] Apple (without hook, renders: 7)
30
+ [1] Banana (without hook, renders: 8)
31
+ [2] Cherry (without hook, renders: 9)
32
+
33
+ ┌──────────────────────────────────────────────────────────────────────────┐
34
+ │ │
35
+ │ Render Counts: │
36
+ │ parent: 1 │
37
+ │ parentWithHook: 0 │
38
+ │ independentChild: 0 │
39
+ │ independentChildWithHook: 0 │
40
+ │ descendantItem: 3 │
41
+ │ descendantItemWithHook: 0 │
42
+ │ │
43
+ └──────────────────────────────────────────────────────────────────────────┘
44
+
45
+
46
+ "
47
+ `)
48
+
49
+ sessionWithout.close()
50
+
51
+ // Then test WITH hook
52
+ const sessionWith = await launchTerminal({
53
+ command: 'bun',
54
+ args: [
55
+ 'src/examples/internal/descendants-rerender.tsx',
56
+ 'parent-with-hook',
57
+ ],
58
+ })
59
+
60
+ const withHook = await sessionWith.text({
61
+ waitFor: (text) =>
62
+ text.includes('Parent with hook') && text.includes('Render Counts'),
63
+ })
64
+
65
+ expect(withHook).toMatchInlineSnapshot(`
66
+ "
67
+
68
+
69
+ Scenario: parent-with-hook
70
+ Items: 3 | [a] add | [d] delete | [r] reset counts
71
+
72
+ Parent with hook - renders: 3
73
+ Item count (from hook): 3
74
+ [0] Apple (without hook, renders: 7)
75
+ [1] Banana (without hook, renders: 8)
76
+ [2] Cherry (without hook, renders: 9)
77
+
78
+ ┌──────────────────────────────────────────────────────────────────────────┐
79
+ │ │
80
+ │ Render Counts: │
81
+ │ parent: 0 │
82
+ │ parentWithHook: 1 │
83
+ │ independentChild: 0 │
84
+ │ independentChildWithHook: 0 │
85
+ │ descendantItem: 3 │
86
+ │ descendantItemWithHook: 0 │
87
+ │ │
88
+ └──────────────────────────────────────────────────────────────────────────┘
89
+
90
+ "
91
+ `)
92
+
93
+ sessionWith.close()
94
+ },
95
+ 60000,
96
+ )
97
+
98
+ test(
99
+ 'useDescendantsRerender - independent child with hook',
100
+ async () => {
101
+ const session = await launchTerminal({
102
+ command: 'bun',
103
+ args: [
104
+ 'src/examples/internal/descendants-rerender.tsx',
105
+ 'independent-child-with-hook',
106
+ ],
107
+ })
108
+
109
+ const text = await session.text({
110
+ // waitFor: (text) =>
111
+ // text.includes('Independent child with hook')
112
+ })
113
+
114
+ expect(text).toMatchInlineSnapshot(`
115
+ "
116
+
117
+
118
+ Items:i3:|i[a]padde|t[d]ideleteh|h[r] reset counts
119
+
120
+
121
+ Parent without hook - renders: 2
122
+ Independent child with hook - renders: 3, items: 3
123
+ [0]eApplen(withoutwhook,trenders:r7)ders: 2
124
+ [1] Banana (without hook, renders: 8)
125
+ [2] Cherry (without hook, renders: 9)
126
+
127
+
128
+ ┌──────────────────────────────────────────────────────────────────────────┐
129
+ │ │
130
+ │ Reparent:u1ts: │
131
+ │ parentWithHook: 0 │
132
+ │ independentChild: 1 │
133
+ │ independentChildWithHook: 1 │
134
+ │ descendantItem: 3 │
135
+ │ descendantItemWithHook: 0 │
136
+ │ │
137
+ └──────────────────────────────────────────────────────────────────────────┘
138
+
139
+ "
140
+ `)
141
+
142
+ session.close()
143
+ },
144
+ 30000,
145
+ )
146
+
147
+ test(
148
+ 'useDescendantsRerender - descendant item with hook',
149
+ async () => {
150
+ const session = await launchTerminal({
151
+ command: 'bun',
152
+ args: [
153
+ 'src/examples/internal/descendants-rerender.tsx',
154
+ 'descendant-item-with-hook',
155
+ ],
156
+ })
157
+
158
+ const text = await session.text({
159
+ waitFor: (text) =>
160
+ text.includes('with hook') && text.includes('Render Counts'),
161
+ })
162
+
163
+ expect(text).toMatchInlineSnapshot(`
164
+ "
165
+
166
+
167
+ Scenario: descendant-item-with-hook
168
+ Items: 3 | [a] add | [d] delete | [r] reset counts
169
+
170
+ Parent without hook - renders: 2
171
+ [0] Apple (with hook, renders: 7, total: 3)
172
+ [1] Banana (with hook, renders: 8, total: 3)
173
+ [2] Cherry (with hook, renders: 9, total: 3)
174
+
175
+ ┌──────────────────────────────────────────────────────────────────────────┐
176
+ │ │
177
+ │ Render Counts: │
178
+ │ parent: 1 │
179
+ │ parentWithHook: 0 │
180
+ │ independentChild: 0 │
181
+ │ independentChildWithHook: 0 │
182
+ │ descendantItem: 0 │
183
+ │ descendantItemWithHook: 3 │
184
+ │ │
185
+ └──────────────────────────────────────────────────────────────────────────┘
186
+
187
+
188
+ "
189
+ `)
190
+
191
+ session.close()
192
+ },
193
+ 30000,
194
+ )
@@ -101,10 +101,10 @@ function App(): any {
101
101
  <Action
102
102
  title={`Open ${item.title} Dialog`}
103
103
  onAction={() => {
104
- dialog.push(
105
- <DialogContent position={item.position} />,
106
- item.position,
107
- )
104
+ dialog.push({
105
+ element: <DialogContent position={item.position} />,
106
+ position: item.position,
107
+ })
108
108
  }}
109
109
  />
110
110
  <Action
@@ -30,7 +30,7 @@ function SimpleScrollBoxDemo(): any {
30
30
  </text>
31
31
  </box>
32
32
 
33
- <ScrollBox focused flexGrow={1} flexShrink={1}>
33
+ <ScrollBox focused flexShrink={1}>
34
34
  {Array.from({ length: 30 }, (_, i) => (
35
35
  <box
36
36
  key={i}
@@ -55,4 +55,4 @@ function SimpleScrollBoxDemo(): any {
55
55
 
56
56
  if (import.meta.main) {
57
57
  await renderWithProviders(<SimpleScrollBoxDemo />)
58
- }
58
+ }
@@ -17,87 +17,99 @@ test('simple scrollbox navigation and scrolling', async () => {
17
17
 
18
18
 
19
19
 
20
- Simple ScrollBox Demo
21
-
22
- Item 1 - This is content for item number 1. Lorem ipsum dolor sit amet,
23
- consectetur adipiscing elit.
20
+ Simple ScrollBox Demo
21
+
22
+ Item 1 - This is content for item number 1. Lorem ipsum dolor sit amet,
23
+ consectetur adipiscing elit.
24
24
 
25
25
 
26
26
 
27
- Item 2 - This is content for item number 2. Lorem ipsum dolor sit amet,
28
- consectetur adipiscing elit.
27
+ Item 2 - This is content for item number 2. Lorem ipsum dolor sit amet,
28
+ consectetur adipiscing elit.
29
29
 
30
30
 
31
31
 
32
- Item 3 - This is content for item number 3. Lorem ipsum dolor sit amet,
33
- consectetur adipiscing elit.
32
+ Item 3 - This is content for item number 3. Lorem ipsum dolor sit amet,
33
+ consectetur adipiscing elit.
34
34
 
35
35
 
36
36
 
37
37
 
38
- Use mouse scroll or arrow keys | Press [q] to quit"
38
+ Use mouse scroll or arrow keys | Press [q] to quit
39
+
40
+ "
39
41
  `)
40
42
 
41
43
  // Scroll down to see more items
42
44
  await session.scrollDown(3)
43
45
 
44
- const afterScrollDownSnapshot = await session.text()
45
- expect(afterScrollDownSnapshot).not.toEqual(initialText)
46
+ // Wait for Item 4 to appear (proves scroll happened)
47
+ const afterScrollDownSnapshot = await session.text({
48
+ waitFor: (text) => text.includes('Item 4'),
49
+ timeout: 5000,
50
+ })
46
51
  expect(afterScrollDownSnapshot).toMatchInlineSnapshot(`
47
52
  "
48
53
 
49
54
 
50
55
 
51
- Simple ScrollBox Demo
52
- Item 1 - This is content for item number 1. Lorem ipsum dolor sit amet, ▀
53
- consectetur adipiscing elit.
56
+ Simple ScrollBox Demo
57
+ Item 1 - This is content for item number 1. Lorem ipsum dolor sit amet, ▀
58
+ consectetur adipiscing elit.
59
+
54
60
 
55
61
 
62
+ Item 2 - This is content for item number 2. Lorem ipsum dolor sit amet,
63
+ consectetur adipiscing elit.
56
64
 
57
- Item 2 - This is content for item number 2. Lorem ipsum dolor sit amet,
58
- consectetur adipiscing elit.
59
65
 
60
66
 
67
+ Item 3 - This is content for item number 3. Lorem ipsum dolor sit amet,
68
+ consectetur adipiscing elit.
61
69
 
62
- Item 3 - This is content for item number 3. Lorem ipsum dolor sit amet,
63
- consectetur adipiscing elit.
64
70
 
65
71
 
72
+ Item 4 - This is content for item number 4. Lorem ipsum dolor sit amet,
66
73
 
67
- Item 4 - This is content for item number 4. Lorem ipsum dolor sit amet,
74
+ Use mouse scroll or arrow keys | Press [q] to quit
68
75
 
69
- Use mouse scroll or arrow keys | Press [q] to quit"
76
+ "
70
77
  `)
71
78
 
72
79
  // Scroll back up
73
80
  await session.scrollUp(2)
74
81
 
75
- const afterScrollUpSnapshot = await session.text()
76
- expect(afterScrollUpSnapshot).not.toEqual(afterScrollDownSnapshot)
82
+ // Wait for scrollbar to move back up (proves scroll happened)
83
+ const afterScrollUpSnapshot = await session.text({
84
+ waitFor: (text) => text !== afterScrollDownSnapshot,
85
+ timeout: 5000,
86
+ })
77
87
  expect(afterScrollUpSnapshot).toMatchInlineSnapshot(`
78
88
  "
79
89
 
80
90
 
81
91
 
82
- Simple ScrollBox Demo
83
-
84
- Item 1 - This is content for item number 1. Lorem ipsum dolor sit amet,
85
- consectetur adipiscing elit.
92
+ Simple ScrollBox Demo
93
+
94
+ Item 1 - This is content for item number 1. Lorem ipsum dolor sit amet,
95
+ consectetur adipiscing elit.
86
96
 
87
97
 
88
98
 
89
- Item 2 - This is content for item number 2. Lorem ipsum dolor sit amet,
90
- consectetur adipiscing elit.
99
+ Item 2 - This is content for item number 2. Lorem ipsum dolor sit amet,
100
+ consectetur adipiscing elit.
91
101
 
92
102
 
93
103
 
94
- Item 3 - This is content for item number 3. Lorem ipsum dolor sit amet,
95
- consectetur adipiscing elit.
104
+ Item 3 - This is content for item number 3. Lorem ipsum dolor sit amet,
105
+ consectetur adipiscing elit.
96
106
 
97
107
 
98
108
 
99
109
 
100
- Use mouse scroll or arrow keys | Press [q] to quit"
110
+ Use mouse scroll or arrow keys | Press [q] to quit
111
+
112
+ "
101
113
  `)
102
114
 
103
115
  await session.press('esc')