freezmo/lib/motion-pref.ts
Martin Dvorak 7da308fa1e Aero button glass + homework report control; reduced-motion sheet glitch
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.
2026-10-14 09:19:54 +00:00

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;
}