Trust geometry-corroborated authored fixed sizes; guard aspect-law width transfer

- Authored fixed h-/w- utilities from the source cascade, when the
  captured geometry corroborates them, suppress fill/flow re-derivation
  and emit as captured computed px per band (root-font-size independent) -
  fixes logo tiles collapsing to h-full and replaced-element sizes
  dropping to intrinsic
- aspectHeightLaw rejects ratios where the CSS aspect-ratio/max-height
  width transfer would clamp width below the captured box, falling back to
  per-band explicit dimensions

419 tests pass (4 new, each verified discriminating), typecheck clean,
double-regen byte-identical.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Samraaj Bath
2026-07-04 04:55:07 -07:00
co-authored by Claude Fable 5
parent 994d2bc21e
commit ec4e4397cc
4 changed files with 318 additions and 9 deletions
+120 -6
View File
@@ -882,7 +882,15 @@ function aspectHeightLaw(node: IRNode, viewports: number[]): Record<number, stri
let chosen: { value: string; ratio: number } | undefined;
for (const r of ordered) {
const predicted = Math.min(s.w / r.ratio, s.maxH);
if (Math.abs(predicted - s.h) <= Math.max(1.5, 0.01 * s.h)) { chosen = r; break; }
if (Math.abs(predicted - s.h) > Math.max(1.5, 0.01 * s.h)) continue;
// Aspect-ratio + max-height also transfers into a max-WIDTH: per CSS transferred sizing, a box
// with `aspect-ratio:R` and `max-height:M` cannot exceed `R × M` wide (the height cap back-computes
// a width cap). The height-only check above accepts any ratio that makes min(w/R, M) ≈ h at a
// clamped viewport, but a ratio whose transferred max-width (R × M) is narrower than the measured
// box would clamp the clone's width below the source (a 1.66:1 box forced square to its 420px cap).
// Require the measured width to fit under the transferred cap at every clamped sample.
if (Number.isFinite(s.maxH) && s.w > r.ratio * s.maxH + Math.max(1.5, 0.01 * s.w)) continue;
chosen = r; break;
}
if (!chosen) return null;
out[s.vp] = chosen.value;
@@ -1169,6 +1177,100 @@ function sourceFixedSizeIntent(node: IRNode): boolean {
return /(?:^|\s)(?:[a-z0-9-]+:)*size-(?!fit(?:\s|$))\S+/.test(node.srcClass || "");
}
// An authored, definite length unit (px/rem/em/…) — the same predicate the capture-side probe uses to
// call a dimension "load-bearing". Rejects auto/%/keywords and viewport/container units (those are
// fluid laws the width/height passes already recover); a bare number (`0`) is not definite. Used to
// read the arbitrary-value inner of a `h-[…]` / `w-[…]` source utility.
function isDefiniteAuthoredLength(inner: string): boolean {
const v = inner.trim().toLowerCase();
if (!v) return false;
if (/^(auto|min-content|max-content|fit-content|inherit|initial|unset|revert)$/.test(v)) return false;
if (/%|vw|vh|vmin|vmax|svw|lvw|dvw|svh|lvh|dvh|\bfr\b/.test(v)) return false;
return /(?:^|[\s(*/+-])[\d.]+(?:px|rem|em|cm|mm|in|pt|pc|ex|ch|q)\b/.test(v);
}
/** Does the SOURCE class author an explicit, definite fixed height on this axis — `h-[<len>]`,
* `min-h-[<len>]`, or the token forms (`h-<n>`, `h-px`) — at one or more breakpoints, corroborated by
* geometry? This is the emission-side twin of the capture probe's authored-height guard: a banded
* authored fixed height (`h-[4rem] sm:h-[4.5rem] md:h-[6.25rem]`) can still probe hFill/hAuto when the
* box is a grid/flex item whose fill child (or stretched track) reproduces the box height — the
* fill↔content cycle — so the probe alone would drop the baked band to `h-full`/auto. When the source
* authors a definite height AND every painted viewport has a definite computed height matching its
* bbox (the authored value actually resolved to that px), trust the baked per-band px. Geometry
* corroboration keeps this from firing on a fluid/percentage box that merely mentions a fixed h-token
* under an inactive variant. */
function sourceFixedHeightIntent(node: IRNode, viewports: number[]): boolean {
if (REPLACED.has(node.tag) || node.tag === "canvas" || node.tag.includes("-")) return false;
if (!authorsFixedAxis(node.srcClass, "h")) return false;
return fixedAxisGeometryHolds(node, "h", viewports);
}
/** The WIDTH twin of sourceFixedHeightIntent: the source authors a definite `w-[<len>]`/`min-w-[<len>]`
* (or token) width, corroborated by a definite computed width == bbox at every painted viewport. A
* banded authored fixed width on a shrink-0 item (a testimonial avatar `h-[2.5rem] w-[2.5rem] shrink-0`
* whose img child fills it) probes wAuto/wFill and would otherwise be dropped, collapsing the box to
* its child's intrinsic size. Trust the authored fixed width. */
function sourceFixedWidthIntent(node: IRNode, viewports: number[]): boolean {
if (REPLACED.has(node.tag) || node.tag === "canvas" || node.tag.includes("-")) return false;
if (!authorsFixedAxis(node.srcClass, "w")) return false;
return fixedAxisGeometryHolds(node, "w", viewports);
}
// True when the source class authors a definite fixed length on the given axis at any breakpoint:
// `h-[4rem]` / `min-w-[24px]` (arbitrary), `h-px`, or a numeric token `w-24` (Tailwind's spacing scale
// is a fixed rem length). `full`/`auto`/`screen`/fraction/`min`/`max`/`fit`/`svh`… tokens are fluid and
// excluded. Variant-prefixed forms count (banded authored heights), matching the source-intent parser.
function authorsFixedAxis(srcClass: string | undefined, axis: "h" | "w"): boolean {
if (!srcClass) return false;
for (const raw of srcClass.split(/\s+/).filter(Boolean)) {
const colon = lastTopLevelColon(raw);
const core = colon >= 0 ? raw.slice(colon + 1) : raw;
const m = new RegExp(`^(?:min-)?${axis}-(.+)$`).exec(core);
if (!m) continue;
const val = m[1]!;
if (val.startsWith("[") && val.endsWith("]")) {
if (isDefiniteAuthoredLength(val.slice(1, -1))) return true;
continue;
}
if (val === "px") return true; // 1px
if (/^\d+(?:\.\d+)?$/.test(val)) return true; // spacing-scale token (fixed rem)
}
return false;
}
// Index of the variant/core separating colon at bracket depth 0 (so `h-[calc(1px:2)]` is not split on
// an interior colon), mirroring parseSourceClass in the tailwind emitter.
function lastTopLevelColon(raw: string): number {
let depth = 0, last = -1;
for (let i = 0; i < raw.length; i++) {
const ch = raw[i]!;
if (ch === "[" || ch === "(") depth++;
else if (ch === "]" || ch === ")") depth = Math.max(0, depth - 1);
else if (ch === ":" && depth === 0) last = i;
}
return last;
}
// Geometry corroboration for an authored fixed height/width: at EVERY painted viewport the node has a
// definite (px, non-auto) computed value on the axis that equals its measured bbox extent. This proves
// the authored fixed length actually resolved to the captured box (so the baked per-band px is the
// authored value), not a fluid/percentage box that merely carries a fixed token under an inactive
// variant. Requires ≥1 painted sample.
function fixedAxisGeometryHolds(node: IRNode, axis: "h" | "w", viewports: number[]): boolean {
let painted = 0;
for (const vp of viewports) {
const cs = node.computedByVp[vp]; const nb = node.bboxByVp[vp];
if (!cs || !nb || !node.visibleByVp[vp] || (cs.display || "") === "none") continue;
const val = axis === "h" ? cs.height : cs.width;
const ext = axis === "h" ? nb.height : nb.width;
if (!val || val === "auto" || !/px$/.test(val)) return false;
if (!(ext > 0)) return false;
if (Math.abs(pf(val) - ext) > Math.max(1.5, 0.01 * ext)) return false;
painted++;
}
return painted >= 1;
}
const ABSOLUTE_MEDIA_FILL_TAGS = new Set(["img", "picture", "video"]);
/** An absolutely-positioned media layer authored as a full-cover background: `position:absolute;
@@ -2352,6 +2454,7 @@ function declsForViewport(
dropViewportMaxWidth = false,
nowrapText = false,
keepIdentityTransform = false,
forceHeight = false,
): Map<string, string> {
const cs = node.computedByVp[vp];
const out = new Map<string, string>();
@@ -2439,7 +2542,11 @@ function declsForViewport(
// absolutely-positioned children). Without emitting it the block collapses to
// its content in the clone. Content-filled blocks stay auto so margin
// collapsing is preserved.
let explicitHeight = false;
// `forceHeight`: the caller proved this node authors a geometry-corroborated fixed height
// (sourceFixedHeightIntent) but the box's own content extent can't re-derive it (a shrink-0 box whose
// child fills it — the avatar case), so none of the structural explicit-height tests below fire. Trust
// the authored value and emit the baked per-band px.
let explicitHeight = forceHeight;
const nb = node.bboxByVp[vp];
// Probe ground truth, emission twin of the heightFlows keep (see the hAuto/hFill guard there):
// the sizing probe proved height:auto did NOT reproduce this box (hAuto:false) AND it is not a
@@ -3295,9 +3402,15 @@ export function collectNodeRules(ir: IR, assetMap: Map<string, string>, includeN
// parent, varies) OR the sizing probe measured that height:100% reproduces and auto does not
// (heightProbeFills — direct ground truth, also catches constant fillers; inert pre-probe captures).
const sourceFixedSize = !isContents && sourceFixedSizeIntent(node);
// Authored, geometry-corroborated fixed height/width on this node's own source class (banded or
// not). These break the fill↔content probe cycle that otherwise drops a banded authored height to
// `h-full` (logo tiles) or a shrink-0 box's authored width/height to auto (testimonial avatars):
// trust the baked per-band px the capture measured, exactly as the capture-side authored guard does.
const sourceFixedHeight = !isContents && sourceFixedHeightIntent(node, sampleVps);
const sourceFixedWidth = !isContents && sourceFixedWidthIntent(node, sampleVps);
const mediaCover = !isContents && absoluteMediaCoversParent(node, parentNode, sampleVps);
const absHeightFill = !isContents && absoluteHeightFill(node, parentNode, sampleVps);
const buttonFixedHeight = !isContents && (fixedHeightButtonLike(node, sampleVps) || sourceFixedSize);
const buttonFixedHeight = !isContents && (fixedHeightButtonLike(node, sampleVps) || sourceFixedSize || sourceFixedHeight);
const heightFill = !isContents && !buttonFixedHeight && (isHeightFill(node, parentNode, parentDefiniteHeight, sampleVps) || absHeightFill || mediaCover || heightProbeFills(node, sampleVps));
// This node's own height-drop decision, computed here so it can also feed childDefiniteHeight (a
// height we drop becomes `auto` → does not confer a definite containing block). Same OR'd signals
@@ -3356,7 +3469,7 @@ export function collectNodeRules(ir: IR, assetMap: Map<string, string>, includeN
const fillCap = !isContents ? fillsToCapWidth(node, parentNode, sampleVps) : null;
// A width that leaves interior free space positions its children (auto margins / justify) — it's
// load-bearing, so the content-sized branches below must NOT drop it (would kill the spacing).
const buttonFixedWidth = !isContents && (fixedWidthButtonLike(node, sampleVps) || sourceFixedSize);
const buttonFixedWidth = !isContents && (fixedWidthButtonLike(node, sampleVps) || sourceFixedSize || sourceFixedWidth);
const lockWidth = !isContents && !fillCap && (hasInteriorFreeSpaceX(node, sampleVps) || buttonFixedWidth);
// Ground-truth sizing probe is the primary signal after authored capped fills:
// `width:100%; max-width:X` can look "auto" in a clone probe because the retained cap still
@@ -3374,6 +3487,7 @@ export function collectNodeRules(ir: IR, assetMap: Map<string, string>, includeN
const mixedFill = (!lockWidth && !isContents && !percentVp) ? mixedFillByVp(node, parentNode, sampleVps) : null;
const widthPlan: WidthPlan =
fillCap ? { kind: "fillcap", cap: fillCap }
: sourceFixedWidth ? { kind: "fixed" }
: sourceFixedSize ? inferred.plan
: circularSlide ? { kind: "fixed" }
: fullWidthSlide ? { kind: "basisFull" }
@@ -3485,7 +3599,7 @@ export function collectNodeRules(ir: IR, assetMap: Map<string, string>, includeN
// gated to a breakpoint owns the transform only where it runs, but freezes a mid-scroll offset
// at the OTHER bands — suppress those frozen deltas at every width).
const animOwned = animOwnedProps(node, [baseVp, ...bands.map((b) => b.vp)]);
const baseDecls = finalizeDecls(declsForViewport(node, parentNode?.computedByVp[baseVp], baseVp, assetMap, centeredBase, colorVar, ir.doc.perViewport[baseVp]?.scrollHeight, widthPlan, gridColsByVp?.get(baseVp), gridRowsByVp?.get(baseVp), flowH, dropInsets, leftPct, heightFill, geometry, dropGridRows, dropViewportMaxWidth, nowrapText, keepIdentityTransform), tokenResolver);
const baseDecls = finalizeDecls(declsForViewport(node, parentNode?.computedByVp[baseVp], baseVp, assetMap, centeredBase, colorVar, ir.doc.perViewport[baseVp]?.scrollHeight, widthPlan, gridColsByVp?.get(baseVp), gridRowsByVp?.get(baseVp), flowH, dropInsets, leftPct, heightFill, geometry, dropGridRows, dropViewportMaxWidth, nowrapText, keepIdentityTransform, sourceFixedHeight), tokenResolver);
const nr: NodeRule = { base: baseDecls, bands: [] };
// Per-band overrides (delta vs base), using the parent's value AT THAT viewport.
@@ -3596,7 +3710,7 @@ export function collectNodeRules(ir: IR, assetMap: Map<string, string>, includeN
}
}
const centeredVp = stableCenter || (layoutParent ? centeredAtVp(node, layoutParent, b.vp) : false);
const vpDecls = finalizeDecls(declsForViewport(node, parentNode?.computedByVp[b.vp], b.vp, assetMap, centeredVp, colorVar, ir.doc.perViewport[b.vp]?.scrollHeight, widthPlan, gridColsByVp?.get(b.vp), gridRowsByVp?.get(b.vp), flowH, dropInsets, leftPct, heightFill, geometry, dropGridRows, dropViewportMaxWidth, nowrapText, keepIdentityTransform), tokenResolver);
const vpDecls = finalizeDecls(declsForViewport(node, parentNode?.computedByVp[b.vp], b.vp, assetMap, centeredVp, colorVar, ir.doc.perViewport[b.vp]?.scrollHeight, widthPlan, gridColsByVp?.get(b.vp), gridRowsByVp?.get(b.vp), flowH, dropInsets, leftPct, heightFill, geometry, dropGridRows, dropViewportMaxWidth, nowrapText, keepIdentityTransform, sourceFixedHeight), tokenResolver);
const delta = new Map<string, string>();
for (const [k, v] of vpDecls) if (baseDecls.get(k) !== v) delta.set(k, v);
for (const [k] of baseDecls) if (!vpDecls.has(k)) delta.set(k, resetValue(k));
+65 -3
View File
@@ -762,6 +762,57 @@ function sourceFluidLengthSuffix(suffix: string): boolean {
return /(%|vw|vh|svw|lvw|dvw|svh|lvh|dvh|fr|min-content|max-content|fit-content)/.test(inner);
}
// A FIXED, definite authored length suffix on a `w-`/`h-` utility: an arbitrary `[<px|rem|em|…>]`, the
// `px` token (1px), or a numeric spacing-scale token (`24`, `2.5` → fixed rem). Unlike
// sourceFluidLengthSuffix these do NOT re-derive from the container, so the source-intent pass must
// re-emit them as the CAPTURED computed px (the clone's root font-size may differ from the source's, so
// echoing `h-[4rem]` verbatim would mis-size), and only where geometry corroborates the resolved box.
function sourceFixedLengthSuffix(suffix: string): boolean {
const inner = sourceArbitraryInner(suffix);
if (inner !== null) {
const v = inner.trim().toLowerCase();
if (/^var\(/.test(v)) return false; // handled by the var-length path
if (/%|vw|vh|vmin|vmax|svw|lvw|dvw|svh|lvh|dvh|\bfr\b|min-content|max-content|fit-content/.test(v)) return false;
return /(?:^|[\s(*/+-])[\d.]+(?:px|rem|em|cm|mm|in|pt|pc|ex|ch|q)\b/.test(v);
}
if (suffix === "px") return true;
return /^\d+(?:\.\d+)?$/.test(suffix);
}
// A `w-`/`h-` utility carrying a fixed authored length (as recognised by sourceFixedLengthSuffix).
function isFixedLengthUtil(util: string): boolean {
const m = /^([wh])-(.+)$/.exec(util);
return !!m && sourceFixedLengthSuffix(m[2]!);
}
// The captured computed px on this axis is definite and matches the measured bbox — i.e. the authored
// fixed length resolved to the box the capture recorded. Geometry corroboration for a fixed w/h util.
function fixedLengthResolvesToBox(node: IRNode, axis: "w" | "h", vp: number): boolean {
const cs = node.computedByVp[vp]; const b = node.bboxByVp[vp];
if (!cs || !b) return false;
const val = axis === "h" ? cs.height : cs.width;
const ext = axis === "h" ? b.height : b.width;
if (!val || val === "auto" || !/px$/.test(val)) return false;
if (!(ext > 0)) return false;
return Math.abs(pf(val) - ext) <= Math.max(1.5, 0.01 * ext);
}
// Rewrite a fixed-length `w-`/`h-` source utility to the node's CAPTURED computed px at a viewport,
// preserving any variant prefix. `h-[4rem]` → `h-[60px]` (the source resolved 4rem against a 15px root;
// the clone's root may differ, so bake the measured px). Returns the utility unchanged when it is not a
// fixed-length util or the computed px is unavailable.
function fixedLengthUtilAsPx(util: string, node: IRNode, vp: number): string {
const m = VARIANT_PREFIX.exec(util);
const prefix = m ? m[1]! : "";
const core = m ? m[2]! : util;
const sm = /^([wh])-(.+)$/.exec(core);
if (!sm || !sourceFixedLengthSuffix(sm[2]!)) return util;
const val = sm[1] === "h" ? node.computedByVp[vp]?.height : node.computedByVp[vp]?.width;
if (!val || !/px$/.test(val)) return util;
const px = Math.round(pf(val) * 100) / 100;
return `${prefix}${sm[1]}-[${px}px]`;
}
function sourceAspectUtility(core: string): string | null {
let m = /^aspect-(\d+)\/(\d+)-box$/.exec(core);
if (m) return m[1] === m[2] ? "aspect-square" : `aspect-[${m[1]}/${m[2]}]`;
@@ -793,7 +844,7 @@ function sourceAxisForCore(core0: string): { axis: SourceAxis; utility: string }
const maxH = /^max-h-(.+)$/.exec(core);
if (maxH && sourceFluidLengthSuffix(maxH[1]!)) return { axis: "max-h", utility: core };
const size = /^([wh])-(.+)$/.exec(core);
if (size && sourceFluidLengthSuffix(size[2]!)) return { axis: size[1] as SourceAxis, utility: core };
if (size && (sourceFluidLengthSuffix(size[2]!) || sourceFixedLengthSuffix(size[2]!))) return { axis: size[1] as SourceAxis, utility: core };
return null;
}
@@ -957,6 +1008,12 @@ function sourceAxisCompatible(node: IRNode, parent: IRNode | undefined, axis: So
if (axis === "h" && v === "h-full") return !!s?.hFill;
if (axis === "h" && v === "h-auto") return !!s?.hAuto;
if (axis === "h" && v && sourceVarName(v)) return /px$/.test(node.computedByVp[vp]?.height || "");
// A FIXED authored length (`h-[4rem]`, `w-24`, `h-px`) — the source-intent pass will re-emit it as
// the captured computed px. Compatible when that px is definite and equals the measured bbox extent
// at this viewport (the authored length actually resolved to the captured box). This recovers a
// banded authored fixed height/width the sizing probe dropped through the fill↔content cycle
// (h-full) or the content/fill drop (auto), without depending on the clone's root font-size.
if (v && isFixedLengthUtil(v)) return fixedLengthResolvesToBox(node, axis, vp);
return false;
});
}
@@ -1023,13 +1080,18 @@ function sourceIntentUtilities(node: IRNode, parent: IRNode | undefined, viewpor
continue;
}
if (!sourceAxisCompatible(node, parent, axis, byVp, viewports)) continue;
const base = byVp.get(canonical)!;
// A fixed-length w/h axis re-emits the CAPTURED computed px per band (root-font-size independent),
// not the source rem token; every other axis keeps its authored utility verbatim.
const asEmit = (vp: number): string =>
(axis === "w" || axis === "h") && isFixedLengthUtil(byVp.get(vp)!)
? fixedLengthUtilAsPx(byVp.get(vp)!, node, vp) : byVp.get(vp)!;
const base = asEmit(canonical);
axes.add(axis);
if (axis === "aspect") axes.add("grid-rows");
utilities.push(base);
for (const b of bands) {
if (!b.media) continue;
const v = byVp.get(b.vp)!;
const v = asEmit(b.vp);
if (v !== base) utilities.push(prefixFor(b.media) + v);
}
css.push(...sourceIntentVarCss(node, axis, byVp, viewports, canonical));