@effect-tui/react 0.15.2 → 0.16.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 (249) hide show
  1. package/README.md +2 -2
  2. package/dist/src/components/ListView.d.ts +4 -4
  3. package/dist/src/components/ListView.d.ts.map +1 -1
  4. package/dist/src/components/ListView.js +16 -17
  5. package/dist/src/components/ListView.js.map +1 -1
  6. package/dist/src/console/ConsolePopover.d.ts +7 -1
  7. package/dist/src/console/ConsolePopover.d.ts.map +1 -1
  8. package/dist/src/console/ConsolePopover.js +55 -74
  9. package/dist/src/console/ConsolePopover.js.map +1 -1
  10. package/dist/src/debug/DebugOverlay.d.ts.map +1 -1
  11. package/dist/src/debug/DebugOverlay.js +3 -57
  12. package/dist/src/debug/DebugOverlay.js.map +1 -1
  13. package/dist/src/debug/DiagnosticsPanel.js +1 -1
  14. package/dist/src/debug/DiagnosticsPanel.js.map +1 -1
  15. package/dist/src/dev.d.ts +5 -117
  16. package/dist/src/dev.d.ts.map +1 -1
  17. package/dist/src/dev.js +3 -333
  18. package/dist/src/dev.js.map +1 -1
  19. package/dist/src/hooks/use-scroll.d.ts +31 -35
  20. package/dist/src/hooks/use-scroll.d.ts.map +1 -1
  21. package/dist/src/hooks/use-scroll.js +51 -90
  22. package/dist/src/hooks/use-scroll.js.map +1 -1
  23. package/dist/src/hosts/canvas.d.ts +2 -2
  24. package/dist/src/hosts/canvas.d.ts.map +1 -1
  25. package/dist/src/hosts/canvas.js +8 -10
  26. package/dist/src/hosts/canvas.js.map +1 -1
  27. package/dist/src/hosts/codeblock.d.ts +2 -2
  28. package/dist/src/hosts/codeblock.js +2 -2
  29. package/dist/src/hosts/flex-container.d.ts +1 -1
  30. package/dist/src/hosts/flex-container.d.ts.map +1 -1
  31. package/dist/src/hosts/flex-container.js +3 -3
  32. package/dist/src/hosts/flex-container.js.map +1 -1
  33. package/dist/src/hosts/index.d.ts +2 -1
  34. package/dist/src/hosts/index.d.ts.map +1 -1
  35. package/dist/src/hosts/index.js +2 -1
  36. package/dist/src/hosts/index.js.map +1 -1
  37. package/dist/src/hosts/layout-helpers.d.ts +10 -0
  38. package/dist/src/hosts/layout-helpers.d.ts.map +1 -0
  39. package/dist/src/hosts/layout-helpers.js +10 -0
  40. package/dist/src/hosts/layout-helpers.js.map +1 -0
  41. package/dist/src/hosts/leaf.d.ts +14 -0
  42. package/dist/src/hosts/leaf.d.ts.map +1 -0
  43. package/dist/src/hosts/leaf.js +31 -0
  44. package/dist/src/hosts/leaf.js.map +1 -0
  45. package/dist/src/hosts/overlay.d.ts.map +1 -1
  46. package/dist/src/hosts/overlay.js +4 -7
  47. package/dist/src/hosts/overlay.js.map +1 -1
  48. package/dist/src/hosts/scroll.d.ts +47 -24
  49. package/dist/src/hosts/scroll.d.ts.map +1 -1
  50. package/dist/src/hosts/scroll.js +68 -51
  51. package/dist/src/hosts/scroll.js.map +1 -1
  52. package/dist/src/hosts/spacer.d.ts +2 -2
  53. package/dist/src/hosts/spacer.js +2 -2
  54. package/dist/src/hosts/text.d.ts +2 -3
  55. package/dist/src/hosts/text.d.ts.map +1 -1
  56. package/dist/src/hosts/text.js +5 -61
  57. package/dist/src/hosts/text.js.map +1 -1
  58. package/dist/src/hosts/vstack.js +1 -1
  59. package/dist/src/hosts/vstack.js.map +1 -1
  60. package/dist/src/hosts/zstack.d.ts +1 -1
  61. package/dist/src/hosts/zstack.d.ts.map +1 -1
  62. package/dist/src/hosts/zstack.js +6 -6
  63. package/dist/src/hosts/zstack.js.map +1 -1
  64. package/dist/src/index.d.ts +1 -1
  65. package/dist/src/index.d.ts.map +1 -1
  66. package/dist/src/internal/dev/hmr.d.ts +20 -0
  67. package/dist/src/internal/dev/hmr.d.ts.map +1 -0
  68. package/dist/src/internal/dev/hmr.js +93 -0
  69. package/dist/src/internal/dev/hmr.js.map +1 -0
  70. package/dist/src/internal/dev/runtime.d.ts +24 -0
  71. package/dist/src/internal/dev/runtime.d.ts.map +1 -0
  72. package/dist/src/internal/dev/runtime.js +135 -0
  73. package/dist/src/internal/dev/runtime.js.map +1 -0
  74. package/dist/src/internal/dev/ui.d.ts +13 -0
  75. package/dist/src/internal/dev/ui.d.ts.map +1 -0
  76. package/dist/src/internal/dev/ui.js +51 -0
  77. package/dist/src/internal/dev/ui.js.map +1 -0
  78. package/dist/src/internal/renderer/context.d.ts +9 -0
  79. package/dist/src/internal/renderer/context.d.ts.map +1 -0
  80. package/dist/src/internal/renderer/context.js +22 -0
  81. package/dist/src/internal/renderer/context.js.map +1 -0
  82. package/dist/src/internal/renderer/core/FrameBuilder.d.ts +18 -0
  83. package/dist/src/internal/renderer/core/FrameBuilder.d.ts.map +1 -0
  84. package/dist/src/internal/renderer/core/FrameBuilder.js +40 -0
  85. package/dist/src/internal/renderer/core/FrameBuilder.js.map +1 -0
  86. package/dist/src/internal/renderer/core/RendererState.d.ts +41 -0
  87. package/dist/src/internal/renderer/core/RendererState.d.ts.map +1 -0
  88. package/dist/src/internal/renderer/core/RendererState.js +70 -0
  89. package/dist/src/internal/renderer/core/RendererState.js.map +1 -0
  90. package/dist/src/internal/renderer/core/index.d.ts +3 -0
  91. package/dist/src/internal/renderer/core/index.d.ts.map +1 -0
  92. package/dist/src/internal/renderer/core/index.js +3 -0
  93. package/dist/src/internal/renderer/core/index.js.map +1 -0
  94. package/dist/src/internal/renderer/index.d.ts +40 -0
  95. package/dist/src/internal/renderer/index.d.ts.map +1 -0
  96. package/dist/src/internal/renderer/index.js +518 -0
  97. package/dist/src/internal/renderer/index.js.map +1 -0
  98. package/dist/src/internal/renderer/input/InputProcessor.d.ts +30 -0
  99. package/dist/src/internal/renderer/input/InputProcessor.d.ts.map +1 -0
  100. package/dist/src/internal/renderer/input/InputProcessor.js +122 -0
  101. package/dist/src/internal/renderer/input/InputProcessor.js.map +1 -0
  102. package/dist/src/internal/renderer/input/index.d.ts +2 -0
  103. package/dist/src/internal/renderer/input/index.d.ts.map +1 -0
  104. package/dist/src/internal/renderer/input/index.js +2 -0
  105. package/dist/src/internal/renderer/input/index.js.map +1 -0
  106. package/dist/src/internal/renderer/lifecycle/EventBus.d.ts +42 -0
  107. package/dist/src/internal/renderer/lifecycle/EventBus.d.ts.map +1 -0
  108. package/dist/src/internal/renderer/lifecycle/EventBus.js +97 -0
  109. package/dist/src/internal/renderer/lifecycle/EventBus.js.map +1 -0
  110. package/dist/src/internal/renderer/lifecycle/ProcessLifecycle.d.ts +13 -0
  111. package/dist/src/internal/renderer/lifecycle/ProcessLifecycle.d.ts.map +1 -0
  112. package/dist/src/internal/renderer/lifecycle/ProcessLifecycle.js +111 -0
  113. package/dist/src/internal/renderer/lifecycle/ProcessLifecycle.js.map +1 -0
  114. package/dist/src/internal/renderer/lifecycle/RenderCache.d.ts +3 -0
  115. package/dist/src/internal/renderer/lifecycle/RenderCache.d.ts.map +1 -0
  116. package/dist/src/internal/renderer/lifecycle/RenderCache.js +9 -0
  117. package/dist/src/internal/renderer/lifecycle/RenderCache.js.map +1 -0
  118. package/dist/src/internal/renderer/lifecycle/index.d.ts +4 -0
  119. package/dist/src/internal/renderer/lifecycle/index.d.ts.map +1 -0
  120. package/dist/src/internal/renderer/lifecycle/index.js +4 -0
  121. package/dist/src/internal/renderer/lifecycle/index.js.map +1 -0
  122. package/dist/src/internal/renderer/modes/FullscreenRenderer.d.ts +12 -0
  123. package/dist/src/internal/renderer/modes/FullscreenRenderer.d.ts.map +1 -0
  124. package/dist/src/internal/renderer/modes/FullscreenRenderer.js +54 -0
  125. package/dist/src/internal/renderer/modes/FullscreenRenderer.js.map +1 -0
  126. package/dist/src/internal/renderer/modes/InlineRenderer.d.ts +25 -0
  127. package/dist/src/internal/renderer/modes/InlineRenderer.d.ts.map +1 -0
  128. package/dist/src/internal/renderer/modes/InlineRenderer.js +166 -0
  129. package/dist/src/internal/renderer/modes/InlineRenderer.js.map +1 -0
  130. package/dist/src/internal/renderer/modes/RendererMode.d.ts +42 -0
  131. package/dist/src/internal/renderer/modes/RendererMode.d.ts.map +1 -0
  132. package/dist/src/internal/renderer/modes/RendererMode.js +2 -0
  133. package/dist/src/internal/renderer/modes/RendererMode.js.map +1 -0
  134. package/dist/src/internal/renderer/modes/StaticContentRenderer.d.ts +25 -0
  135. package/dist/src/internal/renderer/modes/StaticContentRenderer.d.ts.map +1 -0
  136. package/dist/src/internal/renderer/modes/StaticContentRenderer.js +49 -0
  137. package/dist/src/internal/renderer/modes/StaticContentRenderer.js.map +1 -0
  138. package/dist/src/internal/renderer/modes/index.d.ts +5 -0
  139. package/dist/src/internal/renderer/modes/index.d.ts.map +1 -0
  140. package/dist/src/internal/renderer/modes/index.js +4 -0
  141. package/dist/src/internal/renderer/modes/index.js.map +1 -0
  142. package/dist/src/internal/renderer/terminal/KeyboardCapabilityProbe.d.ts +13 -0
  143. package/dist/src/internal/renderer/terminal/KeyboardCapabilityProbe.d.ts.map +1 -0
  144. package/dist/src/internal/renderer/terminal/KeyboardCapabilityProbe.js +75 -0
  145. package/dist/src/internal/renderer/terminal/KeyboardCapabilityProbe.js.map +1 -0
  146. package/dist/src/internal/renderer/terminal/TerminalSetup.d.ts +29 -0
  147. package/dist/src/internal/renderer/terminal/TerminalSetup.d.ts.map +1 -0
  148. package/dist/src/internal/renderer/terminal/TerminalSetup.js +82 -0
  149. package/dist/src/internal/renderer/terminal/TerminalSetup.js.map +1 -0
  150. package/dist/src/internal/renderer/terminal/index.d.ts +3 -0
  151. package/dist/src/internal/renderer/terminal/index.d.ts.map +1 -0
  152. package/dist/src/internal/renderer/terminal/index.js +3 -0
  153. package/dist/src/internal/renderer/terminal/index.js.map +1 -0
  154. package/dist/src/internal/renderer/types.d.ts +118 -0
  155. package/dist/src/internal/renderer/types.d.ts.map +1 -0
  156. package/dist/src/internal/renderer/types.js +2 -0
  157. package/dist/src/internal/renderer/types.js.map +1 -0
  158. package/dist/src/renderer-context.d.ts +1 -8
  159. package/dist/src/renderer-context.d.ts.map +1 -1
  160. package/dist/src/renderer-context.js +1 -21
  161. package/dist/src/renderer-context.js.map +1 -1
  162. package/dist/src/renderer-types.d.ts +1 -115
  163. package/dist/src/renderer-types.d.ts.map +1 -1
  164. package/dist/src/renderer.d.ts +1 -31
  165. package/dist/src/renderer.d.ts.map +1 -1
  166. package/dist/src/renderer.js +1 -495
  167. package/dist/src/renderer.js.map +1 -1
  168. package/dist/src/test/render-tui.d.ts +3 -3
  169. package/dist/src/test/render-tui.d.ts.map +1 -1
  170. package/dist/src/test/render-tui.js +16 -9
  171. package/dist/src/test/render-tui.js.map +1 -1
  172. package/dist/src/utils/alignment.d.ts +1 -1
  173. package/dist/src/utils/alignment.d.ts.map +1 -1
  174. package/dist/src/utils/alignment.js +0 -2
  175. package/dist/src/utils/alignment.js.map +1 -1
  176. package/dist/src/utils/console-helpers.d.ts +19 -0
  177. package/dist/src/utils/console-helpers.d.ts.map +1 -0
  178. package/dist/src/utils/console-helpers.js +61 -0
  179. package/dist/src/utils/console-helpers.js.map +1 -0
  180. package/dist/src/utils/index.d.ts +1 -1
  181. package/dist/src/utils/index.d.ts.map +1 -1
  182. package/dist/src/utils/index.js +1 -1
  183. package/dist/src/utils/index.js.map +1 -1
  184. package/dist/src/utils/styles.d.ts +8 -1
  185. package/dist/src/utils/styles.d.ts.map +1 -1
  186. package/dist/src/utils/styles.js +10 -8
  187. package/dist/src/utils/styles.js.map +1 -1
  188. package/dist/src/utils/text-wrap.d.ts +5 -0
  189. package/dist/src/utils/text-wrap.d.ts.map +1 -1
  190. package/dist/src/utils/text-wrap.js +110 -48
  191. package/dist/src/utils/text-wrap.js.map +1 -1
  192. package/dist/src/visualize/index.js +1 -1
  193. package/dist/src/visualize/index.js.map +1 -1
  194. package/dist/tsconfig.tsbuildinfo +1 -1
  195. package/package.json +2 -2
  196. package/src/components/ListView.tsx +21 -23
  197. package/src/console/ConsolePopover.tsx +124 -107
  198. package/src/debug/DebugOverlay.ts +15 -74
  199. package/src/debug/DiagnosticsPanel.tsx +1 -1
  200. package/src/dev.tsx +5 -458
  201. package/src/hooks/use-scroll.ts +85 -145
  202. package/src/hosts/canvas.ts +8 -11
  203. package/src/hosts/codeblock.ts +2 -2
  204. package/src/hosts/flex-container.ts +4 -4
  205. package/src/hosts/index.ts +10 -1
  206. package/src/hosts/layout-helpers.ts +20 -0
  207. package/src/hosts/leaf.ts +36 -0
  208. package/src/hosts/overlay.ts +11 -9
  209. package/src/hosts/scroll.ts +94 -69
  210. package/src/hosts/spacer.ts +2 -2
  211. package/src/hosts/text.ts +5 -58
  212. package/src/hosts/vstack.ts +1 -1
  213. package/src/hosts/zstack.ts +7 -7
  214. package/src/index.ts +1 -1
  215. package/src/internal/dev/hmr.ts +101 -0
  216. package/src/internal/dev/runtime.ts +170 -0
  217. package/src/internal/dev/ui.tsx +87 -0
  218. package/src/internal/renderer/context.ts +27 -0
  219. package/src/{renderer → internal/renderer}/core/FrameBuilder.ts +2 -2
  220. package/src/internal/renderer/index.ts +656 -0
  221. package/src/{renderer → internal/renderer}/input/InputProcessor.ts +10 -1
  222. package/src/{renderer → internal/renderer}/lifecycle/EventBus.ts +9 -1
  223. package/src/internal/renderer/lifecycle/ProcessLifecycle.ts +125 -0
  224. package/src/internal/renderer/lifecycle/index.ts +3 -0
  225. package/src/{renderer → internal/renderer}/modes/InlineRenderer.ts +5 -2
  226. package/src/{renderer → internal/renderer}/modes/RendererMode.ts +1 -1
  227. package/src/{renderer → internal/renderer}/modes/StaticContentRenderer.ts +5 -2
  228. package/src/internal/renderer/terminal/KeyboardCapabilityProbe.ts +91 -0
  229. package/src/{renderer/lifecycle → internal/renderer/terminal}/TerminalSetup.ts +4 -22
  230. package/src/internal/renderer/terminal/index.ts +2 -0
  231. package/src/internal/renderer/types.ts +125 -0
  232. package/src/renderer-context.ts +1 -27
  233. package/src/renderer-types.ts +10 -123
  234. package/src/renderer.ts +1 -619
  235. package/src/test/render-tui.ts +16 -10
  236. package/src/utils/alignment.ts +1 -3
  237. package/src/utils/console-helpers.ts +86 -0
  238. package/src/utils/index.ts +1 -1
  239. package/src/utils/styles.ts +16 -4
  240. package/src/utils/text-wrap.ts +139 -48
  241. package/src/visualize/index.tsx +1 -1
  242. package/src/renderer/lifecycle/ResizeManager.ts +0 -65
  243. package/src/renderer/lifecycle/index.ts +0 -4
  244. /package/src/{renderer → internal/renderer}/core/RendererState.ts +0 -0
  245. /package/src/{renderer → internal/renderer}/core/index.ts +0 -0
  246. /package/src/{renderer → internal/renderer}/input/index.ts +0 -0
  247. /package/src/{renderer → internal/renderer}/lifecycle/RenderCache.ts +0 -0
  248. /package/src/{renderer → internal/renderer}/modes/FullscreenRenderer.ts +0 -0
  249. /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
  }
