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.
40 lines
1.6 KiB
TypeScript
40 lines
1.6 KiB
TypeScript
"use client";
|
|
|
|
import { motion } from "framer-motion";
|
|
import { useAppReducedMotion as useReducedMotion } from "@/lib/motion-pref";
|
|
import { useEffect, useState } from "react";
|
|
import { createPortal } from "react-dom";
|
|
|
|
/**
|
|
* The wizard's Back / Next bar.
|
|
*
|
|
* Portalled to <body> on purpose: the page content animates its entrance with
|
|
* a transform, and a transform on an ancestor turns it into the containing
|
|
* block for `position: fixed` — the bar would briefly render inside the
|
|
* content and then jump to the viewport bottom. Rendering it outside every
|
|
* animated wrapper (and only fading it in, never transforming it) keeps it
|
|
* pinned to the bottom edge from the first frame.
|
|
*
|
|
* Flat, full-width and above the safe area. The wizard pages hide the tab
|
|
* bar, so this is the only bottom chrome while the flow is open.
|
|
*/
|
|
export function WizardActionBar({ children }: { children: React.ReactNode }) {
|
|
const [mounted, setMounted] = useState(false);
|
|
const reduceMotion = useReducedMotion();
|
|
|
|
useEffect(() => setMounted(true), []);
|
|
if (!mounted) return null;
|
|
|
|
return createPortal(
|
|
<motion.div
|
|
initial={reduceMotion ? false : { opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
transition={{ duration: 0.2, ease: "easeOut" }}
|
|
className="fixed inset-x-0 bottom-0 z-40 border-t border-[var(--hairline)] bg-[color-mix(in_oklab,var(--page)_92%,transparent)] px-4 pb-safe pt-2 backdrop-blur-lg sm:hidden"
|
|
data-wizard-bar
|
|
>
|
|
<div className="mx-auto flex w-full max-w-3xl items-center gap-2">{children}</div>
|
|
</motion.div>,
|
|
document.body,
|
|
);
|
|
}
|