diff --git a/apps/web/src/components/usage/UsagePage.tsx b/apps/web/src/components/usage/UsagePage.tsx index 7a5cdd883db..92e2c5b6fc3 100644 --- a/apps/web/src/components/usage/UsagePage.tsx +++ b/apps/web/src/components/usage/UsagePage.tsx @@ -25,7 +25,7 @@ import { SidebarInset } from "../ui/sidebar"; import { WorkspaceBreadcrumb, WorkspaceBreadcrumbItem } from "../WorkspaceBreadcrumb"; import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "../../workspaceTitlebar"; import { UsageChartLegend, UsageProviderChart, type UsageChartMetric } from "./UsageProviderChart"; -import { PROVIDER_COLOR, PROVIDER_LABEL, PROVIDER_MARK, PROVIDER_ORDER } from "./usageProviders"; +import { PROVIDER_ORDER, PROVIDER_PRESENTATION } from "./usageProviders"; const WINDOW_OPTIONS = [ { days: 1, label: "Past 24h" }, @@ -209,7 +209,7 @@ export function UsagePage() {
- {PROVIDER_LABEL[provider.provider]} + {PROVIDER_PRESENTATION[provider.provider].label} {metric === "cost" @@ -222,7 +222,7 @@ export function UsagePage() { className="h-full" style={{ width: `${(share * 100).toFixed(1)}%`, - backgroundColor: PROVIDER_COLOR[provider.provider], + backgroundColor: PROVIDER_PRESENTATION[provider.provider].color, }} />
@@ -385,7 +385,7 @@ export function UsagePage() { {isPast24Hours ? "Hour" : "Day"} {PROVIDER_ORDER.map((provider) => ( - {PROVIDER_LABEL[provider]} + {PROVIDER_PRESENTATION[provider].label} ))} Total @@ -448,7 +448,7 @@ function ProviderMark({ readonly provider: UsageProviderKind; readonly className: string; }) { - const Mark = PROVIDER_MARK[provider]; + const Mark = PROVIDER_PRESENTATION[provider].mark; return ; } @@ -595,7 +595,7 @@ function UsageSkeleton({ resolution }: { readonly resolution: "day" | "hour" })
- {PROVIDER_LABEL[provider]} + {PROVIDER_PRESENTATION[provider].label}
diff --git a/apps/web/src/components/usage/UsageProviderChart.tsx b/apps/web/src/components/usage/UsageProviderChart.tsx index f41945bfe28..d7582a0e4bd 100644 --- a/apps/web/src/components/usage/UsageProviderChart.tsx +++ b/apps/web/src/components/usage/UsageProviderChart.tsx @@ -9,7 +9,7 @@ import { formatTokens, formatUsd, } from "@t3tools/shared/usageFormat"; -import { PROVIDER_COLOR, PROVIDER_LABEL, PROVIDER_MARK, PROVIDER_ORDER } from "./usageProviders"; +import { PROVIDER_ORDER, PROVIDER_PRESENTATION } from "./usageProviders"; const VIEW_WIDTH = 960; const VIEW_HEIGHT = 260; @@ -339,14 +339,19 @@ export function UsageProviderChart({ {/* Fills first, then every stroke, so no series covers another's line. */} {paths.map(({ provider, area }) => ( - + ))} {paths.map(({ provider, line }) => ( @@ -376,12 +381,12 @@ export function UsageProviderChart({ >
{formatTooltipPeriod(hoveredPeriod)}
{PROVIDER_ORDER.map((provider) => { - const Mark = PROVIDER_MARK[provider]; + const { label, mark: Mark } = PROVIDER_PRESENTATION[provider]; return (
- {PROVIDER_LABEL[provider]} + {label} {format( @@ -423,13 +428,13 @@ export function UsageChartLegend() { return (
{PROVIDER_ORDER.map((provider) => { - // The marks carry the same fills as the bands, so they key the chart - // just as a colour swatch would. - const Mark = PROVIDER_MARK[provider]; + // Brand marks keep monochrome providers identifiable even when their + // chart series use distinct colors. + const { label, mark: Mark } = PROVIDER_PRESENTATION[provider]; return ( - {PROVIDER_LABEL[provider]} + {label} ); })} diff --git a/apps/web/src/components/usage/usageProviders.ts b/apps/web/src/components/usage/usageProviders.ts index f8b65877dcf..00db67e28a8 100644 --- a/apps/web/src/components/usage/usageProviders.ts +++ b/apps/web/src/components/usage/usageProviders.ts @@ -2,32 +2,29 @@ import type { UsageProviderKind } from "@t3tools/contracts"; import { ClaudeAI, type Icon, OpenAI } from "../Icons"; -/** - * Series and table order. The chart layers both providers from a shared zero - * baseline, so this only fixes the reading order of legends, tables and hover - * rows; it does not decide which series sits above the other. - */ -export const PROVIDER_ORDER: readonly UsageProviderKind[] = ["codex", "claude"]; - -export const PROVIDER_LABEL: Record = { - claude: "Claude Code", - codex: "Codex", -}; - -/** Claude's brand orange against a neutral white for Codex. */ -export const PROVIDER_COLOR: Record = { - claude: "#d97757", - codex: "#e6e6e6", +type UsageProviderPresentation = { + readonly label: string; + readonly color: string; + readonly mark: Icon; }; /** - * Brand marks, reused from the provider picker. - * - * These ship their own fills (`#d97757` for Claude, white on dark for OpenAI), - * which are the same colours as the chart bands, so swapping a colour dot for a - * mark keeps the series association intact rather than trading it away. + * Exhaustive presentation for providers supported by the usage contract. + * Declaration order is reused by every chart, table, legend, and skeleton, so + * adding a provider only requires its contract support and one entry here. */ -export const PROVIDER_MARK: Record = { - claude: ClaudeAI, - codex: OpenAI, -}; +export const PROVIDER_PRESENTATION = { + codex: { + label: "Codex", + color: "var(--foreground)", + mark: OpenAI, + }, + claude: { + label: "Claude Code", + color: "#d97757", + mark: ClaudeAI, + }, +} satisfies Record; + +/** The chart layers every series from zero, so order only controls how it is read. */ +export const PROVIDER_ORDER = Object.keys(PROVIDER_PRESENTATION) as UsageProviderKind[];