commit 8932ad0dd46a349cdb457f9abffa21202105b2e3
parent bd910cbe90fb2d025a84edde75abc33b87bdd106
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Fri, 9 Oct 2026 14:42:52 -0400
common, export: the isometric overview — each part of the article beside its slide (release 22 E3)
ReportOverview lays the article and its slides side by side on one plane,
tilted isometrically (rotateX(55deg) rotateZ(-45deg), the plane's box
sized to the tilt so it scrolls whole): the article's parts as a column of
page blocks, each slide beside its part as a 16:9 card, a line between
them. The pair in force is lit and lifted; pointing at a pair lights both;
a block opens the article there, a card the slides. ↑ ↓ (Home, End) walk
the pairs, ← → the block and the card, Enter opens, Esc goes back to the
article at the pair; only the pair in force is in the tab order.
The tilt is never the only way to read it: the same ordered list lies flat
(two columns; stacked in a narrow box) for reduced motion, a narrow box, a
keyboard arrival, or the "Flat" toggle (remembered in this browser). The
switch gains Overview (`?rv=iso`, the pair's anchor in the hash); the
scroller alone moves to the pair, never the article behind.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
5 files changed, 453 insertions(+), 15 deletions(-)
diff --git a/common/components/report/slides/ReportOverview.tsx b/common/components/report/slides/ReportOverview.tsx
@@ -0,0 +1,346 @@
+"use client";
+
+import { forwardRef, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type CSSProperties } from "react";
+import { Rows3 } from "lucide-react";
+import { slidePointText, type SlideView } from "../../../lib/report/slides";
+import type { ReportPageView } from "../../../lib/report/views";
+import { Slide, verdictStyleOf } from "./Slide";
+import { SlidesHostProvider, useSlidesHost, type SlidesHost } from "./SlidesHost";
+
+// THE OVERVIEW — the article and its slides side by side on one plane, tilted
+// isometrically: the article's parts as a column of page blocks, each slide
+// beside its part as a 16:9 card, a line between the two. One pair is the
+// reader's place (lit, lifted); pointing at a pair lights it. Choosing a block
+// opens the article there; choosing a card opens the slides there.
+//
+// THE TILT IS NEVER THE ONLY WAY TO READ IT. The same list of pairs lies flat
+// (two columns; a narrow screen stacks each pair) when motion is reduced, the
+// box is narrow, the reader came in by keyboard, or they asked for it (the
+// "Flat" toggle, remembered in this browser). Flat or tilted, the DOM is the
+// same ordered list, and the keys are the same:
+//
+// ↑ ↓ the pair before / after (Home, End: the first, the last)
+// ← → the block / the card of this pair
+// Enter Space open what is focused
+// Esc back to the article at this pair (`onExit`)
+//
+// The place is the caller's (`focusAnchor`, the URL's hash); walking pairs
+// reports it through `onFocusAnchor`.
+
+const CARD_W = 300;
+const THUMB_SCALE = CARD_W / 1280;
+const NARROW_PX = 900;
+const TILT_KEY = "archilyzer:overview-tilt";
+
+// How the reader last moved: a keyboard user gets the flat plane.
+let lastInput: "keyboard" | "pointer" = "pointer";
+if (typeof window !== "undefined") {
+ window.addEventListener("keydown", () => (lastInput = "keyboard"), true);
+ window.addEventListener("pointerdown", () => (lastInput = "pointer"), true);
+}
+
+function storedTilt(): boolean | null {
+ try {
+ const v = window.localStorage.getItem(TILT_KEY);
+ return v === "flat" ? false : v === "iso" ? true : null;
+ } catch {
+ return null;
+ }
+}
+function storeTilt(tilt: boolean) {
+ try {
+ window.localStorage.setItem(TILT_KEY, tilt ? "iso" : "flat");
+ } catch {
+ // no storage here: the choice lasts this visit
+ }
+}
+
+function kindName(s: SlideView): string {
+ switch (s.kind) {
+ case "title":
+ return "The head";
+ case "summary":
+ return "In brief";
+ case "found":
+ return "What the check found";
+ case "section":
+ return `Section ${s.index}`;
+ case "claim":
+ return "Claim";
+ case "sources":
+ return "Sources";
+ }
+}
+
+// A part of the article as a page block: what the reader would find there,
+// in the article's own type.
+function BlockFace({ s, view }: { s: SlideView; view: ReportPageView }) {
+ let line: string | undefined;
+ let mark: string | undefined;
+ switch (s.kind) {
+ case "title":
+ line = [s.series, s.dates.join(" · ")].filter(Boolean).join(" · ");
+ break;
+ case "summary":
+ line = s.text ? slidePointText(s.text) : s.points?.map(slidePointText).join(" · ");
+ break;
+ case "found":
+ line = s.groups.map((g) => `${g.claims.length} ${verdictStyleOf(view, g.verdict).label}`).join(" · ");
+ break;
+ case "section":
+ line = s.text ? slidePointText(s.text) : s.points ? s.points.map(slidePointText).join(" · ") : `${s.claims.length} claims`;
+ break;
+ case "claim":
+ if (s.verdict) mark = verdictStyleOf(view, s.verdict).color;
+ line = s.gist ?? s.text;
+ break;
+ case "sources":
+ line = `${s.references} references`;
+ break;
+ }
+ const verdict = s.kind === "claim" && s.verdict ? verdictStyleOf(view, s.verdict).label : undefined;
+ return (
+ <span className="rs-ov-face" data-kind={s.kind} style={mark ? ({ "--rs-mark": mark } as CSSProperties) : undefined}>
+ <span className="rs-ov-kind">
+ {kindName(s)}
+ {verdict ? <b> · {verdict}</b> : null}
+ </span>
+ <span className="rs-ov-title">{s.title}</span>
+ {line && <span className="rs-ov-line">{line}</span>}
+ </span>
+ );
+}
+
+// A thumbnail renders its slide's words as text: nothing in it is a link.
+const thumbHost = (base: SlidesHost): SlidesHost => ({
+ ...base,
+ md: (text) => <span className="rs-md">{slidePointText(text)}</span>,
+ cite: (_id, children) => children,
+ onRead: undefined,
+ badge: undefined,
+});
+
+export const ReportOverview = forwardRef<
+ HTMLElement,
+ {
+ slides: readonly SlideView[];
+ view: ReportPageView;
+ // The pair in force: an anchor id (the URL's hash).
+ focusAnchor: string | null;
+ onFocusAnchor: (anchorId: string) => void;
+ onOpenArticle: (slide: SlideView) => void;
+ onOpenSlide: (slide: SlideView) => void;
+ onExit?: (slide: SlideView | undefined) => void;
+ keyScope?: "window" | "stage";
+ }
+>(function ReportOverview({ slides, view, focusAnchor, onFocusAnchor, onOpenArticle, onOpenSlide, onExit, keyScope = "window" }, ref) {
+ const host = useSlidesHost();
+ const thumbs = useMemo(() => thumbHost(host), [host]);
+ const active = Math.max(
+ 0,
+ slides.findIndex((s) => s.anchorId === focusAnchor),
+ );
+ const [side, setSide] = useState<"block" | "card">("card");
+ const scrollerRef = useRef<HTMLDivElement>(null);
+ const planeRef = useRef<HTMLOListElement>(null);
+ const stageRef = useRef<HTMLElement | null>(null);
+
+ // Tilted or flat: the reader's choice, else flat for reduced motion, a
+ // narrow box or a keyboard arrival, else tilted.
+ const [tilt, setTilt] = useState<boolean | null>(null);
+ const [narrow, setNarrow] = useState(false);
+ useLayoutEffect(() => {
+ const reduce = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false;
+ setTilt(storedTilt() ?? !(reduce || lastInput === "keyboard"));
+ const el = scrollerRef.current;
+ if (!el) return;
+ const measure = () => setNarrow(el.clientWidth < NARROW_PX);
+ measure();
+ const ro = new ResizeObserver(measure);
+ ro.observe(el);
+ return () => ro.disconnect();
+ }, []);
+ const iso = tilt === true && !narrow;
+
+ // The tilted plane's box: rotateX(55deg) rotateZ(-45deg) of a W × H plane
+ // spans 0.7071(W + H) across and 0.4056(W + H) down, its top corner raised
+ // 0.4056 W — the sizer holds exactly that, so the plane scrolls whole.
+ const [plane, setPlane] = useState<{ w: number; h: number } | null>(null);
+ useLayoutEffect(() => {
+ const el = planeRef.current;
+ if (!el) return;
+ const measure = () => setPlane((p) => (p && p.w === el.offsetWidth && p.h === el.offsetHeight ? p : { w: el.offsetWidth, h: el.offsetHeight }));
+ measure();
+ const ro = new ResizeObserver(measure);
+ ro.observe(el);
+ return () => ro.disconnect();
+ }, [iso]);
+
+ // The place in view: the pair in force, centred.
+ useEffect(() => {
+ const pair = planeRef.current?.querySelector<HTMLElement>(`[data-pair-index="${active}"]`);
+ const scroller = scrollerRef.current;
+ if (!pair || !scroller) return;
+ // The scroller alone moves (never the article behind): by the pair's
+ // drawn box, which the tilt has moved.
+ const reduce = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false;
+ const p = pair.getBoundingClientRect();
+ const s = scroller.getBoundingClientRect();
+ scroller.scrollTo({
+ left: scroller.scrollLeft + (p.left + p.width / 2) - (s.left + s.width / 2),
+ top: scroller.scrollTop + (p.top + p.height / 2) - (s.top + s.height / 2),
+ behavior: reduce ? "auto" : "smooth",
+ });
+ }, [active, iso, plane]);
+
+ const focusPair = useCallback(
+ (i: number, which: "block" | "card") => {
+ const at = Math.min(Math.max(i, 0), slides.length - 1);
+ setSide(which);
+ onFocusAnchor(slides[at].anchorId);
+ requestAnimationFrame(() => {
+ planeRef.current?.querySelector<HTMLElement>(`[data-pair-index="${at}"] [data-ov-${which}]`)?.focus({ preventScroll: true });
+ });
+ },
+ [slides, onFocusAnchor],
+ );
+
+ const onKey = useCallback(
+ (e: KeyboardEvent | React.KeyboardEvent) => {
+ if (e.defaultPrevented || e.metaKey || e.ctrlKey || e.altKey) return;
+ const t = e.target as HTMLElement | null;
+ if (t?.closest?.("input, textarea, select, [contenteditable=true]")) return;
+ // Inside the plane the focused side is the one moved from; elsewhere
+ // (the toggle, the stage itself) the last one.
+ const from = (t?.closest?.("[data-ov-block]") ? "block" : t?.closest?.("[data-ov-card]") ? "card" : side) as "block" | "card";
+ switch (e.key) {
+ case "ArrowDown":
+ e.preventDefault();
+ focusPair(active + 1, from);
+ break;
+ case "ArrowUp":
+ e.preventDefault();
+ focusPair(active - 1, from);
+ break;
+ case "Home":
+ e.preventDefault();
+ focusPair(0, from);
+ break;
+ case "End":
+ e.preventDefault();
+ focusPair(slides.length - 1, from);
+ break;
+ case "ArrowLeft":
+ e.preventDefault();
+ focusPair(active, "block");
+ break;
+ case "ArrowRight":
+ e.preventDefault();
+ focusPair(active, "card");
+ break;
+ case "Escape":
+ if (onExit) {
+ e.preventDefault();
+ onExit(slides[active]);
+ }
+ break;
+ }
+ },
+ [active, focusPair, onExit, side, slides],
+ );
+ useEffect(() => {
+ if (keyScope !== "window") return;
+ const h = (e: KeyboardEvent) => onKey(e);
+ window.addEventListener("keydown", h);
+ return () => window.removeEventListener("keydown", h);
+ }, [keyScope, onKey]);
+
+ const sizer: CSSProperties | undefined =
+ iso && plane ? { width: 0.7071 * (plane.w + plane.h) + 160, height: 0.4056 * (plane.w + plane.h) + 200 } : undefined;
+ const planeStyle: CSSProperties | undefined =
+ iso && plane ? { transform: `translate(80px, ${0.4056 * plane.w + 100}px) rotateX(55deg) rotateZ(-45deg)` } : undefined;
+
+ return (
+ <section
+ ref={(el) => {
+ stageRef.current = el;
+ if (typeof ref === "function") ref(el);
+ else if (ref) ref.current = el;
+ }}
+ className="rs-root rs-ov"
+ aria-roledescription="overview"
+ aria-label={`Overview: ${view.title}, each part of the article beside its slide`}
+ tabIndex={-1}
+ data-report-overview={iso ? "iso" : "flat"}
+ onKeyDown={keyScope === "stage" ? onKey : undefined}
+ >
+ <div className="rs-ov-tools">
+ <p className="rs-ov-hint" aria-hidden="true">
+ <kbd>↑</kbd> <kbd>↓</kbd> walk · <kbd>←</kbd> article <kbd>→</kbd> slide · <kbd>Enter</kbd> open
+ </p>
+ {!narrow && (
+ <button
+ type="button"
+ className="rs-btn"
+ aria-pressed={!iso}
+ data-ov-flat=""
+ onClick={() => {
+ const next = !iso;
+ setTilt(next);
+ storeTilt(next);
+ }}
+ >
+ <Rows3 aria-hidden="true" width={15} height={15} />
+ Flat
+ </button>
+ )}
+ </div>
+ <div ref={scrollerRef} className="rs-ov-scroll">
+ <div className="rs-ov-sizer" style={sizer}>
+ <ol ref={planeRef} className="rs-ov-plane" style={planeStyle} aria-label="The article and its slides">
+ {slides.map((s, i) => {
+ const here = i === active;
+ return (
+ <li key={s.key} className="rs-ov-pair" data-pair={s.key} data-pair-index={i} data-active={here ? "true" : undefined}>
+ <div className="rs-ov-block">
+ <BlockFace s={s} view={view} />
+ <button
+ type="button"
+ data-ov-block={s.anchorId}
+ tabIndex={here ? 0 : -1}
+ aria-label={`Read in the article: ${kindName(s)}, ${s.title}`}
+ onFocus={() => {
+ if (!here) onFocusAnchor(s.anchorId);
+ setSide("block");
+ }}
+ onClick={() => onOpenArticle(s)}
+ />
+ </div>
+ <span className="rs-ov-link" aria-hidden="true" />
+ <div className="rs-ov-card">
+ <div className="rs-ov-thumb" aria-hidden="true" inert>
+ <SlidesHostProvider host={thumbs}>
+ <Slide slide={s} view={view} total={slides.length} hideRead style={{ transform: `scale(${THUMB_SCALE})` }} />
+ </SlidesHostProvider>
+ </div>
+ <button
+ type="button"
+ data-ov-card={s.n}
+ tabIndex={here ? 0 : -1}
+ aria-label={`Open slide ${s.n} of ${slides.length}: ${s.title}`}
+ onFocus={() => {
+ if (!here) onFocusAnchor(s.anchorId);
+ setSide("card");
+ }}
+ onClick={() => onOpenSlide(s)}
+ />
+ </div>
+ </li>
+ );
+ })}
+ </ol>
+ </div>
+ </div>
+ </section>
+ );
+});
diff --git a/common/components/report/slides/ReportReader.tsx b/common/components/report/slides/ReportReader.tsx
@@ -5,12 +5,14 @@ import { createPortal } from "react-dom";
import { CitationsProvider } from "../../citations/CitationsContext";
import {
buildReportSlides,
+ READER_VIEWS,
reportPageAnchorIds,
slideIndexForPlace,
type ReaderView,
type SlideView,
} from "../../../lib/report/slides";
import type { ReportPageView } from "../../../lib/report/views";
+import { ReportOverview } from "./ReportOverview";
import { ReportSlides } from "./ReportSlides";
import { defaultSlidesHost, SlidesHostProvider, type SlidesHost } from "./SlidesHost";
import { readerUrl, useReaderPlace } from "./useReaderPlace";
@@ -60,15 +62,13 @@ function landOn(anchorId: string) {
el.focus({ preventScroll: true });
}
-const E2_VIEWS: readonly ReaderView[] = ["article", "slides"];
-
type Loaded = { view: ReportPageView; slides: SlideView[] } | { error: string };
export function ReportReader({
pagePath,
title,
series,
- views = E2_VIEWS,
+ views = READER_VIEWS,
}: {
// The report's page view (/reports/<id>/page.json).
pagePath: string;
@@ -203,15 +203,28 @@ export function ReportReader({
) : (
<CitationsProvider citations={loaded.view.citations}>
<SlidesHostProvider host={host}>
- <ReportSlides
- ref={stageRef}
- slides={loaded.slides}
- view={loaded.view}
- index={slideIndex}
- onIndex={(i) => go({ view: "slides", slide: i + 1 }, false)}
- onExit={(s) => toArticle(s.anchorId)}
- pageUrl={pageUrl}
- />
+ {place.view === "slides" ? (
+ <ReportSlides
+ ref={stageRef}
+ slides={loaded.slides}
+ view={loaded.view}
+ index={slideIndex}
+ onIndex={(i) => go({ view: "slides", slide: i + 1 }, false)}
+ onExit={(s) => toArticle(s.anchorId)}
+ pageUrl={pageUrl}
+ />
+ ) : (
+ <ReportOverview
+ ref={stageRef}
+ slides={loaded.slides}
+ view={loaded.view}
+ focusAnchor={place.anchor}
+ onFocusAnchor={(a) => go({ view: "iso", anchor: a }, false)}
+ onOpenArticle={(s) => toArticle(s.anchorId)}
+ onOpenSlide={(s) => go({ view: "slides", slide: s.n }, true)}
+ onExit={(s) => toArticle(s?.anchorId ?? place.anchor)}
+ />
+ )}
</SlidesHostProvider>
</CitationsProvider>
)}
diff --git a/export/app/lib/reports.test.ts b/export/app/lib/reports.test.ts
@@ -94,10 +94,10 @@ test("the report page: header, tally, sections and claims, inline cites, referen
header,
/<\/h1><p data-report-dates=""[^>]*>2026-10-01 · updated 2026-10-04 · <a href="\/reports\/demo-factcheck\/history\/" data-report-revision="2"[^>]*>revision 2<\/a><\/p><div data-report-views=""[^>]*><div role="group" aria-label="Read as" data-view-switch="article"[^]*?<\/div><\/div><div id="source-s0" data-subject-source="s0"/,
);
- // the view switch (release 22): Article pressed, then Slides
+ // the view switch (release 22): Article pressed, then Slides and Overview
assert.deepEqual(
[...header.matchAll(/<button type="button" aria-pressed="(true|false)" data-view="(\w+)"/g)].map((m) => `${m[2]}:${m[1]}`),
- ["article:true", "slides:false"],
+ ["article:true", "slides:false", "iso:false"],
);
assert.match(header, /<\/div><p[^>]*>Four claims about a demo channel[^<]*<\/p>$/, "the subtitle, under the card");
assert.doesNotMatch(header, /data-report-attribution|data-report-byline|Fact-check|Under review|The article/);
diff --git a/export/e2e-report/overview.spec.ts b/export/e2e-report/overview.spec.ts
@@ -0,0 +1,79 @@
+import { expect, test } from "./helpers";
+import type { Page } from "@playwright/test";
+
+// THE OVERVIEW (release 22): the article's parts and their slides side by
+// side on one plane — tilted isometrically for a pointer, flat for reduced
+// motion, a narrow screen, a keyboard arrival or the reader's choice — over
+// the cited fixture site (stage.ts; its deck is listed in slides.spec.ts).
+
+const REPORT = "/reports/demo-factcheck/";
+
+const switchIn = (page: Page) => page.locator("header [data-view-switch]");
+const stage = (page: Page) => page.locator("[data-report-slides]");
+const overview = (page: Page) => page.locator("[data-report-overview]");
+
+test("overview: each part beside its slide; arrows walk the pairs; a card opens its slide, a block the article", async ({ page }) => {
+ await page.goto(`${REPORT}#bridge`);
+ await switchIn(page).getByRole("button", { name: "Overview" }).click();
+ await expect(page).toHaveURL(`${REPORT}?rv=iso#bridge`);
+ const ov = overview(page);
+ await expect(ov).toHaveAttribute("aria-roledescription", "overview");
+ await expect(ov.locator("[data-pair]")).toHaveCount(11);
+ await expect(ov.locator('[data-pair="section:bridge"]')).toHaveAttribute("data-active", "true");
+ await ov.locator('[data-pair="section:bridge"] [data-ov-card]').focus();
+ await page.keyboard.press("ArrowDown");
+ await expect(page).toHaveURL(`${REPORT}?rv=iso#claim-1`);
+ await expect(ov.locator('[data-pair="claim:claim-1"] [data-ov-card]')).toBeFocused();
+ await page.keyboard.press("ArrowLeft");
+ await expect(ov.locator('[data-pair="claim:claim-1"] [data-ov-block]')).toBeFocused();
+ await page.keyboard.press("ArrowDown");
+ await expect(ov.locator('[data-pair="claim:claim-2"] [data-ov-block]')).toBeFocused();
+ // Only the pair in force is in the tab order.
+ await expect(ov.locator('[data-ov-block][tabindex="0"], [data-ov-card][tabindex="0"]')).toHaveCount(2);
+ await page.keyboard.press("ArrowRight");
+ await page.keyboard.press("Enter");
+ await expect(page).toHaveURL(`${REPORT}?rv=slides#s-6`);
+ await expect(stage(page).locator('[data-slide="claim:claim-2"]')).toBeVisible();
+
+ await page.goBack();
+ await expect(page).toHaveURL(`${REPORT}?rv=iso#claim-2`);
+ await overview(page).locator('[data-pair="section:moving"] [data-ov-block]').focus();
+ await page.keyboard.press("Enter");
+ await expect(page).toHaveURL(`${REPORT}#moving`);
+ await expect(page.locator("section#moving")).toBeInViewport();
+});
+
+test("overview: tilted for a pointer; flat for reduced motion; the Flat toggle lays it down", async ({ page, browser }) => {
+ await page.goto(`${REPORT}?rv=iso#claim-1`);
+ await expect(overview(page)).toHaveAttribute("data-report-overview", "iso");
+ const plane = page.locator(".rs-ov-plane");
+ expect(await plane.evaluate((el) => getComputedStyle(el).transform)).not.toBe("none");
+ await page.getByRole("button", { name: "Flat" }).click();
+ await expect(overview(page)).toHaveAttribute("data-report-overview", "flat");
+ expect(await plane.evaluate((el) => getComputedStyle(el).transform)).toBe("none");
+ await expect(page.getByRole("button", { name: "Flat" })).toHaveAttribute("aria-pressed", "true");
+
+ const reduced = await browser.newContext({ reducedMotion: "reduce" });
+ const p2 = await reduced.newPage();
+ await p2.goto(`${REPORT}?rv=iso#claim-1`);
+ await expect(overview(p2)).toHaveAttribute("data-report-overview", "flat");
+ expect(await p2.locator(".rs-ov-plane").evaluate((el) => getComputedStyle(el).transform)).toBe("none");
+ // Flat, the pairs read as two columns: each block beside its card.
+ const block = (await p2.locator('[data-pair="claim:claim-1"] .rs-ov-block').boundingBox())!;
+ const card = (await p2.locator('[data-pair="claim:claim-1"] .rs-ov-card').boundingBox())!;
+ expect(card.x).toBeGreaterThan(block.x + block.width);
+ await reduced.close();
+});
+
+test("a phone: the overview lies flat, each part stacked over its slide; nothing scrolls sideways", async ({ page }) => {
+ await page.setViewportSize({ width: 375, height: 800 });
+ await page.goto(`${REPORT}?rv=slides#s-5`);
+ await page.locator("[data-reader-overlay] [data-view-switch]").getByRole("button", { name: "Overview" }).click();
+ await expect(page).toHaveURL(`${REPORT}?rv=iso#claim-1`);
+ await expect(overview(page)).toHaveAttribute("data-report-overview", "flat");
+ const block = (await page.locator('[data-pair="claim:claim-1"] .rs-ov-block').boundingBox())!;
+ const card = (await page.locator('[data-pair="claim:claim-1"] .rs-ov-card').boundingBox())!;
+ expect(card.y).toBeGreaterThanOrEqual(block.y + block.height);
+ expect(card.x + card.width).toBeLessThanOrEqual(375);
+ expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(375);
+});
diff --git a/export/e2e-report/slides.spec.ts b/export/e2e-report/slides.spec.ts
@@ -26,7 +26,7 @@ test("the switch: Article pressed in the header; every place a slide stands for
await page.goto(REPORT);
const sw = switchIn(page, "header");
await expect(sw).toHaveAttribute("role", "group");
- await expect(sw.getByRole("button")).toHaveText(["Article", "Slides"]);
+ await expect(sw.getByRole("button")).toHaveText(["Article", "Slides", "Overview"]);
await expect(sw.getByRole("button", { name: "Article" })).toHaveAttribute("aria-pressed", "true");
await expect(sw.getByRole("button", { name: "Slides" })).toHaveAttribute("aria-pressed", "false");
for (const id of ["report-head", "in-brief", "found", "bridge", "claim-1", "claim-2", "moving", "claim-3", "claim-4", "claim-5", "report-end"]) {