Fix mobile chip-strip overlap, reveal-replay stagger, scroll-linked text-fill
Three clone-fidelity fixes from manual review of the acceptance screenshots. Fix 1 — mobile nav chip-strip overlap (generate/css.ts). A horizontally scrollable flex strip (overflow-x:auto flex <ul>) of nowrap chips collapsed because the base viewport reported min-width:0px on the chips (a mobile-only strip that is 0-wide at desktop), so the generator emitted `min-w-0`, letting the chips shrink below their content instead of overflowing — the nowrap text then collided. Suppress `min-w-0` for a nowrap flex item whose flex parent scrolls horizontally; scoped away from legitimate min-w-0 truncation (whose parent does not scroll-x). Verified on a fresh ooni.com clone: 0 chip overlaps at 375, scrollWidth 776 > clientWidth 375. Fix 2 — reveal-replay stagger leaves tiles unpainted (generate/motion.ts). DittoMotion re-hid each revealed element and replayed its entrance on scroll-into-view preserving the captured delay/duration, so a fast scroll / full-page screenshot caught most grid tiles mid-entrance. Cap the replayed delay (<=300ms) and duration (<=600ms), settle each tile per-element a bounded time after it enters view, and make the global failsafe settle (not re-animate) so nothing stays hidden. Validator settle path (__dittoMotionStop) unchanged. Verified: cropin cotton renders 12/12 crop tiles in a normal full-page pass; motion gate reveals 24/24. Fix 3 — scroll-linked text-fill frozen at end state (capture/stabilize.ts, capture/capture.ts, generate/css.ts). A view-timeline text-fill (animation-duration resolves to `auto`) was baked filled: the clone has no scroll timeline, so emitting the animation made it jump to its end keyframe via fill-mode:both. Capture-side, cancel scroll/view-timeline animations before each snapshot so the DOM records the at-rest state; generation-side, suppress the animation-* props when animation-duration is `auto` (we render the at-rest state, never replay a scroll-linked animation). Teach the motion gate to exclude these from the static-CSS expectation so the gate stays honest. Verified on a fresh ooni clone: the "world's no.1 pizza ovens" em is white/gray (rgb 240,240, 240), not filled yellow, at rest; motion gate still passes. Tests: +7 fixtures (css scroll-strip + scroll-timeline, reveal-replay caps, motion-gate scroll-timeline exclusion); 123/123 compiler tests green, full test suite green, typecheck clean. No gate regression on the fresh ooni run vs the 20260704-024247 baseline (all gate pass/fail unchanged; perceptual within 0.0004; motion restored to pass). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
f3d433c024
commit
afab12ff14
@@ -233,3 +233,60 @@ describe("generateCss hidden-node banded geometry", () => {
|
||||
assert.ok(!mobile.includes("left:10px"), `ancestor-hidden child must not emit geometry, got: ${mobile}`);
|
||||
});
|
||||
});
|
||||
|
||||
// Fix 1 — mobile nav chip-strip overlap. A horizontally-scrollable flex strip (overflow-x:auto
|
||||
// flex <ul>) holds nowrap chips; the base viewport can report `min-width:0px` on those flex-item
|
||||
// chips (e.g. a mobile-only strip collapsed to 0 at desktop). Emitting `min-w-0` lets the chips
|
||||
// compress below their content width instead of overflowing, collapsing the scroll strip so the
|
||||
// nowrap chip text collides ("Pizza OvenSpiral Mixe…"). The emitter must suppress `min-w-0` for a
|
||||
// nowrap flex item inside an overflow-x:auto/scroll flex parent.
|
||||
describe("generateCss scroll-strip chip min-width", () => {
|
||||
it("suppresses min-w-0 on a nowrap chip inside an overflow-x:auto flex strip", () => {
|
||||
const chip = node("n2", "li", computed({ display: "list-item", minWidth: "0px", whiteSpace: "nowrap" }));
|
||||
const ul = node("n1", "ul", computed({ display: "flex", overflowX: "auto", columnGap: "8px" }), [chip]);
|
||||
const root = node("n0", "body", computed(), [ul]);
|
||||
const css = generateCss(irWith(root), new Map());
|
||||
assert.ok(!baseRule(css, "n2").includes("min-width"), `chip must not carry min-width:0 in a scroll strip, got: ${baseRule(css, "n2")}`);
|
||||
});
|
||||
|
||||
it("still emits min-w-0 for a nowrap flex item whose parent does NOT scroll horizontally", () => {
|
||||
// A truncation/ellipsis flex child (parent overflow-x:visible) legitimately needs min-w-0 to
|
||||
// shrink below content — the fix is scoped to overflow-x:auto/scroll parents, so this is kept.
|
||||
const item = node("n2", "div", computed({ minWidth: "0px", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }));
|
||||
const row = node("n1", "div", computed({ display: "flex" }), [item]);
|
||||
const root = node("n0", "body", computed(), [row]);
|
||||
const css = generateCss(irWith(root), new Map());
|
||||
assert.ok(baseRule(css, "n2").includes("min-width:0"), `non-scrolling flex row keeps min-w-0, got: ${baseRule(css, "n2")}`);
|
||||
});
|
||||
});
|
||||
|
||||
// Fix 3 — scroll-linked text-fill frozen at end state. A scroll/view-timeline animation reports
|
||||
// its resolved `animation-duration` as `auto`. The clone has no scroll timeline, so emitting the
|
||||
// animation-* props makes it jump straight to its END keyframe (fill-mode:both + a 0s time-based
|
||||
// duration), freezing e.g. a text-fill 100% filled at rest. The emitter must suppress the
|
||||
// animation-* longhands when animation-duration is `auto`, leaving the captured at-rest statics.
|
||||
describe("generateCss scroll-timeline animation suppression", () => {
|
||||
it("drops animation-* props for an animation-duration:auto (scroll-timeline) node", () => {
|
||||
const em = node("n1", "em", computed({
|
||||
animationName: "fillAnimation", animationDuration: "auto",
|
||||
animationTimingFunction: "linear", animationFillMode: "both",
|
||||
backgroundClip: "text",
|
||||
}));
|
||||
const root = node("n0", "body", computed(), [em]);
|
||||
const css = generateCss(irWith(root), new Map());
|
||||
const rule = baseRule(css, "n1");
|
||||
assert.ok(!rule.includes("animation-name"), `scroll-timeline node must not emit animation-name, got: ${rule}`);
|
||||
assert.ok(!rule.includes("animation-duration"), `scroll-timeline node must not emit animation-duration, got: ${rule}`);
|
||||
});
|
||||
|
||||
it("still emits animation-* for a normal time-based (finite duration) animation", () => {
|
||||
const el = node("n1", "div", computed({
|
||||
animationName: "fadeInUp", animationDuration: "1.25s",
|
||||
animationTimingFunction: "ease", animationFillMode: "both",
|
||||
}));
|
||||
const root = node("n0", "body", computed(), [el]);
|
||||
const css = generateCss(irWith(root), new Map());
|
||||
const rule = baseRule(css, "n1");
|
||||
assert.ok(rule.includes("animation-name:fadeInUp"), `time-based reveal keeps its animation, got: ${rule}`);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user