Limit motion to one animated element per page (DS-2)

- Course navigation: only the completion percent animates; the
  "N of M lessons" counter is static. NumberFlow is enabled only in the
  visible copy of the navigation (drawer on phones, sidebar otherwise).
- globals.css: disable Vaul drawer and overlay animation under
  prefers-reduced-motion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-11 11:26:44 +05:00
co-authored by Claude Opus 5.5
parent 09b069cbb5
commit 197bbb80ec
2 changed files with 29 additions and 9 deletions
+11
View File
@@ -298,3 +298,14 @@
margin: 0 0 0.5rem; margin: 0 0 0.5rem;
} }
.comment-md pre code { background: none; padding: 0; } .comment-md pre code { background: none; padding: 0; }
/* DS-2 §8: with prefers-reduced-motion there is no motion at all.
Vaul animates the sheet via injected CSS and inline transitions while
dragging — !important overrides both, the sheet just appears/disappears. */
@media (prefers-reduced-motion: reduce) {
[data-vaul-drawer],
[data-vaul-overlay] {
animation: none !important;
transition: none !important;
}
}
+18 -9
View File
@@ -4,7 +4,7 @@ import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
import { Menu, X } from "lucide-react"; import { Menu, X } from "lucide-react";
import NumberFlow, { NumberFlowGroup } from "@number-flow/react"; import NumberFlow from "@number-flow/react";
import { AubadeDrawer, ResponsiveDialogTitle } from "@/components/shared/responsive-dialog"; import { AubadeDrawer, ResponsiveDialogTitle } from "@/components/shared/responsive-dialog";
import { useIsMobile } from "@/hooks/use-media-query"; import { useIsMobile } from "@/hooks/use-media-query";
@@ -30,11 +30,14 @@ function CourseNavBody({
completedLessonIds, completedLessonIds,
pathname, pathname,
onNavigate, onNavigate,
animateProgress,
}: { }: {
course: Course; course: Course;
completedLessonIds: Set<string>; completedLessonIds: Set<string>;
pathname: string; pathname: string;
onNavigate: () => void; onNavigate: () => void;
/** DS-2: one animated moment per page — only the visible copy animates. */
animateProgress: boolean;
}) { }) {
const totalLessons = course.modules.reduce((s, m) => s + m.lessons.length, 0); const totalLessons = course.modules.reduce((s, m) => s + m.lessons.length, 0);
const completedCount = course.modules const completedCount = course.modules
@@ -66,14 +69,18 @@ function CourseNavBody({
{totalLessons > 0 && ( {totalLessons > 0 && (
<div> <div>
<div className="flex items-center justify-between mb-1"> <div className="flex items-center justify-between mb-1">
<NumberFlowGroup> <span className="text-xs" style={{ color: "var(--muted-foreground)" }}>
<span className="text-xs" style={{ color: "var(--muted-foreground)" }}> {completedCount} из {totalLessons} уроков
<NumberFlow value={completedCount} respectMotionPreference /> из {totalLessons} уроков </span>
</span> <span className="text-xs font-bold" style={{ color: "var(--foreground)" }}>
<span className="text-xs font-bold" style={{ color: "var(--foreground)" }}> {/* The page's single animated moment (DS-2 §8); static under reduced motion. */}
<NumberFlow value={progressPct} suffix="%" respectMotionPreference /> <NumberFlow
</span> value={progressPct}
</NumberFlowGroup> suffix="%"
animated={animateProgress}
respectMotionPreference
/>
</span>
</div> </div>
<div <div
className="h-1.5 w-full" className="h-1.5 w-full"
@@ -207,6 +214,7 @@ export function CourseSidebar({
completedLessonIds={completedLessonIds} completedLessonIds={completedLessonIds}
pathname={pathname} pathname={pathname}
onNavigate={() => setOpen(false)} onNavigate={() => setOpen(false)}
animateProgress={!isMobile}
/> />
</aside> </aside>
@@ -218,6 +226,7 @@ export function CourseSidebar({
completedLessonIds={completedLessonIds} completedLessonIds={completedLessonIds}
pathname={pathname} pathname={pathname}
onNavigate={() => setSheetOpen(false)} onNavigate={() => setSheetOpen(false)}
animateProgress
/> />
</AubadeDrawer> </AubadeDrawer>
)} )}