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.
This commit is contained in:
Martin Dvořák 2026-10-14 09:19:54 +00:00
commit 7da308fa1e
26 changed files with 155 additions and 22 deletions

View file

@ -1,6 +1,7 @@
"use client";
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
import { AnimatePresence, motion } from "framer-motion";
import { useAppReducedMotion as useReducedMotion } from "@/lib/motion-pref";
import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react";
import { useTheme } from "@/lib/theme";

View file

@ -1,6 +1,7 @@
"use client";
import { AnimatePresence, motion, useDragControls, useReducedMotion } from "framer-motion";
import { AnimatePresence, motion, useDragControls } from "framer-motion";
import { useAppReducedMotion as useReducedMotion } from "@/lib/motion-pref";
import { useCallback, useEffect, useState } from "react";
import {

View file

@ -5,6 +5,7 @@ import Link from "next/link";
import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
import { DeckCard } from "@/components/DeckCard";
import { useAppReducedMotion as useReducedMotion } from "@/lib/motion-pref";
import { FolderBar } from "@/components/FolderBar";
import { HomeworkCard } from "@/components/HomeworkCard";
import { ChatIcon, EllipsisIcon, FolderIcon, UploadIcon } from "@/components/icons";
@ -32,6 +33,7 @@ import { cn } from "@/lib/utils";
* so the two never mix in a single list.
*/
export function LibraryScreen({ initialTab = "decks" }: { initialTab?: "decks" | "homework" }) {
const reduceMotion = useReducedMotion();
const tab = initialTab;
const decks = useSyncExternalStore(deckStore.subscribe, deckStore.getSnapshot, deckStore.getServerSnapshot);
const homeworkRecords = useSyncExternalStore(
@ -576,10 +578,10 @@ export function LibraryScreen({ initialTab = "decks" }: { initialTab?: "decks" |
role="dialog"
aria-modal="true"
aria-label={t("library.moreTitle")}
initial={{ y: 300 }}
initial={reduceMotion ? false : { y: 300 }}
animate={{ y: 0 }}
exit={{ y: 300 }}
transition={springSheet}
transition={reduceMotion ? { duration: 0 } : springSheet}
className="glass-panel glass-sheet relative z-10 rounded-t-3xl px-4 pb-safe pt-3"
>
<div className="sheet-grabber" aria-hidden />

View file

@ -1,7 +1,8 @@
"use client";
import { useEffect, useState } from "react";
import { motion, useReducedMotion } from "framer-motion";
import { motion } from "framer-motion";
import { useAppReducedMotion as useReducedMotion } from "@/lib/motion-pref";
import { Button, ProgressBar } from "@/components/ui";
import { useI18n } from "@/lib/i18n";

View file

@ -1,6 +1,7 @@
"use client";
import { motion, useReducedMotion } from "framer-motion";
import { motion } from "framer-motion";
import { useAppReducedMotion as useReducedMotion } from "@/lib/motion-pref";
import { usePathname } from "next/navigation";
import { screenVariants, tabVariants } from "@/lib/motion";

View file

@ -1,6 +1,7 @@
"use client";
import { motion, useReducedMotion } from "framer-motion";
import { motion } from "framer-motion";
import { useAppReducedMotion as useReducedMotion } from "@/lib/motion-pref";
import { springSnappy } from "@/lib/motion";
import { cn } from "@/lib/utils";

View file

@ -1,7 +1,8 @@
"use client";
import Link from "next/link";
import { AnimatePresence, motion, useDragControls, useReducedMotion } from "framer-motion";
import { AnimatePresence, motion, useDragControls } from "framer-motion";
import { useAppReducedMotion as useReducedMotion } from "@/lib/motion-pref";
import { useEffect, useRef, useState, useSyncExternalStore } from "react";
import { LOCALES, useI18n, type Locale } from "@/lib/i18n";
@ -184,9 +185,10 @@ export function SettingsDialog({
{open ? (
<motion.div
className="fixed inset-0 z-50 flex items-end justify-center bg-frost-950/35 p-0 backdrop-blur-md sm:items-center sm:p-6"
initial={{ opacity: 0 }}
initial={reduceMotion ? false : { opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={reduceMotion ? { duration: 0 } : { duration: 0.18 }}
onClick={onClose}
>
<motion.div
@ -205,7 +207,7 @@ export function SettingsDialog({
initial={reduceMotion ? { opacity: 0 } : phone ? { y: "100%", opacity: 0 } : { y: 48, opacity: 0 }}
animate={reduceMotion ? { opacity: 1 } : { y: 0, opacity: 1 }}
exit={reduceMotion ? { opacity: 0 } : phone ? { y: "100%", opacity: 0 } : { y: 32, opacity: 0 }}
transition={phone ? springSheet : { type: "spring", stiffness: 340, damping: 32 }}
transition={reduceMotion ? { duration: 0 } : phone ? springSheet : { type: "spring", stiffness: 340, damping: 32 }}
onClick={(event) => event.stopPropagation()}
className="glass-panel glass-sheet relative flex max-h-[90dvh] w-full max-w-lg flex-col overflow-hidden rounded-t-[1.6rem] shadow-2xl sm:rounded-[1.35rem]"
>

View file

@ -1,6 +1,7 @@
"use client";
import { motion, useReducedMotion } from "framer-motion";
import { motion } from "framer-motion";
import { useAppReducedMotion as useReducedMotion } from "@/lib/motion-pref";
import Link from "next/link";
import { usePathname } from "next/navigation";

View file

@ -1,6 +1,7 @@
"use client";
import { motion, useReducedMotion } from "framer-motion";
import { motion } from "framer-motion";
import { useAppReducedMotion as useReducedMotion } from "@/lib/motion-pref";
import { useRouter } from "next/navigation";
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";

View file

@ -1,6 +1,7 @@
"use client";
import { motion, useReducedMotion } from "framer-motion";
import { motion } from "framer-motion";
import { useAppReducedMotion as useReducedMotion } from "@/lib/motion-pref";
import { useEffect, useState } from "react";
import { createPortal } from "react-dom";

View file

@ -1,6 +1,7 @@
"use client";
import { motion, useReducedMotion } from "framer-motion";
import { motion } from "framer-motion";
import { useAppReducedMotion as useReducedMotion } from "@/lib/motion-pref";
import { useState } from "react";
import { useI18n } from "@/lib/i18n";

View file

@ -1,6 +1,7 @@
"use client";
import { motion, useReducedMotion } from "framer-motion";
import { motion } from "framer-motion";
import { useAppReducedMotion as useReducedMotion } from "@/lib/motion-pref";
import { createContext, useContext } from "react";
import { useI18n } from "@/lib/i18n";

View file

@ -2,7 +2,7 @@
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { useReducedMotion } from "framer-motion";
import { useAppReducedMotion as useReducedMotion } from "@/lib/motion-pref";
import Link from "next/link";
import { Composer, type PendingAttachment } from "./Composer";

View file

@ -1,7 +1,8 @@
"use client";
import { useEffect, useRef } from "react";
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
import { AnimatePresence, motion } from "framer-motion";
import { useAppReducedMotion as useReducedMotion } from "@/lib/motion-pref";
import { CheckCircleIcon, FolderIcon, LayersIcon } from "@/components/icons";
import { useI18n } from "@/lib/i18n";

View file

@ -1,6 +1,7 @@
"use client";
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
import { AnimatePresence, motion } from "framer-motion";
import { useAppReducedMotion as useReducedMotion } from "@/lib/motion-pref";
import { useEffect, useState } from "react";
import { CheckCircleIcon, PencilIcon, PlusIcon, XIcon } from "@/components/icons";

View file

@ -1,7 +1,8 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
import { AnimatePresence, motion } from "framer-motion";
import { useAppReducedMotion as useReducedMotion } from "@/lib/motion-pref";
import { CheckCircleIcon } from "@/components/icons";
import { useI18n } from "@/lib/i18n";

View file

@ -2,7 +2,9 @@
import { useState } from "react";
import { ReportQuestionButton } from "@/components/ReportQuestionButton";
import { Button } from "@/components/ui";
import { homeworkReportActivity } from "@/lib/question-reports";
import { AskButton } from "@/components/chat/AskButton";
import { useI18n } from "@/lib/i18n";
import type { HomeworkCheckRecord } from "@/lib/schemas";
@ -130,6 +132,15 @@ export function HomeworkCheckResult({
</Button>
</div>
<p className="text-[11.5px] text-[var(--text-subtle)]">{t("homework.toDeckNote")}</p>
<div className="flex justify-end">
<ReportQuestionButton
deckId={record.id}
deckTitle={record.title}
activity={homeworkReportActivity({ id: record.id, title: record.title, answer: corrected })}
labelKey="homework.report"
/>
</div>
</div>
);
}

View file

@ -1,6 +1,7 @@
"use client";
import { motion, useReducedMotion } from "framer-motion";
import { motion } from "framer-motion";
import { useAppReducedMotion as useReducedMotion } from "@/lib/motion-pref";
import { useRouter } from "next/navigation";
import { useEffect, useRef, useState } from "react";

View file

@ -8,6 +8,9 @@ import { useI18n } from "@/lib/i18n";
import type { HomeworkMakeRecord } from "@/lib/schemas";
import { shareHomework } from "@/lib/share";
import { ReportQuestionButton } from "@/components/ReportQuestionButton";
import { homeworkReportActivity } from "@/lib/question-reports";
import { CopyButton } from "./CopyButton";
import { FollowUpBar } from "./FollowUpBar";
@ -78,6 +81,17 @@ export function HomeworkResult({
</Button>
</div>
<p className="text-[11.5px] text-[var(--text-subtle)]">{t("homework.toDeckNote")}</p>
{/* Same report control as the study feeds and the guide, under the
result (the homework record is the "deck" here). */}
<div className="flex justify-end">
<ReportQuestionButton
deckId={record.id}
deckTitle={record.title}
activity={homeworkReportActivity({ id: record.id, title: record.title, answer })}
labelKey="homework.report"
/>
</div>
</div>
);
}