@@ -5,12 +5,12 @@ import {
5
5
  type BorderKind,
6
6
  borderChars,
7
7
  drawBorder,
8
+ fillRectWithInheritedBg,
8
9
  resolveBgStyle,
9
- resolveInheritedBgStyle,
10
10
  styleIdFromProps,
11
11
  toColorValue,
12
12
  } from "../utils/index.js"
13
- import { BaseHost } from "./base.js"
13
+ import { LeafHost } from "./leaf.js"
14
14
 
15
15
  export type { BorderKind }
16
16
 
@@ -67,7 +67,7 @@ export interface CanvasProps extends CommonProps {
67
67
  inheritBg?: boolean
68
68
  }
69
69
 
70
- export class CanvasHost extends BaseHost {
70
+ export class CanvasHost extends LeafHost {
71
71
  draw: CanvasProps["draw"] = () => {}
72
72
  fixedWidth?: number
73
73
  fixedHeight?: number
@@ -91,15 +91,12 @@ export class CanvasHost extends BaseHost {
91
91
  if (!this.rect) return
92
92
  const { x: ox, y: oy, w, h } = this.rect
93
93
 
94
- // Get inherited background for use in drawing functions
95
- const { value: inheritedBgValue, styleId: inheritedBgStyleId } = this.inheritBg
96
- ? resolveInheritedBgStyle(palette, undefined, this.parent)
97
- : { value: undefined, styleId: 0 }
98
-
99
94
  // Pre-fill with inherited background if requested
100
- if (this.inheritBg && inheritedBgValue !== undefined) {
101
- buffer.fillRect(ox, oy, w, h, " ".codePointAt(0)!, inheritedBgStyleId)
102
- }
95
+ const inheritedBgValue = this.inheritBg
96
+ ? fillRectWithInheritedBg(buffer, palette, { x: ox, y: oy, w, h }, undefined, this.parent, {
97
+ skipWhenUndefined: true,
98
+ }).value
99
+ : undefined
103
100
 
104
101
  // Create draw context
105
102
  const ctx: DrawContext = {
@@ -2,7 +2,7 @@ import { type CellBuffer, type Color, Colors, displayWidth, type Palette } from
2
2
  import type { HighlightLine } from "../highlight.js"
3
3
  import type { CommonProps, HostContext, Size } from "../reconciler/types.js"
4
4
  import { type Padding, type PaddingInput, resolveBgStyle, resolvePadding, styleIdFromProps } from "../utils/index.js"
5
- import { BaseHost } from "./base.js"
5
+ import { LeafHost } from "./leaf.js"
6
6
 
7
7
  export interface CodeBlockProps extends CommonProps {
8
8
  lines: HighlightLine[]
@@ -17,7 +17,7 @@ function lineDisplayWidth(line: HighlightLine): number {
17
17
  return line.reduce((w, token) => w + displayWidth(token.text), 0)
18
18
  }
19
19
 
20
- export class CodeBlockHost extends BaseHost {
20
+ export class CodeBlockHost extends LeafHost {
21
21
  lines: HighlightLine[] = [[]]
22
22
  lineNumbers = false
23
23
  padding: Padding = { top: 0, right: 0, bottom: 0, left: 0 }
@@ -15,7 +15,7 @@ import {
15
15
  import { BaseHost } from "./base.js"
16
16
 
17
17
  export type CrossAlignment<A extends FlexAxis> = A extends "vertical"
18
- ? "leading" | "center" | "trailing"
18
+ ? "left" | "center" | "right"
19
19
  : "top" | "center" | "bottom"
20
20
 
21
21
  export interface FlexContainerProps<A extends FlexAxis> extends CommonProps {
@@ -30,7 +30,7 @@ export interface FlexContainerProps<A extends FlexAxis> extends CommonProps {
30
30
  function toFlexAlignment<A extends FlexAxis>(axis: A, alignment: CrossAlignment<A>): FlexAlignment {
31
31
  if (alignment === "center") return "center"
32
32
  if (axis === "vertical") {
33
- return alignment === "leading" ? "start" : "end"
33
+ return alignment === "left" ? "start" : "end"
34
34
  } else {
35
35
  return alignment === "top" ? "start" : "end"
36
36
  }
@@ -104,7 +104,7 @@ export class FlexContainerHost<A extends FlexAxis> extends BaseHost {
104
104
 
105
105
  override layout(rect: Rect): void {
106
106
  const layoutRect = this.layoutWithConstraints(rect)
107
- const stretchCross = this.axis === "vertical" ? this.alignment === "leading" : this.alignment === "top"
107
+ const stretchCross = this.axis === "vertical" ? this.alignment === "left" : this.alignment === "top"
108
108
  const insetX = this.padding.left + this.padding.right
109
109
  const insetY = this.padding.top + this.padding.bottom
110
110
  const innerRect: Rect = {
@@ -146,7 +146,7 @@ export class FlexContainerHost<A extends FlexAxis> extends BaseHost {
146
146
  // Reset to axis-specific default when undefined
147
147
  this.alignment =
148
148
  (props.alignment as CrossAlignment<A> | undefined) ??
149
- ((this.axis === "vertical" ? "leading" : "top") as CrossAlignment<A>)
149
+ ((this.axis === "vertical" ? "left" : "top") as CrossAlignment<A>)
150
150
  this.padding = resolvePadding(props.padding as FlexContainerProps<A>["padding"])
151
151
  this.bg = props.bg as Color | undefined
152
152
  }
@@ -19,8 +19,17 @@ export { CodeBlockHost, type CodeBlockProps } from "./codeblock.js"
19
19
  export { HStackHost, type HStackProps } from "./hstack.js"
20
20
  export { OverlayHost, type OverlayProps } from "./overlay.js"
21
21
  export { OverlayItemHost, type OverlayItemProps } from "./overlay-item.js"
22
- export { ScrollHost, type ScrollProps } from "./scroll.js"
22
+ export {
23
+ ScrollHost,
24
+ type ScrollAlign,
25
+ type ScrollAlignX,
26
+ type ScrollAlignY,
27
+ type ScrollAxis,
28
+ type ScrollLayoutChange,
29
+ type ScrollProps,
30
+ } from "./scroll.js"
23
31
  export { SingleChildHost } from "./single-child.js"
32
+ export { LeafHost } from "./leaf.js"
24
33
  export { SpacerHost, type SpacerProps } from "./spacer.js"
25
34
  export {
26
35
  RawTextHost,
@@ -0,0 +1,20 @@
1
+ import type { Rect, Size } from "../reconciler/types.js"
2
+ import type { HostInstance } from "../reconciler/types.js"
3
+ import { alignedChildRect, type HAlign, type VAlign } from "../utils/index.js"
4
+
5
+ type Alignment = { h?: HAlign; v?: VAlign }
6
+
7
+ export function layoutAlignedChildren(
8
+ layoutRect: Rect,
9
+ children: HostInstance[],
10
+ cachedSizes: Size[],
11
+ alignmentForChild: (child: HostInstance, index: number) => Alignment,
12
+ startIndex = 0,
13
+ ): void {
14
+ for (let i = startIndex; i < children.length; i++) {
15
+ const child = children[i]
16
+ const size = cachedSizes[i] ?? child.measure(layoutRect.w, layoutRect.h)
17
+ const alignment = alignmentForChild(child, i)
18
+ child.layout(alignedChildRect(layoutRect, size, alignment.h ?? "center", alignment.v ?? "center"))
19
+ }
20
+ }
@@ -0,0 +1,36 @@
1
+ import type { HostInstance } from "../reconciler/types.js"
2
+ import { BaseHost } from "./base.js"
3
+
4
+ /**
5
+ * Base host that rejects children.
6
+ * Any attempted child insertion is ignored with a warning (once).
7
+ */
8
+ export abstract class LeafHost extends BaseHost {
9
+ private warned = false
10
+
11
+ override appendChild(child: HostInstance): void {
12
+ this.rejectChild(child)
13
+ }
14
+
15
+ override insertBefore(child: HostInstance, _before: HostInstance): void {
16
+ this.rejectChild(child)
17
+ }
18
+
19
+ override removeChild(child: HostInstance): void {
20
+ const idx = this.children.indexOf(child)
21
+ if (idx >= 0) {
22
+ this.children.splice(idx, 1)
23
+ }
24
+ if (child.parent === this) {
25
+ child.parent = null
26
+ }
27
+ }
28
+
29
+ private rejectChild(child: HostInstance): void {
30
+ if (!this.warned) {
31
+ console.warn(`[effect-tui] <${this.type}> does not support children; child ignored.`)
32
+ this.warned = true
33
+ }
34
+ child.parent = null
35
+ }
36
+ }
@@ -18,8 +18,8 @@
18
18
 
19
19
  import type { CellBuffer, Palette, Rect, Size } from "@effect-tui/core"
20
20
  import type { CommonProps, HostContext } from "../reconciler/types.js"
21
- import { alignedChildRect } from "../utils/index.js"
22
21
  import { BaseHost } from "./base.js"
22
+ import { layoutAlignedChildren } from "./layout-helpers.js"
23
23
  import type { OverlayItemHost } from "./overlay-item.js"
24
24
 
25
25
  export interface OverlayProps extends CommonProps {}
@@ -66,14 +66,16 @@ export class OverlayHost extends BaseHost {
66
66
  }
67
67
 
68
68
  // Layout overlay children with their alignment
69
- for (let i = 1; i < this.children.length; i++) {
70
- const child = this.children[i]
71
- const size = this.cachedSizes[i] ?? child.measure(layoutRect.w, layoutRect.h)
72
-
73
- // Read alignment from OverlayItemHost (use type check, not instanceof, for bundler compatibility)
74
- const alignment = child.type === "overlayItem" ? (child as OverlayItemHost).alignment : {}
75
- child.layout(alignedChildRect(layoutRect, size, alignment.h ?? "center", alignment.v ?? "center"))
76
- }
69
+ layoutAlignedChildren(
70
+ layoutRect,
71
+ this.children,
72
+ this.cachedSizes,
73
+ (child) => {
74
+ // Read alignment from OverlayItemHost (use type check, not instanceof, for bundler compatibility)
75
+ return child.type === "overlayItem" ? (child as OverlayItemHost).alignment : {}
76
+ },
77
+ 1,
78
+ )
77
79
  }
78
80
 
79
81
  override render(buffer: CellBuffer, palette: Palette): void {