flexily 0.2.0 → 0.3.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 (49) hide show
  1. package/README.md +14 -16
  2. package/dist/classic/layout.d.ts.map +1 -1
  3. package/dist/classic/layout.js +10 -1
  4. package/dist/classic/layout.js.map +1 -1
  5. package/dist/classic/node.js +1 -1
  6. package/dist/constants.d.ts +1 -0
  7. package/dist/constants.d.ts.map +1 -1
  8. package/dist/constants.js +2 -1
  9. package/dist/constants.js.map +1 -1
  10. package/dist/index-classic.d.ts +1 -1
  11. package/dist/index-classic.d.ts.map +1 -1
  12. package/dist/index-classic.js +5 -5
  13. package/dist/index-classic.js.map +1 -1
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +3 -3
  17. package/dist/index.js.map +1 -1
  18. package/dist/layout-helpers.d.ts +1 -4
  19. package/dist/layout-helpers.d.ts.map +1 -1
  20. package/dist/layout-helpers.js +2 -7
  21. package/dist/layout-helpers.js.map +1 -1
  22. package/dist/layout-zero.js +195 -39
  23. package/dist/layout-zero.js.map +1 -1
  24. package/dist/logger.js +2 -2
  25. package/dist/logger.js.map +1 -1
  26. package/dist/node-zero.js +1 -1
  27. package/dist/testing.js +4 -4
  28. package/dist/trace.js +1 -1
  29. package/dist/types.js +2 -2
  30. package/dist/types.js.map +1 -1
  31. package/dist/utils.d.ts +11 -3
  32. package/dist/utils.d.ts.map +1 -1
  33. package/dist/utils.js +46 -21
  34. package/dist/utils.js.map +1 -1
  35. package/package.json +11 -3
  36. package/src/CLAUDE.md +36 -21
  37. package/src/classic/layout.ts +105 -45
  38. package/src/classic/node.ts +60 -0
  39. package/src/constants.ts +2 -1
  40. package/src/index-classic.ts +1 -1
  41. package/src/index.ts +1 -1
  42. package/src/layout-flex-lines.ts +70 -3
  43. package/src/layout-helpers.ts +27 -7
  44. package/src/layout-stats.ts +0 -2
  45. package/src/layout-zero.ts +587 -160
  46. package/src/node-zero.ts +98 -2
  47. package/src/testing.ts +20 -14
  48. package/src/types.ts +22 -15
  49. package/src/utils.ts +47 -21
@@ -6,13 +6,8 @@
6
6
  */
7
7
  import * as C from "./constants.js";
8
8
  import { resolveValue } from "./utils.js";
9
- // ============================================================================
10
- // Constants for Edge Indices (avoid magic numbers)
11
- // ============================================================================
12
- export const EDGE_LEFT = 0;
13
- export const EDGE_TOP = 1;
14
- export const EDGE_RIGHT = 2;
15
- export const EDGE_BOTTOM = 3;
9
+ // Re-export edge constants (canonical definitions in constants.ts)
10
+ export { EDGE_LEFT, EDGE_TOP, EDGE_RIGHT, EDGE_BOTTOM } from "./constants.js";
16
11
  // ============================================================================
17
12
  // Helper Functions
18
13
  // ============================================================================
