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.
64 lines
1.7 KiB
TypeScript
64 lines
1.7 KiB
TypeScript
"use client";
|
|
|
|
import { useSyncExternalStore } from "react";
|
|
import { useReducedMotion } from "framer-motion";
|
|
|
|
/**
|
|
* The Settings "reduce motion" toggle (default off). It mirrors the OS
|
|
* preference: the scene stops moving, CSS transitions collapse, and
|
|
* framer-motion runs in its calm mode via MotionPreference. One shared store,
|
|
* so the toggle and every consumer update together.
|
|
*/
|
|
|
|
const KEY = "freezmo.reduce-motion.v1";
|
|
|
|
let cached: boolean | null = null;
|
|
const listeners = new Set<() => void>();
|
|
|
|
function read(): boolean {
|
|
try {
|
|
return window.localStorage.getItem(KEY) === "1";
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function snapshot(): boolean {
|
|
cached ??= read();
|
|
return cached;
|
|
}
|
|
|
|
export function applyReducedMotion(next: boolean) {
|
|
cached = next;
|
|
document.documentElement.classList.toggle("reduce-motion", next);
|
|
try {
|
|
window.localStorage.setItem(KEY, next ? "1" : "0");
|
|
} catch {
|
|
/* private mode */
|
|
}
|
|
listeners.forEach((listener) => listener());
|
|
}
|
|
|
|
export function useReducedMotionPref() {
|
|
const reduced = useSyncExternalStore(
|
|
(listener) => {
|
|
listeners.add(listener);
|
|
return () => listeners.delete(listener);
|
|
},
|
|
snapshot,
|
|
() => false,
|
|
);
|
|
return { reduced, setReduced: applyReducedMotion };
|
|
}
|
|
|
|
/**
|
|
* The OS preference *or* the in-app toggle. MotionConfig only strips transform
|
|
* animations, so components that branch on `useReducedMotion` (sheets fading
|
|
* from transparent, etc.) must read this one — otherwise a manual "reduce
|
|
* motion" still faded a translucent sheet in, ghosting the page through it.
|
|
*/
|
|
export function useAppReducedMotion(): boolean {
|
|
const system = useReducedMotion();
|
|
const { reduced } = useReducedMotionPref();
|
|
return Boolean(system) || reduced;
|
|
}
|