@effect-tui/react 0.1.7 → 0.2.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 (58) hide show
  1. package/dist/src/hooks/index.d.ts +1 -0
  2. package/dist/src/hooks/index.d.ts.map +1 -1
  3. package/dist/src/hooks/index.js +1 -0
  4. package/dist/src/hooks/index.js.map +1 -1
  5. package/dist/src/hooks/use-quit.d.ts +21 -0
  6. package/dist/src/hooks/use-quit.d.ts.map +1 -0
  7. package/dist/src/hooks/use-quit.js +29 -0
  8. package/dist/src/hooks/use-quit.js.map +1 -0
  9. package/dist/src/hosts/base.d.ts +1 -2
  10. package/dist/src/hosts/base.d.ts.map +1 -1
  11. package/dist/src/hosts/base.js +42 -6
  12. package/dist/src/hosts/base.js.map +1 -1
  13. package/dist/src/hosts/flex-container.d.ts.map +1 -1
  14. package/dist/src/hosts/flex-container.js +4 -1
  15. package/dist/src/hosts/flex-container.js.map +1 -1
  16. package/dist/src/hosts/scroll.d.ts +1 -0
  17. package/dist/src/hosts/scroll.d.ts.map +1 -1
  18. package/dist/src/hosts/scroll.js +11 -2
  19. package/dist/src/hosts/scroll.js.map +1 -1
  20. package/dist/src/hosts/spacer.d.ts +1 -0
  21. package/dist/src/hosts/spacer.d.ts.map +1 -1
  22. package/dist/src/hosts/spacer.js +10 -9
  23. package/dist/src/hosts/spacer.js.map +1 -1
  24. package/dist/src/reconciler/types.d.ts +9 -4
  25. package/dist/src/reconciler/types.d.ts.map +1 -1
  26. package/dist/src/renderer/input/InputProcessor.d.ts +1 -0
  27. package/dist/src/renderer/input/InputProcessor.d.ts.map +1 -1
  28. package/dist/src/renderer/input/InputProcessor.js +6 -1
  29. package/dist/src/renderer/input/InputProcessor.js.map +1 -1
  30. package/dist/src/renderer/lifecycle/TerminalSetup.d.ts +1 -0
  31. package/dist/src/renderer/lifecycle/TerminalSetup.d.ts.map +1 -1
  32. package/dist/src/renderer/lifecycle/TerminalSetup.js +26 -17
  33. package/dist/src/renderer/lifecycle/TerminalSetup.js.map +1 -1
  34. package/dist/src/renderer.d.ts +2 -0
  35. package/dist/src/renderer.d.ts.map +1 -1
  36. package/dist/src/renderer.js +39 -7
  37. package/dist/src/renderer.js.map +1 -1
  38. package/dist/src/test/render-tui.d.ts +5 -0
  39. package/dist/src/test/render-tui.d.ts.map +1 -1
  40. package/dist/src/test/render-tui.js +3 -0
  41. package/dist/src/test/render-tui.js.map +1 -1
  42. package/dist/src/utils/flex-layout.d.ts.map +1 -1
  43. package/dist/src/utils/flex-layout.js +20 -6
  44. package/dist/src/utils/flex-layout.js.map +1 -1
  45. package/dist/tsconfig.tsbuildinfo +1 -1
  46. package/package.json +3 -2
  47. package/src/hooks/index.ts +1 -0
  48. package/src/hooks/use-quit.ts +33 -0
  49. package/src/hosts/base.ts +36 -6
  50. package/src/hosts/flex-container.ts +3 -1
  51. package/src/hosts/scroll.ts +13 -2
  52. package/src/hosts/spacer.ts +11 -9
  53. package/src/reconciler/types.ts +9 -4
  54. package/src/renderer/input/InputProcessor.ts +6 -1
  55. package/src/renderer/lifecycle/TerminalSetup.ts +28 -17
  56. package/src/renderer.ts +44 -6
  57. package/src/test/render-tui.ts +7 -0
  58. package/src/utils/flex-layout.ts +19 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effect-tui/react",
3
- "version": "0.1.7",
3
+ "version": "0.2.0",
4
4
  "description": "React bindings for @effect-tui/core",
5
5
  "type": "module",
6
6
  "files": [
@@ -83,7 +83,7 @@
83
83
  "prepublishOnly": "bun run typecheck && bun run build"
84
84
  },
