From 773283a2f8b49ce3dd851f144d819f3729667aa3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Apr 2026 01:02:05 +0000 Subject: [PATCH 1/3] ci(deps): bump actions/checkout from 5 to 6 Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 4 ++-- .github/workflows/release.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4f4c244..6c4ed0c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: persist-credentials: false @@ -79,7 +79,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: persist-credentials: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 206b47e..049dc15 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,7 +26,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 From 8a6ce31bd0a6b3f48a11fe2dae013c394b77fdad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Apr 2026 01:02:08 +0000 Subject: [PATCH 2/3] ci(deps): bump amannn/action-semantic-pull-request from 6.1.0 to 6.1.1 Bumps [amannn/action-semantic-pull-request](https://github.com/amannn/action-semantic-pull-request) from 6.1.0 to 6.1.1. - [Release notes](https://github.com/amannn/action-semantic-pull-request/releases) - [Changelog](https://github.com/amannn/action-semantic-pull-request/blob/main/CHANGELOG.md) - [Commits](https://github.com/amannn/action-semantic-pull-request/compare/v6.1.0...v6.1.1) --- updated-dependencies: - dependency-name: amannn/action-semantic-pull-request dependency-version: 6.1.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/pr-title.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml index ccf4faa..3f7d240 100644 --- a/.github/workflows/pr-title.yml +++ b/.github/workflows/pr-title.yml @@ -29,7 +29,7 @@ jobs: pull-requests: read steps: - name: Validate pull request title - uses: amannn/action-semantic-pull-request@v6.1.0 + uses: amannn/action-semantic-pull-request@v6.1.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: From 48d33bc23929503441cafdb82f2e347fe58b3026 Mon Sep 17 00:00:00 2001 From: Christopher Alexander Date: Mon, 4 May 2026 12:36:26 -0400 Subject: [PATCH 3/3] fix: apply inline from-keyframe fallback on entering scenes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a screen component causes CSS-in-JS (e.g. Emotion/MUI) to inject styles during the same React commit that starts a transition, the browser may resolve the `animation` property before the co-rendered `@keyframes` rule registers in the cascade. During that brief window, `animation-fill-mode: both` has no keyframe to reference, so the entering scene renders at its natural resting position instead of its off-screen start position. Fix by writing the `from` keyframe values (`transform`, `opacity`) as inline style fallbacks on the entering scene div. CSS animations sit above the inline style layer in the cascade, so the running animation always overrides them. The fallbacks are only visible during the single cascade pass before `@keyframes` registers — which is exactly when the displacement occurred. Co-authored-by: Copilot --- src/components/NavigationStackProvider.tsx | 7 ++----- src/components/NavigationStackScene.tsx | 14 +++++++++++++ src/components/NavigationStackViewport.tsx | 23 +++++++++++++++++++--- src/transitions/buildAnimationKeyframes.ts | 19 +++++++++++++++++- 4 files changed, 54 insertions(+), 9 deletions(-) diff --git a/src/components/NavigationStackProvider.tsx b/src/components/NavigationStackProvider.tsx index f127fb8..24b3d42 100644 --- a/src/components/NavigationStackProvider.tsx +++ b/src/components/NavigationStackProvider.tsx @@ -582,13 +582,10 @@ export function NavigationStackProvider( initialParams: isControlled ? undefined : props.initialParams, }), ); + const timeoutRef = useRef(null); const state = isControlled ? props.state : internalState; const stateRef = useRef(state); - const timeoutRef = useRef(null); - - useEffect(() => { - stateRef.current = state; - }, [state]); + stateRef.current = state; useEffect(() => { return () => { diff --git a/src/components/NavigationStackScene.tsx b/src/components/NavigationStackScene.tsx index 0fbbd24..032d766 100644 --- a/src/components/NavigationStackScene.tsx +++ b/src/components/NavigationStackScene.tsx @@ -33,6 +33,18 @@ export interface NavigationStackSceneProps { animationEasing?: string; /** Stagger-based delay in milliseconds (`spec.stagger × sceneIndex`). */ animationDelay?: number; + /** + * Inline `transform` fallback matching the animation's `from` keyframe. + * Applied only on entering scenes so the element starts at the correct off-screen + * position during the brief window before `@keyframes` registers in the cascade. + * The running animation always overrides this via the animation value layer. + */ + animationFromTransform?: string; + /** + * Inline `opacity` fallback matching the animation's `from` keyframe. + * Same purpose as `animationFromTransform`. + */ + animationFromOpacity?: number; /** When true, applies `overflow: hidden` to clip scene content during the transition. */ clipContent?: boolean; children?: ReactNode; @@ -69,6 +81,8 @@ export function NavigationStackScene( pointerEvents: props.isActive ? 'auto' : 'none', visibility: props.isActive || props.transitionState ? 'visible' : 'hidden', overflow: props.clipContent ? 'hidden' : undefined, + transform: props.animationFromTransform, + opacity: props.animationFromOpacity, animation: props.animationName ? `${props.animationName} ${duration}ms ${easing} ${delay}ms both` : undefined, diff --git a/src/components/NavigationStackViewport.tsx b/src/components/NavigationStackViewport.tsx index 981887d..a833cd5 100644 --- a/src/components/NavigationStackViewport.tsx +++ b/src/components/NavigationStackViewport.tsx @@ -334,6 +334,8 @@ export function NavigationStackViewport( delay: number; easing: string; clip: boolean; + fromTransform: string | undefined; + fromOpacity: number | undefined; } >(), }; @@ -348,7 +350,14 @@ export function NavigationStackViewport( const cssBlocks: string[] = []; const animations = new Map< string, - { name: string | undefined; delay: number; easing: string; clip: boolean } + { + name: string | undefined; + delay: number; + easing: string; + clip: boolean; + fromTransform: string | undefined; + fromOpacity: number | undefined; + } >(); renderableEntries.forEach((entry) => { @@ -386,6 +395,10 @@ export function NavigationStackViewport( delay: keyframeResult.delay, easing, clip: !!spec.clip, + fromTransform: + phase === 'enter' ? keyframeResult.fromTransform : undefined, + fromOpacity: + phase === 'enter' ? keyframeResult.fromOpacity : undefined, }); } else { animations.set(entry.key, { @@ -393,6 +406,8 @@ export function NavigationStackViewport( delay: 0, easing, clip: !!spec.clip, + fromTransform: undefined, + fromOpacity: undefined, }); } }); @@ -435,7 +450,7 @@ export function NavigationStackViewport( ...props.style, }} > - + {props.renderEmpty?.() ?? null} ); @@ -455,7 +470,7 @@ export function NavigationStackViewport( ...props.style, }} > - {dynamicCss ? : null} + {renderableEntries.map((entry) => { const route = routes[entry.routeName]; @@ -509,6 +524,8 @@ export function NavigationStackViewport( animationName={anim?.name} animationEasing={anim?.easing} animationDelay={anim?.delay} + animationFromTransform={anim?.fromTransform} + animationFromOpacity={anim?.fromOpacity} clipContent={anim?.clip} > {isActive || state.isTransitioning ? ( diff --git a/src/transitions/buildAnimationKeyframes.ts b/src/transitions/buildAnimationKeyframes.ts index 7e3e7c6..79339fc 100644 --- a/src/transitions/buildAnimationKeyframes.ts +++ b/src/transitions/buildAnimationKeyframes.ts @@ -13,6 +13,17 @@ export interface AnimationKeyframeResult { css: string; /** Animation delay in milliseconds (derived from spec.stagger × sceneIndex). */ delay: number; + /** + * CSS `transform` value of the animation's `from` keyframe (e.g. `"translateX(100%)"`). + * Apply as an inline style fallback on the entering scene so it starts at the correct + * off-screen position during the brief window before @keyframes registers in the cascade. + */ + fromTransform: string | undefined; + /** + * CSS `opacity` value of the animation's `from` keyframe. + * Apply as an inline style fallback on the entering scene for the same reason. + */ + fromOpacity: number | undefined; } function negateValue(value: number | string): number | string { @@ -272,5 +283,11 @@ export function buildAnimationKeyframes( `}`, ].join('\n'); - return { name, css, delay }; + return { + name, + css, + delay, + fromTransform: hasTranslate || hasScale ? fromTransform : undefined, + fromOpacity: hasOpacity ? opacityFrom : undefined, + }; }