@effect-tui/react 0.1.6 → 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 (119) hide show
  1. package/dist/src/dev.js +2 -2
  2. package/dist/src/dev.js.map +1 -1
  3. package/dist/src/hooks/index.d.ts +1 -0
  4. package/dist/src/hooks/index.d.ts.map +1 -1
  5. package/dist/src/hooks/index.js +1 -0
  6. package/dist/src/hooks/index.js.map +1 -1
  7. package/dist/src/hooks/use-quit.d.ts +21 -0
  8. package/dist/src/hooks/use-quit.d.ts.map +1 -0
  9. package/dist/src/hooks/use-quit.js +29 -0
  10. package/dist/src/hooks/use-quit.js.map +1 -0
  11. package/dist/src/hosts/base.d.ts +6 -2
  12. package/dist/src/hosts/base.d.ts.map +1 -1
  13. package/dist/src/hosts/base.js +56 -6
  14. package/dist/src/hosts/base.js.map +1 -1
  15. package/dist/src/hosts/box.d.ts.map +1 -1
  16. package/dist/src/hosts/box.js +3 -5
  17. package/dist/src/hosts/box.js.map +1 -1
  18. package/dist/src/hosts/flex-container.d.ts.map +1 -1
  19. package/dist/src/hosts/flex-container.js +4 -1
  20. package/dist/src/hosts/flex-container.js.map +1 -1
  21. package/dist/src/hosts/overlay-item.d.ts +2 -2
  22. package/dist/src/hosts/overlay-item.d.ts.map +1 -1
  23. package/dist/src/hosts/overlay-item.js +7 -22
  24. package/dist/src/hosts/overlay-item.js.map +1 -1
  25. package/dist/src/hosts/overlay.d.ts.map +1 -1
  26. package/dist/src/hosts/overlay.js +3 -35
  27. package/dist/src/hosts/overlay.js.map +1 -1
  28. package/dist/src/hosts/scroll.d.ts +1 -0
  29. package/dist/src/hosts/scroll.d.ts.map +1 -1
  30. package/dist/src/hosts/scroll.js +14 -7
  31. package/dist/src/hosts/scroll.js.map +1 -1
  32. package/dist/src/hosts/spacer.d.ts +1 -0
  33. package/dist/src/hosts/spacer.d.ts.map +1 -1
  34. package/dist/src/hosts/spacer.js +10 -9
  35. package/dist/src/hosts/spacer.js.map +1 -1
  36. package/dist/src/hosts/text.js +2 -2
  37. package/dist/src/hosts/text.js.map +1 -1
  38. package/dist/src/hosts/zstack.d.ts +3 -2
  39. package/dist/src/hosts/zstack.d.ts.map +1 -1
  40. package/dist/src/hosts/zstack.js +3 -18
  41. package/dist/src/hosts/zstack.js.map +1 -1
  42. package/dist/src/reconciler/types.d.ts +9 -4
  43. package/dist/src/reconciler/types.d.ts.map +1 -1
  44. package/dist/src/remote/Procedures.d.ts +4 -0
  45. package/dist/src/remote/Procedures.d.ts.map +1 -1
  46. package/dist/src/remote/Procedures.js +4 -0
  47. package/dist/src/remote/Procedures.js.map +1 -1
  48. package/dist/src/remote/Router.d.ts +2 -0
  49. package/dist/src/remote/Router.d.ts.map +1 -1
  50. package/dist/src/remote/Router.js.map +1 -1
  51. package/dist/src/remote/index.d.ts +8 -2
  52. package/dist/src/remote/index.d.ts.map +1 -1
  53. package/dist/src/remote/index.js +31 -3
  54. package/dist/src/remote/index.js.map +1 -1
  55. package/dist/src/renderer/input/InputProcessor.d.ts +1 -0
  56. package/dist/src/renderer/input/InputProcessor.d.ts.map +1 -1
  57. package/dist/src/renderer/input/InputProcessor.js +6 -1
  58. package/dist/src/renderer/input/InputProcessor.js.map +1 -1
  59. package/dist/src/renderer/lifecycle/TerminalSetup.d.ts +1 -0
  60. package/dist/src/renderer/lifecycle/TerminalSetup.d.ts.map +1 -1
  61. package/dist/src/renderer/lifecycle/TerminalSetup.js +26 -17
  62. package/dist/src/renderer/lifecycle/TerminalSetup.js.map +1 -1
  63. package/dist/src/renderer/modes/FullscreenRenderer.d.ts.map +1 -1
  64. package/dist/src/renderer/modes/FullscreenRenderer.js +8 -6
  65. package/dist/src/renderer/modes/FullscreenRenderer.js.map +1 -1
  66. package/dist/src/renderer/modes/InlineRenderer.d.ts.map +1 -1
  67. package/dist/src/renderer/modes/InlineRenderer.js +9 -7
  68. package/dist/src/renderer/modes/InlineRenderer.js.map +1 -1
  69. package/dist/src/renderer.d.ts +2 -0
  70. package/dist/src/renderer.d.ts.map +1 -1
  71. package/dist/src/renderer.js +39 -7
  72. package/dist/src/renderer.js.map +1 -1
  73. package/dist/src/test/render-tui.d.ts +5 -0
  74. package/dist/src/test/render-tui.d.ts.map +1 -1
  75. package/dist/src/test/render-tui.js +3 -0
  76. package/dist/src/test/render-tui.js.map +1 -1
  77. package/dist/src/utils/alignment.d.ts +15 -0
  78. package/dist/src/utils/alignment.d.ts.map +1 -0
  79. package/dist/src/utils/alignment.js +37 -0
  80. package/dist/src/utils/alignment.js.map +1 -0
  81. package/dist/src/utils/flex-layout.d.ts.map +1 -1
  82. package/dist/src/utils/flex-layout.js +20 -6
  83. package/dist/src/utils/flex-layout.js.map +1 -1
  84. package/dist/src/utils/index.d.ts +2 -1
  85. package/dist/src/utils/index.d.ts.map +1 -1
  86. package/dist/src/utils/index.js +2 -1
  87. package/dist/src/utils/index.js.map +1 -1
  88. package/dist/src/utils/styles.d.ts +9 -0
  89. package/dist/src/utils/styles.d.ts.map +1 -1
  90. package/dist/src/utils/styles.js +9 -0
  91. package/dist/src/utils/styles.js.map +1 -1
  92. package/dist/tsconfig.tsbuildinfo +1 -1
  93. package/package.json +3 -2
  94. package/src/dev.tsx +2 -2
  95. package/src/hooks/index.ts +1 -0
  96. package/src/hooks/use-quit.ts +33 -0
  97. package/src/hosts/base.ts +50 -6
  98. package/src/hosts/box.ts +3 -6
  99. package/src/hosts/flex-container.ts +3 -1
  100. package/src/hosts/overlay-item.ts +7 -22
  101. package/src/hosts/overlay.ts +3 -37
  102. package/src/hosts/scroll.ts +16 -7
  103. package/src/hosts/spacer.ts +11 -9
  104. package/src/hosts/text.ts +3 -3
  105. package/src/hosts/zstack.ts +5 -18
  106. package/src/reconciler/types.ts +9 -4
  107. package/src/remote/Procedures.ts +4 -0
  108. package/src/remote/Router.ts +7 -1
  109. package/src/remote/index.ts +41 -3
  110. package/src/renderer/input/InputProcessor.ts +6 -1
  111. package/src/renderer/lifecycle/TerminalSetup.ts +28 -17
  112. package/src/renderer/modes/FullscreenRenderer.ts +8 -6
  113. package/src/renderer/modes/InlineRenderer.ts +9 -7
  114. package/src/renderer.ts +44 -6
  115. package/src/test/render-tui.ts +7 -0
  116. package/src/utils/alignment.ts +50 -0
  117. package/src/utils/flex-layout.ts +19 -6
  118. package/src/utils/index.ts +10 -1
  119. package/src/utils/styles.ts +16 -0