85
85
  "dependencies": {
86
- "@effect-tui/core": "^0.1.7",
86
+ "@effect-tui/core": "^0.2.0",
87
87
  "@effect/platform": "^0.94.0",
88
88
  "@effect/platform-bun": "^0.87.0",
89
89
  "@effect/rpc": "^0.73.0",
@@ -110,6 +110,7 @@
110
110
  }
111
111
  },
112
112
  "devDependencies": {
113
+ "@effect-tui/emulator": "workspace:^",
113
114
  "@effect/opentelemetry": "^0.60.0",
114
115
  "@effect/vitest": "^0.27.0",
115
116
  "@opentelemetry/api": "^1.9.0",
@@ -3,5 +3,6 @@ export type { UseKeyboardOptions } from "./use-keyboard.js"
3
3
  export { useMouse } from "./use-mouse.js"
4
4
  export type { UseMouseOptions } from "./use-mouse.js"
5
5
  export { usePaste } from "./use-paste.js"
6
+ export { useQuit } from "./use-quit.js"
6
7
  export { useScroll } from "./use-scroll.js"
7
8
  export type { UseScrollOptions, UseScrollReturn, ScrollState } from "./use-scroll.js"
@@ -0,0 +1,33 @@
1
+ import { useCallback } from "react"
2
+ import { useRenderer } from "../renderer-context.js"
3
+
4
+ /**
5
+ * Hook that returns a quit function for cleanly exiting the TUI application.
6
+ *
7
+ * This ensures terminal state is properly restored (alternate buffer exit,
8
+ * cursor shown, raw mode disabled) before the process exits.
9
+ *
10
+ * @example
11
+ * ```tsx
12
+ * function App() {
13
+ * const quit = useQuit()
14
+ *
15
+ * useKeyboard((key) => {
16
+ * if (key.text === "q") quit()
17
+ * })
18
+ *
19
+ * return <text>Press q to quit</text>
20
+ * }
21
+ * ```
22
+ */
23
+ export function useQuit(): (code?: number) => void {
24
+ const renderer = useRenderer()
25
+
26
+ return useCallback(
27
+ (code = 0) => {
28
+ renderer.stop()
29
+ process.exit(code)
30
+ },
31
+ [renderer],
32
+ )
33
+ }
package/src/hosts/base.ts CHANGED
@@ -32,9 +32,11 @@ export abstract class BaseHost implements HostInstance {
32
32
  children: HostInstance[] = []
33
33
  rect: Rect | null = null
34
34
 
35
- // Common flex props
36
- flexGrow = 0
37
- flexShrink = 1
35
+ // Greedy layout - expands to fill remaining space
36
+ // undefined = not greedy (hug content)
37
+ // true or 1 = 1 share
38
+ // number > 1 = proportionally more shares
39
+ greedy: boolean | number | undefined = undefined
38
40
 
39
41
  // ─────────────────────────────────────────────────────────────
40
42
  // Frame constraints (like SwiftUI's .frame modifier)
@@ -65,7 +67,20 @@ export abstract class BaseHost implements HostInstance {
65
67
  abstract render(buffer: CellBuffer, palette: Palette): void
66
68
 
67
69
  layout(rect: Rect): void {
68
- this.rect = rect
70
+ // Apply frame constraints to the assigned rect
71
+ let { w, h } = rect
72
+
73
+ // Fixed width/height override
74
+ if (this.frameWidth !== undefined) w = this.frameWidth
75
+ if (this.frameHeight !== undefined) h = this.frameHeight
76
+
77
+ // Apply min/max clamps
78
+ if (this.frameMinWidth !== undefined) w = Math.max(this.frameMinWidth, w)
79
+ if (this.frameMaxWidth !== undefined) w = Math.min(this.frameMaxWidth, w)
80
+ if (this.frameMinHeight !== undefined) h = Math.max(this.frameMinHeight, h)
81
+ if (this.frameMaxHeight !== undefined) h = Math.min(this.frameMaxHeight, h)
82
+
83
+ this.rect = { x: rect.x, y: rect.y, w, h }
69
84
  }
70
85
 
71
86
  /**
@@ -144,8 +159,23 @@ export abstract class BaseHost implements HostInstance {
144
159
  }
145
160
 
146
161
  updateProps(props: Record<string, unknown>): void {
147
- this.flexGrow = (props.flexGrow as number | undefined) ?? 0
148
- this.flexShrink = (props.flexShrink as number | undefined) ?? 1
162
+ // Greedy layout - reset to undefined unless explicitly set
163
+ // Subclasses like Spacer/Scroll set their own default before calling super
164
+ if ("greedy" in props) {
165
+ const greedy = props.greedy
166
+ if (greedy === true) {
167
+ this.greedy = 1
168
+ } else if (typeof greedy === "number") {
169
+ this.greedy = greedy
170
+ } else {
171
+ // greedy is false, undefined, or null
172
+ this.greedy = undefined
173
+ }
174
+ } else {
175
+ // No greedy prop - reset to default (undefined)
176
+ // Subclasses like Spacer/Scroll handle this before calling super
177
+ this.greedy = undefined
178
+ }
149
179
 
150
180
  // Frame constraints - only accept valid numbers (ignore strings like "100%")
151
181
  this.frameWidth = typeof props.width === "number" ? props.width : undefined
@@ -84,12 +84,14 @@ export class FlexContainerHost<A extends FlexAxis> extends BaseHost {
84
84
 
85
85
  override layout(rect: Rect): void {
86
86
  super.layout(rect)
87
+ // Use this.rect (constrained) not rect (raw input)
88
+ if (!this.rect) return
87
89
  const stretchCross = this.axis === "vertical" ? this.alignment === "leading" : this.alignment === "top"
88
90
  layoutFlex(
89
91
  this.axis,
90
92
  this.layoutChildren,
91
93
  this.cachedSizes,
92
- rect,
94
+ this.rect,
93
95
  this.spacing,
94
96
  toFlexAlignment(this.axis, this.alignment),
95
97
  stretchCross,
@@ -38,6 +38,9 @@ export class ScrollHost extends SingleChildHost {
38
38
  bg?: Color
39
39
  showScrollbar = true
40
40
  sticky = false
41
+
42
+ // Scroll is greedy by default - expands to fill available space
43
+ override greedy: boolean | number | undefined = 1
41
44
  onContentSize?: (width: number, height: number) => void
42
45
  onViewportSize?: (width: number, height: number) => void
43
46
  onEffectiveOffset?: (offset: number) => void
@@ -85,8 +88,12 @@ export class ScrollHost extends SingleChildHost {
85
88
  // Note: onContentSize callback is deferred to layout() to keep measure() pure
86
89
  }
87
90
 
88
- // Scroll container is greedy - takes all available space (after constraints)
89
- return this.constrainResult({ w: constrained.w, h: constrained.h })
91
+ // Report natural content size (clamped to constrained bounds)
92
+ // Greedy expansion happens in layout phase via layoutFlex
93
+ const naturalW = Math.min(this.contentWidth, constrained.w)
94
+ const naturalH = Math.min(this.contentHeight, constrained.h)
95
+
96
+ return this.constrainResult({ w: naturalW, h: naturalH })
90
97
  }
91
98
 
92
99
  override layout(rect: Rect): void {
@@ -239,6 +246,10 @@ export class ScrollHost extends SingleChildHost {
239
246
 
240
247
  override updateProps(props: Record<string, unknown>): void {
241
248
  super.updateProps(props)
249
+ // Scroll is greedy by default unless explicitly set to false
250
+ if (!("greedy" in props)) {
251
+ this.greedy = 1
252
+ }
242
253
  if (props.axis !== undefined) this.axis = (props.axis as ScrollProps["axis"]) ?? "vertical"
243
254
  if (props.offset !== undefined) this.offset = props.offset as number
244
255
  if (props.offsetX !== undefined) this.offsetX = props.offsetX as number
@@ -13,15 +13,16 @@ export class SpacerHost extends BaseHost {
13
13
  minWidth = 0
14
14
  minHeight = 0
15
15
 
16
+ // Spacers are greedy by default
17
+ override greedy: boolean | number | undefined = 1
18
+
16
19
  constructor(props: SpacerProps, ctx: HostContext) {
17
- // Spacers have flexGrow=1 by default
18
- const propsWithDefaults = { flexGrow: 1, ...props }
19
- super("spacer", propsWithDefaults, ctx)
20
- this.updateProps(propsWithDefaults)
20
+ super("spacer", props, ctx)
21
+ this.updateProps(props)
21
22
  }
22
23
 
23
24
  measure(_maxW: number, _maxH: number): Size {
24
- // Spacers have no natural size, they expand via flexGrow
25
+ // Spacers have no natural size, they expand via greedy
25
26
  return { w: this.minWidth, h: this.minHeight }
26
27
  }
27
28
 
@@ -34,10 +35,11 @@ export class SpacerHost extends BaseHost {
34
35
  }
35
36
 
36
37
  override updateProps(props: Record<string, unknown>): void {
37
- // Keep spacer default flexGrow=1 unless explicitly provided
38
- const flexGrow = props.flexGrow as number | undefined
39
- const propsWithDefaults = flexGrow === undefined ? { ...props, flexGrow: 1 } : props
40
- super.updateProps(propsWithDefaults)
38
+ super.updateProps(props)
39
+ // Spacer is greedy by default unless explicitly set to false
40
+ if (!("greedy" in props)) {
41
+ this.greedy = 1
42
+ }
41
43
  if (props.minWidth !== undefined) this.minWidth = props.minWidth as number
42
44
  if (props.minHeight !== undefined) this.minHeight = props.minHeight as number
43
45
  }
@@ -92,10 +92,15 @@ export interface HostContext {
92
92
  * With the constraint, it reports exactly 10, so spacer gets the rest.
93
93
  */
94
94
  export interface CommonProps {
95
- /** Flex grow factor - how much extra space this view takes (default: 0) */
96
- flexGrow?: number
97
- /** Flex shrink factor - how much this view shrinks when space is tight (default: 1) */
98
- flexShrink?: number
95
+ /**
96
+ * Greedy layout - element expands to fill remaining space.
97
+ * - `true` or `1`: takes 1 share of remaining space
98
+ * - number > 1: takes proportionally more (e.g., `greedy={2}` gets 2x space)
99
+ * - `false` or undefined: hugs content (default)
100
+ *
101
+ * Scroll and Spacer are greedy by default.
102
+ */
103
+ greedy?: boolean | number
99
104
 
100
105
  // ─────────────────────────────────────────────────────────────
101
106
  // Frame constraints (like SwiftUI's .frame modifier)
@@ -7,6 +7,7 @@ export interface InputProcessorConfig {
7
7
  dispatchPaste: (text: string) => void
8
8
  flushSync: <T>(fn: () => T) => T
9
9
  onInputProcessed: () => void
10
+ onQuit?: () => void // Called instead of process.exit() for proper cleanup
10
11
  }
11
12
 
12
13
  /**
@@ -86,7 +87,11 @@ export class InputProcessor {
86
87
  // Default Ctrl+C handling - exit unless user called preventDefault()
87
88
  // Only match Ctrl+C (not Ctrl+Shift+C which should be available for copy)
88
89
  if (this.config.exitOnCtrlC && !wrapped.defaultPrevented && event.ctrl && !event.shift && event.text === "c") {
89
- process.exit(0)
90
+ if (this.config.onQuit) {
91
+ this.config.onQuit()
92
+ } else {
93
+ process.exit(0)
94
+ }
90
95
  }
91
96
  }
92
97
  }
@@ -70,36 +70,47 @@ export class TerminalSetup {
70
70
  /**
71
71
  * Restore terminal to normal state.
72
72
  * Call this when stopping the renderer.
73
+ * Uses a single write call to ensure atomic output and reliable flushing.
73
74
  */
74
75
  teardown(): void {
75
76
  if (this.config.skipTerminalSetup) return
76
77
 
77
- if (this.config.mode === "fullscreen") {
78
- this.stdout.write(Terminal.exitFullscreen)
79
- } else {
80
- // Re-enable reflow on exit
81
- this.stdout.write(ANSI.reflow.enable)
82
- this.stdout.write("\r\n")
78
+ // Disable raw mode FIRST - this is critical for proper terminal restoration
79
+ // Must happen before writing escape sequences so the terminal processes them correctly
80
+ if (this.stdin.isTTY && this.stdin.setRawMode) {
81
+ this.stdin.setRawMode(false)
83
82
  }
84
83
 
85
- if (this.config.enablePaste) {
86
- this.stdout.write(ANSI.paste.disable)
84
+ // Build all escape sequences into a single string for atomic write
85
+ // This ensures all sequences are flushed together before process exit
86
+ let output = ""
87
+
88
+ // Disable enhanced keyboard protocols first
89
+ if (this.config.enableKittyKeyboard !== false) {
90
+ output += ANSI.keyboard.disable
91
+ output += ANSI.modifyOtherKeys.disable
87
92
  }
88
93
 
89
94
  if (this.config.enableMouse) {
90
- this.stdout.write(ANSI.mouse.disable)
95
+ output += ANSI.mouse.disable
91
96
  }
92
97
 
93
- // Disable enhanced keyboard protocols
94
- if (this.config.enableKittyKeyboard !== false) {
95
- this.stdout.write(ANSI.keyboard.disable)
96
- this.stdout.write(ANSI.modifyOtherKeys.disable)
98
+ if (this.config.enablePaste) {
99
+ output += ANSI.paste.disable
97
100
  }
98
101
 
99
- this.stdout.write(Terminal.showCursor)
100
-
101
- if (this.stdin.isTTY && this.stdin.setRawMode) {
102
- this.stdin.setRawMode(false)
102
+ // Exit fullscreen or re-enable reflow
103
+ if (this.config.mode === "fullscreen") {
104
+ output += Terminal.exitFullscreen
105
+ } else {
106
+ output += ANSI.reflow.enable
107
+ output += "\r\n"
103
108
  }
109
+
110
+ // Always show cursor at the end
111
+ output += Terminal.showCursor
112
+
113
+ // Single atomic write - more reliable for process exit scenarios
114
+ this.stdout.write(output)
104
115
  }
105
116
  }
package/src/renderer.ts CHANGED
@@ -71,6 +71,11 @@ export function createRenderer(options?: RendererOptions): TuiRenderer {
71
71
  onInputProcessed: () => {
72
72
  if (!manualMode) renderFrame()
73
73
  },
74
+ onQuit: () => {
75
+ // Clean up terminal state before exiting
76
+ renderer.stop()
77
+ process.exit(0)
78
+ },
74
79
  })
75
80
 
76
81
  // The render frame logic
@@ -300,6 +305,27 @@ export function createRenderer(options?: RendererOptions): TuiRenderer {
300
305
  }, frameMs)
301
306
  }
302
307
 
308
+ // Process exit handlers - ensure terminal is restored on any exit
309
+ // These handlers are critical for proper cleanup when process.exit() is called
310
+ let cleanedUp = false
311
+ const cleanup = () => {
312
+ if (cleanedUp) return
313
+ cleanedUp = true
314
+ renderer.stop()
315
+ }
316
+
317
+ // Handle normal process exit (synchronous - runs before exit completes)
318
+ const onExit = () => cleanup()
319
+ process.on("exit", onExit)
320
+
321
+ // Handle SIGINT (Ctrl+C from shell, not from TUI input) and SIGTERM
322
+ const onSignal = () => {
323
+ cleanup()
324
+ process.exit(0)
325
+ }
326
+ process.on("SIGINT", onSignal)
327
+ process.on("SIGTERM", onSignal)
328
+
303
329
  ;(renderer as TuiRendererInternal)._container = null
304
330
  return renderer
305
331
  }
@@ -365,6 +391,8 @@ export interface RenderInstance {
365
391
  rerender(element: ReactNode): void
366
392
  unmount(): void
367
393
  waitUntilExit(): Promise<void>
394
+ /** Cleanly exit the application, restoring terminal state before process.exit() */
395
+ quit(code?: number): void
368
396
  }
369
397
 
370
398
  export function render(element: ReactNode, options?: RendererOptions): RenderInstance {
@@ -383,18 +411,20 @@ export function render(element: ReactNode, options?: RendererOptions): RenderIns
383
411
  }
384
412
  })
385
413
 
414
+ // Resolve the exit promise on process exit
415
+ // Note: Terminal cleanup is handled by createRenderer's exit handlers
386
416
  const onExit = () => {
387
- if (!resolved) {
388
- renderer.stop()
389
- resolveExit?.()
390
- }
417
+ resolveExit?.()
391
418
  }
392
419
  process.once("exit", onExit)
393
420
 
394
421
  const unmount = () => {
395
422
  process.off("exit", onExit)
396
- renderer.stop()
397
- resolveExit?.()
423
+ if (!resolved) {
424
+ resolved = true
425
+ renderer.stop()
426
+ resolveExit?.()
427
+ }
398
428
  }
399
429
 
400
430
  const rerender = (next: ReactNode) => {
@@ -402,11 +432,19 @@ export function render(element: ReactNode, options?: RendererOptions): RenderIns
402
432
  root.render(next)
403
433
  }
404
434
 
435
+ // Clean quit function - renderer.stop() + process.exit()
436
+ // The createRenderer exit handler will also run, but it's idempotent
437
+ const quit = (code = 0) => {
438
+ renderer.stop()
439
+ process.exit(code)
440
+ }
441
+
405
442
  return {
406
443
  renderer,
407
444
  root,
408
445
  rerender,
409
446
  unmount,
410
447
  waitUntilExit: () => exitPromise,
448
+ quit,
411
449
  }
412
450
  }
