Buttons: the recipe's thin veils let btn-secondary/btn-ghost/glass-pill blend into the grass and sky - they now carry a stronger pale-glass veil (the Ask-pill look) over the blurred scene, ghosts plus a rim and full-contrast text. Homework make/check results gained the same red "!" report control as the feeds and the guide (a synthetic short-answer activity maps into the report flow; pinned in phase3). Reduced motion: the manual toggle only fed framer's MotionConfig, which strips transform animations - while components branched on the OS-only useReducedMotion - so translucent sheets still spring-faded their opacity in over the page (the "ugly visual glitch with the settings panel"). All 16 components now read useAppReducedMotion() (OS or toggle), and the settings sheet/scrim and the library's phone sheet go instant. Verified frame by frame: opacity 1.00 from the first frame. Contrast 0 findings, axe clean; mobile 126/126, chat 102/102, providers 20/20, wizard 95/95, phase3 31/31, bubbles 34/34, i18n 13/13, native 33/33. Version 0.5.4, SW -> freezmo-v76.
47 lines
1.6 KiB
TypeScript
47 lines
1.6 KiB
TypeScript
"use client";
|
|
|
|
import { motion } from "framer-motion";
|
|
import { useAppReducedMotion as useReducedMotion } from "@/lib/motion-pref";
|
|
import { usePathname } from "next/navigation";
|
|
|
|
import { screenVariants, tabVariants } from "@/lib/motion";
|
|
|
|
/**
|
|
* Direction-aware screen entrance for the page content column.
|
|
*
|
|
* Deeper paths (library → deck, library → create/homework) slide in from the
|
|
* right; going back slides from the left; same-depth switches crossfade.
|
|
* The wrapper only ever wraps the scrolling content column — fixed chrome
|
|
* (tab bar, sheets) stays outside it, so a transform can never capture it.
|
|
*
|
|
* Note: the App Router unmounts the outgoing page immediately, so this is an
|
|
* entrance animation (exits animate for in-page elements via AnimatePresence,
|
|
* e.g. list items).
|
|
*/
|
|
|
|
/** Module-level so the direction survives the per-route remount. */
|
|
let lastPath: string | null = null;
|
|
|
|
function depth(path: string): number {
|
|
if (path === "/") return 0;
|
|
return path.split("/").filter(Boolean).length;
|
|
}
|
|
|
|
export function RouteTransition({ children }: { children: React.ReactNode }) {
|
|
const pathname = usePathname();
|
|
const reduceMotion = useReducedMotion();
|
|
|
|
const previous = lastPath;
|
|
lastPath = pathname;
|
|
|
|
const isTabSwitch = previous !== null && depth(previous) === depth(pathname) && previous !== pathname;
|
|
const direction: 1 | -1 =
|
|
previous === null || depth(pathname) >= depth(previous) ? 1 : -1;
|
|
const variants = isTabSwitch ? tabVariants(!!reduceMotion) : screenVariants(direction, !!reduceMotion);
|
|
|
|
return (
|
|
<motion.div initial="initial" animate="animate" variants={variants}>
|
|
{children}
|
|
</motion.div>
|
|
);
|
|
}
|