@effect-tui/react 0.15.2 → 2.0.0

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 (313) hide show
  1. package/README.md +11 -2
  2. package/dist/src/codeblock.d.ts +1 -1
  3. package/dist/src/codeblock.d.ts.map +1 -1
  4. package/dist/src/codeblock.js +2 -2
  5. package/dist/src/codeblock.js.map +1 -1
  6. package/dist/src/components/ListView.d.ts +4 -4
  7. package/dist/src/components/ListView.d.ts.map +1 -1
  8. package/dist/src/components/ListView.js +16 -17
  9. package/dist/src/components/ListView.js.map +1 -1
  10. package/dist/src/components/Markdown.js +3 -3
  11. package/dist/src/components/Markdown.js.map +1 -1
  12. package/dist/src/components/MultilineTextInput.d.ts.map +1 -1
  13. package/dist/src/components/MultilineTextInput.js +133 -305
  14. package/dist/src/components/MultilineTextInput.js.map +1 -1
  15. package/dist/src/components/TextInput.d.ts.map +1 -1
  16. package/dist/src/components/TextInput.js +51 -98
  17. package/dist/src/components/TextInput.js.map +1 -1
  18. package/dist/src/components/text-editing.d.ts +61 -0
  19. package/dist/src/components/text-editing.d.ts.map +1 -1
  20. package/dist/src/components/text-editing.js +131 -0
  21. package/dist/src/components/text-editing.js.map +1 -1
  22. package/dist/src/console/ConsolePopover.d.ts +7 -1
  23. package/dist/src/console/ConsolePopover.d.ts.map +1 -1
  24. package/dist/src/console/ConsolePopover.js +55 -74
  25. package/dist/src/console/ConsolePopover.js.map +1 -1
  26. package/dist/src/debug/DebugOverlay.d.ts.map +1 -1
  27. package/dist/src/debug/DebugOverlay.js +3 -57
  28. package/dist/src/debug/DebugOverlay.js.map +1 -1
  29. package/dist/src/debug/DiagnosticsPanel.js +1 -1
  30. package/dist/src/debug/DiagnosticsPanel.js.map +1 -1
  31. package/dist/src/dev.d.ts +5 -117
  32. package/dist/src/dev.d.ts.map +1 -1
  33. package/dist/src/dev.js +3 -333
  34. package/dist/src/dev.js.map +1 -1
  35. package/dist/src/hooks/use-scroll.d.ts +31 -35
  36. package/dist/src/hooks/use-scroll.d.ts.map +1 -1
  37. package/dist/src/hooks/use-scroll.js +51 -90
  38. package/dist/src/hooks/use-scroll.js.map +1 -1
  39. package/dist/src/hosts/base.d.ts +13 -2
  40. package/dist/src/hosts/base.d.ts.map +1 -1
  41. package/dist/src/hosts/base.js +74 -2
  42. package/dist/src/hosts/base.js.map +1 -1
  43. package/dist/src/hosts/box.d.ts +2 -2
  44. package/dist/src/hosts/box.d.ts.map +1 -1
  45. package/dist/src/hosts/box.js +29 -2
  46. package/dist/src/hosts/box.js.map +1 -1
  47. package/dist/src/hosts/canvas.d.ts +24 -4
  48. package/dist/src/hosts/canvas.d.ts.map +1 -1
  49. package/dist/src/hosts/canvas.js +107 -41
  50. package/dist/src/hosts/canvas.js.map +1 -1
  51. package/dist/src/hosts/codeblock.d.ts +10 -12
  52. package/dist/src/hosts/codeblock.d.ts.map +1 -1
  53. package/dist/src/hosts/codeblock.js +38 -35
  54. package/dist/src/hosts/codeblock.js.map +1 -1
  55. package/dist/src/hosts/flex-container.d.ts +3 -3
  56. package/dist/src/hosts/flex-container.d.ts.map +1 -1
  57. package/dist/src/hosts/flex-container.js +20 -5
  58. package/dist/src/hosts/flex-container.js.map +1 -1
  59. package/dist/src/hosts/index.d.ts +3 -2
  60. package/dist/src/hosts/index.d.ts.map +1 -1
  61. package/dist/src/hosts/index.js +2 -1
  62. package/dist/src/hosts/index.js.map +1 -1
  63. package/dist/src/hosts/layout-helpers.d.ts +10 -0
  64. package/dist/src/hosts/layout-helpers.d.ts.map +1 -0
  65. package/dist/src/hosts/layout-helpers.js +10 -0
  66. package/dist/src/hosts/layout-helpers.js.map +1 -0
  67. package/dist/src/hosts/leaf.d.ts +14 -0
  68. package/dist/src/hosts/leaf.d.ts.map +1 -0
  69. package/dist/src/hosts/leaf.js +31 -0
  70. package/dist/src/hosts/leaf.js.map +1 -0
  71. package/dist/src/hosts/overlay-item.d.ts +2 -2
  72. package/dist/src/hosts/overlay-item.d.ts.map +1 -1
  73. package/dist/src/hosts/overlay-item.js +7 -2
  74. package/dist/src/hosts/overlay-item.js.map +1 -1
  75. package/dist/src/hosts/overlay.d.ts +2 -2
  76. package/dist/src/hosts/overlay.d.ts.map +1 -1
  77. package/dist/src/hosts/overlay.js +6 -9
  78. package/dist/src/hosts/overlay.js.map +1 -1
  79. package/dist/src/hosts/scroll.d.ts +54 -26
  80. package/dist/src/hosts/scroll.d.ts.map +1 -1
  81. package/dist/src/hosts/scroll.js +185 -87
  82. package/dist/src/hosts/scroll.js.map +1 -1
  83. package/dist/src/hosts/single-child.d.ts.map +1 -1
  84. package/dist/src/hosts/single-child.js +2 -0
  85. package/dist/src/hosts/single-child.js.map +1 -1
  86. package/dist/src/hosts/spacer.d.ts +3 -3
  87. package/dist/src/hosts/spacer.d.ts.map +1 -1
  88. package/dist/src/hosts/spacer.js +8 -3
  89. package/dist/src/hosts/spacer.js.map +1 -1
  90. package/dist/src/hosts/text.d.ts +22 -18
  91. package/dist/src/hosts/text.d.ts.map +1 -1
  92. package/dist/src/hosts/text.js +108 -131
  93. package/dist/src/hosts/text.js.map +1 -1
  94. package/dist/src/hosts/vstack.js +1 -1
  95. package/dist/src/hosts/vstack.js.map +1 -1
  96. package/dist/src/hosts/zstack.d.ts +3 -3
  97. package/dist/src/hosts/zstack.d.ts.map +1 -1
  98. package/dist/src/hosts/zstack.js +13 -8
  99. package/dist/src/hosts/zstack.js.map +1 -1
  100. package/dist/src/index.d.ts +2 -2
  101. package/dist/src/index.d.ts.map +1 -1
  102. package/dist/src/internal/dev/hmr.d.ts +20 -0
  103. package/dist/src/internal/dev/hmr.d.ts.map +1 -0
  104. package/dist/src/internal/dev/hmr.js +93 -0
  105. package/dist/src/internal/dev/hmr.js.map +1 -0
  106. package/dist/src/internal/dev/runtime.d.ts +24 -0
  107. package/dist/src/internal/dev/runtime.d.ts.map +1 -0
  108. package/dist/src/internal/dev/runtime.js +135 -0
  109. package/dist/src/internal/dev/runtime.js.map +1 -0
  110. package/dist/src/internal/dev/ui.d.ts +13 -0
  111. package/dist/src/internal/dev/ui.d.ts.map +1 -0
  112. package/dist/src/internal/dev/ui.js +51 -0
  113. package/dist/src/internal/dev/ui.js.map +1 -0
  114. package/dist/src/internal/renderer/context.d.ts +9 -0
  115. package/dist/src/internal/renderer/context.d.ts.map +1 -0
  116. package/dist/src/internal/renderer/context.js +22 -0
  117. package/dist/src/internal/renderer/context.js.map +1 -0
  118. package/dist/src/internal/renderer/core/FrameBuilder.d.ts +18 -0
  119. package/dist/src/internal/renderer/core/FrameBuilder.d.ts.map +1 -0
  120. package/dist/src/internal/renderer/core/FrameBuilder.js +40 -0
  121. package/dist/src/internal/renderer/core/FrameBuilder.js.map +1 -0
  122. package/dist/src/internal/renderer/core/RendererState.d.ts +41 -0
  123. package/dist/src/internal/renderer/core/RendererState.d.ts.map +1 -0
  124. package/dist/src/internal/renderer/core/RendererState.js +70 -0
  125. package/dist/src/internal/renderer/core/RendererState.js.map +1 -0
  126. package/dist/src/internal/renderer/core/index.d.ts +3 -0
  127. package/dist/src/internal/renderer/core/index.d.ts.map +1 -0
  128. package/dist/src/internal/renderer/core/index.js +3 -0
  129. package/dist/src/internal/renderer/core/index.js.map +1 -0
  130. package/dist/src/internal/renderer/index.d.ts +40 -0
  131. package/dist/src/internal/renderer/index.d.ts.map +1 -0
  132. package/dist/src/internal/renderer/index.js +543 -0
  133. package/dist/src/internal/renderer/index.js.map +1 -0
  134. package/dist/src/internal/renderer/input/InputProcessor.d.ts +30 -0
  135. package/dist/src/internal/renderer/input/InputProcessor.d.ts.map +1 -0
  136. package/dist/src/internal/renderer/input/InputProcessor.js +122 -0
  137. package/dist/src/internal/renderer/input/InputProcessor.js.map +1 -0
  138. package/dist/src/internal/renderer/input/index.d.ts +2 -0
  139. package/dist/src/internal/renderer/input/index.d.ts.map +1 -0
  140. package/dist/src/internal/renderer/input/index.js +2 -0
  141. package/dist/src/internal/renderer/input/index.js.map +1 -0
  142. package/dist/src/internal/renderer/lifecycle/EventBus.d.ts +42 -0
  143. package/dist/src/internal/renderer/lifecycle/EventBus.d.ts.map +1 -0
  144. package/dist/src/internal/renderer/lifecycle/EventBus.js +97 -0
  145. package/dist/src/internal/renderer/lifecycle/EventBus.js.map +1 -0
  146. package/dist/src/internal/renderer/lifecycle/ProcessLifecycle.d.ts +13 -0
  147. package/dist/src/internal/renderer/lifecycle/ProcessLifecycle.d.ts.map +1 -0
  148. package/dist/src/internal/renderer/lifecycle/ProcessLifecycle.js +111 -0
  149. package/dist/src/internal/renderer/lifecycle/ProcessLifecycle.js.map +1 -0
  150. package/dist/src/internal/renderer/lifecycle/RenderCache.d.ts +3 -0
  151. package/dist/src/internal/renderer/lifecycle/RenderCache.d.ts.map +1 -0
  152. package/dist/src/internal/renderer/lifecycle/RenderCache.js +9 -0
  153. package/dist/src/internal/renderer/lifecycle/RenderCache.js.map +1 -0
  154. package/dist/src/internal/renderer/lifecycle/index.d.ts +4 -0
  155. package/dist/src/internal/renderer/lifecycle/index.d.ts.map +1 -0
  156. package/dist/src/internal/renderer/lifecycle/index.js +4 -0
  157. package/dist/src/internal/renderer/lifecycle/index.js.map +1 -0
  158. package/dist/src/internal/renderer/modes/FullscreenRenderer.d.ts +12 -0
  159. package/dist/src/internal/renderer/modes/FullscreenRenderer.d.ts.map +1 -0
  160. package/dist/src/internal/renderer/modes/FullscreenRenderer.js +54 -0
  161. package/dist/src/internal/renderer/modes/FullscreenRenderer.js.map +1 -0
  162. package/dist/src/internal/renderer/modes/InlineRenderer.d.ts +25 -0
  163. package/dist/src/internal/renderer/modes/InlineRenderer.d.ts.map +1 -0
  164. package/dist/src/internal/renderer/modes/InlineRenderer.js +166 -0
  165. package/dist/src/internal/renderer/modes/InlineRenderer.js.map +1 -0
  166. package/dist/src/internal/renderer/modes/RendererMode.d.ts +42 -0
  167. package/dist/src/internal/renderer/modes/RendererMode.d.ts.map +1 -0
  168. package/dist/src/internal/renderer/modes/RendererMode.js +2 -0
  169. package/dist/src/internal/renderer/modes/RendererMode.js.map +1 -0
  170. package/dist/src/internal/renderer/modes/StaticContentRenderer.d.ts +25 -0
  171. package/dist/src/internal/renderer/modes/StaticContentRenderer.d.ts.map +1 -0
  172. package/dist/src/internal/renderer/modes/StaticContentRenderer.js +49 -0
  173. package/dist/src/internal/renderer/modes/StaticContentRenderer.js.map +1 -0
  174. package/dist/src/internal/renderer/modes/index.d.ts +5 -0
  175. package/dist/src/internal/renderer/modes/index.d.ts.map +1 -0
  176. package/dist/src/internal/renderer/modes/index.js +4 -0
  177. package/dist/src/internal/renderer/modes/index.js.map +1 -0
  178. package/dist/src/internal/renderer/terminal/KeyboardCapabilityProbe.d.ts +13 -0
  179. package/dist/src/internal/renderer/terminal/KeyboardCapabilityProbe.d.ts.map +1 -0
  180. package/dist/src/internal/renderer/terminal/KeyboardCapabilityProbe.js +75 -0
  181. package/dist/src/internal/renderer/terminal/KeyboardCapabilityProbe.js.map +1 -0
  182. package/dist/src/internal/renderer/terminal/TerminalSetup.d.ts +29 -0
  183. package/dist/src/internal/renderer/terminal/TerminalSetup.d.ts.map +1 -0
  184. package/dist/src/internal/renderer/terminal/TerminalSetup.js +82 -0
  185. package/dist/src/internal/renderer/terminal/TerminalSetup.js.map +1 -0
  186. package/dist/src/internal/renderer/terminal/index.d.ts +3 -0
  187. package/dist/src/internal/renderer/terminal/index.d.ts.map +1 -0
  188. package/dist/src/internal/renderer/terminal/index.js +3 -0
  189. package/dist/src/internal/renderer/terminal/index.js.map +1 -0
  190. package/dist/src/internal/renderer/types.d.ts +122 -0
  191. package/dist/src/internal/renderer/types.d.ts.map +1 -0
  192. package/dist/src/internal/renderer/types.js +2 -0
  193. package/dist/src/internal/renderer/types.js.map +1 -0
  194. package/dist/src/motion/hooks.d.ts +1 -1
  195. package/dist/src/motion/hooks.js +1 -1
  196. package/dist/src/reconciler/host-config.js +2 -2
  197. package/dist/src/reconciler/host-config.js.map +1 -1
  198. package/dist/src/reconciler/types.d.ts +5 -1
  199. package/dist/src/reconciler/types.d.ts.map +1 -1
  200. package/dist/src/renderer-context.d.ts +1 -8
  201. package/dist/src/renderer-context.d.ts.map +1 -1
  202. package/dist/src/renderer-context.js +1 -21
  203. package/dist/src/renderer-context.js.map +1 -1
  204. package/dist/src/renderer-types.d.ts +1 -115
  205. package/dist/src/renderer-types.d.ts.map +1 -1
  206. package/dist/src/renderer.d.ts +1 -31
  207. package/dist/src/renderer.d.ts.map +1 -1
  208. package/dist/src/renderer.js +1 -495
  209. package/dist/src/renderer.js.map +1 -1
  210. package/dist/src/test/render-tui.d.ts +3 -3
  211. package/dist/src/test/render-tui.d.ts.map +1 -1
  212. package/dist/src/test/render-tui.js +16 -9
  213. package/dist/src/test/render-tui.js.map +1 -1
  214. package/dist/src/utils/alignment.d.ts +1 -1
  215. package/dist/src/utils/alignment.d.ts.map +1 -1
  216. package/dist/src/utils/alignment.js +0 -2
  217. package/dist/src/utils/alignment.js.map +1 -1
  218. package/dist/src/utils/border.d.ts +1 -1
  219. package/dist/src/utils/border.d.ts.map +1 -1
  220. package/dist/src/utils/border.js +2 -0
  221. package/dist/src/utils/border.js.map +1 -1
  222. package/dist/src/utils/console-helpers.d.ts +19 -0
  223. package/dist/src/utils/console-helpers.d.ts.map +1 -0
  224. package/dist/src/utils/console-helpers.js +61 -0
  225. package/dist/src/utils/console-helpers.js.map +1 -0
  226. package/dist/src/utils/index.d.ts +2 -1
  227. package/dist/src/utils/index.d.ts.map +1 -1
  228. package/dist/src/utils/index.js +2 -1
  229. package/dist/src/utils/index.js.map +1 -1
  230. package/dist/src/utils/styles.d.ts +8 -1
  231. package/dist/src/utils/styles.d.ts.map +1 -1
  232. package/dist/src/utils/styles.js +10 -8
  233. package/dist/src/utils/styles.js.map +1 -1
  234. package/dist/src/utils/text-layout.d.ts +22 -0
  235. package/dist/src/utils/text-layout.d.ts.map +1 -0
  236. package/dist/src/utils/text-layout.js +37 -0
  237. package/dist/src/utils/text-layout.js.map +1 -0
  238. package/dist/src/utils/text-wrap.d.ts +31 -1
  239. package/dist/src/utils/text-wrap.d.ts.map +1 -1
  240. package/dist/src/utils/text-wrap.js +205 -48
  241. package/dist/src/utils/text-wrap.js.map +1 -1
  242. package/dist/src/visualize/index.js +1 -1
  243. package/dist/src/visualize/index.js.map +1 -1
  244. package/dist/tsconfig.tsbuildinfo +1 -1
  245. package/package.json +2 -2
  246. package/src/codeblock.tsx +2 -2
  247. package/src/components/ListView.tsx +21 -23
  248. package/src/components/Markdown.tsx +3 -3
  249. package/src/components/MultilineTextInput.tsx +138 -344
  250. package/src/components/TextInput.tsx +54 -99
  251. package/src/components/text-editing.ts +180 -0
  252. package/src/console/ConsolePopover.tsx +124 -107
  253. package/src/debug/DebugOverlay.ts +15 -74
  254. package/src/debug/DiagnosticsPanel.tsx +1 -1
  255. package/src/dev.tsx +5 -458
  256. package/src/hooks/use-scroll.ts +85 -145
  257. package/src/hosts/base.ts +86 -3
  258. package/src/hosts/box.ts +37 -2
  259. package/src/hosts/canvas.ts +128 -42
  260. package/src/hosts/codeblock.ts +48 -35
  261. package/src/hosts/flex-container.ts +25 -6
  262. package/src/hosts/index.ts +11 -2
  263. package/src/hosts/layout-helpers.ts +20 -0
  264. package/src/hosts/leaf.ts +36 -0
  265. package/src/hosts/overlay-item.ts +8 -2
  266. package/src/hosts/overlay.ts +13 -11
  267. package/src/hosts/scroll.ts +228 -106
  268. package/src/hosts/single-child.ts +2 -0
  269. package/src/hosts/spacer.ts +8 -3
  270. package/src/hosts/text.ts +126 -132
  271. package/src/hosts/vstack.ts +1 -1
  272. package/src/hosts/zstack.ts +14 -9
  273. package/src/index.ts +2 -2
  274. package/src/internal/dev/hmr.ts +101 -0
  275. package/src/internal/dev/runtime.ts +170 -0
  276. package/src/internal/dev/ui.tsx +87 -0
  277. package/src/internal/renderer/context.ts +27 -0
  278. package/src/{renderer → internal/renderer}/core/FrameBuilder.ts +2 -2
  279. package/src/internal/renderer/index.ts +689 -0
  280. package/src/{renderer → internal/renderer}/input/InputProcessor.ts +10 -1
  281. package/src/{renderer → internal/renderer}/lifecycle/EventBus.ts +9 -1
  282. package/src/internal/renderer/lifecycle/ProcessLifecycle.ts +125 -0
  283. package/src/internal/renderer/lifecycle/index.ts +3 -0
  284. package/src/{renderer → internal/renderer}/modes/InlineRenderer.ts +5 -2
  285. package/src/{renderer → internal/renderer}/modes/RendererMode.ts +1 -1
  286. package/src/{renderer → internal/renderer}/modes/StaticContentRenderer.ts +5 -2
  287. package/src/internal/renderer/terminal/KeyboardCapabilityProbe.ts +91 -0
  288. package/src/{renderer/lifecycle → internal/renderer/terminal}/TerminalSetup.ts +4 -22
  289. package/src/internal/renderer/terminal/index.ts +2 -0
  290. package/src/internal/renderer/types.ts +129 -0
  291. package/src/motion/hooks.ts +1 -1
  292. package/src/reconciler/host-config.ts +2 -2
  293. package/src/reconciler/types.ts +7 -1
  294. package/src/renderer-context.ts +1 -27
  295. package/src/renderer-types.ts +10 -123
  296. package/src/renderer.ts +1 -619
  297. package/src/test/render-tui.ts +16 -10
  298. package/src/utils/alignment.ts +1 -3
  299. package/src/utils/border.ts +11 -1
  300. package/src/utils/console-helpers.ts +86 -0
  301. package/src/utils/index.ts +15 -1
  302. package/src/utils/styles.ts +16 -4
  303. package/src/utils/text-layout.ts +65 -0
  304. package/src/utils/text-wrap.ts +261 -48
  305. package/src/visualize/index.tsx +1 -1
  306. package/src/renderer/lifecycle/ResizeManager.ts +0 -65
  307. package/src/renderer/lifecycle/index.ts +0 -4
  308. /package/src/{renderer → internal/renderer}/core/RendererState.ts +0 -0
  309. /package/src/{renderer → internal/renderer}/core/index.ts +0 -0
  310. /package/src/{renderer → internal/renderer}/input/index.ts +0 -0
  311. /package/src/{renderer → internal/renderer}/lifecycle/RenderCache.ts +0 -0
  312. /package/src/{renderer → internal/renderer}/modes/FullscreenRenderer.ts +0 -0
  313. /package/src/{renderer → internal/renderer}/modes/index.ts +0 -0