@@ -7,6 +7,8 @@ import { MockStdout, MockStdin, stripAnsi, getVisibleLines } from "./mock-stream
7
7
  export interface RenderTUIOptions {
8
8
  width?: number
9
9
  height?: number
10
+ mode?: "fullscreen" | "inline"
11
+ diff?: boolean
10
12
  }
11
13
 
12
14
  export interface RenderTUIResult {
@@ -26,6 +28,8 @@ export interface RenderTUIResult {
26
28
  stdout: MockStdout
27
29
  /** Access mock stdin for advanced input simulation */
28
30
  stdin: MockStdin
31
+ /** Access renderer for advanced assertions */
32
+ renderer: ReturnType<typeof createRenderer>
29
33
  /** Resize the terminal */
30
34
  resize(width: number, height: number): void
31
35
  }
@@ -60,6 +64,8 @@ export function renderTUI(element: ReactElement, options?: RenderTUIOptions): Re
60
64
  stdin: stdin as any,
61
65
  manualMode: true,
62
66
  skipTerminalSetup: true,
67
+ mode: options?.mode,
68
+ diff: options?.diff,
63
69
  })
64
70
 
65
71
  const root = createRoot(renderer)
@@ -106,6 +112,7 @@ export function renderTUI(element: ReactElement, options?: RenderTUIOptions): Re
106
112
 
