@effect-tui/react 0.1.1 → 0.1.4

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 (442) hide show
  1. package/dist/jsx-runtime.d.ts +13 -0
  2. package/dist/jsx-runtime.d.ts.map +1 -1
  3. package/dist/jsx-runtime.js.map +1 -1
  4. package/dist/src/codeblock.d.ts.map +1 -1
  5. package/dist/src/codeblock.js.map +1 -1
  6. package/dist/src/components/Divider.d.ts +18 -0
  7. package/dist/src/components/Divider.d.ts.map +1 -0
  8. package/dist/src/components/Divider.js +17 -0
  9. package/dist/src/components/Divider.js.map +1 -0
  10. package/dist/src/components/Markdown.d.ts +66 -0
  11. package/dist/src/components/Markdown.d.ts.map +1 -0
  12. package/dist/src/components/Markdown.js +226 -0
  13. package/dist/src/components/Markdown.js.map +1 -0
  14. package/dist/src/components/MultilineTextInput.d.ts +65 -0
  15. package/dist/src/components/MultilineTextInput.d.ts.map +1 -0
  16. package/dist/src/components/MultilineTextInput.js +607 -0
  17. package/dist/src/components/MultilineTextInput.js.map +1 -0
  18. package/dist/src/components/Overlay.d.ts +46 -0
  19. package/dist/src/components/Overlay.d.ts.map +1 -0
  20. package/dist/src/components/Overlay.js +11 -0
  21. package/dist/src/components/Overlay.js.map +1 -0
  22. package/dist/src/components/Static.d.ts +44 -0
  23. package/dist/src/components/Static.d.ts.map +1 -0
  24. package/dist/src/components/Static.js +53 -0
  25. package/dist/src/components/Static.js.map +1 -0
  26. package/dist/src/components/TextInput.d.ts +53 -0
  27. package/dist/src/components/TextInput.d.ts.map +1 -0
  28. package/dist/src/components/TextInput.js +210 -0
  29. package/dist/src/components/TextInput.js.map +1 -0
  30. package/dist/src/components/index.d.ts +7 -0
  31. package/dist/src/components/index.d.ts.map +1 -0
  32. package/dist/src/components/index.js +7 -0
  33. package/dist/src/components/index.js.map +1 -0
  34. package/dist/src/components/text-editing.d.ts +62 -0
  35. package/dist/src/components/text-editing.d.ts.map +1 -0
  36. package/dist/src/components/text-editing.js +385 -0
  37. package/dist/src/components/text-editing.js.map +1 -0
  38. package/dist/src/console/ConsoleCapture.d.ts +36 -0
  39. package/dist/src/console/ConsoleCapture.d.ts.map +1 -0
  40. package/dist/src/console/ConsoleCapture.js +210 -0
  41. package/dist/src/console/ConsoleCapture.js.map +1 -0
  42. package/dist/src/console/ConsolePopover.d.ts +18 -0
  43. package/dist/src/console/ConsolePopover.d.ts.map +1 -0
  44. package/dist/src/console/ConsolePopover.js +324 -0
  45. package/dist/src/console/ConsolePopover.js.map +1 -0
  46. package/dist/src/console/clipboard.d.ts +10 -0
  47. package/dist/src/console/clipboard.d.ts.map +1 -0
  48. package/dist/src/console/clipboard.js +74 -0
  49. package/dist/src/console/clipboard.js.map +1 -0
  50. package/dist/src/console/index.d.ts +5 -0
  51. package/dist/src/console/index.d.ts.map +1 -0
  52. package/dist/src/console/index.js +33 -0
  53. package/dist/src/console/index.js.map +1 -0
  54. package/dist/src/console/useConsole.d.ts +44 -0
  55. package/dist/src/console/useConsole.d.ts.map +1 -0
  56. package/dist/src/console/useConsole.js +91 -0
  57. package/dist/src/console/useConsole.js.map +1 -0
  58. package/dist/src/debug/DebugOverlay.d.ts +49 -0
  59. package/dist/src/debug/DebugOverlay.d.ts.map +1 -0
  60. package/dist/src/debug/DebugOverlay.js +438 -0
  61. package/dist/src/debug/DebugOverlay.js.map +1 -0
  62. package/dist/src/debug/DiagnosticsPanel.d.ts.map +1 -1
  63. package/dist/src/debug/DiagnosticsPanel.js.map +1 -1
  64. package/dist/src/dev/Toast.d.ts +19 -0
  65. package/dist/src/dev/Toast.d.ts.map +1 -0
  66. package/dist/src/dev/Toast.js +72 -0
  67. package/dist/src/dev/Toast.js.map +1 -0
  68. package/dist/src/dev/index.d.ts +2 -0
  69. package/dist/src/dev/index.d.ts.map +1 -0
  70. package/dist/src/dev/index.js +3 -0
  71. package/dist/src/dev/index.js.map +1 -0
  72. package/dist/src/dev.d.ts +114 -0
  73. package/dist/src/dev.d.ts.map +1 -0
  74. package/dist/src/dev.js +373 -0
  75. package/dist/src/dev.js.map +1 -0
  76. package/dist/src/highlight.d.ts +3 -3
  77. package/dist/src/highlight.d.ts.map +1 -1
  78. package/dist/src/highlight.js.map +1 -1
  79. package/dist/src/hmr-plugin.d.ts +2 -0
  80. package/dist/src/hmr-plugin.d.ts.map +1 -0
  81. package/dist/src/hmr-plugin.js +53 -0
  82. package/dist/src/hmr-plugin.js.map +1 -0
  83. package/dist/src/hooks/index.d.ts +4 -0
  84. package/dist/src/hooks/index.d.ts.map +1 -1
  85. package/dist/src/hooks/index.js +2 -0
  86. package/dist/src/hooks/index.js.map +1 -1
  87. package/dist/src/hooks/use-keyboard.d.ts +11 -0
  88. package/dist/src/hooks/use-keyboard.d.ts.map +1 -1
  89. package/dist/src/hooks/use-keyboard.js +22 -4
  90. package/dist/src/hooks/use-keyboard.js.map +1 -1
  91. package/dist/src/hooks/use-mouse.d.ts +24 -0
  92. package/dist/src/hooks/use-mouse.d.ts.map +1 -0
  93. package/dist/src/hooks/use-mouse.js +41 -0
  94. package/dist/src/hooks/use-mouse.js.map +1 -0
  95. package/dist/src/hooks/use-paste.d.ts +11 -0
  96. package/dist/src/hooks/use-paste.d.ts.map +1 -1
  97. package/dist/src/hooks/use-paste.js +17 -3
  98. package/dist/src/hooks/use-paste.js.map +1 -1
  99. package/dist/src/hooks/use-scroll.d.ts +79 -0
  100. package/dist/src/hooks/use-scroll.d.ts.map +1 -0
  101. package/dist/src/hooks/use-scroll.js +239 -0
  102. package/dist/src/hooks/use-scroll.js.map +1 -0
  103. package/dist/src/hooks/useFrameStats.js.map +1 -1
  104. package/dist/src/hosts/base.d.ts +62 -1
  105. package/dist/src/hosts/base.d.ts.map +1 -1
  106. package/dist/src/hosts/base.js +118 -5
  107. package/dist/src/hosts/base.js.map +1 -1
  108. package/dist/src/hosts/box.d.ts +7 -7
  109. package/dist/src/hosts/box.d.ts.map +1 -1
  110. package/dist/src/hosts/box.js +31 -26
  111. package/dist/src/hosts/box.js.map +1 -1
  112. package/dist/src/hosts/canvas.d.ts +8 -8
  113. package/dist/src/hosts/canvas.d.ts.map +1 -1
  114. package/dist/src/hosts/canvas.js +13 -22
  115. package/dist/src/hosts/canvas.js.map +1 -1
  116. package/dist/src/hosts/codeblock.d.ts +7 -7
  117. package/dist/src/hosts/codeblock.d.ts.map +1 -1
  118. package/dist/src/hosts/codeblock.js +11 -20
  119. package/dist/src/hosts/codeblock.js.map +1 -1
  120. package/dist/src/hosts/flex-container.d.ts +45 -0
  121. package/dist/src/hosts/flex-container.d.ts.map +1 -0
  122. package/dist/src/hosts/flex-container.js +90 -0
  123. package/dist/src/hosts/flex-container.js.map +1 -0
  124. package/dist/src/hosts/hstack.d.ts +6 -11
  125. package/dist/src/hosts/hstack.d.ts.map +1 -1
  126. package/dist/src/hosts/hstack.js +6 -41
  127. package/dist/src/hosts/hstack.js.map +1 -1
  128. package/dist/src/hosts/index.d.ts +4 -0
  129. package/dist/src/hosts/index.d.ts.map +1 -1
  130. package/dist/src/hosts/index.js +10 -0
  131. package/dist/src/hosts/index.js.map +1 -1
  132. package/dist/src/hosts/overlay-item.d.ts +32 -0
  133. package/dist/src/hosts/overlay-item.d.ts.map +1 -0
  134. package/dist/src/hosts/overlay-item.js +54 -0
  135. package/dist/src/hosts/overlay-item.js.map +1 -0
  136. package/dist/src/hosts/overlay.d.ts +30 -0
  137. package/dist/src/hosts/overlay.d.ts.map +1 -0
  138. package/dist/src/hosts/overlay.js +105 -0
  139. package/dist/src/hosts/overlay.js.map +1 -0
  140. package/dist/src/hosts/scroll.d.ts +56 -0
  141. package/dist/src/hosts/scroll.d.ts.map +1 -0
  142. package/dist/src/hosts/scroll.js +204 -0
  143. package/dist/src/hosts/scroll.js.map +1 -0
  144. package/dist/src/hosts/single-child.d.ts +16 -0
  145. package/dist/src/hosts/single-child.d.ts.map +1 -0
  146. package/dist/src/hosts/single-child.js +45 -0
  147. package/dist/src/hosts/single-child.js.map +1 -0
  148. package/dist/src/hosts/spacer.d.ts.map +1 -1
  149. package/dist/src/hosts/spacer.js +7 -3
  150. package/dist/src/hosts/spacer.js.map +1 -1
  151. package/dist/src/hosts/text.d.ts +9 -6
  152. package/dist/src/hosts/text.d.ts.map +1 -1
  153. package/dist/src/hosts/text.js +49 -22
  154. package/dist/src/hosts/text.js.map +1 -1
  155. package/dist/src/hosts/vstack.d.ts +6 -11
  156. package/dist/src/hosts/vstack.d.ts.map +1 -1
  157. package/dist/src/hosts/vstack.js +6 -41
  158. package/dist/src/hosts/vstack.js.map +1 -1
  159. package/dist/src/hosts/zstack.d.ts.map +1 -1
  160. package/dist/src/hosts/zstack.js +16 -5
  161. package/dist/src/hosts/zstack.js.map +1 -1
  162. package/dist/src/index.d.ts +9 -2
  163. package/dist/src/index.d.ts.map +1 -1
  164. package/dist/src/index.js +10 -2
  165. package/dist/src/index.js.map +1 -1
  166. package/dist/src/inline/index.d.ts.map +1 -1
  167. package/dist/src/inline/index.js.map +1 -1
  168. package/dist/src/motion/color-motion-value.d.ts.map +1 -1
  169. package/dist/src/motion/color-motion-value.js.map +1 -1
  170. package/dist/src/motion/color.d.ts +1 -29
  171. package/dist/src/motion/color.d.ts.map +1 -1
  172. package/dist/src/motion/color.js +2 -170
  173. package/dist/src/motion/color.js.map +1 -1
  174. package/dist/src/motion/color.test.js.map +1 -1
  175. package/dist/src/motion/event-emitter.d.ts.map +1 -1
  176. package/dist/src/motion/event-emitter.js.map +1 -1
  177. package/dist/src/motion/frame.js.map +1 -1
  178. package/dist/src/motion/hooks.d.ts.map +1 -1
  179. package/dist/src/motion/hooks.js +8 -3
  180. package/dist/src/motion/hooks.js.map +1 -1
  181. package/dist/src/motion/index.d.ts.map +1 -1
  182. package/dist/src/motion/index.js.map +1 -1
  183. package/dist/src/motion/motion-value.d.ts.map +1 -1
  184. package/dist/src/motion/motion-value.js.map +1 -1
  185. package/dist/src/motion/motion-value.test.js.map +1 -1
  186. package/dist/src/motion/spring-math.d.ts +6 -1
  187. package/dist/src/motion/spring-math.d.ts.map +1 -1
  188. package/dist/src/motion/spring-math.js +6 -1
  189. package/dist/src/motion/spring-math.js.map +1 -1
  190. package/dist/src/motion/types.d.ts.map +1 -1
  191. package/dist/src/motion/types.js.map +1 -1
  192. package/dist/src/profiler.js.map +1 -1
  193. package/dist/src/reconciler/host-config.d.ts +5 -5
  194. package/dist/src/reconciler/host-config.d.ts.map +1 -1
  195. package/dist/src/reconciler/host-config.js +43 -51
  196. package/dist/src/reconciler/host-config.js.map +1 -1
  197. package/dist/src/reconciler/noop-methods.d.ts +29 -0
  198. package/dist/src/reconciler/noop-methods.d.ts.map +1 -0
  199. package/dist/src/reconciler/noop-methods.js +43 -0
  200. package/dist/src/reconciler/noop-methods.js.map +1 -0
  201. package/dist/src/reconciler/types.d.ts +68 -14
  202. package/dist/src/reconciler/types.d.ts.map +1 -1
  203. package/dist/src/remote/Procedures.d.ts +22 -0
  204. package/dist/src/remote/Procedures.d.ts.map +1 -0
  205. package/dist/src/remote/Procedures.js +42 -0
  206. package/dist/src/remote/Procedures.js.map +1 -0
  207. package/dist/src/remote/Router.d.ts +20 -0
  208. package/dist/src/remote/Router.d.ts.map +1 -0
  209. package/dist/src/remote/Router.js +26 -0
  210. package/dist/src/remote/Router.js.map +1 -0
  211. package/dist/src/remote/Server.d.ts +6 -0
  212. package/dist/src/remote/Server.d.ts.map +1 -0
  213. package/dist/src/remote/Server.js +53 -0
  214. package/dist/src/remote/Server.js.map +1 -0
  215. package/dist/src/remote/index.d.ts +18 -0
  216. package/dist/src/remote/index.d.ts.map +1 -0
  217. package/dist/src/remote/index.js +74 -0
  218. package/dist/src/remote/index.js.map +1 -0
  219. package/dist/src/renderer/core/FrameBuilder.d.ts +18 -0
  220. package/dist/src/renderer/core/FrameBuilder.d.ts.map +1 -0
  221. package/dist/src/renderer/core/FrameBuilder.js +38 -0
  222. package/dist/src/renderer/core/FrameBuilder.js.map +1 -0
  223. package/dist/src/renderer/core/RendererState.d.ts +41 -0
  224. package/dist/src/renderer/core/RendererState.d.ts.map +1 -0
  225. package/dist/src/renderer/core/RendererState.js +70 -0
  226. package/dist/src/renderer/core/RendererState.js.map +1 -0
  227. package/dist/src/renderer/core/index.d.ts +3 -0
  228. package/dist/src/renderer/core/index.d.ts.map +1 -0
  229. package/dist/src/renderer/core/index.js +3 -0
  230. package/dist/src/renderer/core/index.js.map +1 -0
  231. package/dist/src/renderer/input/InputProcessor.d.ts +25 -0
  232. package/dist/src/renderer/input/InputProcessor.d.ts.map +1 -0
  233. package/dist/src/renderer/input/InputProcessor.js +81 -0
  234. package/dist/src/renderer/input/InputProcessor.js.map +1 -0
  235. package/dist/src/renderer/input/index.d.ts +2 -0
  236. package/dist/src/renderer/input/index.d.ts.map +1 -0
  237. package/dist/src/renderer/input/index.js +2 -0
  238. package/dist/src/renderer/input/index.js.map +1 -0
  239. package/dist/src/renderer/lifecycle/EventBus.d.ts +41 -0
  240. package/dist/src/renderer/lifecycle/EventBus.d.ts.map +1 -0
  241. package/dist/src/renderer/lifecycle/EventBus.js +78 -0
  242. package/dist/src/renderer/lifecycle/EventBus.js.map +1 -0
  243. package/dist/src/renderer/lifecycle/ResizeManager.d.ts +34 -0
  244. package/dist/src/renderer/lifecycle/ResizeManager.d.ts.map +1 -0
  245. package/dist/src/renderer/lifecycle/ResizeManager.js +47 -0
  246. package/dist/src/renderer/lifecycle/ResizeManager.js.map +1 -0
  247. package/dist/src/renderer/lifecycle/TerminalSetup.d.ts +36 -0
  248. package/dist/src/renderer/lifecycle/TerminalSetup.d.ts.map +1 -0
  249. package/dist/src/renderer/lifecycle/TerminalSetup.js +82 -0
  250. package/dist/src/renderer/lifecycle/TerminalSetup.js.map +1 -0
  251. package/dist/src/renderer/lifecycle/index.d.ts +4 -0
  252. package/dist/src/renderer/lifecycle/index.d.ts.map +1 -0
  253. package/dist/src/renderer/lifecycle/index.js +4 -0
  254. package/dist/src/renderer/lifecycle/index.js.map +1 -0
  255. package/dist/src/renderer/modes/FullscreenRenderer.d.ts +12 -0
  256. package/dist/src/renderer/modes/FullscreenRenderer.d.ts.map +1 -0
  257. package/dist/src/renderer/modes/FullscreenRenderer.js +52 -0
  258. package/dist/src/renderer/modes/FullscreenRenderer.js.map +1 -0
  259. package/dist/src/renderer/modes/InlineRenderer.d.ts +22 -0
  260. package/dist/src/renderer/modes/InlineRenderer.d.ts.map +1 -0
  261. package/dist/src/renderer/modes/InlineRenderer.js +154 -0
  262. package/dist/src/renderer/modes/InlineRenderer.js.map +1 -0
  263. package/dist/src/renderer/modes/RendererMode.d.ts +42 -0
  264. package/dist/src/renderer/modes/RendererMode.d.ts.map +1 -0
  265. package/dist/src/renderer/modes/RendererMode.js +2 -0
  266. package/dist/src/renderer/modes/RendererMode.js.map +1 -0
  267. package/dist/src/renderer/modes/StaticContentRenderer.d.ts +25 -0
  268. package/dist/src/renderer/modes/StaticContentRenderer.d.ts.map +1 -0
  269. package/dist/src/renderer/modes/StaticContentRenderer.js +47 -0
  270. package/dist/src/renderer/modes/StaticContentRenderer.js.map +1 -0
  271. package/dist/src/renderer/modes/index.d.ts +5 -0
  272. package/dist/src/renderer/modes/index.d.ts.map +1 -0
  273. package/dist/src/renderer/modes/index.js +4 -0
  274. package/dist/src/renderer/modes/index.js.map +1 -0
  275. package/dist/src/renderer-context.d.ts +9 -0
  276. package/dist/src/renderer-context.d.ts.map +1 -0
  277. package/dist/src/renderer-context.js +22 -0
  278. package/dist/src/renderer-context.js.map +1 -0
  279. package/dist/src/renderer-types.d.ts +103 -0
  280. package/dist/src/renderer-types.d.ts.map +1 -0
  281. package/dist/src/renderer-types.js +2 -0
  282. package/dist/src/renderer-types.js.map +1 -0
  283. package/dist/src/renderer.d.ts +4 -86
  284. package/dist/src/renderer.d.ts.map +1 -1
  285. package/dist/src/renderer.js +213 -384
  286. package/dist/src/renderer.js.map +1 -1
  287. package/dist/src/test/index.d.ts.map +1 -1
  288. package/dist/src/test/index.js.map +1 -1
  289. package/dist/src/test/mock-streams.d.ts.map +1 -1
  290. package/dist/src/test/mock-streams.js.map +1 -1
  291. package/dist/src/test/render-tui.d.ts.map +1 -1
  292. package/dist/src/test/render-tui.js +2 -5
  293. package/dist/src/test/render-tui.js.map +1 -1
  294. package/dist/src/trace/SpanTree.d.ts.map +1 -1
  295. package/dist/src/trace/SpanTree.js +21 -11
  296. package/dist/src/trace/SpanTree.js.map +1 -1
  297. package/dist/src/trace/format-value.d.ts +15 -0
  298. package/dist/src/trace/format-value.d.ts.map +1 -0
  299. package/dist/src/trace/format-value.js +77 -0
  300. package/dist/src/trace/format-value.js.map +1 -0
  301. package/dist/src/trace/index.d.ts.map +1 -1
  302. package/dist/src/trace/index.js.map +1 -1
  303. package/dist/src/trace/location.js +1 -1
  304. package/dist/src/trace/location.js.map +1 -1
  305. package/dist/src/trace/span-processor.d.ts.map +1 -1
  306. package/dist/src/trace/span-processor.js.map +1 -1
  307. package/dist/src/trace/span-state.d.ts +19 -2
  308. package/dist/src/trace/span-state.d.ts.map +1 -1
  309. package/dist/src/trace/span-state.js +62 -31
  310. package/dist/src/trace/span-state.js.map +1 -1
  311. package/dist/src/trace/tui-logger.js.map +1 -1
  312. package/dist/src/utils/border.d.ts +1 -1
  313. package/dist/src/utils/border.d.ts.map +1 -1
  314. package/dist/src/utils/border.js +6 -0
  315. package/dist/src/utils/border.js.map +1 -1
  316. package/dist/src/utils/flex-layout.d.ts +2 -1
  317. package/dist/src/utils/flex-layout.d.ts.map +1 -1
  318. package/dist/src/utils/flex-layout.js +22 -33
  319. package/dist/src/utils/flex-layout.js.map +1 -1
  320. package/dist/src/utils/index.d.ts +1 -1
  321. package/dist/src/utils/index.d.ts.map +1 -1
  322. package/dist/src/utils/index.js +1 -1
  323. package/dist/src/utils/index.js.map +1 -1
  324. package/dist/src/utils/padding.d.ts.map +1 -1
  325. package/dist/src/utils/padding.js.map +1 -1
  326. package/dist/src/utils/styles.d.ts +20 -1
  327. package/dist/src/utils/styles.d.ts.map +1 -1
  328. package/dist/src/utils/styles.js +36 -1
  329. package/dist/src/utils/styles.js.map +1 -1
  330. package/dist/src/visualize/index.d.ts +8 -19
  331. package/dist/src/visualize/index.d.ts.map +1 -1
  332. package/dist/src/visualize/index.js +11 -25
  333. package/dist/src/visualize/index.js.map +1 -1
  334. package/dist/tsconfig.tsbuildinfo +1 -1
  335. package/jsx-dev-runtime.ts +5 -0
  336. package/jsx-runtime.ts +54 -0
  337. package/package.json +124 -92
  338. package/src/codeblock.tsx +34 -34
  339. package/src/components/Divider.tsx +23 -0
  340. package/src/components/Markdown.tsx +380 -0
  341. package/src/components/MultilineTextInput.tsx +749 -0
  342. package/src/components/Overlay.tsx +56 -0
  343. package/src/components/Static.tsx +68 -0
  344. package/src/components/TextInput.tsx +285 -0
  345. package/src/components/index.ts +6 -0
  346. package/src/components/text-editing.ts +464 -0
  347. package/src/console/ConsoleCapture.ts +272 -0
  348. package/src/console/ConsolePopover.tsx +487 -0
  349. package/src/console/clipboard.ts +81 -0
  350. package/src/console/index.ts +42 -0
  351. package/src/console/useConsole.ts +129 -0
  352. package/src/debug/DebugOverlay.ts +557 -0
  353. package/src/debug/DiagnosticsPanel.tsx +27 -27
  354. package/src/dev/Toast.tsx +117 -0
  355. package/src/dev/index.ts +2 -0
  356. package/src/dev.tsx +489 -0
  357. package/src/highlight.ts +46 -46
  358. package/src/hmr-plugin.ts +61 -0
  359. package/src/hooks/index.ts +4 -0
  360. package/src/hooks/use-keyboard.ts +44 -24
  361. package/src/hooks/use-mouse.ts +51 -0
  362. package/src/hooks/use-paste.ts +21 -6
  363. package/src/hooks/use-scroll.ts +386 -0
  364. package/src/hooks/useFrameStats.ts +17 -17
  365. package/src/hosts/base.ts +180 -59
  366. package/src/hosts/box.ts +117 -96
  367. package/src/hosts/canvas.ts +137 -141
  368. package/src/hosts/codeblock.ts +117 -133
  369. package/src/hosts/flex-container.ts +124 -0
  370. package/src/hosts/hstack.ts +11 -59
  371. package/src/hosts/index.ts +24 -14
  372. package/src/hosts/overlay-item.ts +72 -0
  373. package/src/hosts/overlay.ts +125 -0
  374. package/src/hosts/scroll.ts +255 -0
  375. package/src/hosts/single-child.ts +52 -0
  376. package/src/hosts/spacer.ts +30 -26
  377. package/src/hosts/text.ts +198 -164
  378. package/src/hosts/vstack.ts +11 -59
  379. package/src/hosts/zstack.ts +79 -67
  380. package/src/index.ts +44 -19
  381. package/src/inline/index.tsx +123 -123
  382. package/src/motion/color-motion-value.ts +67 -67
  383. package/src/motion/color.test.ts +107 -107
  384. package/src/motion/color.ts +9 -190
  385. package/src/motion/event-emitter.ts +20 -20
  386. package/src/motion/frame.ts +35 -35
  387. package/src/motion/hooks.ts +144 -139
  388. package/src/motion/index.ts +10 -10
  389. package/src/motion/motion-value.test.ts +207 -207
  390. package/src/motion/motion-value.ts +112 -112
  391. package/src/motion/spring-math.ts +88 -83
  392. package/src/motion/types.ts +25 -25
  393. package/src/profiler.ts +50 -50
  394. package/src/reconciler/host-config.ts +152 -174
  395. package/src/reconciler/noop-methods.ts +55 -0
  396. package/src/reconciler/types.ts +112 -46
  397. package/src/remote/Procedures.ts +52 -0
  398. package/src/remote/Router.ts +58 -0
  399. package/src/remote/Server.ts +76 -0
  400. package/src/remote/index.ts +90 -0
  401. package/src/renderer/core/FrameBuilder.ts +49 -0
  402. package/src/renderer/core/RendererState.ts +80 -0
  403. package/src/renderer/core/index.ts +2 -0
  404. package/src/renderer/input/InputProcessor.ts +94 -0
  405. package/src/renderer/input/index.ts +1 -0
  406. package/src/renderer/lifecycle/EventBus.ts +90 -0
  407. package/src/renderer/lifecycle/ResizeManager.ts +65 -0
  408. package/src/renderer/lifecycle/TerminalSetup.ts +105 -0
  409. package/src/renderer/lifecycle/index.ts +3 -0
  410. package/src/renderer/modes/FullscreenRenderer.ts +53 -0
  411. package/src/renderer/modes/InlineRenderer.ts +178 -0
  412. package/src/renderer/modes/RendererMode.ts +46 -0
  413. package/src/renderer/modes/StaticContentRenderer.ts +56 -0
  414. package/src/renderer/modes/index.ts +4 -0
  415. package/src/renderer-context.ts +27 -0
  416. package/src/renderer-types.ts +109 -0
  417. package/src/renderer.ts +391 -642
  418. package/src/test/index.ts +5 -5
  419. package/src/test/mock-streams.ts +115 -115
  420. package/src/test/render-tui.ts +84 -87
  421. package/src/utils/border.ts +79 -73
  422. package/src/utils/flex-layout.ts +80 -93
  423. package/src/utils/index.ts +1 -1
  424. package/src/utils/padding.ts +27 -27
  425. package/src/utils/styles.ts +50 -7
  426. package/src/visualize/index.tsx +225 -240
  427. package/dist/src/output.d.ts +0 -47
  428. package/dist/src/output.d.ts.map +0 -1
  429. package/dist/src/output.js +0 -125
  430. package/dist/src/output.js.map +0 -1
  431. package/dist/src/terminal.d.ts +0 -37
  432. package/dist/src/terminal.d.ts.map +0 -1
  433. package/dist/src/terminal.js +0 -65
  434. package/dist/src/terminal.js.map +0 -1
  435. package/src/output.ts +0 -156
  436. package/src/terminal.ts +0 -67
  437. package/src/trace/SpanTree.tsx +0 -195
  438. package/src/trace/index.tsx +0 -205
  439. package/src/trace/location.ts +0 -90
  440. package/src/trace/span-processor.ts +0 -65
  441. package/src/trace/span-state.ts +0 -286
  442. package/src/trace/tui-logger.ts +0 -72
