authorgravatar for 71498452+StiensWout@users.noreply.github.comWout Stiens <71498452+StiensWout@users.noreply.github.com> 2026-08-15 10:42:35+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-15 04:42:35-04:00
logf0ebc628c6dd83fd0c7963078ad7778ce6028d0c
treea257c98ea9e3c745c69b75bb279bdd0c310abde8
parent6ae9662d8ed215476e697adc12403ce035500828
signature Signed by PGP key B5690EEEBB952194

fix(web): improve Codex usage graph contrast (#6669)


3 files changed, 42 insertions(+), 40 deletions(-)

apps/web/src/components/usage/UsagePage.tsx+6-6
......@@ -25,7 +25,7 @@ import { SidebarInset } from "../ui/sidebar";
2525import { WorkspaceBreadcrumb, WorkspaceBreadcrumbItem } from "../WorkspaceBreadcrumb";
2626import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "../../workspaceTitlebar";
2727import { UsageChartLegend, UsageProviderChart, type UsageChartMetric } from "./UsageProviderChart";
28import { PROVIDER_COLOR, PROVIDER_LABEL, PROVIDER_MARK, PROVIDER_ORDER } from "./usageProviders";
28import { PROVIDER_ORDER, PROVIDER_PRESENTATION } from "./usageProviders";
2929
3030const WINDOW_OPTIONS = [
3131 { days: 1, label: "Past 24h" },
......@@ -209,7 +209,7 @@ export function UsagePage() {
209209 <div className="flex items-baseline justify-between">
210210 <span className="flex items-center gap-2 text-sm text-foreground">
211211 <ProviderMark provider={provider.provider} className="size-4" />
212 {PROVIDER_LABEL[provider.provider]}
212 {PROVIDER_PRESENTATION[provider.provider].label}
213213 </span>
214214 <span className="text-sm text-foreground tabular-nums">
215215 {metric === "cost"
......@@ -222,7 +222,7 @@ export function UsagePage() {
222222 className="h-full"
223223 style={{
224224 width: `${(share * 100).toFixed(1)}%`,
225 backgroundColor: PROVIDER_COLOR[provider.provider],
225 backgroundColor: PROVIDER_PRESENTATION[provider.provider].color,
226226 }}
227227 />
228228 </div>
......@@ -385,7 +385,7 @@ export function UsagePage() {
385385 <th className="py-2 font-normal">{isPast24Hours ? "Hour" : "Day"}</th>
386386 {PROVIDER_ORDER.map((provider) => (
387387 <th key={provider} className="py-2 text-right font-normal">
388 {PROVIDER_LABEL[provider]}
388 {PROVIDER_PRESENTATION[provider].label}
389389 </th>
390390 ))}
391391 <th className="py-2 text-right font-normal">Total</th>
......@@ -448,7 +448,7 @@ function ProviderMark({
448448 readonly provider: UsageProviderKind;
449449 readonly className: string;
450450}) {
451 const Mark = PROVIDER_MARK[provider];
451 const Mark = PROVIDER_PRESENTATION[provider].mark;
452452 return <Mark className={cn("shrink-0", className)} aria-hidden />;
453453}
454454
......@@ -595,7 +595,7 @@ function UsageSkeleton({ resolution }: { readonly resolution: "day" | "hour" })
595595 <div className="flex items-center justify-between">
596596 <span className="flex items-center gap-2 text-sm text-foreground">
597597 <ProviderMark provider={provider} className="size-4" />
598 {PROVIDER_LABEL[provider]}
598 {PROVIDER_PRESENTATION[provider].label}
599599 </span>
600600 <div className="h-3.5 w-14 rounded-sm bg-muted" />
601601 </div>
apps/web/src/components/usage/UsageProviderChart.tsx+14-9
......@@ -9,7 +9,7 @@ import {
99 formatTokens,
1010 formatUsd,
1111} from "@t3tools/shared/usageFormat";
12import { PROVIDER_COLOR, PROVIDER_LABEL, PROVIDER_MARK, PROVIDER_ORDER } from "./usageProviders";
12import { PROVIDER_ORDER, PROVIDER_PRESENTATION } from "./usageProviders";
1313
1414const VIEW_WIDTH = 960;
1515const VIEW_HEIGHT = 260;
......@@ -339,14 +339,19 @@ export function UsageProviderChart({
339339
340340 {/* Fills first, then every stroke, so no series covers another's line. */}
341341 {paths.map(({ provider, area }) => (
342 <path key={provider} d={area} fill={PROVIDER_COLOR[provider]} fillOpacity={0.12} />
342 <path
343 key={provider}
344 d={area}
345 fill={PROVIDER_PRESENTATION[provider].color}
346 fillOpacity={0.12}
347 />
343348 ))}
344349 {paths.map(({ provider, line }) => (
345350 <path
346351 key={provider}
347352 d={line}
348353 fill="none"
349 stroke={PROVIDER_COLOR[provider]}
354 stroke={PROVIDER_PRESENTATION[provider].color}
350355 strokeWidth={2}
351356 vectorEffect="non-scaling-stroke"
352357 />
......@@ -376,12 +381,12 @@ export function UsageProviderChart({
376381 >
377382 <div className="mb-1 text-muted-foreground">{formatTooltipPeriod(hoveredPeriod)}</div>
378383 {PROVIDER_ORDER.map((provider) => {
379 const Mark = PROVIDER_MARK[provider];
384 const { label, mark: Mark } = PROVIDER_PRESENTATION[provider];
380385 return (
381386 <div key={provider} className="flex items-center justify-between gap-3">
382387 <span className="flex items-center gap-1.5 text-muted-foreground">
383388 <Mark className="size-3 shrink-0" aria-hidden />
384 {PROVIDER_LABEL[provider]}
389 {label}
385390 </span>
386391 <span className="text-foreground tabular-nums">
387392 {format(
......@@ -423,13 +428,13 @@ export function UsageChartLegend() {
423428 return (
424429 <div className="flex items-center gap-4">
425430 {PROVIDER_ORDER.map((provider) => {
426 // The marks carry the same fills as the bands, so they key the chart
427 // just as a colour swatch would.
428 const Mark = PROVIDER_MARK[provider];
431 // Brand marks keep monochrome providers identifiable even when their
432 // chart series use distinct colors.
433 const { label, mark: Mark } = PROVIDER_PRESENTATION[provider];
429434 return (
430435 <span key={provider} className="flex items-center gap-1.5 text-xs text-muted-foreground">
431436 <Mark className="size-3.5 shrink-0" aria-hidden />
432 {PROVIDER_LABEL[provider]}
437 {label}
433438 </span>
434439 );
435440 })}
apps/web/src/components/usage/usageProviders.ts+22-25
......@@ -2,32 +2,29 @@ import type { UsageProviderKind } from "@t3tools/contracts";
22
33import { ClaudeAI, type Icon, OpenAI } from "../Icons";
44
5/**
6 * Series and table order. The chart layers both providers from a shared zero
7 * baseline, so this only fixes the reading order of legends, tables and hover
8 * rows; it does not decide which series sits above the other.
9 */
10export const PROVIDER_ORDER: readonly UsageProviderKind[] = ["codex", "claude"];
11
12export const PROVIDER_LABEL: Record<UsageProviderKind, string> = {
13 claude: "Claude Code",
14 codex: "Codex",
15};
16
17/** Claude's brand orange against a neutral white for Codex. */
18export const PROVIDER_COLOR: Record<UsageProviderKind, string> = {
19 claude: "#d97757",
20 codex: "#e6e6e6",
5type UsageProviderPresentation = {
6 readonly label: string;
7 readonly color: string;
8 readonly mark: Icon;
219};
2210
2311/**
24 * Brand marks, reused from the provider picker.
25 *
26 * These ship their own fills (`#d97757` for Claude, white on dark for OpenAI),
27 * which are the same colours as the chart bands, so swapping a colour dot for a
28 * mark keeps the series association intact rather than trading it away.
12 * Exhaustive presentation for providers supported by the usage contract.
13 * Declaration order is reused by every chart, table, legend, and skeleton, so
14 * adding a provider only requires its contract support and one entry here.
2915 */
30export const PROVIDER_MARK: Record<UsageProviderKind, Icon> = {
31 claude: ClaudeAI,
32 codex: OpenAI,
33};
16export const PROVIDER_PRESENTATION = {
17 codex: {
18 label: "Codex",
19 color: "var(--foreground)",
20 mark: OpenAI,
21 },
22 claude: {
23 label: "Claude Code",
24 color: "#d97757",
25 mark: ClaudeAI,
26 },
27} satisfies Record<UsageProviderKind, UsageProviderPresentation>;
28
29/** The chart layers every series from zero, so order only controls how it is read. */
30export const PROVIDER_ORDER = Object.keys(PROVIDER_PRESENTATION) as UsageProviderKind[];