From e555209516534890e8b1cea402fb19f28e524c7b Mon Sep 17 00:00:00 2001 From: Stringadmin Date: Tue, 2 Jun 2026 22:19:14 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20page=20transition=20UI=20jitter=20?= =?UTF-8?q?=E2=80=94=20remove=20enter=20phase=20to=20prevent=20double=20an?= =?UTF-8?q?imation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The three-phase exit→enter→idle flow caused a visible "double refresh" jitter. During the enter phase (220ms), the wrapper animated from opacity:0 while cancelling child .page-motion with animation:none !important. When phase switched to idle, the !important rule was removed and child .page-motion re-triggered, creating a second entrance animation — the jitter. Fix: remove the enter phase entirely. After exit animation (180ms), phase goes directly to idle. The child page's own .page-motion class handles entrance naturally via React's fresh DOM mount. No wrapper animation on enter, no double-animation conflict. Co-Authored-By: Claude Opus 4.7 --- src/components/PageTransition.tsx | 30 +++-------------------- src/styles/components/page-transition.css | 20 --------------- 2 files changed, 3 insertions(+), 47 deletions(-) diff --git a/src/components/PageTransition.tsx b/src/components/PageTransition.tsx index 0e429c8..7a9a85b 100644 --- a/src/components/PageTransition.tsx +++ b/src/components/PageTransition.tsx @@ -6,7 +6,6 @@ interface PageTransitionProps { } const EXIT_DURATION_MS = 180; -const ENTER_DURATION_MS = 220; const NAV_ORDER: string[] = [ "home", @@ -40,7 +39,7 @@ function getNavIndex(key: string): number { export default function PageTransition({ viewKey, children }: PageTransitionProps) { const [displayedChildren, setDisplayedChildren] = useState(children); - const [phase, setPhase] = useState<"idle" | "exit" | "enter">("idle"); + const [phase, setPhase] = useState<"idle" | "exit">("idle"); const [exitDirection, setExitDirection] = useState<"forward" | "backward" | "neutral">("neutral"); const prevKeyRef = useRef(viewKey); const timerRef = useRef>(); @@ -73,38 +72,15 @@ export default function PageTransition({ viewKey, children }: PageTransitionProp setPhase("exit"); timerRef.current = setTimeout(() => { setDisplayedChildren(children); - setPhase("enter"); + setPhase("idle"); }, EXIT_DURATION_MS); return () => clearTimeout(timerRef.current); }, [viewKey, children]); - // After enter animation completes, go back to idle - useEffect(() => { - if (phase !== "enter") return; - const timer = setTimeout(() => setPhase("idle"), ENTER_DURATION_MS); - return () => clearTimeout(timer); - }, [phase]); - const dirClass = exitDirection === "forward" ? " is-forward" : exitDirection === "backward" ? " is-backward" : ""; - if (phase === "exit") { - return ( -
- {displayedChildren} -
- ); - } - - if (phase === "enter") { - return ( -
- {displayedChildren} -
- ); - } - return ( -
+
{displayedChildren}
); diff --git a/src/styles/components/page-transition.css b/src/styles/components/page-transition.css index a722b12..733af99 100644 --- a/src/styles/components/page-transition.css +++ b/src/styles/components/page-transition.css @@ -53,24 +53,4 @@ opacity: 0; transform: translateX(16px); } -} - -/* Enter: explicit wrapper entrance animation overrides child page-motion */ -.page-motion--enter { - animation: page-enter-fade 220ms ease both; -} - -.page-motion--enter .page-motion { - animation: none !important; -} - -@keyframes page-enter-fade { - from { - opacity: 0; - transform: translateY(6px); - } - to { - opacity: 1; - transform: translateY(0); - } } \ No newline at end of file