@@ -3,36 +3,36 @@ import { useFrameStats } from "../hooks/useFrameStats.js"
3
3
  import "../jsx.js"
4
4
 
5
5
  export interface DiagnosticsPanelProps {
6
- sampleMs?: number
7
- title?: string
6
+ sampleMs?: number
7
+ title?: string
8
8
  }
9
9
 
10
10
  export function DiagnosticsPanel({ sampleMs = 200, title = "Diagnostics" }: DiagnosticsPanelProps) {
11
- const stats = useFrameStats(sampleMs)
11
+ const stats = useFrameStats(sampleMs)
12
12
 
13
- const fps = stats ? (stats.frameMs > 0 ? (1000 / stats.frameMs).toFixed(1) : "∞") : "—"
14
- const bytes = stats ? stats.bytes : 0
15
- const h = stats ? stats.contentHeight : 0
16
- const phases = stats?.phases
13
+ const fps = stats ? (stats.frameMs > 0 ? (1000 / stats.frameMs).toFixed(1) : "∞") : "—"
14
+ const bytes = stats ? stats.bytes : 0
15
+ const h = stats ? stats.contentHeight : 0
16
+ const phases = stats?.phases
17
17
 
18
- return (
19
- <vstack spacing={1}>
20
- <text fg={Colors.cyan} bold>
21
- {title} ({stats?.mode ?? "?"})
22
- </text>
23
- {stats ? (
24
- <>
25
- <text fg={Colors.gray(12)}>
26
- size {stats.width}×{stats.height} content {h} rows — {bytes} bytes/frame — {fps} fps
27
- </text>
28
- <text fg={Colors.gray(12)}>
29
- clear {phases?.clear.toFixed(2)}ms · layout {phases?.layout.toFixed(2)}ms · render{" "}
30
- {phases?.render.toFixed(2)}ms · diff {phases?.diffAnsi.toFixed(2)}ms · write {phases?.write.toFixed(2)}ms
31
- </text>
32
- </>
33
- ) : (
34
- <text fg={Colors.gray(10)}>Waiting for first frame…</text>
35
- )}
36
- </vstack>
37
- )
18
+ return (
19
+ <vstack spacing={1}>
20
+ <text fg={Colors.cyan} bold>
21
+ {title} ({stats?.mode ?? "?"})
22
+ </text>
23
+ {stats ? (
24
+ <>
25
+ <text fg={Colors.gray(12)}>
26
+ size {stats.width}×{stats.height} content {h} rows — {bytes} bytes/frame — {fps} fps
27
+ </text>
28
+ <text fg={Colors.gray(12)}>
29
+ clear {phases?.clear.toFixed(2)}ms · layout {phases?.layout.toFixed(2)}ms · render{" "}
30
+ {phases?.render.toFixed(2)}ms · diff {phases?.diffAnsi.toFixed(2)}ms · write {phases?.write.toFixed(2)}ms
31
+ </text>
32
+ </>
33
+ ) : (
34
+ <text fg={Colors.gray(10)}>Waiting for first frame…</text>
35
+ )}
36
+ </vstack>
37
+ )
38
38
  }
