@effect-tui/react 0.16.0 → 2.0.1

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 (126) hide show
  1. package/README.md +9 -0
  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/Markdown.js +3 -3
  7. package/dist/src/components/Markdown.js.map +1 -1
  8. package/dist/src/components/MultilineTextInput.d.ts.map +1 -1
  9. package/dist/src/components/MultilineTextInput.js +133 -305
  10. package/dist/src/components/MultilineTextInput.js.map +1 -1
  11. package/dist/src/components/TextInput.d.ts.map +1 -1
  12. package/dist/src/components/TextInput.js +51 -98
  13. package/dist/src/components/TextInput.js.map +1 -1
  14. package/dist/src/components/text-editing.d.ts +61 -0
  15. package/dist/src/components/text-editing.d.ts.map +1 -1
  16. package/dist/src/components/text-editing.js +131 -0
  17. package/dist/src/components/text-editing.js.map +1 -1
  18. package/dist/src/hosts/base.d.ts +13 -2
  19. package/dist/src/hosts/base.d.ts.map +1 -1
  20. package/dist/src/hosts/base.js +74 -2
  21. package/dist/src/hosts/base.js.map +1 -1
  22. package/dist/src/hosts/box.d.ts +2 -2
  23. package/dist/src/hosts/box.d.ts.map +1 -1
  24. package/dist/src/hosts/box.js +29 -2
  25. package/dist/src/hosts/box.js.map +1 -1
  26. package/dist/src/hosts/canvas.d.ts +22 -2
  27. package/dist/src/hosts/canvas.d.ts.map +1 -1
  28. package/dist/src/hosts/canvas.js +99 -31
  29. package/dist/src/hosts/canvas.js.map +1 -1
  30. package/dist/src/hosts/codeblock.d.ts +8 -10
  31. package/dist/src/hosts/codeblock.d.ts.map +1 -1
  32. package/dist/src/hosts/codeblock.js +36 -33
  33. package/dist/src/hosts/codeblock.js.map +1 -1
  34. package/dist/src/hosts/flex-container.d.ts +2 -2
  35. package/dist/src/hosts/flex-container.d.ts.map +1 -1
  36. package/dist/src/hosts/flex-container.js +17 -2
  37. package/dist/src/hosts/flex-container.js.map +1 -1
  38. package/dist/src/hosts/index.d.ts +1 -1
  39. package/dist/src/hosts/index.d.ts.map +1 -1
  40. package/dist/src/hosts/index.js.map +1 -1
  41. package/dist/src/hosts/overlay-item.d.ts +2 -2
  42. package/dist/src/hosts/overlay-item.d.ts.map +1 -1
  43. package/dist/src/hosts/overlay-item.js +7 -2
  44. package/dist/src/hosts/overlay-item.js.map +1 -1
  45. package/dist/src/hosts/overlay.d.ts +2 -2
  46. package/dist/src/hosts/overlay.d.ts.map +1 -1
  47. package/dist/src/hosts/overlay.js +2 -2
  48. package/dist/src/hosts/overlay.js.map +1 -1
  49. package/dist/src/hosts/scroll.d.ts +7 -2
  50. package/dist/src/hosts/scroll.d.ts.map +1 -1
  51. package/dist/src/hosts/scroll.js +126 -45
  52. package/dist/src/hosts/scroll.js.map +1 -1
  53. package/dist/src/hosts/single-child.d.ts.map +1 -1
  54. package/dist/src/hosts/single-child.js +2 -0
  55. package/dist/src/hosts/single-child.js.map +1 -1
  56. package/dist/src/hosts/spacer.d.ts +1 -1
  57. package/dist/src/hosts/spacer.d.ts.map +1 -1
  58. package/dist/src/hosts/spacer.js +6 -1
  59. package/dist/src/hosts/spacer.js.map +1 -1
  60. package/dist/src/hosts/text.d.ts +20 -15
  61. package/dist/src/hosts/text.d.ts.map +1 -1
  62. package/dist/src/hosts/text.js +104 -71
  63. package/dist/src/hosts/text.js.map +1 -1
  64. package/dist/src/hosts/zstack.d.ts +2 -2
  65. package/dist/src/hosts/zstack.d.ts.map +1 -1
  66. package/dist/src/hosts/zstack.js +7 -2
  67. package/dist/src/hosts/zstack.js.map +1 -1
  68. package/dist/src/index.d.ts +1 -1
  69. package/dist/src/index.d.ts.map +1 -1
  70. package/dist/src/internal/renderer/index.d.ts.map +1 -1
  71. package/dist/src/internal/renderer/index.js +41 -16
  72. package/dist/src/internal/renderer/index.js.map +1 -1
  73. package/dist/src/internal/renderer/types.d.ts +4 -0
  74. package/dist/src/internal/renderer/types.d.ts.map +1 -1
  75. package/dist/src/motion/hooks.d.ts +1 -1
  76. package/dist/src/motion/hooks.js +1 -1
  77. package/dist/src/reconciler/host-config.js +2 -2
  78. package/dist/src/reconciler/host-config.js.map +1 -1
  79. package/dist/src/reconciler/types.d.ts +5 -1
  80. package/dist/src/reconciler/types.d.ts.map +1 -1
  81. package/dist/src/utils/border.d.ts +1 -1
  82. package/dist/src/utils/border.d.ts.map +1 -1
  83. package/dist/src/utils/border.js +2 -0
  84. package/dist/src/utils/border.js.map +1 -1
  85. package/dist/src/utils/index.d.ts +2 -1
  86. package/dist/src/utils/index.d.ts.map +1 -1
  87. package/dist/src/utils/index.js +2 -1
  88. package/dist/src/utils/index.js.map +1 -1
  89. package/dist/src/utils/text-layout.d.ts +22 -0
  90. package/dist/src/utils/text-layout.d.ts.map +1 -0
  91. package/dist/src/utils/text-layout.js +37 -0
  92. package/dist/src/utils/text-layout.js.map +1 -0
  93. package/dist/src/utils/text-wrap.d.ts +26 -1
  94. package/dist/src/utils/text-wrap.d.ts.map +1 -1
  95. package/dist/src/utils/text-wrap.js +106 -11
  96. package/dist/src/utils/text-wrap.js.map +1 -1
  97. package/dist/tsconfig.tsbuildinfo +1 -1
  98. package/package.json +2 -2
  99. package/src/codeblock.tsx +2 -2
  100. package/src/components/Markdown.tsx +3 -3
  101. package/src/components/MultilineTextInput.tsx +138 -344
  102. package/src/components/TextInput.tsx +54 -99
  103. package/src/components/text-editing.ts +180 -0
  104. package/src/hosts/base.ts +86 -3
  105. package/src/hosts/box.ts +37 -2
  106. package/src/hosts/canvas.ts +120 -31
  107. package/src/hosts/codeblock.ts +46 -33
  108. package/src/hosts/flex-container.ts +21 -2
  109. package/src/hosts/index.ts +1 -1
  110. package/src/hosts/overlay-item.ts +8 -2
  111. package/src/hosts/overlay.ts +2 -2
  112. package/src/hosts/scroll.ts +142 -45
  113. package/src/hosts/single-child.ts +2 -0
  114. package/src/hosts/spacer.ts +6 -1
  115. package/src/hosts/text.ts +122 -75
  116. package/src/hosts/zstack.ts +7 -2
  117. package/src/index.ts +1 -1
  118. package/src/internal/renderer/index.ts +53 -20
  119. package/src/internal/renderer/types.ts +4 -0
  120. package/src/motion/hooks.ts +1 -1
  121. package/src/reconciler/host-config.ts +2 -2
  122. package/src/reconciler/types.ts +7 -1
  123. package/src/utils/border.ts +11 -1
  124. package/src/utils/index.ts +15 -1
  125. package/src/utils/text-layout.ts +65 -0
  126. package/src/utils/text-wrap.ts +135 -13
