diff --git a/src/features/auth/components/FloatingShape.tsx b/src/features/auth/components/FloatingShape.tsx new file mode 100644 index 0000000..5682dd6 --- /dev/null +++ b/src/features/auth/components/FloatingShape.tsx @@ -0,0 +1,46 @@ +import { motion } from 'framer-motion' + +/** + * FloatingShape — the shared decorative background orb for every auth shell + * (dashboard login + register + MFA + the standalone auth pages). + * + * ONE-SHOT fade/scale-in only (no `repeat: Infinity`) with a soft radial fill + * instead of an animated `backdrop-filter: blur`. The previous orb ran an + * INFINITE framer-motion loop AND re-blurred its backdrop every frame, which — + * layered over the (now removed) animated shell gradient — was a continuous + * main-thread compositing cost (worst with hardware-acceleration off) that + * contributed to the auth-page jank. After the entrance transition this orb is + * fully static, so it adds zero per-frame work while staying as brand + * decoration. See loginBackground.ts for the matching static-gradient fix. + * + * Callers gate the orbs behind `!usePrefersReducedMotion()` so users who ask + * for reduced motion get no decorative chrome at all. + */ +const FloatingShape = ({ delay, size, left, top }: { + delay: number + size: number + left: string + top: string +}) => ( + +) + +export default FloatingShape diff --git a/src/features/auth/components/LoginPage.tsx b/src/features/auth/components/LoginPage.tsx index 7cba457..9562d43 100644 --- a/src/features/auth/components/LoginPage.tsx +++ b/src/features/auth/components/LoginPage.tsx @@ -55,6 +55,7 @@ import { hasPasswordLayer1, selectPuzzleConfig, type LoginConfig } from '@domain import StepProgress from '../../../verify-app/StepProgress' import { usePrefersReducedMotion } from '@hooks/usePrefersReducedMotion' import { loginShellBackgroundSx, glassCardBackdropFilter } from './loginBackground' +import FloatingShape from './FloatingShape' /** * Login form validation schema @@ -108,40 +109,6 @@ const logoVariants: Variants = { }, } -// Decorative background orb. ONE-SHOT fade/scale-in only (no `repeat: Infinity`) -// and a soft radial fill instead of an animated `backdrop-filter: blur` — the -// continuous orb animation + per-frame backdrop re-blur was a main-thread compositing -// cost (worst with hardware-acceleration off) that contributed to the login jank. -// After the entrance it is fully static, so it adds zero per-frame work — the orbs -// stay as brand decoration without the cost. See loginBackground.ts for the matching -// static-gradient fix. -const FloatingShape = ({ delay, size, left, top }: { - delay: number - size: number - left: string - top: string -}) => ( - -) - /** * Login Page Component * Beautiful animated login with glassmorphism design diff --git a/src/features/auth/components/RegisterPage.tsx b/src/features/auth/components/RegisterPage.tsx index c5a8b44..9d7d9c1 100644 --- a/src/features/auth/components/RegisterPage.tsx +++ b/src/features/auth/components/RegisterPage.tsx @@ -39,6 +39,9 @@ import { getService } from '@core/di/container' import { TYPES } from '@core/di/types' import type { IHttpClient } from '@domain/interfaces/IHttpClient' import { isLikelyValidEmail } from '@domain/validators/emailValidator' +import { usePrefersReducedMotion } from '@hooks/usePrefersReducedMotion' +import { loginShellBackgroundSx } from './loginBackground' +import FloatingShape from './FloatingShape' /** * Register form validation schema @@ -117,39 +120,6 @@ const logoVariants: Variants = { }, } -// Floating shapes for background -const FloatingShape = ({ delay, size, left, top }: { - delay: number - size: number - left: string - top: string -}) => ( - -) - /** * Register Page Component * Beautiful animated registration with glassmorphism design @@ -157,6 +127,7 @@ const FloatingShape = ({ delay, size, left, top }: { export default function RegisterPage() { const { t } = useTranslation() const navigate = useNavigate() + const prefersReducedMotion = usePrefersReducedMotion() const [showPassword, setShowPassword] = useState(false) const [showConfirmPassword, setShowConfirmPassword] = useState(false) const [loading, setLoading] = useState(false) @@ -226,23 +197,20 @@ export default function RegisterPage() { justifyContent: 'center', position: 'relative', overflow: 'hidden', - background: 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f64f59 100%)', - backgroundSize: '400% 400%', - animation: 'gradientShift 15s ease infinite', - '@keyframes gradientShift': { - '0%': { backgroundPosition: '0% 50%' }, - '50%': { backgroundPosition: '100% 50%' }, - '100%': { backgroundPosition: '0% 50%' }, - }, py: 4, + ...loginShellBackgroundSx(), }} > - {/* Animated background shapes */} - - - - - + {/* Decorative background shapes — skipped for reduced-motion users. */} + {!prefersReducedMotion && ( + <> + + + + + + + )} ( - -) - export default function AcceptInvitePage() { const navigate = useNavigate() const { t } = useTranslation() + const prefersReducedMotion = usePrefersReducedMotion() const [searchParams] = useSearchParams() const token = searchParams.get('token') || '' const [loading, setLoading] = useState(false) @@ -203,21 +184,19 @@ export default function AcceptInvitePage() { justifyContent: 'center', position: 'relative', overflow: 'hidden', - background: 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f64f59 100%)', - backgroundSize: '400% 400%', - animation: 'gradientShift 15s ease infinite', - '@keyframes gradientShift': { - '0%': { backgroundPosition: '0% 50%' }, - '50%': { backgroundPosition: '100% 50%' }, - '100%': { backgroundPosition: '0% 50%' }, - }, + ...loginShellBackgroundSx(), }} > - - - - - + {/* Decorative background shapes — skipped for reduced-motion users. */} + {!prefersReducedMotion && ( + <> + + + + + + + )} ( - -) - export default function ForgotPasswordPage() { const navigate = useNavigate() const { t } = useTranslation() + const prefersReducedMotion = usePrefersReducedMotion() const [loading, setLoading] = useState(false) const [error, setError] = useState(null) @@ -148,21 +120,19 @@ export default function ForgotPasswordPage() { justifyContent: 'center', position: 'relative', overflow: 'hidden', - background: 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f64f59 100%)', - backgroundSize: '400% 400%', - animation: 'gradientShift 15s ease infinite', - '@keyframes gradientShift': { - '0%': { backgroundPosition: '0% 50%' }, - '50%': { backgroundPosition: '100% 50%' }, - '100%': { backgroundPosition: '0% 50%' }, - }, + ...loginShellBackgroundSx(), }} > - - - - - + {/* Decorative background shapes — skipped for reduced-motion users. */} + {!prefersReducedMotion && ( + <> + + + + + + + )} ( - -) - export default function OnboardingPage() { const navigate = useNavigate() const { t } = useTranslation() + const prefersReducedMotion = usePrefersReducedMotion() const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const [submittedEmail, setSubmittedEmail] = useState(null) @@ -201,21 +182,19 @@ export default function OnboardingPage() { position: 'relative', overflow: 'hidden', py: 4, - background: 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f64f59 100%)', - backgroundSize: '400% 400%', - animation: 'gradientShift 15s ease infinite', - '@keyframes gradientShift': { - '0%': { backgroundPosition: '0% 50%' }, - '50%': { backgroundPosition: '100% 50%' }, - '100%': { backgroundPosition: '0% 50%' }, - }, + ...loginShellBackgroundSx(), }} > - - - - - + {/* Decorative background shapes — skipped for reduced-motion users. */} + {!prefersReducedMotion && ( + <> + + + + + + + )} ( - -) - export default function ResetPasswordPage() { const navigate = useNavigate() const { t } = useTranslation() + const prefersReducedMotion = usePrefersReducedMotion() const [searchParams] = useSearchParams() const email = searchParams.get('email') || '' const [loading, setLoading] = useState(false) @@ -210,21 +182,19 @@ export default function ResetPasswordPage() { justifyContent: 'center', position: 'relative', overflow: 'hidden', - background: 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f64f59 100%)', - backgroundSize: '400% 400%', - animation: 'gradientShift 15s ease infinite', - '@keyframes gradientShift': { - '0%': { backgroundPosition: '0% 50%' }, - '50%': { backgroundPosition: '100% 50%' }, - '100%': { backgroundPosition: '0% 50%' }, - }, + ...loginShellBackgroundSx(), }} > - - - - - + {/* Decorative background shapes — skipped for reduced-motion users. */} + {!prefersReducedMotion && ( + <> + + + + + + + )} ( - -) - type VerifyState = 'verifying' | 'success' | 'error' | 'noToken' export default function VerifyEmailPage() { const navigate = useNavigate() const { t } = useTranslation() + const prefersReducedMotion = usePrefersReducedMotion() const [searchParams] = useSearchParams() const token = searchParams.get('token') || '' const [state, setState] = useState(token ? 'verifying' : 'noToken') @@ -138,20 +119,18 @@ export default function VerifyEmailPage() { justifyContent: 'center', position: 'relative', overflow: 'hidden', - background: 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f64f59 100%)', - backgroundSize: '400% 400%', - animation: 'gradientShift 15s ease infinite', - '@keyframes gradientShift': { - '0%': { backgroundPosition: '0% 50%' }, - '50%': { backgroundPosition: '100% 50%' }, - '100%': { backgroundPosition: '0% 50%' }, - }, + ...loginShellBackgroundSx(), }} > - - - - + {/* Decorative background shapes — skipped for reduced-motion users. */} + {!prefersReducedMotion && ( + <> + + + + + + )}