@@ -1 +1 @@
1
- {"version":3,"file":"layout-helpers.js","sourceRoot":"","sources":["../src/layout-helpers.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"layout-helpers.js","sourceRoot":"","sources":["../src/layout-helpers.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Flexture Layout Algorithm — Main Entry Point
2
+ * Flexily Layout Algorithm — Main Entry Point
3
3
  *
4
4
  * Core flexbox layout computation. This file contains:
5
5
  * - computeLayout(): top-level entry point
@@ -606,8 +606,14 @@ function layoutNode(node, availableWidth, availableHeight, offsetX, offsetY, abs
606
606
  // For ALIGN_BASELINE in row direction, we need to know the max baseline first
607
607
  // Zero-alloc: store baseline in child.flex.baseline, not a temporary array
608
608
  let maxBaseline = 0;
609
+ // baselineZoneHeight: the effective cross-axis size that non-baseline children
610
+ // align within when baseline alignment is present. This is max(maxBaseline, maxChildHeight).
611
+ // Only meaningful when alignItems != ALIGN_BASELINE but some children have alignSelf=baseline.
612
+ let baselineZoneHeight = 0;
613
+ const alignItemsIsBaseline = style.alignItems === C.ALIGN_BASELINE;
609
614
  if (hasBaselineAlignment && isRow) {
610
615
  // First pass: compute each child's baseline and find the maximum
616
+ let maxChildHeight = 0;
611
617
  for (const child of node.children) {
612
618
  if (child.flex.relativeIndex < 0)
613
619
  continue;
@@ -674,8 +680,20 @@ function layoutNode(node, availableWidth, availableHeight, offsetX, offsetY, abs
674
680
  // This is a simplification from CSS spec but acceptable for TUI use cases
675
681
  child.flex.baseline = topMargin + childHeight;
676
682
  }
677
- maxBaseline = Math.max(maxBaseline, child.flex.baseline);
683
+ // Track max child height (including margin) for baseline zone calculation
684
+ maxChildHeight = Math.max(maxChildHeight, topMargin + childHeight + child.flex.marginB);
685
+ // When alignItems is baseline, ALL children participate in baseline computation.
686
+ // When alignItems is NOT baseline, only children with alignSelf=baseline participate.
687
+ // This matches Yoga's behavior: non-baseline children are positioned within the
688
+ // "baseline zone" (the effective height determined by baseline-aligned children),
689
+ // not the full container cross-axis.
690
+ if (alignItemsIsBaseline || childStyle.alignSelf === C.ALIGN_BASELINE) {
691
+ maxBaseline = Math.max(maxBaseline, child.flex.baseline);
692
+ }
678
693
  }
694
+ // Baseline zone height: the max of maxBaseline and the tallest child.
695
+ // Non-baseline children are aligned within this zone, not the full container.
696
+ baselineZoneHeight = Math.max(maxBaseline, maxChildHeight);
679
697
  }
680
698
  // -----------------------------------------------------------------------
681
699
  // PHASE 7a: Estimate Flex Line Cross-Axis Sizes (Tentative)
@@ -709,16 +727,26 @@ function layoutNode(node, availableWidth, availableHeight, offsetX, offsetY, abs
709
727
  else if (crossDim.unit === C.UNIT_PERCENT && !Number.isNaN(crossAxisSize)) {
710
728
  childCross = crossAxisSize * (crossDim.value / 100);
711
729
  }
712
- else {
713
- // Auto - use a default or measure. For now, use 0 and let stretch handle it.
714
- childCross = 0;
730
+ else if (child.hasMeasureFunc()) {
731
+ // Auto-sized with measureFunc: get tentative cross size from cached measure.
732
+ // Phase 5 already called cachedMeasure, so this is typically a cache hit (no alloc).
733
+ const crossMargin = crossMarginStart + crossMarginEnd;
734
+ const availCross = Number.isNaN(crossAxisSize) ? Infinity : crossAxisSize - crossMargin;
735
+ const mW = isRow ? mainAxisSize : availCross;
736
+ const mH = isRow ? availCross : mainAxisSize;
737
+ const mWMode = Number.isNaN(mW) ? C.MEASURE_MODE_UNDEFINED : C.MEASURE_MODE_AT_MOST;
738
+ const mHMode = Number.isNaN(mH) ? C.MEASURE_MODE_UNDEFINED : C.MEASURE_MODE_AT_MOST;
739
+ const measured = child.cachedMeasure(Number.isNaN(mW) ? Infinity : mW, mWMode, Number.isNaN(mH) ? Infinity : mH, mHMode);
740
+ if (measured) {
741
+ childCross = isRow ? measured.height : measured.width;
742
+ }
715
743
  }
716
744
  maxLineCross = Math.max(maxLineCross, childCross + crossMarginStart + crossMarginEnd);
717
745
  }
718
- // Fallback cross size: use measured max, or divide available space among lines
719
- // Guard against NaN/division-by-zero: if crossAxisSize is NaN or numLines is 0, use 0
720
- const fallbackCross = numLines > 0 && !Number.isNaN(crossAxisSize) ? crossAxisSize / numLines : 0;
721
- const lineCrossSize = maxLineCross > 0 ? maxLineCross : fallbackCross;
746
+ // Use measured max cross size. If all children are auto-sized (maxLineCross === 0),
747
+ // use 0 NOT crossAxisSize/numLines, which would consume all free space and
748
+ // prevent alignContent from distributing it. Actual sizes are computed in Phase 8.
749
+ const lineCrossSize = maxLineCross;
722
750
  _lineCrossSizes[lineIdx] = lineCrossSize;
723
751
  cumulativeCrossOffset += lineCrossSize + crossGap;
724
752
  }
@@ -770,6 +798,15 @@ function layoutNode(node, availableWidth, availableHeight, offsetX, offsetY, abs
770
798
  }
771
799
  }
772
800
  break;
801
+ case C.ALIGN_SPACE_EVENLY:
802
+ // Equal spacing between lines and at edges
803
+ if (numLines > 0) {
804
+ const gap = freeSpace / (numLines + 1);
805
+ for (let i = 0; i < numLines; i++) {
806
+ _lineCrossOffsets[i] += gap * (i + 1);
807
+ }
808
+ }
809
+ break;
773
810
  case C.ALIGN_STRETCH:
774
811
  // Distribute extra space evenly among lines
775
812
  if (freeSpace > 0 && numLines > 0) {
@@ -906,6 +943,20 @@ function layoutNode(node, availableWidth, availableHeight, offsetX, offsetY, abs
906
943
  if (childStyle.alignSelf !== C.ALIGN_AUTO) {
907
944
  alignment = childStyle.alignSelf;
908
945
  }
946
+ // CSS Alignment spec: aspect-ratio fallback alignment
947
+ // When a flex item has aspect-ratio and auto cross-axis dimension,
948
+ // the fallback alignment is flex-start (not stretch). This prevents
949
+ // stretch from overriding the AR-derived dimension.
950
+ // Only applies when stretch is inherited (align-self: auto), not explicit.
951
+ const childCrossDimForAR = isRow ? childStyle.height : childStyle.width;
952
+ const childCrossIsAutoForAR = childCrossDimForAR.unit === C.UNIT_AUTO || childCrossDimForAR.unit === C.UNIT_UNDEFINED;
953
+ if (alignment === C.ALIGN_STRETCH &&
954
+ childStyle.alignSelf === C.ALIGN_AUTO &&
955
+ !Number.isNaN(childStyle.aspectRatio) &&
956
+ childStyle.aspectRatio > 0 &&
957
+ childCrossIsAutoForAR) {
958
+ alignment = C.ALIGN_FLEX_START;
959
+ }
909
960
  // Cross axis size depends on alignment and child's explicit dimensions
910
961
  // IMPORTANT: Resolve percent against parent's cross axis, not child's available
911
962
  let childCrossSize;
@@ -928,8 +979,10 @@ function layoutNode(node, availableWidth, availableHeight, offsetX, offsetY, abs
928
979
  childCrossSize = resolveValue(crossDim, crossAxisSize);
929
980
  }
930
981
  else if (parentHasDefiniteCross && alignment === C.ALIGN_STRETCH) {
931
- // Stretch alignment with definite parent cross size - fill the cross axis
932
- childCrossSize = crossAxisSize - crossMargin;
982
+ // Stretch alignment with definite parent cross size - fill the line's cross axis
983
+ // For wrapping layouts, stretch to line cross size, not full container cross size
984
+ const lineCross = numLines > 1 && childLineIdx < MAX_FLEX_LINES ? _lineCrossSizes[childLineIdx] : crossAxisSize;
985
+ childCrossSize = lineCross - crossMargin;
933
986
  }
934
987
  else {
935
988
  // Non-stretch alignment or no definite cross size - shrink-wrap to content
@@ -954,7 +1007,9 @@ function layoutNode(node, availableWidth, availableHeight, offsetX, offsetY, abs
954
1007
  // For auto main size children, use flex-computed size if flexGrow > 0,
955
1008
  // otherwise pass remaining available space for shrink-wrap behavior
956
1009
  const mainDim = isRow ? childStyle.width : childStyle.height;
957
- const mainIsAutoChild = mainDim.unit === C.UNIT_AUTO || mainDim.unit === C.UNIT_UNDEFINED;
1010
+ // A child has definite main size if it has explicit width/height OR non-auto flexBasis
1011
+ const hasDefiniteFlexBasis = childStyle.flexBasis.unit === C.UNIT_POINT || childStyle.flexBasis.unit === C.UNIT_PERCENT;
1012
+ const mainIsAutoChild = (mainDim.unit === C.UNIT_AUTO || mainDim.unit === C.UNIT_UNDEFINED) && !hasDefiniteFlexBasis;
958
1013
  const hasFlexGrow = cflex.flexGrow > 0;
959
1014
  // Use flex-computed mainSize for all cases - it includes padding/border as minimum
960
1015
  // The flex algorithm already computed the proper size based on content/padding/border
@@ -1094,8 +1149,12 @@ function layoutNode(node, availableWidth, availableHeight, offsetX, offsetY, abs
1094
1149
  // Calculate child's RELATIVE position (stored in layout)
1095
1150
  // Yoga behavior: position is rounded locally, size uses absolute edge rounding
1096
1151
  // This ensures sizes are pixel-perfect at document level while positions remain intuitive
1097
- const childLeft = Math.round(fractionalLeft + posOffsetX);
1098
- const childTop = Math.round(fractionalTop + posOffsetY);
1152
+ // Yoga 3.x quirk: measureFunc leaf nodes use Math.floor for position rounding,
1153
+ // while explicit-sized children use Math.round. This affects any justify/align mode
1154
+ // that produces fractional offsets (center, space-around, space-evenly).
1155
+ const posRound = shouldMeasure ? Math.floor : Math.round;
1156
+ const childLeft = posRound(fractionalLeft + posOffsetX);
1157
+ const childTop = posRound(fractionalTop + posOffsetY);
1099
1158
  // Check if cross axis is auto-sized (needed for deciding what to pass to layoutNode)
1100
1159
  const crossDimForLayoutCall = isRow ? childStyle.height : childStyle.width;
1101
1160
  const crossIsAutoForLayoutCall = crossDimForLayoutCall.unit === C.UNIT_AUTO || crossDimForLayoutCall.unit === C.UNIT_UNDEFINED;
@@ -1217,7 +1276,17 @@ function layoutNode(node, availableWidth, availableHeight, offsetX, offsetY, abs
1217
1276
  const crossEndIndex = isRow ? 3 : 2; // bottom for row, right for column
1218
1277
  const hasAutoStartMargin = isEdgeAuto(childStyle.margin, crossStartIndex, style.flexDirection, direction);
1219
1278
  const hasAutoEndMargin = isEdgeAuto(childStyle.margin, crossEndIndex, style.flexDirection, direction);
1220
- const availableCrossSpace = crossAxisSize - finalCrossSize - crossMargin;
1279
+ // When baseline alignment is present (hasBaselineAlignment) and this child is NOT using
1280
+ // baseline alignment, align within the baseline zone instead of the full container.
1281
+ // Yoga behavior: non-baseline children are positioned relative to the effective height
1282
+ // of the baseline group (max of maxBaseline and tallest child), not the container.
1283
+ const useBaselineZone = hasBaselineAlignment &&
1284
+ isRow &&
1285
+ !alignItemsIsBaseline &&
1286
+ alignment !== C.ALIGN_BASELINE &&
1287
+ baselineZoneHeight > 0;
1288
+ const effectiveCrossSize = useBaselineZone ? baselineZoneHeight : crossAxisSize;
1289
+ const availableCrossSpace = effectiveCrossSize - finalCrossSize - crossMargin;
1221
1290
  if (hasAutoStartMargin && hasAutoEndMargin) {
1222
1291
  // Both auto: center the item
1223
1292
  // CSS spec: auto margins don't absorb negative free space (clamp to 0)
@@ -1252,11 +1321,14 @@ function layoutNode(node, availableWidth, availableHeight, offsetX, offsetY, abs
1252
1321
  }
1253
1322
  }
1254
1323
  if (crossOffset > 0) {
1324
+ // Yoga 3.x quirk: measureFunc leaf nodes use Math.floor for cross-axis alignment
1325
+ // offset, matching the main-axis floor rounding behavior
1326
+ const crossRound = shouldMeasure ? Math.floor : Math.round;
1255
1327
  if (isRow) {
1256
- child.layout.top += Math.round(crossOffset);
1328
+ child.layout.top += crossRound(crossOffset);
1257
1329
  }
1258
1330
  else {
1259
- child.layout.left += Math.round(crossOffset);
1331
+ child.layout.left += crossRound(crossOffset);
1260
1332
  }
1261
1333
  }
1262
1334
  // Position advancement: use the right size depending on Phase 8 behavior.
@@ -1307,45 +1379,62 @@ function layoutNode(node, availableWidth, availableHeight, offsetX, offsetY, abs
1307
1379
  actualUsedMain += childMainSize + totalMainMargin;
1308
1380
  }
1309
1381
  actualUsedMain += totalGaps;
1310
- if (isRow && style.width.unit !== C.UNIT_POINT && style.width.unit !== C.UNIT_PERCENT) {
1382
+ // Skip main-axis shrink-wrap when aspect ratio determined this dimension
1383
+ const hasAR = !Number.isNaN(aspectRatio) && aspectRatio > 0;
1384
+ if (isRow && style.width.unit !== C.UNIT_POINT && style.width.unit !== C.UNIT_PERCENT && !hasAR) {
1311
1385
  // Auto-width row: shrink-wrap to content
1312
1386
  nodeWidth = actualUsedMain + innerLeft + innerRight;
1313
1387
  }
1314
- if (!isRow && style.height.unit !== C.UNIT_POINT && style.height.unit !== C.UNIT_PERCENT) {
1388
+ if (!isRow && style.height.unit !== C.UNIT_POINT && style.height.unit !== C.UNIT_PERCENT && !hasAR) {
1315
1389
  // Auto-height column: shrink-wrap to content
1316
1390
  nodeHeight = actualUsedMain + innerTop + innerBottom;
1317
1391
  }
1318
- // For cross axis, find the max child size
1319
- // CSS spec: percentage margins resolve against containing block's WIDTH only
1320
- // Use resolveEdgeValue to respect logical EDGE_START/END
1321
- let maxCrossSize = 0;
1322
- for (const child of node.children) {
1323
- if (child.flex.relativeIndex < 0)
1324
- continue;
1325
- const childCross = isRow ? child.layout.height : child.layout.width;
1326
- const childMargin = isRow
1327
- ? resolveEdgeValue(child.style.margin, 1, style.flexDirection, contentWidth, direction) +
1328
- resolveEdgeValue(child.style.margin, 3, style.flexDirection, contentWidth, direction)
1329
- : resolveEdgeValue(child.style.margin, 0, style.flexDirection, contentWidth, direction) +
1330
- resolveEdgeValue(child.style.margin, 2, style.flexDirection, contentWidth, direction);
1331
- maxCrossSize = Math.max(maxCrossSize, childCross + childMargin);
1392
+ // For cross axis, compute shrink-wrap size
1393
+ // For multi-line (flex-wrap), sum line cross sizes + cross gaps
1394
+ // For single line, use max child cross size (existing behavior)
1395
+ let totalCrossSize = 0;
1396
+ if (numLines > 1) {
1397
+ // Multi-line: sum line cross sizes + cross gaps between lines
1398
+ for (let i = 0; i < numLines; i++) {
1399
+ totalCrossSize += _lineCrossSizes[i];
1400
+ }
1401
+ totalCrossSize += crossGap * (numLines - 1);
1402
+ }
1403
+ else {
1404
+ // Single line: max child cross size
1405
+ // CSS spec: percentage margins resolve against containing block's WIDTH only
1406
+ // Use resolveEdgeValue to respect logical EDGE_START/END
1407
+ for (const child of node.children) {
1408
+ if (child.flex.relativeIndex < 0)
1409
+ continue;
1410
+ const childCross = isRow ? child.layout.height : child.layout.width;
1411
+ const childMargin = isRow
1412
+ ? resolveEdgeValue(child.style.margin, 1, style.flexDirection, contentWidth, direction) +
1413
+ resolveEdgeValue(child.style.margin, 3, style.flexDirection, contentWidth, direction)
1414
+ : resolveEdgeValue(child.style.margin, 0, style.flexDirection, contentWidth, direction) +
1415
+ resolveEdgeValue(child.style.margin, 2, style.flexDirection, contentWidth, direction);
1416
+ totalCrossSize = Math.max(totalCrossSize, childCross + childMargin);
1417
+ }
1332
1418
  }
1333
1419
  // Cross-axis shrink-wrap for auto-sized dimension
1334
1420
  // Only shrink-wrap when the available dimension is NaN (unconstrained)
1335
1421
  // When availableHeight/Width is defined, Yoga uses it for AUTO-sized root nodes
1422
+ // Skip if aspect ratio already determined this dimension (aspect ratio > shrink-wrap)
1336
1423
  if (isRow &&
1337
1424
  style.height.unit !== C.UNIT_POINT &&
1338
1425
  style.height.unit !== C.UNIT_PERCENT &&
1339
- Number.isNaN(availableHeight)) {
1340
- // Auto-height row: shrink-wrap to max child height
1341
- nodeHeight = maxCrossSize + innerTop + innerBottom;
1426
+ Number.isNaN(availableHeight) &&
1427
+ !hasAR) {
1428
+ // Auto-height row: shrink-wrap to total cross size (accounts for multi-line)
1429
+ nodeHeight = totalCrossSize + innerTop + innerBottom;
1342
1430
  }
1343
1431
  if (!isRow &&
1344
1432
  style.width.unit !== C.UNIT_POINT &&
1345
1433
  style.width.unit !== C.UNIT_PERCENT &&
1346
- Number.isNaN(availableWidth)) {
1347
- // Auto-width column: shrink-wrap to max child width
1348
- nodeWidth = maxCrossSize + innerLeft + innerRight;
1434
+ Number.isNaN(availableWidth) &&
1435
+ !hasAR) {
1436
+ // Auto-width column: shrink-wrap to total cross size (accounts for multi-line)
1437
+ nodeWidth = totalCrossSize + innerLeft + innerRight;
1349
1438
  }
1350
1439
  }
1351
1440
  // Re-apply min/max constraints after any shrink-wrap adjustments
@@ -1360,6 +1449,73 @@ function layoutNode(node, availableWidth, availableHeight, offsetX, offsetY, abs
1360
1449
  if (!Number.isNaN(nodeHeight) && nodeHeight < minInnerHeight) {
1361
1450
  nodeHeight = minInnerHeight;
1362
1451
  }
1452
+ // -----------------------------------------------------------------------
1453
+ // PHASE 9b: Re-stretch children after shrink-wrap (Yoga compat)
1454
+ // -----------------------------------------------------------------------
1455
+ // When the parent's cross axis was auto (NaN during Phase 8), children with
1456
+ // stretch alignment were shrink-wrapped to content. Now that the cross size
1457
+ // is known from shrink-wrap, re-layout those children with the definite size.
1458
+ // This matches Yoga's two-pass approach for auto-sized containers.
1459
+ if (Number.isNaN(crossAxisSize) && relativeCount > 0) {
1460
+ const finalCross = isRow ? nodeHeight - innerTop - innerBottom : nodeWidth - innerLeft - innerRight;
1461
+ if (!Number.isNaN(finalCross) && finalCross > 0) {
1462
+ for (const child of node.children) {
1463
+ if (child.flex.relativeIndex < 0)
1464
+ continue;
1465
+ const cstyle = child.style;
1466
+ // Determine alignment for this child
1467
+ let childAlign = style.alignItems;
1468
+ if (cstyle.alignSelf !== C.ALIGN_AUTO) {
1469
+ childAlign = cstyle.alignSelf;
1470
+ }
1471
+ // AR fallback: aspect-ratio prevents implicit stretch
1472
+ const cCrossDim = isRow ? cstyle.height : cstyle.width;
1473
+ const cCrossIsAuto = cCrossDim.unit === C.UNIT_AUTO || cCrossDim.unit === C.UNIT_UNDEFINED;
1474
+ if (childAlign === C.ALIGN_STRETCH &&
1475
+ cstyle.alignSelf === C.ALIGN_AUTO &&
1476
+ !Number.isNaN(cstyle.aspectRatio) &&
1477
+ cstyle.aspectRatio > 0 &&
1478
+ cCrossIsAuto) {
1479
+ childAlign = C.ALIGN_FLEX_START;
1480
+ }
1481
+ if (childAlign !== C.ALIGN_STRETCH)
1482
+ continue;
1483
+ if (!cCrossIsAuto)
1484
+ continue;
1485
+ // Compute child's cross margin
1486
+ const cCrossMargin = isRow
1487
+ ? resolveEdgeValue(cstyle.margin, 1, style.flexDirection, contentWidth, direction) +
1488
+ resolveEdgeValue(cstyle.margin, 3, style.flexDirection, contentWidth, direction)
1489
+ : resolveEdgeValue(cstyle.margin, 0, style.flexDirection, contentWidth, direction) +
1490
+ resolveEdgeValue(cstyle.margin, 2, style.flexDirection, contentWidth, direction);
1491
+ const stretchedCross = finalCross - cCrossMargin;
1492
+ // Only re-layout if the cross size actually changed
1493
+ const currentCross = isRow ? child.layout.height : child.layout.width;
1494
+ if (Math.round(stretchedCross) <= currentCross)
1495
+ continue;
1496
+ // Re-layout child with the definite cross size
1497
+ // Save position — layoutNode overwrites layout.left/top
1498
+ const savedLeft = child.layout.left;
1499
+ const savedTop = child.layout.top;
1500
+ const cMarginL = resolveEdgeValue(cstyle.margin, 0, style.flexDirection, contentWidth, direction);
1501
+ const cMarginT = resolveEdgeValue(cstyle.margin, 1, style.flexDirection, contentWidth, direction);
1502
+ const cAbsX = absX + innerLeft + savedLeft - cMarginL;
1503
+ const cAbsY = absY + innerTop + savedTop - cMarginT;
1504
+ const passW = isRow ? child.layout.width : stretchedCross;
1505
+ const passH = isRow ? stretchedCross : child.layout.height;
1506
+ layoutNode(child, passW, passH, savedLeft, savedTop, cAbsX, cAbsY, direction);
1507
+ // Restore position and override cross dimension to stretched size
1508
+ child.layout.left = savedLeft;
1509
+ child.layout.top = savedTop;
1510
+ if (isRow) {
1511
+ child.layout.height = Math.round(stretchedCross);
1512
+ }
1513
+ else {
1514
+ child.layout.width = Math.round(stretchedCross);
1515
+ }
1516
+ }
1517
+ }
1518
+ }
1363
1519
  // =========================================================================
1364
1520
  // PHASE 10: Final Output - Set Node Layout
1365
1521
  // =========================================================================