@@ -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
  }
@@ -1,4 +1,4 @@
1
- import { ANSI, emitRowWithReset, findChangeWindow } from "@effect-tui/core"
1
+ import { ANSI, emitRowWithReset, rowChanged } from "@effect-tui/core"
2
2
  import type { RendererMode, RenderContext, RenderOutput } from "./RendererMode.js"
3
3
 
4
4
  /**
@@ -15,20 +15,22 @@ export class FullscreenRenderer implements RendererMode {
15
15
  // If a full clear was requested (e.g., on resize), prepend it to output
16
16
  // so clear + content are written atomically (no visible flash)
17
17
  if (this.needsFullClear) {
18
- output += ANSI.cursor.to(1, 1)
18
+ output += ANSI.screen.clear + ANSI.cursor.to(1, 1)
19
19
  this.needsFullClear = false
20
20
  // After clear, we must redraw everything - can't rely on diff
21
21
  for (let y = 0; y < frameHeight; y++) {
22
22
  if (y > 0) output += ANSI.cursor.to(1, y + 1)
23
+ output += palette.sgr(0) + ANSI.line.clear
23
24
  output += emitRowWithReset(nextBuffer, palette, y, frameWidth)
24
25
  }
25
26
  } else if (enableDiff && prevBuffer) {
26
27
  // Diff per line for minimal writes
27
28
  for (let y = 0; y < frameHeight; y++) {
28
- const change = findChangeWindow(prevBuffer, nextBuffer, y, frameWidth)
29
- if (!change) continue
30
- output += ANSI.cursor.to(change.left + 1, y + 1)
31
- output += emitRowWithReset(nextBuffer, palette, y, frameWidth, change.left, change.right + 1)
29
+ if (!rowChanged(prevBuffer, nextBuffer, y, frameWidth)) continue
30
+ output += ANSI.cursor.to(1, y + 1)
31
+ // Clear the row using default style to avoid carrying stale backgrounds
32
+ output += palette.sgr(0) + ANSI.line.clear
33
+ output += emitRowWithReset(nextBuffer, palette, y, frameWidth)
32
34
  }
33
35
  } else {
34
36
  // Full redraw (tests/manual mode)
@@ -32,6 +32,7 @@ export class InlineRenderer implements RendererMode {
32
32
  output += ANSI.cursor.up(this.previousHeight)
33
33
  }
34
34
  output += ANSI.cursor.startOfLine
35
+ output += ANSI.screen.clearToEnd
35
36
  this.previousStartRow = startRow
36
37
  this.printedWidths = []
37
38
  } else if (this.previousHeight > 0) {
@@ -80,7 +81,7 @@ export class InlineRenderer implements RendererMode {
80
81
  const prevW = this.printedWidths[screenY] ?? 0
81
82
  if (prevW > 0) {
82
83
  moveToRow(screenY)
83
- output += ANSI.cursor.toCol(1) + palette.sgr(0) + " ".repeat(prevW)
84
+ output += ANSI.cursor.toCol(1) + palette.sgr(0) + ANSI.line.clear
84
85
  this.printedWidths[screenY] = 0
85
86
  }
86
87
  continue
@@ -94,7 +95,7 @@ export class InlineRenderer implements RendererMode {
94
95
  // No change; maybe need to clear tail if content shrunk
95
96
  if (prevW > newW) {
96
97
  moveToRow(screenY)
97
- output += ANSI.cursor.toCol(newW + 1) + palette.sgr(0) + " ".repeat(prevW - newW)
98
+ output += ANSI.cursor.toCol(newW + 1) + palette.sgr(0) + ANSI.line.clearToEnd
98
99
  this.printedWidths[screenY] = newW
99
100
  }
100
101
  continue
@@ -108,9 +109,9 @@ export class InlineRenderer implements RendererMode {
108
109
  // Clear tail if shrunk
109
110
  const effectiveW = Math.max(newW, change.right + 1)
110
111
  if (prevW > effectiveW) {
111
- output += ANSI.cursor.toCol(effectiveW + 1) + palette.sgr(0) + " ".repeat(prevW - effectiveW)
112
+ output += ANSI.cursor.toCol(effectiveW + 1) + ANSI.line.clearToEnd
112
113
  }
113
- this.printedWidths[screenY] = newW
114
+ this.printedWidths[screenY] = effectiveW
114
115
  }
115
116
 
116
117
  // Ensure cursor ends just after the dynamic block for next frame positioning
@@ -128,8 +129,9 @@ export class InlineRenderer implements RendererMode {
128
129
  for (let bufferY = startRow; bufferY < endRow; bufferY++) {
129
130
  const screenY = bufferY - startRow
130
131
  const trimmedWidth = rowContentWidth(nextBuffer, bufferY, frameWidth)
131
- // Full-width emit avoids reliance on line clears.
132
- output += emitRowWithReset(nextBuffer, palette, bufferY, frameWidth, 0, frameWidth)
132
+ // Clear ENTIRE line first to handle resize artifacts
133
+ output += ANSI.line.clear
134
+ output += emitRowWithReset(nextBuffer, palette, bufferY, frameWidth, 0, trimmedWidth)
133
135
  output += "\r\n"
134
136
  // Track line widths for resize reflow calculation
135
137
  this.printedWidths[screenY] = trimmedWidth
@@ -137,7 +139,7 @@ export class InlineRenderer implements RendererMode {
137
139
 
138
140
  // Clear any extra lines if content shrank
139
141
  for (let screenY = rowCount; screenY < this.previousHeight; screenY++) {
140
- output += palette.sgr(0) + " ".repeat(frameWidth) + "\r\n"
142
+ output += palette.sgr(0) + ANSI.line.clear + "\r\n"
141
143
  this.printedWidths[screenY] = 0
142
144
  }
143
145
 
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)
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Alignment utilities for layout positioning.
3
+ */
4
+
5
+ import type { Rect, Size } from "../reconciler/types.js"
6
+
7
+ export type HAlign = "left" | "center" | "right" | "leading" | "trailing"
8
+ export type VAlign = "top" | "center" | "bottom"
9
+
10
+ /**
11
+ * Calculate position to align a child size within a container rect.
12
+ * Returns the x,y coordinates for the child's top-left corner.
13
+ */
14
+ export function alignInRect(
15
+ rect: Rect,
16
+ size: Size,
17
+ hAlign: HAlign = "center",
18
+ vAlign: VAlign = "center",
19
+ ): { x: number; y: number } {
20
+ let x = rect.x
21
+ let y = rect.y
22
+
23
+ switch (hAlign) {
24
+ case "left":
25
+ case "leading":
26
+ x = rect.x
27
+ break
28
+ case "center":
29
+ x = rect.x + Math.floor((rect.w - size.w) / 2)
30
+ break
31
+ case "right":
32
+ case "trailing":
33
+ x = rect.x + Math.max(0, rect.w - size.w)
34
+ break
35
+ }
36
+
37
+ switch (vAlign) {
38
+ case "top":
39
+ y = rect.y
40
+ break
41
+ case "center":
42
+ y = rect.y + Math.floor((rect.h - size.h) / 2)
43
+ break
44
+ case "bottom":
45
+ y = rect.y + Math.max(0, rect.h - size.h)
46
+ break
47
+ }
48
+
49
+ return { x, y }
50
+ }
@@ -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
@@ -1,4 +1,13 @@
1
+ export { type HAlign, type VAlign, alignInRect } from "./alignment.js"
1
2
  export { type BorderKind, type BorderChars, borderChars, type ClipRect, drawBorder } from "./border.js"
