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
12 changes: 6 additions & 6 deletions apps/web/src/components/usage/UsagePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down Expand Up @@ -209,7 +209,7 @@ export function UsagePage() {
<div className="flex items-baseline justify-between">
<span className="flex items-center gap-2 text-sm text-foreground">
<ProviderMark provider={provider.provider} className="size-4" />
{PROVIDER_LABEL[provider.provider]}
{PROVIDER_PRESENTATION[provider.provider].label}
</span>
<span className="text-sm text-foreground tabular-nums">
{metric === "cost"
Expand All @@ -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,
}}
/>
</div>
Expand Down Expand Up @@ -385,7 +385,7 @@ export function UsagePage() {
<th className="py-2 font-normal">{isPast24Hours ? "Hour" : "Day"}</th>
{PROVIDER_ORDER.map((provider) => (
<th key={provider} className="py-2 text-right font-normal">
{PROVIDER_LABEL[provider]}
{PROVIDER_PRESENTATION[provider].label}
</th>
))}
<th className="py-2 text-right font-normal">Total</th>
Expand Down Expand Up @@ -448,7 +448,7 @@ function ProviderMark({
readonly provider: UsageProviderKind;
readonly className: string;
}) {
const Mark = PROVIDER_MARK[provider];
const Mark = PROVIDER_PRESENTATION[provider].mark;
return <Mark className={cn("shrink-0", className)} aria-hidden />;
}

Expand Down Expand Up @@ -595,7 +595,7 @@ function UsageSkeleton({ resolution }: { readonly resolution: "day" | "hour" })
<div className="flex items-center justify-between">
<span className="flex items-center gap-2 text-sm text-foreground">
<ProviderMark provider={provider} className="size-4" />
{PROVIDER_LABEL[provider]}
{PROVIDER_PRESENTATION[provider].label}
</span>
<div className="h-3.5 w-14 rounded-sm bg-muted" />
</div>
Expand Down
23 changes: 14 additions & 9 deletions apps/web/src/components/usage/UsageProviderChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -339,14 +339,19 @@ export function UsageProviderChart({

{/* Fills first, then every stroke, so no series covers another's line. */}
{paths.map(({ provider, area }) => (
<path key={provider} d={area} fill={PROVIDER_COLOR[provider]} fillOpacity={0.12} />
<path
key={provider}
d={area}
fill={PROVIDER_PRESENTATION[provider].color}
fillOpacity={0.12}
/>
))}
{paths.map(({ provider, line }) => (
<path
key={provider}
d={line}
fill="none"
stroke={PROVIDER_COLOR[provider]}
stroke={PROVIDER_PRESENTATION[provider].color}
strokeWidth={2}
vectorEffect="non-scaling-stroke"
/>
Expand Down Expand Up @@ -376,12 +381,12 @@ export function UsageProviderChart({
>
<div className="mb-1 text-muted-foreground">{formatTooltipPeriod(hoveredPeriod)}</div>
{PROVIDER_ORDER.map((provider) => {
const Mark = PROVIDER_MARK[provider];
const { label, mark: Mark } = PROVIDER_PRESENTATION[provider];
return (
<div key={provider} className="flex items-center justify-between gap-3">
<span className="flex items-center gap-1.5 text-muted-foreground">
<Mark className="size-3 shrink-0" aria-hidden />
{PROVIDER_LABEL[provider]}
{label}
</span>
<span className="text-foreground tabular-nums">
{format(
Expand Down Expand Up @@ -423,13 +428,13 @@ export function UsageChartLegend() {
return (
<div className="flex items-center gap-4">
{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 (
<span key={provider} className="flex items-center gap-1.5 text-xs text-muted-foreground">
<Mark className="size-3.5 shrink-0" aria-hidden />
{PROVIDER_LABEL[provider]}
{label}
</span>
);
})}
Expand Down
47 changes: 22 additions & 25 deletions apps/web/src/components/usage/usageProviders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<UsageProviderKind, string> = {
claude: "Claude Code",
codex: "Codex",
};

/** Claude's brand orange against a neutral white for Codex. */
export const PROVIDER_COLOR: Record<UsageProviderKind, string> = {
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<UsageProviderKind, Icon> = {
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<UsageProviderKind, UsageProviderPresentation>;

/** 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[];
Loading