fix(web,learner): navigation swaps at once; content-only entrance, loading line, focus (R19)

Drops document-level View Transitions and the lesson hold: they dropped clicks for 120-180 ms, froze the screen
while route code loaded, and let the dock act on a stale lesson. Navigation now swaps immediately; only the
content region enters (opacity and a 6 px rise, 160 ms); a 2 px loading line appears when code or data takes over
150 ms; the dock works from the outline and the URL, so a second Next advances again; focus moves to the new h1
and a polite live region announces it. Route chunks of a feature preload together so the sidebar never drops.
Scroll restore stops on user input; pdf.js never starts a load after cleanup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas Winther
2026-10-30 21:49:49 +01:00
co-authored by Claude Opus 5.5
parent 305c262ff1
commit b5e00af4f7
18 changed files with 231 additions and 275 deletions
@@ -4,7 +4,7 @@ import { MemoryRouter } from "react-router-dom";
import { learnerWeb } from "../src/web";
import { filterCourses, LibraryView } from "../src/web/library/LibraryPage";
import { CourseFrame, moduleNumber } from "../src/web/shell/CourseFrame";
import { nextLesson } from "../src/web/course/pages";
import { lessonNeighbours, nextLesson } from "../src/web/course/pages";
import { safeNext } from "../src/web/account/AccountPage";
import type { CourseCard, CoursePayload, LibraryPayload } from "../src/shared/types";
@@ -97,3 +97,9 @@ test("sign-in only returns to same-site paths", () => {
expect(safeNext("https://evil.example")).toBe("/");
expect(safeNext(null)).toBe("/");
});
test("lessonNeighbours: the dock's previous and next come from the outline in flat course order", () => {
expect(lessonNeighbours(course, "a")).toMatchObject({ current: { id: "a" }, prev: null, next: { id: "b" } });
expect(lessonNeighbours(course, "b")).toMatchObject({ current: { id: "b" }, prev: { id: "a" }, next: null });
expect(lessonNeighbours(course, "zz")).toEqual({ current: null, prev: null, next: null });
});