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 (
-