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";...@@ -25,7 +25,7 @@ import { SidebarInset } from "../ui/sidebar";
25import { WorkspaceBreadcrumb, WorkspaceBreadcrumbItem } from "../WorkspaceBreadcrumb";25import { WorkspaceBreadcrumb, WorkspaceBreadcrumbItem } from "../WorkspaceBreadcrumb";
26import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "../../workspaceTitlebar";26import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "../../workspaceTitlebar";
27import { UsageChartLegend, UsageProviderChart, type UsageChartMetric } from "./UsageProviderChart";27import { 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
30const WINDOW_OPTIONS = [30const WINDOW_OPTIONS = [
31 { days: 1, label: "Past 24h" },31 { days: 1, label: "Past 24h" },
...@@ -209,7 +209,7 @@ export function UsagePage() {...@@ -209,7 +209,7 @@ export function UsagePage() {
209 <div className="flex items-baseline justify-between">209 <div className="flex items-baseline justify-between">
210 <span className="flex items-center gap-2 text-sm text-foreground">210 <span className="flex items-center gap-2 text-sm text-foreground">
211 <ProviderMark provider={provider.provider} className="size-4" />211 <ProviderMark provider={provider.provider} className="size-4" />
212 {PROVIDER_LABEL[provider.provider]}212 {PROVIDER_PRESENTATION[provider.provider].label}
213 </span>213 </span>
214 <span className="text-sm text-foreground tabular-nums">214 <span className="text-sm text-foreground tabular-nums">
215 {metric === "cost"215 {metric === "cost"
...@@ -222,7 +222,7 @@ export function UsagePage() {...@@ -222,7 +222,7 @@ export function UsagePage() {
222 className="h-full"222 className="h-full"
223 style={{223 style={{
224 width: `${(share * 100).toFixed(1)}%`,224 width: `${(share * 100).toFixed(1)}%`,
225 backgroundColor: PROVIDER_COLOR[provider.provider],225 backgroundColor: PROVIDER_PRESENTATION[provider.provider].color,
226 }}226 }}
227 />227 />
228 </div>228 </div>
...@@ -385,7 +385,7 @@ export function UsagePage() {...@@ -385,7 +385,7 @@ export function UsagePage() {
385 <th className="py-2 font-normal">{isPast24Hours ? "Hour" : "Day"}</th>385 <th className="py-2 font-normal">{isPast24Hours ? "Hour" : "Day"}</th>
386 {PROVIDER_ORDER.map((provider) => (386 {PROVIDER_ORDER.map((provider) => (
387 <th key={provider} className="py-2 text-right font-normal">387 <th key={provider} className="py-2 text-right font-normal">
388 {PROVIDER_LABEL[provider]}388 {PROVIDER_PRESENTATION[provider].label}
389 </th>389 </th>
390 ))}390 ))}
391 <th className="py-2 text-right font-normal">Total</th>391 <th className="py-2 text-right font-normal">Total</th>
...@@ -448,7 +448,7 @@ function ProviderMark({...@@ -448,7 +448,7 @@ function ProviderMark({
448 readonly provider: UsageProviderKind;448 readonly provider: UsageProviderKind;
449 readonly className: string;449 readonly className: string;
450}) {450}) {
451 const Mark = PROVIDER_MARK[provider];451 const Mark = PROVIDER_PRESENTATION[provider].mark;
452 return <Mark className={cn("shrink-0", className)} aria-hidden />;452 return <Mark className={cn("shrink-0", className)} aria-hidden />;
453}453}
454454
...@@ -595,7 +595,7 @@ function UsageSkeleton({ resolution }: { readonly resolution: "day" | "hour" })...@@ -595,7 +595,7 @@ function UsageSkeleton({ resolution }: { readonly resolution: "day" | "hour" })
595 <div className="flex items-center justify-between">595 <div className="flex items-center justify-between">
596 <span className="flex items-center gap-2 text-sm text-foreground">596 <span className="flex items-center gap-2 text-sm text-foreground">
597 <ProviderMark provider={provider} className="size-4" />597 <ProviderMark provider={provider} className="size-4" />
598 {PROVIDER_LABEL[provider]}598 {PROVIDER_PRESENTATION[provider].label}
599 </span>599 </span>
600 <div className="h-3.5 w-14 rounded-sm bg-muted" />600 <div className="h-3.5 w-14 rounded-sm bg-muted" />
601 </div>601 </div>
apps/web/src/components/usage/UsageProviderChart.tsx+14-9
...@@ -9,7 +9,7 @@ import {...@@ -9,7 +9,7 @@ import {
9 formatTokens,9 formatTokens,
10 formatUsd,10 formatUsd,
11} from "@t3tools/shared/usageFormat";11} from "@t3tools/shared/usageFormat";
12import { PROVIDER_COLOR, PROVIDER_LABEL, PROVIDER_MARK, PROVIDER_ORDER } from "./usageProviders";12import { PROVIDER_ORDER, PROVIDER_PRESENTATION } from "./usageProviders";
1313
14const VIEW_WIDTH = 960;14const VIEW_WIDTH = 960;
15const VIEW_HEIGHT = 260;15const VIEW_HEIGHT = 260;
...@@ -339,14 +339,19 @@ export function UsageProviderChart({...@@ -339,14 +339,19 @@ export function UsageProviderChart({
339339
340 {/* Fills first, then every stroke, so no series covers another's line. */}340 {/* Fills first, then every stroke, so no series covers another's line. */}
341 {paths.map(({ provider, area }) => (341 {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 />
343 ))}348 ))}
344 {paths.map(({ provider, line }) => (349 {paths.map(({ provider, line }) => (
345 <path350 <path
346 key={provider}351 key={provider}
347 d={line}352 d={line}
348 fill="none"353 fill="none"
349 stroke={PROVIDER_COLOR[provider]}354 stroke={PROVIDER_PRESENTATION[provider].color}
350 strokeWidth={2}355 strokeWidth={2}
351 vectorEffect="non-scaling-stroke"356 vectorEffect="non-scaling-stroke"
352 />357 />
...@@ -376,12 +381,12 @@ export function UsageProviderChart({...@@ -376,12 +381,12 @@ export function UsageProviderChart({
376 >381 >
377 <div className="mb-1 text-muted-foreground">{formatTooltipPeriod(hoveredPeriod)}</div>382 <div className="mb-1 text-muted-foreground">{formatTooltipPeriod(hoveredPeriod)}</div>
378 {PROVIDER_ORDER.map((provider) => {383 {PROVIDER_ORDER.map((provider) => {
379 const Mark = PROVIDER_MARK[provider];384 const { label, mark: Mark } = PROVIDER_PRESENTATION[provider];
380 return (385 return (
381 <div key={provider} className="flex items-center justify-between gap-3">386 <div key={provider} className="flex items-center justify-between gap-3">
382 <span className="flex items-center gap-1.5 text-muted-foreground">387 <span className="flex items-center gap-1.5 text-muted-foreground">
383 <Mark className="size-3 shrink-0" aria-hidden />388 <Mark className="size-3 shrink-0" aria-hidden />
384 {PROVIDER_LABEL[provider]}389 {label}
385 </span>390 </span>
386 <span className="text-foreground tabular-nums">391 <span className="text-foreground tabular-nums">
387 {format(392 {format(
...@@ -423,13 +428,13 @@ export function UsageChartLegend() {...@@ -423,13 +428,13 @@ export function UsageChartLegend() {
423 return (428 return (
424 <div className="flex items-center gap-4">429 <div className="flex items-center gap-4">
425 {PROVIDER_ORDER.map((provider) => {430 {PROVIDER_ORDER.map((provider) => {
426 // The marks carry the same fills as the bands, so they key the chart431 // Brand marks keep monochrome providers identifiable even when their
427 // just as a colour swatch would.432 // chart series use distinct colors.
428 const Mark = PROVIDER_MARK[provider];433 const { label, mark: Mark } = PROVIDER_PRESENTATION[provider];
429 return (434 return (
430 <span key={provider} className="flex items-center gap-1.5 text-xs text-muted-foreground">435 <span key={provider} className="flex items-center gap-1.5 text-xs text-muted-foreground">
431 <Mark className="size-3.5 shrink-0" aria-hidden />436 <Mark className="size-3.5 shrink-0" aria-hidden />
432 {PROVIDER_LABEL[provider]}437 {label}
433 </span>438 </span>
434 );439 );
435 })}440 })}
apps/web/src/components/usage/usageProviders.ts+22-25
...@@ -2,32 +2,29 @@ import type { UsageProviderKind } from "@t3tools/contracts";...@@ -2,32 +2,29 @@ import type { UsageProviderKind } from "@t3tools/contracts";
22
3import { ClaudeAI, type Icon, OpenAI } from "../Icons";3import { ClaudeAI, type Icon, OpenAI } from "../Icons";
44
5/**5type UsageProviderPresentation = {
6 * Series and table order. The chart layers both providers from a shared zero6 readonly label: string;
7 * baseline, so this only fixes the reading order of legends, tables and hover7 readonly color: string;
8 * rows; it does not decide which series sits above the other.8 readonly mark: Icon;
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",
21};9};
2210
23/**11/**
24 * Brand marks, reused from the provider picker.12 * Exhaustive presentation for providers supported by the usage contract.
25 *13 * Declaration order is reused by every chart, table, legend, and skeleton, so
26 * These ship their own fills (`#d97757` for Claude, white on dark for OpenAI),14 * adding a provider only requires its contract support and one entry here.
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.
29 */15 */
30export const PROVIDER_MARK: Record<UsageProviderKind, Icon> = {16export const PROVIDER_PRESENTATION = {
31 claude: ClaudeAI,17 codex: {
32 codex: OpenAI,18 label: "Codex",
33};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[];