From e7bbd0dba5bd173aba31760723b0fc79d2fcbe03 Mon Sep 17 00:00:00 2001 From: frapercan Date: Fri, 8 May 2026 18:48:54 +0200 Subject: [PATCH] fix(web): a11y essentials + restore lost benchmark viewMode after auto-rebase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A11y improvements - Skip-to-content link in the locale layout: hidden until focused, lands focus on
. Reachable on the first Tab press; was the most-flagged item by automated scanners. - NavLinks dropdown buttons now expose `aria-haspopup="menu"`, `aria-expanded`, `aria-current="page"` on active groups. Inner items carry `role="menuitem"`. Mobile menu locks body scroll while open (restores prior overflow on close). - StatusBadge gains a redundant leading glyph per state (clock, pulse, check, ✕, slash-circle). Color-blind users still parse the badge at a glance. Wrapper carries `role="status"` and a verbose aria-label. - Benchmark filter chips (Pipeline stage, Neighbours K) gain `aria-pressed` plus `role="group"` with descriptive `aria-label`. - Heatmap | Table toggle (#81) already shipped with `role="tablist"` and `aria-selected`. Drive-by fix - Restore the `viewMode` state declaration on /benchmark. PR #81's toggle UI references `viewMode` / `setViewMode`, but the auto-rebase ladder against #82 dropped the hook line on develop, leaving the page broken at build time. Adds the line back; default "heatmap". (Also re-imports BenchmarkHeatmap, similarly lost.) CI: next build green; backend untouched. --- apps/web/app/[locale]/benchmark/page.tsx | 10 ++++- apps/web/app/[locale]/layout.tsx | 14 ++++++- apps/web/components/NavLinks.tsx | 24 ++++++++++- apps/web/components/StatusBadge.tsx | 52 ++++++++++++++++++++++-- 4 files changed, 92 insertions(+), 8 deletions(-) diff --git a/apps/web/app/[locale]/benchmark/page.tsx b/apps/web/app/[locale]/benchmark/page.tsx index 9b829b9..913cecc 100644 --- a/apps/web/app/[locale]/benchmark/page.tsx +++ b/apps/web/app/[locale]/benchmark/page.tsx @@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from "react"; import Link from "next/link"; +import { BenchmarkHeatmap } from "@/components/BenchmarkHeatmap"; import { Skeleton } from "@/components/Skeleton"; import { useUrlNumber, useUrlParam } from "@/lib/useUrlParam"; import { @@ -150,6 +151,9 @@ export default function BenchmarkPage() { const evalSetId = (evalSetIdRaw ?? "all") as string | "all"; const setEvalSetId = (v: string | "all") => setEvalSetIdRaw(v === "all" ? null : v); const [selectedK, setSelectedK] = useUrlNumber("k", null); + // Default view: heatmap small-multiples (#81). The full numeric matrix is + // still one click away under the toggle for export workflows. + const [viewMode, setViewMode] = useState<"heatmap" | "table">("heatmap"); // Unfiltered catalog fetch — populates the full set of known stages and // eval sets, so selector chips don't disappear when a filtered query @@ -348,11 +352,12 @@ export default function BenchmarkPage() { -
+
{stageList.map((s) => ( {open && ( -
+

{group.title} @@ -68,6 +75,8 @@ function DropdownGroup({ group, pathname }: { group: NavGroup; pathname: string setOpen(false)} className={`flex items-center gap-2 mx-1.5 px-3 py-2 rounded-lg text-[13px] transition-colors ${ active @@ -76,6 +85,7 @@ function DropdownGroup({ group, pathname }: { group: NavGroup; pathname: string }`} > { setOpen(false); }, [pathname]); + // Body scroll lock while the mobile menu is open. Restore the user's + // previous overflow on close so we don't fight other modals. + useEffect(() => { + if (typeof document === "undefined") return; + if (!open) return; + const previous = document.body.style.overflow; + document.body.style.overflow = "hidden"; + return () => { + document.body.style.overflow = previous; + }; + }, [open]); + return ( <> {/* Desktop nav */} diff --git a/apps/web/components/StatusBadge.tsx b/apps/web/components/StatusBadge.tsx index 62c89b1..6f7c7ea 100644 --- a/apps/web/components/StatusBadge.tsx +++ b/apps/web/components/StatusBadge.tsx @@ -15,6 +15,48 @@ const STYLES: Record = { const KNOWN_STATUSES = ["queued", "running", "succeeded", "failed", "cancelled"] as const; type KnownStatus = typeof KNOWN_STATUSES[number]; +/** + * Leading glyph per status. Communicates the state via shape, redundant + * with color so colorblind users (and tiny-screen readers) still parse + * the badge at a glance. + */ +function StatusGlyph({ status }: { status: string }) { + switch (status) { + case "queued": + return ( + + + + + ); + case "running": + return ( + + ); + case "succeeded": + return ( + + + + ); + case "failed": + return ( + + + + ); + case "cancelled": + return ( + + + + + ); + default: + return null; + } +} + export function StatusBadge({ status }: { status: Status }) { const t = useTranslations("components.statusBadge"); const key = status.toLowerCase(); @@ -22,10 +64,12 @@ export function StatusBadge({ status }: { status: Status }) { const isKnown = KNOWN_STATUSES.includes(key as KnownStatus); const label = isKnown ? t(key as KnownStatus) : status.toUpperCase(); return ( - - {key === "running" && ( - - )} + + {label} );