@@ -2,7 +2,7 @@
2
2
 
3
3
  import type { KeyMsg } from "@effect-tui/core"
4
4
  import { useCallback, useLayoutEffect, useMemo, useReducer, useRef } from "react"
5
- import type { ScrollProps } from "../hosts/scroll.js"
5
+ import type { ScrollAlign, ScrollAxis, ScrollLayoutChange, ScrollProps } from "../hosts/scroll.js"
6
6
  import { useTerminalSize } from "../renderer.js"
7
7
  import { useKeyboard } from "./use-keyboard.js"
8
8
 
@@ -86,21 +86,21 @@ class MacOSScrollAccel implements ScrollAcceleration {
86
86
  // ============================================================================
87
87
 
88
88
  export interface ScrollState {
89
- /** Current scroll offset (pixels from start) for the primary axis */
90
- offset: number
91
- /** Maximum scroll offset for the primary axis */
92
- maxOffset: number
93
- /** Viewport size for the primary axis */
94
- viewportSize: number
95
- /** Whether viewport size has been measured by the host (primary axis) */
96
- viewportMeasured: boolean
97
- /** Total content size for the primary axis */
98
- contentSize: number
99
- /** Whether we're at the start edge (primary axis) */
100
- atStart: boolean
101
- /** Whether we're at the end edge (primary axis) */
102
- atEnd: boolean
103
- /** Current horizontal offset (pixels from start) */
89
+ /** Current vertical offset (pixels from top) */
90
+ offsetY: number
91
+ /** Maximum vertical offset */
92
+ maxOffsetY: number
93
+ /** Vertical viewport size */
94
+ viewportSizeY: number
95
+ /** Whether vertical viewport size has been measured by the host */
96
+ viewportMeasuredY: boolean
97
+ /** Total vertical content size */
98
+ contentSizeY: number
99
+ /** Whether we're at the top edge */
100
+ atStartY: boolean
101
+ /** Whether we're at the bottom edge */
102
+ atEndY: boolean
103
+ /** Current horizontal offset (pixels from left) */
104
104
  offsetX: number
105
105
  /** Maximum horizontal offset */
106
106
  maxOffsetX: number
@@ -118,31 +118,25 @@ export interface ScrollState {
118
118
 
119
119
  export interface UseScrollOptions {
120
120
  /** Scroll axis: "vertical" (default), "horizontal", or "both" */
121
- axis?: "vertical" | "horizontal" | "both"
122
- /** Controlled content size for the primary axis (skips host measurement when provided) */
123
- contentSize?: number
121
+ axis?: ScrollAxis
124
122
  /** Controlled content width (skips host measurement when provided) */
125
123
  contentWidth?: number
126
124
  /** Controlled content height (skips host measurement when provided) */
127
125
  contentHeight?: number
128
- /** @internal Initial viewport size override (useful for tests) */
129
- initialViewportSize?: number
130
126
  /** @internal Initial viewport width override (useful for tests) */
131
127
  initialViewportWidth?: number
132
128
  /** @internal Initial viewport height override (useful for tests) */
133
129
  initialViewportHeight?: number
134
- /** @internal Initial content size override (useful for tests) */
135
- initialContentSize?: number
136
130
  /** @internal Initial content width override (useful for tests) */
137
131
  initialContentWidth?: number
138
132
  /** @internal Initial content height override (useful for tests) */
139
133
  initialContentHeight?: number
140
- /** Initial scroll offset for the primary axis */
141
- initialOffset?: number
134
+ /** Initial vertical scroll offset */
135
+ initialOffsetY?: number
142
136
  /** Initial horizontal scroll offset */
143
137
  initialOffsetX?: number
144
138
  /** Alignment when content is smaller than viewport */
145
- align?: "start" | "end"
139
+ align?: ScrollAlign
146
140
  /** Whether to show scrollbars */
147
141
  showScrollbar?: boolean
148
142
  /** Enable keyboard navigation (default: true) */
@@ -157,23 +151,25 @@ export interface UseScrollOptions {
157
151
  arrowSpeed?: number
158
152
  /** Scroll speed for page up/down (fraction of viewport, default: 0.5) */
159
153
  pageSpeed?: number
154
+ /** Optional layout callback (content/viewport/offset/rect). */
155
+ onScrollLayoutChange?: (event: ScrollLayoutChange) => void
160
156
  }
161
157
 
162
158
  export interface UseScrollReturn {
163
159
  /** Current scroll state */
164
160
  state: ScrollState
165
- /** Set scroll offset directly (primary axis) */
166
- setOffset: (offset: number) => void
161
+ /** Set vertical scroll offset directly */
162
+ setOffsetY: (offsetY: number) => void
167
163
  /** Set horizontal scroll offset directly */
168
164
  setOffsetX: (offsetX: number) => void
169
- /** Scroll by delta pixels (primary axis) */
170
- scrollBy: (delta: number) => void
165
+ /** Scroll by delta pixels vertically */
166
+ scrollByY: (delta: number) => void
171
167
  /** Scroll by delta pixels horizontally */
172
168
  scrollByX: (delta: number) => void
173
- /** Scroll to start (primary axis) */
174
- scrollToStart: () => void
175
- /** Scroll to end (primary axis) */
176
- scrollToEnd: () => void
169
+ /** Scroll to top */
170
+ scrollToStartY: () => void
171
+ /** Scroll to bottom */
172
+ scrollToEndY: () => void
177
173
  /** Scroll to horizontal start */
178
174
  scrollToStartX: () => void
179
175
  /** Scroll to horizontal end */
@@ -187,7 +183,7 @@ export interface UseScrollReturn {
187
183
  * @param totalSize - Optional known total content size (avoids stale state issues)
188
184
  * @param axis - Axis to scroll (defaults to vertical)
189
185
  */
190
- scrollToVisible: (
186
+ scrollIntoView: (
191
187
  position: number,
192
188
  itemSize?: number,
193
189
  padding?: number,
@@ -287,18 +283,15 @@ const reduceScroll = (state: ScrollInternalState, action: ScrollAction): ScrollI
287
283
  export function useScroll(options: UseScrollOptions = {}): UseScrollReturn {
288
284
  const {
289
285
  axis = "vertical",
290
- contentSize,
291
286
  contentWidth,
292
287
  contentHeight,
293
- initialViewportSize,
294
288
  initialViewportWidth,
295
289
  initialViewportHeight,
296
- initialContentSize,
297
290
  initialContentWidth,
298
291
  initialContentHeight,
299
- initialOffset = 0,
292
+ initialOffsetY = 0,
300
293
  initialOffsetX = 0,
301
- align = "start",
294
+ align,
302
295
  showScrollbar = true,
303
296
  enableKeyboard = true,
304
297
  enableMouseWheel = true,
@@ -306,31 +299,30 @@ export function useScroll(options: UseScrollOptions = {}): UseScrollReturn {
306
299
  sticky = false,
307
300
  arrowSpeed = 1,
308
301
  pageSpeed = 0.5,
302
+ onScrollLayoutChange,
309
303
  } = options
310
304
 
311
305
  const { width: termWidth, height: termHeight } = useTerminalSize()
312
306
  const enableY = axis === "vertical" || axis === "both"
313
307
  const enableX = axis === "horizontal" || axis === "both"
314
- const primaryAxis: "vertical" | "horizontal" = axis === "horizontal" ? "horizontal" : "vertical"
308
+ const useHorizontalPrimary = axis === "horizontal"
315
309
 
316
- const controlledContentHeight = contentHeight ?? (axis !== "horizontal" ? contentSize : undefined)
317
- const controlledContentWidth = contentWidth ?? (axis === "horizontal" ? contentSize : undefined)
310
+ const controlledContentHeight = contentHeight
311
+ const controlledContentWidth = contentWidth
318
312
 
319
- const baseViewportHeight =
320
- initialViewportHeight ?? (axis !== "horizontal" ? initialViewportSize : undefined) ?? termHeight
321
- const baseViewportWidth =
322
- initialViewportWidth ?? (axis === "horizontal" ? initialViewportSize : undefined) ?? termWidth
313
+ const baseViewportHeight = initialViewportHeight ?? termHeight
314
+ const baseViewportWidth = initialViewportWidth ?? termWidth
323
315
 
324
316
  const baseContentHeight =
325
- controlledContentHeight ?? initialContentHeight ?? (axis !== "horizontal" ? initialContentSize : undefined) ?? 0
317
+ controlledContentHeight ?? initialContentHeight ?? 0
326
318
  const baseContentWidth =
327
- controlledContentWidth ?? initialContentWidth ?? (axis === "horizontal" ? initialContentSize : undefined) ?? 0
319
+ controlledContentWidth ?? initialContentWidth ?? 0
328
320
 
329
- const initialOffsetY = axis === "horizontal" ? 0 : initialOffset
330
- const initialOffsetXResolved = axis === "horizontal" ? initialOffset : initialOffsetX
321
+ const initialOffsetYResolved = axis === "horizontal" ? 0 : initialOffsetY
322
+ const initialOffsetXResolved = axis === "vertical" ? 0 : initialOffsetX
331
323
 
332
324
  const [internalY, dispatchY] = useReducer(reduceScroll, {
333
- offset: clampOffset(initialOffsetY, baseContentHeight, baseViewportHeight),
325
+ offset: clampOffset(initialOffsetYResolved, baseContentHeight, baseViewportHeight),
334
326
  contentSize: baseContentHeight,
335
327
  viewportSize: baseViewportHeight,
336
328
  viewportMeasured: false,
@@ -398,17 +390,6 @@ export function useScroll(options: UseScrollOptions = {}): UseScrollReturn {
398
390
  [dispatchX, enableX],
399
391
  )
400
392
 
401
- const scrollBy = useCallback(
402
- (delta: number) => {
403
- if (primaryAxis === "horizontal") {
404
- scrollByX(delta)
405
- } else {
406
- scrollByY(delta)
407
- }
408
- },
409
- [primaryAxis, scrollByX, scrollByY],
410
- )
411
-
412
393
  const scrollToStartY = useCallback(() => {
413
394
  dispatchY({ type: "set-offset", offset: 0 })
414
395
  accumulatorYRef.current = 0
@@ -421,14 +402,6 @@ export function useScroll(options: UseScrollOptions = {}): UseScrollReturn {
421
402
  accelX.reset()
422
403
  }, [dispatchX, accelX])
423
404
 
424
- const scrollToStart = useCallback(() => {
425
- if (primaryAxis === "horizontal") {
426
- scrollToStartX()
427
- } else {
428
- scrollToStartY()
429
- }
430
- }, [primaryAxis, scrollToStartX, scrollToStartY])
431
-
432
405
  const scrollToEndY = useCallback(() => {
433
406
  const current = stateYRef.current
434
407
  const maxOffset = Math.max(0, current.contentSize - current.viewportSize)
@@ -445,27 +418,6 @@ export function useScroll(options: UseScrollOptions = {}): UseScrollReturn {
445
418
  accelX.reset()
446
419
  }, [dispatchX, accelX])
447
420
 
448
- const scrollToEnd = useCallback(() => {
449
- if (primaryAxis === "horizontal") {
450
- scrollToEndX()
451
- } else {
452
- scrollToEndY()
453
- }
454
- }, [primaryAxis, scrollToEndX, scrollToEndY])
455
-
456
- // Handle content size changes (for sticky scroll)
457
- const handleContentSize = useCallback(
458
- (width: number, height: number) => {
459
- if (enableY && controlledContentHeight === undefined) {
460
- dispatchY({ type: "set-content", size: height })
461
- }
462
- if (enableX && controlledContentWidth === undefined) {
463
- dispatchX({ type: "set-content", size: width })
464
- }
465
- },
466
- [controlledContentHeight, controlledContentWidth, dispatchX, dispatchY, enableX, enableY],
467
- )
468
-
469
421
  useLayoutEffect(() => {
470
422
  if (!enableY || controlledContentHeight === undefined) return
471
423
  dispatchY({ type: "set-content", size: controlledContentHeight })
@@ -476,19 +428,35 @@ export function useScroll(options: UseScrollOptions = {}): UseScrollReturn {
476
428
  dispatchX({ type: "set-content", size: controlledContentWidth })
477
429
  }, [controlledContentWidth, dispatchX, enableX])
478
430
 
479
- // Handle viewport size changes (reported by scroll component)
480
- const handleViewportSize = useCallback(
481
- (width: number, height: number) => {
431
+ const handleScrollLayout = useCallback(
432
+ (event: ScrollLayoutChange) => {
482
433
  if (enableY) {
483
- viewportYRef.current = height
484
- dispatchY({ type: "set-viewport", size: height })
434
+ viewportYRef.current = event.viewport.height
435
+ dispatchY({ type: "set-viewport", size: event.viewport.height })
436
+ if (controlledContentHeight === undefined) {
437
+ dispatchY({ type: "set-content", size: event.content.height })
438
+ }
439
+ dispatchY({ type: "sync-effective-offset", offset: event.offset.y })
485
440
  }
486
441
  if (enableX) {
487
- viewportXRef.current = width
488
- dispatchX({ type: "set-viewport", size: width })
442
+ viewportXRef.current = event.viewport.width
443
+ dispatchX({ type: "set-viewport", size: event.viewport.width })
444
+ if (controlledContentWidth === undefined) {
445
+ dispatchX({ type: "set-content", size: event.content.width })
446
+ }
447
+ dispatchX({ type: "sync-effective-offset", offset: event.offset.x })
489
448
  }
449
+ onScrollLayoutChange?.(event)
490
450
  },
491
- [dispatchX, dispatchY, enableX, enableY],
451
+ [
452
+ controlledContentHeight,
453
+ controlledContentWidth,
454
+ dispatchX,
455
+ dispatchY,
456
+ enableX,
457
+ enableY,
458
+ onScrollLayoutChange,
459
+ ],
492
460
  )
493
461
 
494
462
  // Keyboard handler
@@ -497,7 +465,7 @@ export function useScroll(options: UseScrollOptions = {}): UseScrollReturn {
497
465
  // Mouse wheel comes as pageup/pagedown with meta=true
498
466
  // Handle separately from keyboard since enableKeyboard shouldn't disable mouse
499
467
  if (key.meta && enableMouseWheel && (key.name === "pageup" || key.name === "pagedown")) {
500
- const multiplier = primaryAxis === "horizontal" && !enableY ? accelX.tick() : accelY.tick()
468
+ const multiplier = useHorizontalPrimary && !enableY ? accelX.tick() : accelY.tick()
501
469
  const delta = Math.ceil(arrowSpeed * multiplier)
502
470
  if (axis === "horizontal") {
503
471
  scrollByX(key.name === "pageup" ? -delta : delta)
@@ -569,7 +537,7 @@ export function useScroll(options: UseScrollOptions = {}): UseScrollReturn {
569
537
  accelX,
570
538
  accelY,
571
539
  enableY,
572
- primaryAxis,
540
+ useHorizontalPrimary,
573
541
  scrollByX,
574
542
  scrollByY,
575
543
  scrollToEndX,
@@ -585,7 +553,7 @@ export function useScroll(options: UseScrollOptions = {}): UseScrollReturn {
585
553
  // Uses refs to avoid stale closures, but re-creates when viewport size changes
586
554
  // so selection effects can re-run after measurement updates.
587
555
  // Bypasses clampOffset because it uses totalSize for accurate clamping
588
- const scrollToVisible = useCallback(
556
+ const scrollIntoView = useCallback(
589
557
  (
590
558
  position: number,
591
559
  itemSize = 1,
@@ -633,17 +601,6 @@ export function useScroll(options: UseScrollOptions = {}): UseScrollReturn {
633
601
  [dispatchX],
634
602
  )
635
603
 
636
- const setOffset = useCallback(
637
- (newOffset: number) => {
638
- if (primaryAxis === "horizontal") {
639
- setOffsetX(newOffset)
640
- } else {
641
- setOffsetY(newOffset)
642
- }
643
- },
644
- [primaryAxis, setOffsetX, setOffsetY],
645
- )
646
-
647
604
  // Calculate derived state
648
605
  const maxOffsetY = Math.max(0, internalY.contentSize - internalY.viewportSize)
649
606
  const maxOffsetX = Math.max(0, internalX.contentSize - internalX.viewportSize)
@@ -652,19 +609,14 @@ export function useScroll(options: UseScrollOptions = {}): UseScrollReturn {
652
609
  const atStartX = internalX.offset <= 0
653
610
  const atEndX = internalX.offset >= maxOffsetX
654
611
 
655
- const primaryState = primaryAxis === "horizontal" ? internalX : internalY
656
- const primaryMaxOffset = primaryAxis === "horizontal" ? maxOffsetX : maxOffsetY
657
- const primaryAtStart = primaryAxis === "horizontal" ? atStartX : atStartY
658
- const primaryAtEnd = primaryAxis === "horizontal" ? atEndX : atEndY
659
-
660
612
  const state: ScrollState = {
661
- offset: primaryState.offset,
662
- maxOffset: primaryMaxOffset,
663
- viewportSize: primaryState.viewportSize,
664
- viewportMeasured: primaryState.viewportMeasured,
665
- contentSize: primaryState.contentSize,
666
- atStart: primaryAtStart,
667
- atEnd: primaryAtEnd,
613
+ offsetY: internalY.offset,
614
+ maxOffsetY,
615
+ viewportSizeY: internalY.viewportSize,
616
+ viewportMeasuredY: internalY.viewportMeasured,
617
+ contentSizeY: internalY.contentSize,
618
+ atStartY,
619
+ atEndY,
668
620
  offsetX: internalX.offset,
669
621
  maxOffsetX,
670
622
  viewportSizeX: internalX.viewportSize,
@@ -674,39 +626,27 @@ export function useScroll(options: UseScrollOptions = {}): UseScrollReturn {
674
626
  atEndX,
675
627
  }
676
628
 
677
- // Handle effective offset sync from host (when sticky adjusts the offset)
678
- const handleEffectiveOffset = useCallback((effectiveOffset: number) => {
679
- dispatchY({ type: "sync-effective-offset", offset: effectiveOffset })
680
- }, [dispatchY])
681
-
682
- const handleEffectiveOffsetX = useCallback((effectiveOffsetX: number) => {
683
- dispatchX({ type: "sync-effective-offset", offset: effectiveOffsetX })
684
- }, [dispatchX])
685
-
686
629
  const scrollProps: ScrollProps = {
687
- offset: enableY ? internalY.offset : 0,
630
+ offsetY: enableY ? internalY.offset : 0,
688
631
  offsetX: enableX ? internalX.offset : 0,
689
632
  axis,
690
633
  align,
691
634
  sticky,
692
635
  showScrollbar,
693
- onContentSize: handleContentSize,
694
- onViewportSize: handleViewportSize,
695
- onEffectiveOffset: enableY ? handleEffectiveOffset : undefined,
696
- onEffectiveOffsetX: enableX ? handleEffectiveOffsetX : undefined,
636
+ onScrollLayoutChange: handleScrollLayout,
697
637
  }
698
638
 
699
639
  return {
700
640
  state,
701
- setOffset,
641
+ setOffsetY,
702
642
  setOffsetX,
703
- scrollBy,
643
+ scrollByY,
704
644
  scrollByX,
705
- scrollToStart,
706
- scrollToEnd,
645
+ scrollToStartY,
646
+ scrollToEndY,
707
647
  scrollToStartX,
708
648
  scrollToEndX,
709
- scrollToVisible,
649
+ scrollIntoView,
710
650
  scrollProps,
711
651
  }
712
652
  }
package/src/hosts/base.ts CHANGED
@@ -67,6 +67,12 @@ export abstract class BaseHost implements HostInstance {
67
67
  parent: HostInstance | null = null
68
68
  children: HostInstance[] = []
69
69
  rect: Rect | null = null
70
+ private _layoutDirty = true
71
+ private _renderDirty = true
72
+ private _lastMeasureW = -1
73
+ private _lastMeasureH = -1
74
+ private _lastMeasuredSize: Size | null = null
75
+ private _lastLayoutRect: Rect | null = null
70
76
 
71
77
  // Greedy layout - expands to fill remaining space
72
78
  // undefined = not greedy (hug content)
@@ -117,17 +123,38 @@ export abstract class BaseHost implements HostInstance {
117
123
  * Optional pre-frame hook. BaseHost will call prepareSelf() and then recurse into children.
118
124
  */
119
125
  prepareFrame(): void {
120
- this.prepareSelf()
126
+ this.ensurePrepared()
121
127
  for (const child of this.children) {
122
128
  child.prepareFrame?.()
123
129
  }
124
130
  }
125
131
 
126
132
  /** Override in subclasses to precompute caches once per frame. */
127
- protected prepareSelf(): void {
133
+ protected prepareSelf(_layoutDirty: boolean, _renderDirty: boolean): void {
128
134
  // Default no-op
129
135
  }
130
136
 
137
+ protected ensurePrepared(): void {
138
+ if (!this._layoutDirty && !this._renderDirty) return
139
+ const layoutDirty = this._layoutDirty
140
+ const renderDirty = this._renderDirty
141
+ this._renderDirty = false
142
+ this.prepareSelf(layoutDirty, renderDirty)
143
+ }
144
+
145
+ invalidateLayout(): void {
146
+ this._layoutDirty = true
147
+ this._renderDirty = true
148
+ this._lastMeasuredSize = null
149
+ this.ctx.requestRender()
150
+ this.parent?.invalidateLayout?.()
151
+ }
152
+
153
+ invalidateRender(): void {
154
+ this._renderDirty = true
155
+ this.ctx.requestRender()
156
+ }
157
+
131
158
  /**
132
159
  * Resolve a prop that may be a MotionValue/ColorMotionValue.
133
160
  * If it's a spring, subscribes to changes and returns current value.
@@ -204,10 +231,39 @@ export abstract class BaseHost implements HostInstance {
204
231
  this._springSubscriptions.clear()
205
232
  }
206
233
 
207
- abstract measure(maxW: number, maxH: number): Size
234
+ measure(maxW: number, maxH: number): Size {
235
+ const constraintsUnchanged = this._lastMeasureW === maxW && this._lastMeasureH === maxH
236
+ if (!this._layoutDirty && constraintsUnchanged && this._lastMeasuredSize) {
237
+ return this._lastMeasuredSize
238
+ }
239
+
240
+ if (!constraintsUnchanged) {
241
+ this._layoutDirty = true
242
+ }
243
+
244
+ const size = this.measureSelf(maxW, maxH)
245
+ this._lastMeasureW = maxW
246
+ this._lastMeasureH = maxH
247
+ this._lastMeasuredSize = size
248
+ return size
249
+ }
250
+
251
+ protected abstract measureSelf(maxW: number, maxH: number): Size
208
252
  abstract render(buffer: CellBuffer, palette: Palette): void
209
253
 
210
254
  layout(rect: Rect): void {
255
+ const prev = this._lastLayoutRect
256
+ const rectUnchanged =
257
+ prev?.x === rect.x && prev?.y === rect.y && prev?.w === rect.w && prev?.h === rect.h
258
+
259
+ if (!this._layoutDirty && rectUnchanged) return
260
+
261
+ this.layoutSelf(rect)
262
+ this._layoutDirty = false
263
+ this._lastLayoutRect = this.rect ?? rect
264
+ }
265
+
266
+ protected layoutSelf(rect: Rect): void {
211
267
  this.layoutWithConstraints(rect)
212
268
  }
213
269
 
@@ -319,6 +375,7 @@ export abstract class BaseHost implements HostInstance {
319
375
  updateProps(props: Record<string, unknown>): void {
320
376
  // Greedy layout - reset to undefined unless explicitly set
321
377
  // Subclasses like Spacer/Scroll set their own default before calling super
378
+ const prevGreedy = this.greedy
322
379
  if ("greedy" in props) {
323
380
  const greedy = props.greedy
324
381
  if (greedy === true) {
@@ -336,6 +393,12 @@ export abstract class BaseHost implements HostInstance {
336
393
  }
337
394
 
338
395
  // Frame constraints - only accept valid numbers (ignore strings like "100%")
396
+ const prevFrameWidth = this.frameWidth
397
+ const prevFrameHeight = this.frameHeight
398
+ const prevFrameMinWidth = this.frameMinWidth
399
+ const prevFrameMaxWidth = this.frameMaxWidth
400
+ const prevFrameMinHeight = this.frameMinHeight
401
+ const prevFrameMaxHeight = this.frameMaxHeight
339
402
  this.frameWidth = typeof props.width === "number" ? props.width : undefined
340
403
  this.frameHeight = typeof props.height === "number" ? props.height : undefined
341
404
  this.frameMinWidth = typeof props.minWidth === "number" ? props.minWidth : undefined
@@ -345,6 +408,19 @@ export abstract class BaseHost implements HostInstance {
345
408
 
346
409
  // onLayout callback
347
410
  this.onLayout = typeof props.onLayout === "function" ? (props.onLayout as typeof this.onLayout) : undefined
411
+
412
+ const layoutChanged =
413
+ prevGreedy !== this.greedy ||
414
+ prevFrameWidth !== this.frameWidth ||
415
+ prevFrameHeight !== this.frameHeight ||
416
+ prevFrameMinWidth !== this.frameMinWidth ||
417
+ prevFrameMaxWidth !== this.frameMaxWidth ||
418
+ prevFrameMinHeight !== this.frameMinHeight ||
419
+ prevFrameMaxHeight !== this.frameMaxHeight
420
+
421
+ if (layoutChanged) {
422
+ this.invalidateLayout()
423
+ }
348
424
  }
349
425
 
350
426
  /**
@@ -353,7 +429,11 @@ export abstract class BaseHost implements HostInstance {
353
429
  */
354
430
  protected applyGreedyDefault(props: Record<string, unknown>, fallback: number): void {
355
431
  if (!("greedy" in props)) {
432
+ const prevGreedy = this.greedy
356
433
  this.greedy = fallback
434
+ if (prevGreedy !== this.greedy) {
435
+ this.invalidateLayout()
436
+ }
357
437
  }
358
438
  }
359
439
 
@@ -366,6 +446,7 @@ export abstract class BaseHost implements HostInstance {
366
446
  appendChild(child: HostInstance): void {
367
447
  this.children.push(child)
368
448
  child.parent = this
449
+ this.invalidateLayout()
369
450
  }
370
451
 
371
452
  removeChild(child: HostInstance): void {
@@ -374,6 +455,7 @@ export abstract class BaseHost implements HostInstance {
374
455
  this.children.splice(idx, 1)
375
456
  child.parent = null
376
457
  }
458
+ this.invalidateLayout()
377
459
  }
378
460
 
379
461
  insertBefore(child: HostInstance, before: HostInstance): void {
@@ -388,6 +470,7 @@ export abstract class BaseHost implements HostInstance {
388
470
  this.children.push(child)
389
471
  }
390
472
  child.parent = this
473
+ this.invalidateLayout()
391
474
  }
392
475
 
393
476
  /**
package/src/hosts/box.ts CHANGED
@@ -65,7 +65,7 @@ export class BoxHost extends SingleChildHost {
65
65
  return this.borderThickness + titleHeight + this.padding.top + this.padding.bottom + this.borderThickness
66
66
  }
67
67
 
68
- measure(maxW: number, maxH: number): Size {
68
+ protected measureSelf(maxW: number, maxH: number): Size {
69
69
  // Apply frame constraints first
70
70
  const constrained = this.constrainProposal(maxW, maxH)
71
71
 
@@ -91,7 +91,7 @@ export class BoxHost extends SingleChildHost {
91
91
  return this.constrainResult(naturalSize)
92
92
  }
93
93
 
94
- override layout(rect: Rect): void {
94
+ protected override layoutSelf(rect: Rect): void {
95
95
  const layoutRect = this.layoutWithConstraints(rect)
96
96
 
97
97
  const t = this.borderThickness
@@ -173,6 +173,15 @@ export class BoxHost extends SingleChildHost {
173
173
 
174
174
  override updateProps(props: Record<string, unknown>): void {
175
175
  super.updateProps(props)
176
+ const prevPadding = this.padding
177
+ const prevBorder = this.border
178
+ const prevBorderColor = this.borderColor
179
+ const prevBg = this.bg
180
+ const prevTitle = this.title
181
+ const prevTitleColor = this.titleColor
182
+ const prevTitleBold = this.titleBold
183
+ const prevTitleDivider = this.titleDivider
184
+
176
185
  this.padding = resolvePadding(props.padding as BoxProps["padding"])
177
186
  this.border = (props.border as BorderKind | undefined) ?? "none"
178
187
  // Color props support MotionValue/ColorMotionValue - auto-subscribe and animate
@@ -188,5 +197,31 @@ export class BoxHost extends SingleChildHost {
188
197
  }) as Color | undefined
189
198
  this.titleBold = Boolean(props.titleBold)
190
199
  this.titleDivider = Boolean(props.titleDivider)
200
+
201
+ const paddingChanged =
202
+ prevPadding.top !== this.padding.top ||
203
+ prevPadding.right !== this.padding.right ||
204
+ prevPadding.bottom !== this.padding.bottom ||
205
+ prevPadding.left !== this.padding.left
206
+ const layoutChanged =
207
+ paddingChanged ||
208
+ prevBorder !== this.border ||
209
+ prevTitle !== this.title ||
210
+ prevTitleDivider !== this.titleDivider
211
+
212
+ if (layoutChanged) {
213
+ this.invalidateLayout()
214
+ return
215
+ }
216
+
217
+ const renderChanged =
218
+ prevBorderColor !== this.borderColor ||
219
+ prevBg !== this.bg ||
220
+ prevTitleColor !== this.titleColor ||
221
+ prevTitleBold !== this.titleBold
222
+
223
+ if (renderChanged) {
224
+ this.invalidateRender()
225
+ }
191
226
  }
192
227
  }