107
113
  stdout,
108
114
  stdin,
115
+ renderer,
109
116
 
110
117
  resize(w: number, h: number) {
111
118
  stdout.resize(w, h)
@@ -53,6 +53,19 @@ export function measureFlex(
53
53
  return { sizes, totalSize }
54
54
  }
55
55
 
56
+ /**
57
+ * Get the greedy weight for a child.
58
+ * - undefined/false = 0 (not greedy, hugs content)
59
+ * - true = 1
60
+ * - number = that weight
61
+ */
62
+ function getGreedyWeight(child: HostInstance): number {
63
+ const greedy = (child as BaseHost).greedy
64
+ if (greedy === undefined || greedy === false) return 0
65
+ if (greedy === true) return 1
66
+ return greedy
67
+ }
68
+
56
69
  /**
57
70
  * Layout children along a flex axis using cached sizes.
58
71
  */
@@ -67,17 +80,17 @@ export function layoutFlex(
67
80
  ): void {
68
81
  // Calculate totals
69
82
  let totalNaturalMain = 0
70
- let totalFlexGrow = 0
83
+ let totalGreedyWeight = 0
71
84
  const totalSpacing = Math.max(0, (children.length - 1) * spacing)
72
85
 
73
86
  for (let i = 0; i < children.length; i++) {
74
87
  const child = children[i]
75
88
  const size = cachedSizes[i] ?? child.measure(rect.w, rect.h)
76
89
  totalNaturalMain += mainSize(axis, size)
77
- totalFlexGrow += (child as BaseHost).flexGrow ?? 0
90
+ totalGreedyWeight += getGreedyWeight(child)
78
91
  }
79
92
 
80
- // Calculate extra space to distribute
93
+ // Calculate extra space to distribute to greedy children
81
94
  const availableMain = mainDim(axis, rect)
82
95
  const extraSpace = Math.max(0, availableMain - totalNaturalMain - totalSpacing)
83
96
 
@@ -89,10 +102,10 @@ export function layoutFlex(
89
102
  for (let i = 0; i < children.length; i++) {
90
103
  const child = children[i]
91
104
  const size = cachedSizes[i] ?? { w: 0, h: 0 }
92
- const flexGrow = (child as BaseHost).flexGrow ?? 0
93
- const flexExtra = totalFlexGrow > 0 ? (extraSpace * flexGrow) / totalFlexGrow : 0
105
+ const greedyWeight = getGreedyWeight(child)
106
+ const greedyExtra = totalGreedyWeight > 0 ? (extraSpace * greedyWeight) / totalGreedyWeight : 0
94
107
 
95
- const childMainDim = mainSize(axis, size) + flexExtra
108
+ const childMainDim = mainSize(axis, size) + greedyExtra
96
109
  const childCrossDim = crossSize(axis, size)
97
110
 
98
111
  // Calculate cross position based on alignment