diff --git a/src/components/animations/PageTransition.css b/src/components/animations/PageTransition.css index afc073b..37b1661 100644 --- a/src/components/animations/PageTransition.css +++ b/src/components/animations/PageTransition.css @@ -1,58 +1,93 @@ -/* Page Transition Animations */ +/* Page Wrapper */ .page-wrapper { position: relative; width: 100%; min-height: 100vh; } -/* Enter animations */ -.page-enter { - opacity: 0; - transform: translateX(20px); +/* View Transitions API - Native browser page transitions */ +/* These run automatically when the browser supports view transitions */ + +/* Forward navigation (going deeper) */ +::view-transition-old(root):only-child { + animation: slide-out-left 250ms cubic-bezier(0.4, 0, 0.2, 1); } -.page-enter-active { - opacity: 1; - transform: translateX(0); - transition: opacity 300ms ease-out, transform 300ms ease-out; +::view-transition-new(root):only-child { + animation: slide-in-right 250ms cubic-bezier(0.4, 0, 0.2, 1); } -/* Exit animations */ -.page-exit { - opacity: 1; - transform: translateX(0); +/* Back navigation (going up) */ +html[data-transition='back']::view-transition-old(root):only-child { + animation: slide-out-right 250ms cubic-bezier(0.4, 0, 0.2, 1); } -.page-exit-active { - opacity: 0; - transform: translateX(-20px); - transition: opacity 200ms ease-in, transform 200ms ease-in; +html[data-transition='back']::view-transition-new(root):only-child { + animation: slide-in-left 250ms cubic-bezier(0.4, 0, 0.2, 1); } -/* Specific animation for back navigation */ -.page-back-enter { - opacity: 0; - transform: translateX(-20px); +/* Fade for auth pages */ +html[data-transition='fade']::view-transition-old(root):only-child, +html[data-transition='fade']::view-transition-new(root):only-child { + animation: fade 200ms ease-in-out; } -.page-back-enter-active { - opacity: 1; - transform: translateX(0); - transition: opacity 300ms ease-out, transform 300ms ease-out; +/* Animation keyframes */ +@keyframes slide-out-left { + from { + transform: translateX(0); + opacity: 1; + } + to { + transform: translateX(-30%); + opacity: 0; + } } -.page-back-exit { - opacity: 1; - transform: translateX(0); +@keyframes slide-in-right { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } } -.page-back-exit-active { - opacity: 0; - transform: translateX(20px); - transition: opacity 200ms ease-in, transform 200ms ease-in; +@keyframes slide-out-right { + from { + transform: translateX(0); + opacity: 1; + } + to { + transform: translateX(100%); + opacity: 0; + } } -/* Modal/overlay animations */ +@keyframes slide-in-left { + from { + transform: translateX(-30%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +@keyframes fade { + 0%, + 100% { + opacity: 0; + } + 50% { + opacity: 1; + } +} + +/* Modal/overlay animations (still useful for non-route transitions) */ .modal-enter { opacity: 0; transform: scale(0.95) translateY(10px); @@ -61,7 +96,9 @@ .modal-enter-active { opacity: 1; transform: scale(1) translateY(0); - transition: opacity 250ms ease-out, transform 250ms ease-out; + transition: + opacity 250ms ease-out, + transform 250ms ease-out; } .modal-exit { @@ -72,26 +109,9 @@ .modal-exit-active { opacity: 0; transform: scale(0.95) translateY(10px); - transition: opacity 200ms ease-in, transform 200ms ease-in; -} - -/* Fade animation for simple transitions */ -.fade-enter { - opacity: 0; -} - -.fade-enter-active { - opacity: 1; - transition: opacity 250ms ease-out; -} - -.fade-exit { - opacity: 1; -} - -.fade-exit-active { - opacity: 0; - transition: opacity 200ms ease-in; + transition: + opacity 200ms ease-in, + transform 200ms ease-in; } /* Slide up animation for bottom sheets/panels */ @@ -103,7 +123,9 @@ .slide-up-enter-active { opacity: 1; transform: translateY(0); - transition: opacity 300ms ease-out, transform 300ms ease-out; + transition: + opacity 300ms ease-out, + transform 300ms ease-out; } .slide-up-exit { @@ -114,7 +136,9 @@ .slide-up-exit-active { opacity: 0; transform: translateY(100%); - transition: opacity 250ms ease-in, transform 250ms ease-in; + transition: + opacity 250ms ease-in, + transform 250ms ease-in; } /* Loading spinner animation */ @@ -171,33 +195,49 @@ .stagger-enter-active { opacity: 1; transform: translateY(0); - transition: opacity 300ms ease-out, transform 300ms ease-out; + transition: + opacity 300ms ease-out, + transform 300ms ease-out; } /* Add animation delays for staggered effects */ -.stagger-enter-active:nth-child(1) { transition-delay: 0ms; } -.stagger-enter-active:nth-child(2) { transition-delay: 50ms; } -.stagger-enter-active:nth-child(3) { transition-delay: 100ms; } -.stagger-enter-active:nth-child(4) { transition-delay: 150ms; } -.stagger-enter-active:nth-child(5) { transition-delay: 200ms; } -.stagger-enter-active:nth-child(6) { transition-delay: 250ms; } -.stagger-enter-active:nth-child(7) { transition-delay: 300ms; } -.stagger-enter-active:nth-child(8) { transition-delay: 350ms; } +.stagger-enter-active:nth-child(1) { + transition-delay: 0ms; +} +.stagger-enter-active:nth-child(2) { + transition-delay: 50ms; +} +.stagger-enter-active:nth-child(3) { + transition-delay: 100ms; +} +.stagger-enter-active:nth-child(4) { + transition-delay: 150ms; +} +.stagger-enter-active:nth-child(5) { + transition-delay: 200ms; +} +.stagger-enter-active:nth-child(6) { + transition-delay: 250ms; +} +.stagger-enter-active:nth-child(7) { + transition-delay: 300ms; +} +.stagger-enter-active:nth-child(8) { + transition-delay: 350ms; +} /* Reduced motion for accessibility */ @media (prefers-reduced-motion: reduce) { - .page-enter, - .page-enter-active, - .page-exit, - .page-exit-active, + /* Disable View Transitions for users who prefer reduced motion */ + ::view-transition-old(root), + ::view-transition-new(root) { + animation: none !important; + } + .modal-enter, .modal-enter-active, .modal-exit, .modal-exit-active, - .fade-enter, - .fade-enter-active, - .fade-exit, - .fade-exit-active, .interactive-element, .stagger-enter, .stagger-enter-active { diff --git a/src/components/animations/PageTransition.jsx b/src/components/animations/PageTransition.jsx index 7471b65..e46b500 100644 --- a/src/components/animations/PageTransition.jsx +++ b/src/components/animations/PageTransition.jsx @@ -1,6 +1,6 @@ -import { useEffect, useRef } from 'react' +import { useEffect, useLayoutEffect, useRef, useState } from 'react' +import { flushSync } from 'react-dom' import { useLocation } from 'react-router-dom' -import { CSSTransition, TransitionGroup } from 'react-transition-group' import './PageTransition.css' // Route hierarchy for determining navigation direction @@ -14,9 +14,11 @@ const routeHierarchy = { '/activities': 1, '/points': 1, '/labels': 1, + '/projects': 1, '/login': 0, '/signup': 1, '/landing': 0, + '/archived': 1, } const getRouteLevel = pathname => { @@ -32,16 +34,23 @@ const getRouteLevel = pathname => { if (pathname.includes('/chores/') && pathname.includes('/history')) { return 3 } + if (pathname.includes('/chores/') && pathname.includes('/timer')) { + return 3 + } if ( pathname.includes('/chores/') && !pathname.includes('/edit') && - !pathname.includes('/history') + !pathname.includes('/history') && + !pathname.includes('/timer') ) { return 2 } if (pathname.includes('/things/')) { return 2 } + if (pathname.includes('/settings/')) { + return 2 + } // Default level return 1 @@ -49,55 +58,72 @@ const getRouteLevel = pathname => { const PageTransition = ({ children }) => { const location = useLocation() - const prevLocation = useRef(location) - const isNavigatingBack = useRef(false) - const nodeRef = useRef(null) + const prevLevel = useRef(0) + const [displayLocation, setDisplayLocation] = useState(location) + const isFirstRender = useRef(true) - useEffect(() => { - const currentLevel = getRouteLevel(location.pathname) - const previousLevel = getRouteLevel(prevLocation.current.pathname) - - // Determine if we're navigating back (to a higher level in hierarchy) - // isNavigatingBack.current = currentLevel < previousLevel - window.scrollTo({ top: 0, left: 0, behavior: 'instant' }) - - prevLocation.current = location - }, [location]) - - const getTransitionClasses = () => { - if ( - location.pathname.includes('/login') || - location.pathname.includes('/signup') || - location.pathname.includes('/landing') - ) { - return 'fade' // Use fade for auth pages + useLayoutEffect(() => { + // Skip transition on first render + if (isFirstRender.current) { + isFirstRender.current = false + setDisplayLocation(location) + prevLevel.current = getRouteLevel(location.pathname) + return } - return isNavigatingBack.current ? 'page-back' : 'page' - } + // Don't transition if location hasn't actually changed + if (location.pathname === displayLocation.pathname) { + return + } + + const currentLevel = getRouteLevel(location.pathname) + const previousLevel = prevLevel.current + + // Determine navigation direction + const isBack = currentLevel < previousLevel + const isFade = + location.pathname.includes('/login') || + location.pathname.includes('/signup') || + location.pathname.includes('/landing') || + location.pathname.includes('/auth/') + + // Apply transition type as data attribute for CSS + document.documentElement.dataset.transition = isFade + ? 'fade' + : isBack + ? 'back' + : 'forward' + + // Trigger View Transition if supported + if (document.startViewTransition) { + document.startViewTransition(() => { + // flushSync forces React to update the DOM synchronously + // This ensures the View Transition captures the actual DOM change + flushSync(() => { + setDisplayLocation(location) + }) + // Scroll to top after DOM update + window.scrollTo({ top: 0, left: 0, behavior: 'instant' }) + }) + } else { + // Fallback for browsers without View Transitions API + setDisplayLocation(location) + window.scrollTo({ top: 0, left: 0, behavior: 'instant' }) + } + + // Update refs + prevLevel.current = currentLevel + }, [location, displayLocation.pathname]) return ( - - -
- {children} -
-
-
+
+ {displayLocation.pathname === location.pathname ? children : null} +
) }