@@ -0,0 +1,117 @@
1
+ // Toast notification system for dev mode
2
+ // Beautiful, minimal notifications that appear at the top of the screen
3
+
4
+ import { useState, useCallback, createContext, useContext, type ReactNode } from "react"
5
+ import { Colors } from "@effect-tui/core"
6
+ import { useTerminalSize } from "../renderer-context.js"
7
+
8
+ // ─────────────────────────────────────────────────────────────
9
+ // Types
10
+ // ─────────────────────────────────────────────────────────────
11
+
12
+ export type ToastType = "success" | "info" | "warning" | "error"
13
+
14
+ export interface Toast {
15
+ id: number
16
+ message: string
17
+ type: ToastType
18
+ }
19
+
20
+ export interface ToastContextValue {
21
+ toasts: Toast[]
22
+ show: (message: string, type?: ToastType, durationMs?: number) => void
23
+ dismiss: (id: number) => void
24
+ }
25
+
26
+ // ─────────────────────────────────────────────────────────────
27
+ // Context
28
+ // ─────────────────────────────────────────────────────────────
29
+
30
+ const ToastContext = createContext<ToastContextValue | null>(null)
31
+
32
+ export function useToast(): ToastContextValue {
33
+ const ctx = useContext(ToastContext)
34
+ if (!ctx) {
35
+ throw new Error("useToast must be used within a ToastProvider")
36
+ }
37
+ return ctx
38
+ }
39
+
40
+ // ─────────────────────────────────────────────────────────────
41
+ // Styling
42
+ // ─────────────────────────────────────────────────────────────
43
+
44
+ const TOAST_STYLES: Record<ToastType, { bg: ReturnType<typeof Colors.rgb>; fg: ReturnType<typeof Colors.rgb>; icon: string }> = {
45
+ success: { bg: Colors.rgb(30, 70, 40), fg: Colors.rgb(140, 230, 140), icon: "✓" },
46
+ info: { bg: Colors.rgb(30, 50, 80), fg: Colors.rgb(140, 180, 230), icon: "ℹ" },
47
+ warning: { bg: Colors.rgb(80, 60, 20), fg: Colors.rgb(230, 200, 100), icon: "⚠" },
48
+ error: { bg: Colors.rgb(80, 30, 30), fg: Colors.rgb(230, 140, 140), icon: "✗" },
49
+ }
50
+
51
+ // ─────────────────────────────────────────────────────────────
52
+ // Provider
53
+ // ─────────────────────────────────────────────────────────────
54
+
55
+ let toastId = 0
56
+
57
+ export function ToastProvider({ children }: { children: ReactNode }) {
58
+ const [toasts, setToasts] = useState<Toast[]>([])
59
+
60
+ const show = useCallback((message: string, type: ToastType = "info", durationMs = 2000) => {
61
+ const id = ++toastId
62
+ setToasts((prev) => [...prev, { id, message, type }])
63
+
64
+ setTimeout(() => {
65
+ setToasts((prev) => prev.filter((t) => t.id !== id))
66
+ }, durationMs)
67
+ }, [])
68
+
69
+ const dismiss = useCallback((id: number) => {
70
+ setToasts((prev) => prev.filter((t) => t.id !== id))
71
+ }, [])
72
+
73
+ return (
74
+ <ToastContext.Provider value={{ toasts, show, dismiss }}>
75
+ {children}
76
+ </ToastContext.Provider>
77
+ )
78
+ }
79
+
80
+ // ─────────────────────────────────────────────────────────────
81
+ // Toast Display Component
82
+ // ─────────────────────────────────────────────────────────────
83
+
84
+ export function ToastContainer() {
85
+ const { toasts } = useToast()
86
+ const { width } = useTerminalSize()
87
+
88
+ if (toasts.length === 0) return null
89
+
90
+ // Show only the most recent toast
91
+ const toast = toasts[toasts.length - 1]
92
+ const style = TOAST_STYLES[toast.type]
93
+
94
+ // Center the message with padding
95
+ const content = ` ${style.icon} ${toast.message} `
96
+ const padding = Math.max(0, Math.floor((width - content.length) / 2))
97
+ const paddedContent = " ".repeat(padding) + content + " ".repeat(width - padding - content.length)
98
+
99
+ // Just render the toast line - parent zstack handles positioning
100
+ return (
101
+ <text bg={style.bg} fg={style.fg}>
102
+ {paddedContent}
103
+ </text>
104
+ )
105
+ }
106
+
107
+ // ─────────────────────────────────────────────────────────────
108
+ // Convenience hook for screenshot toast
109
+ // ─────────────────────────────────────────────────────────────
110
+
111
+ export function useScreenshotToast() {
112
+ const { show } = useToast()
113
+
114
+ return useCallback((path: string) => {
115
+ show(`Screenshot saved: ${path}`, "success", 2500)
116
+ }, [show])
117
+ }
@@ -0,0 +1,2 @@
1
+ // Dev mode utilities
2
+ export { ToastProvider, ToastContainer, useToast, useScreenshotToast, type Toast, type ToastType } from "./Toast.js"
package/src/dev.tsx ADDED
@@ -0,0 +1,489 @@
1
+ /**
2
+ * Development utilities for hot module replacement (HMR).
3
+ *
4
+ * Enables hot reload during development:
5
+ * - File changes trigger re-render without process restart
6
+ * - Terminal stays stable (no screen flash)
7
+ * - State preserved when using Effect's globalValue with effect-atom
8
+ * - Debug console panel (` to toggle, ~ for screenshot)
9
+ * - Remote control when EFFECT_TUI_REMOTE=1
10
+ */
11
+
12
+ import { globalValue } from "effect/GlobalValue"
13
+ import * as watcher from "@parcel/watcher"
14
+ import { dirname } from "path"
15
+ import { stat } from "fs/promises"
16
+ import { readFileSync } from "fs"
17
+ import React from "react"
18
+ import { Colors } from "@effect-tui/core"
19
+ import { createRenderer, createRoot, type Root } from "./renderer.js"
20
+ import type { RendererOptions } from "./renderer-types.js"
21
+ import type { TuiRenderer } from "./renderer-types.js"
22
+ import { ConsolePopover } from "./console/ConsolePopover.js"
23
+ import { useConsole } from "./console/useConsole.js"
24
+ import { enableRemote } from "./remote/index.js"
25
+ import { ToastProvider, ToastContainer, useToast } from "./dev/Toast.js"
26
+ import { useKeyboard } from "./hooks/use-keyboard.js"
27
+ import { useRenderer, useTerminalSize } from "./renderer-context.js"
28
+ import { copyToClipboardSync, copyToClipboard } from "./console/clipboard.js"
29
+ import { DiagnosticsPanel } from "./debug/DiagnosticsPanel.js"
30
+ import { Overlay } from "./components/Overlay.js"
31
+
32
+ /**
33
+ * Pipeable combinator that makes an atom persist across hot reloads.
34
+ *
35
+ * This is the cleanest way to define HMR-persistent atoms - just pipe it!
36
+ * Combines globalValue (for persistence) + keepAlive (prevents registry cleanup).
37
+ *
38
+ * @example
39
+ * ```tsx
40
+ * import { Atom } from "@effect-atom/atom-react"
41
+ * import { hmr, devRender } from "@effect-tui/react"
42
+ *
43
+ * // Just pipe hmr() - that's it!
44
+ * const countAtom = Atom.make(0).pipe(hmr("count"))
45
+ * const userAtom = Atom.make<User | null>(null).pipe(hmr("user"))
46
+ *
47
+ * export default function App() {
48
+ * const count = useAtomValue(countAtom)
49
+ * // Edit this file - count persists!
50
+ * }
51
+ * ```
52
+ */
53
+ export function hmr<A>(key: string): (self: A) => A {
54
+ return (self: A): A => {
55
+ return globalValue(Symbol.for(`hmr/${key}`), () => {
56
+ // Apply keepAlive if it's an atom (just sets keepAlive: true)
57
+ if (typeof self === "object" && self !== null && "keepAlive" in self) {
58
+ return Object.assign(Object.create(Object.getPrototypeOf(self)), {
59
+ ...self,
60
+ keepAlive: true,
61
+ })
62
+ }
63
+ return self
64
+ }) as A
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Wrap any value creation in HMR persistence.
70
+ *
71
+ * @example
72
+ * ```tsx
73
+ * const countAtom = hmrState("count", () => Atom.make(0).pipe(Atom.keepAlive))
74
+ * ```
75
+ */
76
+ export function hmrState<T>(key: string, create: () => T): T {
77
+ return globalValue(Symbol.for(`hmr/${key}`), create)
78
+ }
79
+
80
+ /**
81
+ * Simple self-running dev mode. Just add this at the bottom of your file:
82
+ *
83
+ * @example
84
+ * ```tsx
85
+ * export default function App() { ... }
86
+ *
87
+ * devMain(import.meta)
88
+ * ```
89
+ *
90
+ * That's it! Handles:
91
+ * - Only runs when file is the entry point (import.meta.main)
92
+ * - Only initializes once across HMR reloads
93
+ * - Automatically finds the file path
94
+ */
95
+ export function devMain(importMeta: { main?: boolean; url: string }, options?: Omit<DevRenderOptions, never>): void {
96
+ // Skip if not main entry point
97
+ if (!importMeta.main) return
98
+
99
+ // Skip if this is a HMR re-import (has cache-busting query string)
100
+ if (importMeta.url.includes("?")) return
101
+
102
+ const key = `devMain/${importMeta.url}`
103
+ const initialized = hmrState(key, () => ({ done: false }))
104
+
105
+ if (!initialized.done) {
106
+ initialized.done = true
107
+ const filePath = new URL(importMeta.url).pathname
108
+ devRender(filePath, options)
109
+ }
110
+ }
111
+
112
+ // Cache for runtime key extraction
113
+ const keyCache = new Map<string, string>()
114
+
115
+ /**
116
+ * Parse stack trace to get file path and line number.
117
+ * Returns null if parsing fails.
118
+ */
119
+ function parseStack(stack: string): { file: string; line: number; col: number } | null {
120
+ // Bun stack format: " at functionName (file:line:col)" or " at file:line:col"
121
+ const lines = stack.split("\n")
122
+ // Skip first line (Error message) and find caller (skip autoHmr itself)
123
+ for (let i = 2; i < lines.length; i++) {
124
+ const match = lines[i].match(/\((.+?):(\d+):(\d+)\)/) || lines[i].match(/at\s+(.+?):(\d+):(\d+)/)
125
+ if (match) {
126
+ return { file: match[1], line: parseInt(match[2], 10), col: parseInt(match[3], 10) }
127
+ }
128
+ }
129
+ return null
130
+ }
131
+
132
+ /**
133
+ * Extract variable name from source line using regex.
134
+ * Returns null if extraction fails.
135
+ */
136
+ function extractVarName(source: string, line: number): string | null {
137
+ const lines = source.split("\n")
138
+ if (line < 1 || line > lines.length) return null
139
+
140
+ const sourceLine = lines[line - 1]
141
+ const match = sourceLine.match(/(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=/)
142
+ return match?.[1] ?? null
143
+ }
144
+
145
+ /**
146
+ * Auto-keyed HMR persistence for atoms.
147
+ *
148
+ * When used with the HMR plugin (recommended), keys are injected at load time
149
+ * based on the variable name. Without the plugin, falls back to runtime
150
+ * stack trace parsing.
151
+ *
152
+ * @example
153
+ * ```tsx
154
+ * import { Atom } from "@effect-atom/atom-react"
155
+ * import { autoHmr, devRender } from "@effect-tui/react"
156
+ *
157
+ * // No manual key needed - derived from variable name!
158
+ * const countAtom = Atom.make(0).pipe(autoHmr)
159
+ * const userAtom = Atom.make<User | null>(null).pipe(autoHmr)
160
+ *
161
+ * // To enable compile-time transform (faster, more reliable):
162
+ * // bun --preload @effect-tui/react/hmr-plugin run src/app.tsx
163
+ * ```
164
+ */
165
+ export function autoHmr<A>(self: A): A {
166
+ // Get stack trace for caller location
167
+ const stack = new Error().stack ?? ""
168
+ const location = parseStack(stack)
169
+
170
+ if (!location) {
171
+ // Fallback: use a hash of the stack trace
172
+ const fallbackKey = `unknown:${stack.slice(0, 100)}`
173
+ return hmr<A>(fallbackKey)(self)
174
+ }
175
+
176
+ const cacheKey = `${location.file}:${location.line}:${location.col}`
177
+
178
+ // Check cache first
179
+ let key = keyCache.get(cacheKey)
180
+ if (!key) {
181
+ try {
182
+ // Read source file and extract variable name
183
+ const source = readFileSync(location.file, "utf-8")
184
+ const varName = extractVarName(source, location.line)
185
+ key = `${location.file}:${varName ?? `line${location.line}`}`
186
+ } catch {
187
+ // File read failed, use line-based key
188
+ key = `${location.file}:line${location.line}`
189
+ }
190
+ keyCache.set(cacheKey, key)
191
+ }
192
+
193
+ return hmr<A>(key)(self)
194
+ }
195
+
196
+ export interface DevRenderOptions extends RendererOptions {
197
+ /** Directories to watch (defaults to entry file's directory) */
198
+ watchDirs?: string[]
199
+ /** File extensions to watch (defaults to [".ts", ".tsx"]) */
200
+ watchExtensions?: string[]
201
+ /** Debounce delay in ms (defaults to 150) */
202
+ debounce?: number
203
+ /** Wait for file writes to stabilize (defaults to 50ms) */
204
+ awaitWriteFinish?: number
205
+ /** Called before each hot reload */
206
+ onReload?: () => void
207
+ /** Called on errors */
208
+ onError?: (error: Error) => void
209
+ /** Show renderer diagnostics overlay. */
210
+ showStats?: boolean
211
+ /** Sampling window for renderer stats (ms). */
212
+ statsSampleMs?: number
213
+ /** Override diagnostics panel title. */
214
+ statsTitle?: string
215
+ }
216
+
217
+ export interface DevRenderResult {
218
+ renderer: TuiRenderer
219
+ root: Root
220
+ /** Manually trigger a reload */
221
+ rerender: () => Promise<void>
222
+ /** Stop watching and cleanup */
223
+ stop: () => Promise<void>
224
+ }
225
+
226
+ /**
227
+ * Wait for a file to stabilize (no size changes) before proceeding.
228
+ * This avoids importing partially-written files.
229
+ */
230
+ async function awaitWriteFinish(path: string, stabilityMs: number): Promise<void> {
231
+ let lastSize = -1
232
+ let stableCount = 0
233
+ const checkInterval = 10
234
+
235
+ while (stableCount < stabilityMs / checkInterval) {
236
+ try {
237
+ const stats = await stat(path)
238
+ if (stats.size === lastSize) {
239
+ stableCount++
240
+ } else {
241
+ lastSize = stats.size
242
+ stableCount = 0
243
+ }
244
+ } catch {
245
+ // File might be temporarily unavailable during write
246
+ stableCount = 0
247
+ }
248
+ await new Promise((r) => setTimeout(r, checkInterval))
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Render a TUI app with hot module replacement support.
254
+ *
255
+ * Uses @parcel/watcher for reliable, cross-platform file watching.
256
+ * When files change, the module is re-imported and the app re-renders
257
+ * without restarting the process or tearing down the terminal.
258
+ *
259
+ * For state preservation, use effect-atom with the hmr() combinator:
260
+ *
261
+ * @example
262
+ * ```tsx
263
+ * import { Atom, useAtomValue } from "@effect-atom/atom-react"
264
+ * import { hmr, devRender } from "@effect-tui/react"
265
+ *
266
+ * const countAtom = Atom.make(0).pipe(hmr("count"))
267
+ *
268
+ * export default function App() {
269
+ * const count = useAtomValue(countAtom)
270
+ * return <text>Count: {count}</text>
271
+ * }
272
+ *
273
+ * devRender(import.meta.path, { mode: "inline" })
274
+ * ```
275
+ */
276
+ /**
277
+ * Internal component that handles screenshot with toast notification
278
+ */
279
+ function ScreenshotHandler() {
280
+ const renderer = useRenderer()
281
+ const { show } = useToast()
282
+
283
+ useKeyboard((key) => {
284
+ // ~ (tilde) - screenshot
285
+ if (key.name === "char" && key.text === "~") {
286
+ const ansiOutput = renderer.getScreenshot()
287
+ if (!ansiOutput) return
288
+
289
+ // Copy actual content to clipboard
290
+ const copied = copyToClipboardSync(ansiOutput)
291
+ if (!copied) {
292
+ copyToClipboard(ansiOutput)
293
+ }
294
+
295
+ // Also save to file as backup
296
+ const tmpPath = `/tmp/tui-screenshot-${Date.now()}.txt`
297
+ Bun.write(tmpPath, ansiOutput)
298
+
299
+ show("Screenshot copied!", "success", 1500)
300
+ key.preventDefault?.()
301
+ }
302
+ })
303
+
304
+ return null
305
+ }
306
+
307
+ /**
308
+ * Overlay for renderer diagnostics stats.
309
+ */
310
+ function StatsOverlay({ sampleMs, title }: { sampleMs?: number; title?: string }) {
311
+ const { width, height } = useTerminalSize()
312
+
313
+ return (
314
+ <vstack width={width} height={height}>
315
+ <hstack width={width}>
316
+ <spacer />
317
+ <box padding={1} border="rounded" borderColor={Colors.gray(8)} bg={Colors.gray(2)}>
318
+ <DiagnosticsPanel sampleMs={sampleMs} title={title ?? "Renderer Stats"} />
319
+ </box>
320
+ </hstack>
321
+ <spacer />
322
+ </vstack>
323
+ )
324
+ }
325
+
326
+ /**
327
+ * Internal wrapper component that provides dev mode features:
328
+ * - Debug console panel (` backtick to toggle)
329
+ * - Screenshot support (~ tilde) with toast notification
330
+ * - Auto-show console on errors
331
+ * - Optional renderer stats overlay (showStats)
332
+ */
333
+ function DevWrapper({
334
+ children,
335
+ showStats,
336
+ statsSampleMs,
337
+ statsTitle,
338
+ mode,
339
+ }: {
340
+ children?: React.ReactNode
341
+ showStats?: boolean
342
+ statsSampleMs?: number
343
+ statsTitle?: string
344
+ mode?: "fullscreen" | "inline"
345
+ }) {
346
+ const { visible } = useConsole({ autoShowOnError: true, initiallyVisible: false })
347
+
348
+ // Inline mode: content flows naturally in vstack
349
+ if (mode === "inline") {
350
+ return (
351
+ <ToastProvider>
352
+ <vstack>
353
+ <ScreenshotHandler />
354
+ <ToastContainer />
355
+ {children}
356
+ {visible && <ConsolePopover fixedHeight={5} mode="inline" />}
357
+ </vstack>
358
+ </ToastProvider>
359
+ )
360
+ }
361
+
362
+ // Fullscreen mode: Overlay with toast/console positioned by alignment
363
+ return (
364
+ <ToastProvider>
365
+ <Overlay>
366
+ {/* Base - determines container size */}
367
+ <>
368
+ <ScreenshotHandler />
369
+ {children}
370
+ </>
371
+ {/* Overlays */}
372
+ {visible && (
373
+ <Overlay.Item alignment={{ v: "bottom" }}>
374
+ <ConsolePopover fixedHeight={12} />
375
+ </Overlay.Item>
376
+ )}
377
+ {showStats && (
378
+ <Overlay.Item alignment={{ v: "top", h: "right" }}>
379
+ <StatsOverlay sampleMs={statsSampleMs} title={statsTitle} />
380
+ </Overlay.Item>
381
+ )}
382
+ <Overlay.Item alignment={{ v: "top" }}>
383
+ <ToastContainer />
384
+ </Overlay.Item>
385
+ </Overlay>
386
+ </ToastProvider>
387
+ )
388
+ }
389
+
390
+ export async function devRender(entryPath: string, options?: DevRenderOptions): Promise<DevRenderResult> {
391
+ const watchExtensions = options?.watchExtensions ?? [".ts", ".tsx"]
392
+ const debounceMs = options?.debounce ?? 150
393
+ const stabilityMs = options?.awaitWriteFinish ?? 50
394
+
395
+ const renderer = createRenderer(options)
396
+ const root = createRoot(renderer)
397
+
398
+ // Dev mode always enables remote control
399
+ const stopRemote = enableRemote(renderer)
400
+
401
+ let debounceTimer: ReturnType<typeof setTimeout> | null = null
402
+ let version = 0
403
+ let subscription: watcher.AsyncSubscription | null = null
404
+
405
+ // Import and render the module
406
+ const render = async () => {
407
+ const thisVersion = ++version
408
+
409
+ try {
410
+ // Cache-bust by adding query string with version
411
+ const mod = await import(`${entryPath}?v=${thisVersion}`)
412
+
413
+ // Skip if a newer render was triggered
414
+ if (thisVersion !== version) return
415
+
416
+ const App = mod.default
417
+
418
+ if (!App) {
419
+ const err = new Error(`No default export found in ${entryPath}`)
420
+ options?.onError?.(err)
421
+ console.error("[devRender]", err.message)
422
+ return
423
+ }
424
+
425
+ // Render the component wrapped in DevWrapper for console panel
426
+ const appElement = typeof App === "function" ? React.createElement(App) : App
427
+ const wrapped = React.createElement(
428
+ DevWrapper,
429
+ {
430
+ showStats: options?.showStats,
431
+ statsSampleMs: options?.statsSampleMs,
432
+ statsTitle: options?.statsTitle,
433
+ mode: options?.mode,
434
+ },
435
+ appElement,
436
+ )
437
+ root.render(wrapped)
438
+ } catch (err) {
439
+ const error = err instanceof Error ? err : new Error(String(err))
440
+ options?.onError?.(error)
441
+ console.error("[devRender] Error loading module:", error.message)
442
+ }
443
+ }
444
+
445
+ // Initial render
446
+ await render()
447
+
448
+ // Watch for changes using @parcel/watcher
449
+ const watchDir = options?.watchDirs?.[0] ?? dirname(entryPath)
450
+
451
+ try {
452
+ subscription = await watcher.subscribe(watchDir, async (err, events) => {
453
+ if (err) {
454
+ options?.onError?.(err)
455
+ console.error("[devRender] Watcher error:", err)
456
+ return
457
+ }
458
+
459
+ // Filter to relevant file extensions
460
+ const relevantEvents = events.filter((event) => watchExtensions.some((ext) => event.path.endsWith(ext)))
461
+
462
+ if (relevantEvents.length === 0) return
463
+
464
+ // Debounce rapid changes
465
+ if (debounceTimer) clearTimeout(debounceTimer)
466
+ debounceTimer = setTimeout(async () => {
467
+ // Wait for file writes to stabilize
468
+ const changedPath = relevantEvents[0].path
469
+ await awaitWriteFinish(changedPath, stabilityMs)
470
+
471
+ options?.onReload?.()
472
+ await render()
473
+ }, debounceMs)
474
+ })
475
+ } catch (err) {
476
+ console.error(`[devRender] Failed to watch ${watchDir}:`, err)
477
+ }
478
+
479
+ const stop = async () => {
480
+ if (debounceTimer) clearTimeout(debounceTimer)
481
+ if (subscription) {
482
+ await subscription.unsubscribe()
483
+ }
484
+ stopRemote()
485
+ renderer.stop()
486
+ }
487
+
488
+ return { renderer, root, rerender: render, stop }
489
+ }