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
62 changes: 41 additions & 21 deletions src/components/benchmark-body.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,7 @@
import { DistributionChart } from "@/components/distribution-chart";
import { DonutChart } from "@/components/donut-chart";
import { RegionGrid } from "@/components/region-grid";
import { MetricPanelGrid } from "@/components/metric-panel-grid";
import { MetricViewTabs } from "@/components/metric-view-tabs";
import { CountLeaderboard } from "@/components/count-leaderboard";
import { SummaryStat } from "@/components/summary-stat";
import { ViewSwitcher } from "@/components/view-switcher";
Expand DownExpand Up@@ -181,7 +181,7 @@
// they always saw.
const allowedViews = viewsForBenchmark(benchmark);
const defaultView = defaultViewFor(benchmark);
const [view, setView, viewMounted] = useViewPreference(

Check failure on line 184 in src/components/benchmark-body.tsx

View workflow job for this annotation

GitHub Actions/ check

React Hook "useViewPreference" is called conditionally. React Hooks must be called in the exact same order in every component render
benchmark.slug,
defaultView,
allowedViews,
Expand All@@ -192,7 +192,7 @@
// hidden when they switch to distribution or donut - the model is
// "this is the field of providers the reader chose to focus on",
// not "what each view chose to drop". Resets on bench navigation.
const [excluded, setExcluded] = useState<Set<string>>(() => new Set());

Check failure on line 195 in src/components/benchmark-body.tsx

View workflow job for this annotation

GitHub Actions/ check

React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render
const toggleExclude = (slug: string) =>
setExcluded((prev) => {
const next = new Set(prev);
Expand All@@ -208,8 +208,16 @@
// being split between the dimension row and the chart toolbar.
const chartRegions = chartOnlyRegions(benchmark);
const showChartRegionRow = regionOptions.length === 0 && chartRegions.length > 1;
const [chartRegion, setChartRegion] = useState<string>("all");

Check failure on line 211 in src/components/benchmark-body.tsx

View workflow job for this annotation

GitHub Actions/ check

React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?

// Active companion-metric panel. null = main spec metric (default chart
// data, default unit, default header). When a panel id is set, the chart
// pulls its per-provider series from panel.seriesByProvider, swaps the
// header label to panel.label, and the Y-axis unit to panel.unit.
const [activePanelId, setActivePanelId] = useState<string | null>(null);

Check failure on line 217 in src/components/benchmark-body.tsx

View workflow job for this annotation

GitHub Actions/ check

React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?
const activePanel =
benchmark.metricPanels?.find((p) => p.id === activePanelId) ?? null;
const chartRegionOptions: ChainOption[] = useMemo(

Check failure on line 220 in src/components/benchmark-body.tsx

View workflow job for this annotation

GitHub Actions/ check

React Hook "useMemo" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?
() => [
{ value: "all", label: "All" },
...chartRegions.map((r) => ({ value: r, label: REGION_DISPLAY[r] ?? r })),
Expand DownExpand Up@@ -336,20 +344,38 @@
/>
)}
{view === "timeseries" && (
<TimeSeriesChart
benchmark={benchmark}
region={
regionOptions.length > 0
? (region ?? fallbackRegion ?? undefined)
: showChartRegionRow
? chartRegion
: undefined
}
excluded={excluded}
onToggleExclude={toggleExclude}
onResetExcluded={resetExcluded}
headerActions={<ViewSwitcher allowed={allowedViews} value={view} onChange={setView} />}
/>
<>
{benchmark.metricPanels && benchmark.metricPanels.length > 0 && (
<MetricViewTabs
panels={benchmark.metricPanels}
mainLabel={benchmark.metric}
activeId={activePanelId}
onSelect={setActivePanelId}
/>
)}
<TimeSeriesChart
benchmark={benchmark}
region={
regionOptions.length > 0
? (region ?? fallbackRegion ?? undefined)
: showChartRegionRow
? chartRegion
: undefined
}
excluded={excluded}
onToggleExclude={toggleExclude}
onResetExcluded={resetExcluded}
headerActions={<ViewSwitcher allowed={allowedViews} value={view} onChange={setView} />}
seriesOverride={activePanel?.seriesByProvider}
metricLabelOverride={activePanel?.label}
unitOverride={activePanel?.unit}
/>
{activePanel?.description && (
<p className="mt-3 text-[12px] text-ink-muted max-w-2xl">
{activePanel.description}
</p>
)}
</>
)}
</div>
</div>
Expand All@@ -363,12 +389,6 @@
<LedgerTable benchmark={benchmark} />
</div>

{benchmark.metricPanels && benchmark.metricPanels.length > 0 && (
<div className="mt-8 card-soft rounded-xl p-4 sm:p-6 lg:p-8">
<MetricPanelGrid benchmark={benchmark} />
</div>
)}

{benchmark.unit !== "count" &&
Object.keys(benchmark.extras.regions).length > 0 && (
<div className="mt-8 card-soft rounded-xl p-4 sm:p-6 lg:p-8">
Expand Down
179 changes: 0 additions & 179 deletions src/components/metric-panel-grid.tsx

This file was deleted.

69 changes: 69 additions & 0 deletions src/components/metric-view-tabs.tsx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
"use client";

import type { MetricPanel } from "@/types/benchmark";

/**
* Tab row that lives directly above the time-series chart. One pill per
* metric panel, plus a "Default" pill at the front that switches the chart
* back to the bench's main spec-defined metric.
*
* Pure presentation: holds no state. The parent (benchmark-body) owns the
* active panel id and passes it down with the swap callback.
*/
export function MetricViewTabs({
panels,
mainLabel,
activeId,
onSelect,
}: {
panels: MetricPanel[];
mainLabel: string;
activeId: string | null;
onSelect: (id: string | null) => void;
}) {
return (
<div className="mb-3 flex flex-wrap items-center gap-1">
<span className="mr-2 text-[10px] uppercase tracking-[0.16em] text-ink-faint">
View
</span>
<Tab label={mainLabel} active={activeId == null} onClick={() => onSelect(null)} />
{panels.map((p) => (
<Tab
key={p.id}
label={p.label}
active={activeId === p.id}
onClick={() => onSelect(p.id)}
title={p.metric}
/>
))}
</div>
);
}

function Tab({
label,
active,
onClick,
title,
}: {
label: string;
active: boolean;
onClick: () => void;
title?: string;
}) {
return (
<button
type="button"
onClick={onClick}
title={title}
className={[
"rounded px-2.5 py-1 text-[11px] font-sans tabular uppercase tracking-[0.1em] font-medium transition-colors",
active
? "bg-ink text-paper"
: "text-ink-muted hover:text-ink hover:bg-paper-soft",
].join(" ")}
>
{label}
</button>
);
}
Loading
Loading