Skip to content

fix(core): justify-content honors min-resolved root main size (closes #165)#173

Open
zhijiewong wants to merge 9 commits into
mainfrom
fix/root-min-justify-content
Open

fix(core): justify-content honors min-resolved root main size (closes #165)#173
zhijiewong wants to merge 9 commits into
mainfrom
fix/root-min-justify-content

Conversation

@zhijiewong

Copy link
Copy Markdown
Contributor

What

A bare-auto root container resolved its main-axis size from min-width/min-height
after positioning its children, so justify-content collapsed them to
flex-start at 0 (root width became 50 but the child stayed at left: 0).
Yoga distributes within the resolved size.

root.setFlexDirection('row'); root.setJustifyContent('center'); root.setMinWidth(50);
// child 20×20 → was left:0, now left:15  (Yoga parity)

Fix

At the positioning step only, for a single-line, forward-direction bare-auto
root main axis, both engines position against max(innerMain, clamp(content, min, max) − pads):

  • Classic (index.ts + main-axis.ts): threads a rootMainAuto flag
    (via the now-exported axisIsBareZero) into the flex pipeline; substitutes at
    positionItemsInLine, gated rootMainAuto && noWrap && !isReverse.
  • Spineless (flex-grammar.ts): feeds the root's min/max main fields into
    emitJustifiedMainPos under the existing rootAxisIsBareZero structural gate
    (forward, no-wrap).

The max(…) guard makes the change a no-op for every concrete-size container.
The two engines are kept byte-identical; the differential fuzzer confirms it
(its imperative reference was also updated to pass rootMainAuto, restoring it
as a real guard for this code).

Tests

  • 7 unit tests in layout.test.ts (classic + spineless paths, multi-child
    space-between, shrink-wrap regression, and a wrap-deferral consistency guard).
  • Reinstated the 2 fixtures PR fix(core): root layout offsets by own margin (closes #163) #166 deferred:
    row-min-width-and-margin, column-min-height-and-margin.
  • pnpm run ci green (lint → build → typecheck → test 1770 → test:differential 976).

Scope / follow-up

No-wrap, forward-direction main axis only. The cross-axis (align-items) analog
and the wrap-justify case are deferred and tracked in #172 — both kept
consistent across engines.

Closes #165.

🤖 Generated with Claude Code

zhijiewong and others added 9 commits June 7, 2026 16:55
…tent (#165)

Root cause + main-axis-only fix design: resolve a bare-auto root's main
container size from clamp(content, min, max) at the positioning step so
justify-content distributes against the resolved size instead of 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nt (#165)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ved root main size (#165)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…165)

Code-review follow-up. Reverse directions reflect about innerMain in
flipMainAxis (and the spineless applyReverseMainPos reflects about the same
unresolved 0), so substituting a larger forward main size would desync the
engines and rely on the autoSizeRootFromContent shift to mask negatives.
Gate the substitution to !isReverse — reverse + bare-auto stays deferred and
identical across engines. Also renames the inner `items` binding to `lineItems`
to avoid shadowing the step-1 `items`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…#165)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… fixtures (#165)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…uzzer guard (#165)

Final-review follow-up (two cross-engine gaps on the untouched flexWrap path):

1. The classic substitution gated on `lines.length === 1`, which is true for a
   single-child WRAP root (packIntoLines short-circuits at <=1 item), so classic
   applied the #165 fix to wrap roots while the spineless wrap path
   (evaluateWrappedChild) never does — kid.left 15 vs 0. Gate classic to no-wrap
   so both engines defer wrap identically.

2. Both differential fuzzers' imperative reference called `layoutChildren(root)`
   with the default rootMainAuto=false, no longer mirroring index.ts — so the
   fuzzer compared a fixed spineless side against an unfixed imperative side and
   would spuriously diverge (or silently stop guarding) the changed code. Mirror
   index.ts in both imperativeFloats helpers.

Adds a multi-child space-between test and a wrap-deferral consistency test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Pilates justify-content ignored when main-axis size resolved from min-width/min-height

1 participant