@@ -80,6 +80,12 @@ export class ScrollHost extends SingleChildHost {
80
80
  // Measured content dimensions (full size before clipping)
81
81
  private contentWidth = 0
82
82
  private contentHeight = 0
83
+ // Effective viewport dimensions (excludes scrollbar gutter when visible)
84
+ private viewportWidth = 0
85
+ private viewportHeight = 0
86
+ // Scrollbar visibility after accounting for content overflow
87
+ private showVerticalScrollbar = false
88
+ private showHorizontalScrollbar = false
83
89
  // Track last reported sizes to avoid redundant callbacks
84
90
  private lastReportedContentW = -1
85
91
  private lastReportedContentH = -1
@@ -103,45 +109,114 @@ export class ScrollHost extends SingleChildHost {
103
109
  this.updateProps(props as unknown as Record<string, unknown>)
104
110
  }
105
111
 
106
- measure(maxW: number, maxH: number): Size {
112
+ private computeViewport(baseW: number, baseH: number, contentW: number, contentH: number): {
113
+ viewportW: number
114
+ viewportH: number
115
+ showV: boolean
116
+ showH: boolean
117
+ } {
118
+ const allowV = this.showScrollbar && (this.axis === "vertical" || this.axis === "both")
119
+ const allowH = this.showScrollbar && (this.axis === "horizontal" || this.axis === "both")
120
+ let viewportW = Math.max(0, baseW)
121
+ let viewportH = Math.max(0, baseH)
122
+ let showV = false
123
+ let showH = false
124
+
125
+ for (let i = 0; i < 2; i++) {
126
+ const nextShowV = allowV && contentH > viewportH
127
+ const nextShowH = allowH && contentW > viewportW
128
+ if (nextShowV === showV && nextShowH === showH) break
129
+ showV = nextShowV
130
+ showH = nextShowH
131
+ viewportW = Math.max(0, baseW - (showV ? 1 : 0))
132
+ viewportH = Math.max(0, baseH - (showH ? 1 : 0))
133
+ }
134
+
135
+ return { viewportW, viewportH, showV, showH }
136
+ }
137
+
138
+ protected measureSelf(maxW: number, maxH: number): Size {
107
139
  // Apply frame constraints to determine our size
108
140
  const constrained = this.constrainProposal(maxW, maxH)
109
141
 
110
- // Measure child with unbounded dimension(s) based on axis
111
- let childMaxW = constrained.w
112
- let childMaxH = constrained.h
113
-
114
- if (this.axis === "vertical" || this.axis === "both") {
115
- childMaxH = Number.MAX_SAFE_INTEGER
142
+ const child = this.child
143
+ if (!child) {
144
+ this.contentWidth = 0
145
+ this.contentHeight = 0
146
+ const viewport = this.computeViewport(constrained.w, constrained.h, 0, 0)
147
+ this.viewportWidth = viewport.viewportW
148
+ this.viewportHeight = viewport.viewportH
149
+ this.showVerticalScrollbar = viewport.showV
150
+ this.showHorizontalScrollbar = viewport.showH
151
+ return this.constrainResult({ w: 0, h: 0 })
116
152
  }
117
- if (this.axis === "horizontal" || this.axis === "both") {
118
- childMaxW = Number.MAX_SAFE_INTEGER
153
+
154
+ const measureChild = (proposalW: number, proposalH: number): Size => {
155
+ let childMaxW = proposalW
156
+ let childMaxH = proposalH
157
+
158
+ if (this.axis === "vertical" || this.axis === "both") {
159
+ childMaxH = Number.MAX_SAFE_INTEGER
160
+ }
161
+ if (this.axis === "horizontal" || this.axis === "both") {
162
+ childMaxW = Number.MAX_SAFE_INTEGER
163
+ }
164
+
165
+ return child.measure(childMaxW, childMaxH)
119
166
  }
120
167
 
121
- // Measure single child (scroll should have at most one child)
122
- const child = this.child
123
- if (child) {
124
- const childSize = child.measure(childMaxW, childMaxH)
125
- this.contentWidth = childSize.w
126
- this.contentHeight = childSize.h
127
- // Note: onScrollLayoutChange callback is deferred to layout() to keep measure() pure
168
+ let viewportW = constrained.w
169
+ let viewportH = constrained.h
170
+ let contentW = 0
171
+ let contentH = 0
172
+ let showV = false
173
+ let showH = false
174
+
175
+ for (let i = 0; i < 3; i++) {
176
+ const childSize = measureChild(viewportW, viewportH)
177
+ contentW = childSize.w
178
+ contentH = childSize.h
179
+ const viewport = this.computeViewport(constrained.w, constrained.h, contentW, contentH)
180
+ if (viewport.viewportW === viewportW && viewport.viewportH === viewportH) {
181
+ showV = viewport.showV
182
+ showH = viewport.showH
183
+ break
184
+ }
185
+ viewportW = viewport.viewportW
186
+ viewportH = viewport.viewportH
187
+ showV = viewport.showV
188
+ showH = viewport.showH
128
189
  }
129
190
 
191
+ this.contentWidth = contentW
192
+ this.contentHeight = contentH
193
+ this.viewportWidth = viewportW
194
+ this.viewportHeight = viewportH
195
+ this.showVerticalScrollbar = showV
196
+ this.showHorizontalScrollbar = showH
197
+
130
198
  // Report natural content size (clamped to constrained bounds)
131
199
  // Greedy expansion happens in layout phase via layoutFlex
132
- const naturalW = Math.min(this.contentWidth, constrained.w)
133
- const naturalH = Math.min(this.contentHeight, constrained.h)
200
+ const naturalW = Math.min(this.contentWidth + (this.showVerticalScrollbar ? 1 : 0), constrained.w)
201
+ const naturalH = Math.min(this.contentHeight + (this.showHorizontalScrollbar ? 1 : 0), constrained.h)
134
202
 
135
203
  return this.constrainResult({ w: naturalW, h: naturalH })
136
204
  }
137
205
 
138
- override layout(rect: Rect): void {
206
+ protected override layoutSelf(rect: Rect): void {
139
207
  const layoutRect = this.layoutWithConstraints(rect)
208
+ const viewport = this.computeViewport(layoutRect.w, layoutRect.h, this.contentWidth, this.contentHeight)
209
+ this.viewportWidth = viewport.viewportW
210
+ this.viewportHeight = viewport.viewportH
211
+ this.showVerticalScrollbar = viewport.showV
212
+ this.showHorizontalScrollbar = viewport.showH
213
+
140
214
  const { x: alignX, y: alignY } = resolveAlign(this.align)
141
215
 
142
216
  const contentChanged =
143
217
  this.contentWidth !== this.lastReportedContentW || this.contentHeight !== this.lastReportedContentH
144
- const viewportChanged = layoutRect.w !== this.lastViewportW || layoutRect.h !== this.lastViewportH
218
+ const viewportChanged =
219
+ this.viewportWidth !== this.lastViewportW || this.viewportHeight !== this.lastViewportH
145
220
  const rectChanged =
146
221
  layoutRect.x !== this.lastRectX ||
147
222
  layoutRect.y !== this.lastRectY ||
@@ -152,8 +227,8 @@ export class ScrollHost extends SingleChildHost {
152
227
  if (!child) return
153
228
 
154
229
  // Calculate max scroll offsets
155
- const maxScrollY = Math.max(0, this.contentHeight - layoutRect.h)
156
- const maxScrollX = Math.max(0, this.contentWidth - layoutRect.w)
230
+ const maxScrollY = Math.max(0, this.contentHeight - this.viewportHeight)
231
+ const maxScrollX = Math.max(0, this.contentWidth - this.viewportWidth)
157
232
 
158
233
  // Start with the offset from props (controlled by useScroll)
159
234
  let scrollY = this.offsetY
@@ -200,18 +275,18 @@ export class ScrollHost extends SingleChildHost {
200
275
  const offsetChanged = this.effectiveOffset !== this.lastOffsetY || this.effectiveOffsetX !== this.lastOffsetX
201
276
 
202
277
  // Handle alignment when content is smaller than viewport
203
- if (this.contentHeight < layoutRect.h && (this.axis === "vertical" || this.axis === "both")) {
278
+ if (this.contentHeight < this.viewportHeight && (this.axis === "vertical" || this.axis === "both")) {
204
279
  if (alignY === "bottom") {
205
- scrollY = -(layoutRect.h - this.contentHeight)
280
+ scrollY = -(this.viewportHeight - this.contentHeight)
206
281
  } else if (alignY === "center") {
207
- scrollY = -Math.floor((layoutRect.h - this.contentHeight) / 2)
282
+ scrollY = -Math.floor((this.viewportHeight - this.contentHeight) / 2)
208
283
  }
209
284
  }
210
- if (this.contentWidth < layoutRect.w && (this.axis === "horizontal" || this.axis === "both")) {
285
+ if (this.contentWidth < this.viewportWidth && (this.axis === "horizontal" || this.axis === "both")) {
211
286
  if (alignX === "right") {
212
- scrollX = -(layoutRect.w - this.contentWidth)
287
+ scrollX = -(this.viewportWidth - this.contentWidth)
213
288
  } else if (alignX === "center") {
214
- scrollX = -Math.floor((layoutRect.w - this.contentWidth) / 2)
289
+ scrollX = -Math.floor((this.viewportWidth - this.contentWidth) / 2)
215
290
  }
216
291
  }
217
292
 
@@ -227,8 +302,8 @@ export class ScrollHost extends SingleChildHost {
227
302
  if (this.onScrollLayoutChange && (contentChanged || viewportChanged || rectChanged || offsetChanged)) {
228
303
  this.lastReportedContentW = this.contentWidth
229
304
  this.lastReportedContentH = this.contentHeight
230
- this.lastViewportW = layoutRect.w
231
- this.lastViewportH = layoutRect.h
305
+ this.lastViewportW = this.viewportWidth
306
+ this.lastViewportH = this.viewportHeight
232
307
  this.lastRectX = layoutRect.x
233
308
  this.lastRectY = layoutRect.y
234
309
  this.lastRectW = layoutRect.w
@@ -238,7 +313,7 @@ export class ScrollHost extends SingleChildHost {
238
313
 
239
314
  this.onScrollLayoutChange({
240
315
  content: { width: this.contentWidth, height: this.contentHeight },
241
- viewport: { width: layoutRect.w, height: layoutRect.h },
316
+ viewport: { width: this.viewportWidth, height: this.viewportHeight },
242
317
  offset: { x: this.effectiveOffsetX, y: this.effectiveOffset },
243
318
  rect: { x: layoutRect.x, y: layoutRect.y, w: layoutRect.w, h: layoutRect.h },
244
319
  axis: this.axis,
@@ -253,8 +328,8 @@ export class ScrollHost extends SingleChildHost {
253
328
  // Fill background (inherit from parent if not explicitly set)
254
329
  fillRectWithInheritedBg(buffer, palette, { x, y, w, h }, this.bg, this.parent)
255
330
 
256
- // Render children with clipping
257
- buffer.withClip(x, y, w, h, () => {
331
+ // Render children with clipping (exclude scrollbar gutters)
332
+ buffer.withClip(x, y, this.viewportWidth, this.viewportHeight, () => {
258
333
  const child = this.child
259
334
  if (child) {
260
335
  child.render(buffer, palette)
@@ -272,18 +347,18 @@ export class ScrollHost extends SingleChildHost {
272
347
  const { x, y, w, h } = this.rect
273
348
 
274
349
  // Vertical scrollbar
275
- if ((this.axis === "vertical" || this.axis === "both") && this.contentHeight > h) {
276
- const maxScroll = this.contentHeight - h
350
+ if (this.showVerticalScrollbar) {
351
+ const maxScroll = Math.max(1, this.contentHeight - this.viewportHeight)
277
352
  const scrollRatio = Math.min(1, this.effectiveOffset / maxScroll)
278
- const thumbRatio = h / this.contentHeight
279
- const thumbHeight = Math.max(1, Math.floor(h * thumbRatio))
280
- const thumbY = Math.floor((h - thumbHeight) * scrollRatio)
353
+ const thumbRatio = this.viewportHeight / this.contentHeight
354
+ const thumbHeight = Math.max(1, Math.floor(this.viewportHeight * thumbRatio))
355
+ const thumbY = Math.floor((this.viewportHeight - thumbHeight) * scrollRatio)
281
356
 
282
357
  const trackStyle = palette.id({ fg: 8 }) // dim
283
358
  const thumbStyle = palette.id({ fg: 7 }) // brighter
284
359
 
285
360
  // Draw track
286
- for (let row = 0; row < h; row++) {
361
+ for (let row = 0; row < this.viewportHeight; row++) {
287
362
  const char = row >= thumbY && row < thumbY + thumbHeight ? "┃" : "│"
288
363
  const style = row >= thumbY && row < thumbY + thumbHeight ? thumbStyle : trackStyle
289
364
  buffer.drawCP(x + w - 1, y + row, char.codePointAt(0)!, style)
@@ -291,18 +366,18 @@ export class ScrollHost extends SingleChildHost {
291
366
  }
292
367
 
293
368
  // Horizontal scrollbar
294
- if ((this.axis === "horizontal" || this.axis === "both") && this.contentWidth > w) {
295
- const maxScroll = this.contentWidth - w
369
+ if (this.showHorizontalScrollbar) {
370
+ const maxScroll = Math.max(1, this.contentWidth - this.viewportWidth)
296
371
  const scrollRatio = Math.min(1, this.effectiveOffsetX / maxScroll)
297
- const thumbRatio = w / this.contentWidth
298
- const thumbWidth = Math.max(1, Math.floor(w * thumbRatio))
299
- const thumbX = Math.floor((w - thumbWidth) * scrollRatio)
372
+ const thumbRatio = this.viewportWidth / this.contentWidth
373
+ const thumbWidth = Math.max(1, Math.floor(this.viewportWidth * thumbRatio))
374
+ const thumbX = Math.floor((this.viewportWidth - thumbWidth) * scrollRatio)
300
375
 
301
376
  const trackStyle = palette.id({ fg: 8 })
302
377
  const thumbStyle = palette.id({ fg: 7 })
303
378
 
304
379
  // Draw track
305
- for (let col = 0; col < w; col++) {
380
+ for (let col = 0; col < this.viewportWidth; col++) {
306
381
  const char = col >= thumbX && col < thumbX + thumbWidth ? "━" : "─"
307
382
  const style = col >= thumbX && col < thumbX + thumbWidth ? thumbStyle : trackStyle
308
383
  buffer.drawCP(x + col, y + h - 1, char.codePointAt(0)!, style)
@@ -314,6 +389,14 @@ export class ScrollHost extends SingleChildHost {
314
389
  super.updateProps(props)
315
390
  // Scroll is greedy by default unless explicitly set to false
316
391
  this.applyGreedyDefault(props, 1)
392
+ const prevAxis = this.axis
393
+ const prevOffsetY = this.offsetY
394
+ const prevOffsetX = this.offsetX
395
+ const prevAlign = this.align
396
+ const prevBg = this.bg
397
+ const prevShowScrollbar = this.showScrollbar
398
+ const prevSticky = this.sticky
399
+
317
400
  if (props.axis !== undefined) this.axis = (props.axis as ScrollProps["axis"]) ?? "vertical"
318
401
  if (props.offsetY !== undefined) this.offsetY = props.offsetY as number
319
402
  if (props.offsetX !== undefined) this.offsetX = props.offsetX as number
@@ -322,5 +405,19 @@ export class ScrollHost extends SingleChildHost {
322
405
  if (props.showScrollbar !== undefined) this.showScrollbar = props.showScrollbar as boolean
323
406
  if (props.sticky !== undefined) this.sticky = props.sticky as boolean
324
407
  this.onScrollLayoutChange = props.onScrollLayoutChange as ScrollProps["onScrollLayoutChange"]
408
+
409
+ const layoutChanged =
410
+ prevAxis !== this.axis ||
411
+ prevOffsetY !== this.offsetY ||
412
+ prevOffsetX !== this.offsetX ||
413
+ prevSticky !== this.sticky ||
414
+ prevAlign !== this.align ||
415
+ prevShowScrollbar !== this.showScrollbar
416
+
417
+ if (layoutChanged) {
418
+ this.invalidateLayout()
419
+ } else if (prevBg !== this.bg) {
420
+ this.invalidateRender()
421
+ }
325
422
  }
326
423
  }
@@ -26,6 +26,7 @@ export abstract class SingleChildHost extends BaseHost {
26
26
  this.children.splice(0, 1)
27
27
  }
28
28
  child.parent = null
29
+ this.invalidateLayout()
29
30
  }
30
31
 
31
32
  private setSingleChild(child: HostInstance): void {
@@ -44,5 +45,6 @@ export abstract class SingleChildHost extends BaseHost {
44
45
  }
45
46
 
46
47
  child.parent = this
48
+ this.invalidateLayout()
47
49
  }
48
50
  }
@@ -21,7 +21,7 @@ export class SpacerHost extends LeafHost {
21
21
  this.updateProps(props as unknown as Record<string, unknown>)
22
22
  }
23
23
 
24
- measure(_maxW: number, _maxH: number): Size {
24
+ protected measureSelf(_maxW: number, _maxH: number): Size {
25
25
  // Spacers have no natural size, they expand via greedy
26
26
  return { w: this.minWidth, h: this.minHeight }
27
27
  }
@@ -34,7 +34,12 @@ export class SpacerHost extends LeafHost {
34
34
  super.updateProps(props)
35
35
  // Spacer is greedy by default unless explicitly set to false
36
36
  this.applyGreedyDefault(props, 1)
37
+ const prevMinWidth = this.minWidth
38
+ const prevMinHeight = this.minHeight
37
39
  if (props.minWidth !== undefined) this.minWidth = props.minWidth as number
38
40
  if (props.minHeight !== undefined) this.minHeight = props.minHeight as number
41
+ if (prevMinWidth !== this.minWidth || prevMinHeight !== this.minHeight) {
42
+ this.invalidateLayout()
43
+ }
39
44
  }
40
45
  }