Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions apps/web/app/[locale]/benchmark/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -348,11 +352,12 @@ export default function BenchmarkPage() {
<label className="block text-xs font-medium text-slate-500 uppercase tracking-wider mb-1">
Pipeline stage
</label>
<div className="flex flex-wrap gap-1 rounded-lg bg-slate-100 p-0.5">
<div role="group" aria-label="Pipeline stage" className="flex flex-wrap gap-1 rounded-lg bg-slate-100 p-0.5">
{stageList.map((s) => (
<button
key={s.name}
onClick={() => setStage(s.name)}
aria-pressed={stage === s.name}
title={`${s.kind}${s.is_baseline ? " · baseline" : ""}`}
className={`px-3 py-1.5 text-xs font-medium rounded-md transition-colors ${
stage === s.name
Expand All @@ -374,11 +379,12 @@ export default function BenchmarkPage() {
<label className="block text-xs font-medium text-slate-500 uppercase tracking-wider mb-1">
Neighbours (K)
</label>
<div className="flex gap-1 rounded-lg bg-slate-100 p-0.5">
<div role="group" aria-label="Neighbours (K)" className="flex gap-1 rounded-lg bg-slate-100 p-0.5">
{catalog.ks.map((n) => (
<button
key={n}
onClick={() => setSelectedK(n)}
aria-pressed={selectedK === n}
className={`px-3 py-1.5 text-xs font-medium rounded-md transition-colors ${
selectedK === n
? "bg-white text-slate-900 shadow-sm"
Expand Down
14 changes: 13 additions & 1 deletion apps/web/app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ export default async function LocaleLayout({
<NextIntlClientProvider messages={messages}>
<UsagePolicyModal />
<ToastProvider>
{/* Skip-to-content for screen-reader / keyboard users.
Hidden until focused; lands focus on <main>. */}
<a
href="#main"
className="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-[110] focus:rounded-lg focus:bg-blue-600 focus:px-3 focus:py-2 focus:text-sm focus:font-semibold focus:text-white focus:shadow-lg"
>
Skip to main content
</a>
<header className="sticky top-0 z-40 border-b border-slate-200/70 bg-white/85 backdrop-blur-md supports-[backdrop-filter]:bg-white/70">
<div className="mx-auto flex h-16 max-w-7xl items-center gap-3 px-4 sm:px-6 lg:px-8">
<a
Expand Down Expand Up @@ -95,7 +103,11 @@ export default async function LocaleLayout({
</div>
</header>

<main className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-6 sm:py-8 lg:py-10">
<main
id="main"
tabIndex={-1}
className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-6 sm:py-8 lg:py-10 focus:outline-none"
>
{children}
</main>

Expand Down
24 changes: 23 additions & 1 deletion apps/web/components/NavLinks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ function DropdownGroup({ group, pathname }: { group: NavGroup; pathname: string
<div ref={ref} className="relative">
<button
onClick={() => setOpen((v) => !v)}
aria-haspopup="menu"
aria-expanded={open}
aria-current={groupActive ? "page" : undefined}
className={`relative flex items-center gap-1.5 px-3 h-10 rounded-lg text-[14px] font-medium transition-all ${
groupActive
? "text-blue-700 bg-blue-50/70"
Expand All @@ -56,7 +59,11 @@ function DropdownGroup({ group, pathname }: { group: NavGroup; pathname: string
)}
</button>
{open && (
<div className="absolute top-full left-0 mt-2 py-2 bg-white rounded-xl border border-slate-200 shadow-xl z-50 min-w-[220px] animate-[fadeIn_120ms_ease-out]">
<div
role="menu"
aria-label={group.title}
className="absolute top-full left-0 mt-2 py-2 bg-white rounded-xl border border-slate-200 shadow-xl z-50 min-w-[220px] animate-[fadeIn_120ms_ease-out]"
>
<div className="px-3 pb-1.5 mb-1 border-b border-slate-100">
<p className="text-[10px] font-semibold uppercase tracking-[0.12em] text-slate-400">
{group.title}
Expand All @@ -68,6 +75,8 @@ function DropdownGroup({ group, pathname }: { group: NavGroup; pathname: string
<Link
key={href}
href={href}
role="menuitem"
aria-current={active ? "page" : undefined}
onClick={() => setOpen(false)}
className={`flex items-center gap-2 mx-1.5 px-3 py-2 rounded-lg text-[13px] transition-colors ${
active
Expand All @@ -76,6 +85,7 @@ function DropdownGroup({ group, pathname }: { group: NavGroup; pathname: string
}`}
>
<span
aria-hidden
className={`h-1.5 w-1.5 rounded-full transition-colors ${
active ? "bg-blue-600" : "bg-slate-300"
}`}
Expand Down Expand Up @@ -133,6 +143,18 @@ export function NavLinks({ mobileExtras }: { mobileExtras?: React.ReactNode }) {
// Close menu on route change
useEffect(() => { 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 */}
Expand Down
52 changes: 48 additions & 4 deletions apps/web/components/StatusBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,61 @@ const STYLES: Record<string, string> = {
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 (
<svg className="h-3 w-3" aria-hidden fill="none" viewBox="0 0 12 12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="6" cy="6" r="4.5" />
<path d="M6 3.5v2.7l1.7 1.1" />
</svg>
);
case "running":
return (
<span aria-hidden className="inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-blue-500" />
);
case "succeeded":
return (
<svg className="h-3 w-3" aria-hidden fill="none" viewBox="0 0 12 12" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M2.5 6.5l2.5 2.5L9.5 3.5" />
</svg>
);
case "failed":
return (
<svg className="h-3 w-3" aria-hidden fill="none" viewBox="0 0 12 12" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 3l6 6M9 3l-6 6" />
</svg>
);
case "cancelled":
return (
<svg className="h-3 w-3" aria-hidden fill="none" viewBox="0 0 12 12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="6" cy="6" r="4.5" />
<path d="M3 9L9 3" />
</svg>
);
default:
return null;
}
}

export function StatusBadge({ status }: { status: Status }) {
const t = useTranslations("components.statusBadge");
const key = status.toLowerCase();
const cls = STYLES[key] ?? "bg-slate-100 text-slate-600 border-slate-200";
const isKnown = KNOWN_STATUSES.includes(key as KnownStatus);
const label = isKnown ? t(key as KnownStatus) : status.toUpperCase();
return (
<span className={`inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-xs font-medium ${cls}`}>
{key === "running" && (
<span className="inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-blue-500" />
)}
<span
role="status"
aria-label={`Status: ${label}`}
className={`inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-xs font-medium ${cls}`}
>
<StatusGlyph status={key} />
{label}
</span>
);
Expand Down
Loading