2
3
  export { type Padding, type PaddingInput, resolvePadding } from "./padding.js"
3
- export { type StyleOptions, type StyleInput, toColorValue, styleSpecFromProps, styleIdFromProps, resolveBgStyle } from "./styles.js"
4
+ export {
5
+ type StyleOptions,
6
+ type StyleInput,
7
+ toColorValue,
8
+ styleSpecFromProps,
9
+ styleIdFromProps,
10
+ resolveBgStyle,
11
+ resolveInheritedBgStyle,
12
+ } from "./styles.js"
4
13
  export { type FlexAxis, type FlexAlignment, type FlexMeasureResult, measureFlex, layoutFlex } from "./flex-layout.js"
@@ -55,3 +55,19 @@ export function resolveBgStyle(palette: Palette, bg?: Color): { value?: ColorVal
55
55
  const styleId = value === undefined ? 0 : palette.id({ bg: value })
56
56
  return { value, styleId }
57
57
  }
58
+
59
+ import type { HostInstance } from "../reconciler/types.js"
60
+ import { getInheritedBg } from "../hosts/base.js"
61
+
62
+ /**
63
+ * Resolve background style, inheriting from parent if not explicitly set.
64
+ * Common pattern used by hosts that need background color inheritance.
65
+ */
66
+ export function resolveInheritedBgStyle(
67
+ palette: Palette,
68
+ explicitBg: Color | undefined,
69
+ parent: HostInstance | null,
70
+ ): { value?: ColorValue; styleId: number } {
71
+ const bg = explicitBg ?? getInheritedBg(parent)
72
+ return resolveBgStyle(palette, bg)
73
+ }