From 2b283a0166f77cc65306fafef89b87007c64e1ad Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Fri, 27 Mar 2026 22:06:17 +0000 Subject: [PATCH] [#308] Add page-flip animation to Reading Mode Track navigation direction (left/right) on Prev/Next/TOC navigation. Apply CSS-only slide animation (translateX + opacity) on the content area. Works for button clicks, keyboard arrows, and TOC jumps. 250ms ease-out duration keeps it smooth on mobile. Content wrapper uses overflow-x-hidden to prevent horizontal scroll during animation. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/globals.css | 16 +++++++++++++++ src/components/ReadingMode.tsx | 37 ++++++++++++++++++++-------------- 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index d9077b8d..53adebfc 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -210,6 +210,22 @@ code, pre, .font-mono { } } +/* Reading Mode page-flip transitions */ +@keyframes flip-in-left { + from { opacity: 0; transform: translateX(40px); } + to { opacity: 1; transform: translateX(0); } +} +@keyframes flip-in-right { + from { opacity: 0; transform: translateX(-40px); } + to { opacity: 1; transform: translateX(0); } +} +.page-flip-left { + animation: flip-in-left 0.25s ease-out; +} +.page-flip-right { + animation: flip-in-right 0.25s ease-out; +} + /* Custom select dropdown styling */ select { appearance: none; diff --git a/src/components/ReadingMode.tsx b/src/components/ReadingMode.tsx index 199e84e2..67e90a5c 100644 --- a/src/components/ReadingMode.tsx +++ b/src/components/ReadingMode.tsx @@ -27,6 +27,7 @@ export function ReadingMode({ }: ReadingModeProps) { const [currentIdx, setCurrentIdx] = useState(initialChapterIndex); const [showToc, setShowToc] = useState(false); + const [flipDir, setFlipDir] = useState<"left" | "right" | null>(null); const contentRef = useRef(null); const { isMiniApp } = usePlatformDetection(); @@ -38,25 +39,29 @@ export function ReadingMode({ contentRef.current?.scrollTo(0, 0); }, []); - const goPrev = useCallback(() => { - if (hasPrev) { - setCurrentIdx((i) => i - 1); + const navigate = useCallback((idx: number, dir: "left" | "right" | null) => { + setFlipDir(dir); + // Brief delay to trigger the CSS animation before content swap + requestAnimationFrame(() => { + setCurrentIdx(idx); scrollToTop(); - } - }, [hasPrev, scrollToTop]); + // Clear the animation class after transition completes + setTimeout(() => setFlipDir(null), 250); + }); + }, [scrollToTop]); + + const goPrev = useCallback(() => { + if (hasPrev) navigate(currentIdx - 1, "right"); + }, [hasPrev, currentIdx, navigate]); const goNext = useCallback(() => { - if (hasNext) { - setCurrentIdx((i) => i + 1); - scrollToTop(); - } - }, [hasNext, scrollToTop]); + if (hasNext) navigate(currentIdx + 1, "left"); + }, [hasNext, currentIdx, navigate]); const goToChapter = useCallback((idx: number) => { - setCurrentIdx(idx); setShowToc(false); - scrollToTop(); - }, [scrollToTop]); + navigate(idx, idx > currentIdx ? "left" : "right"); + }, [currentIdx, navigate]); // Esc to close, arrow keys for navigation useEffect(() => { @@ -105,8 +110,10 @@ export function ReadingMode({ {/* Content area */} -
-
+
+
{chapter?.content ? ( ) : (