molecule-core/canvas/src/components/WorkspaceUsage.tsx
Molecule AI Core-UIUX b837d3b065 fix(canvas): text-ink-soft → text-ink-mid for WCAG AA contrast
Replace all text-ink-soft usages across canvas components and app pages.
ink-soft (#8d92a0) on dark zinc (#0e1014) yields ~2.2:1 contrast,
failing WCAG 2.1 AA minimum of 4.5:1 for normal text.

ink-mid (#c8c2b4) on dark zinc yields ~7.6:1 — well above AA.

text-ink-mid is already the semantic token for secondary/caption text
in the warm-paper light mode; the dark-mode override was the gap.

52 files, 268 replacements. No functional change beyond contrast.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 23:18:14 +00:00

139 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import { useState, useEffect } from "react";
import { api } from "@/lib/api";
export interface WorkspaceUsageProps {
workspaceId: string;
}
interface WorkspaceMetrics {
input_tokens?: number; // optional — provisioning-stuck workspaces return partial shapes
output_tokens?: number; // optional — same
total_calls?: number;
estimated_cost_usd?: string; // optional — same
period_start: string;
period_end: string;
}
export function WorkspaceUsage({ workspaceId }: WorkspaceUsageProps) {
const [metrics, setMetrics] = useState<WorkspaceMetrics | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let ignore = false;
setLoading(true);
setError(null);
api
.get<WorkspaceMetrics>(`/workspaces/${workspaceId}/metrics`)
.then((data) => {
if (!ignore) setMetrics(data);
})
.catch((e) => {
if (!ignore)
setError(e instanceof Error ? e.message : "Failed to load metrics");
})
.finally(() => {
if (!ignore) setLoading(false);
});
return () => {
ignore = true;
};
}, [workspaceId]);
return (
<div
className="rounded-md border border-line bg-surface-sunken p-3 space-y-2"
data-testid="workspace-usage"
>
<div className="flex items-center justify-between">
<h4 className="text-xs font-semibold text-ink-mid uppercase tracking-wider">
Usage
</h4>
{!loading && metrics && (
<span
className="text-[10px] text-ink-mid font-mono"
data-testid="usage-period"
>
{formatPeriod(metrics.period_start, metrics.period_end)}
</span>
)}
</div>
<div className="space-y-1.5" data-testid="usage-stats">
{loading ? (
<>
<SkeletonRow />
<SkeletonRow />
<SkeletonRow />
</>
) : error ? (
<p className="text-xs text-bad" data-testid="usage-error">
{error}
</p>
) : metrics ? (
<>
<StatRow
label="Input tokens"
value={`${(metrics.input_tokens ?? 0).toLocaleString()} tokens`}
testId="usage-input-tokens"
/>
<StatRow
label="Output tokens"
value={`${(metrics.output_tokens ?? 0).toLocaleString()} tokens`}
testId="usage-output-tokens"
/>
<StatRow
label="Estimated cost"
value={`$${parseFloat(metrics.estimated_cost_usd ?? "0").toFixed(6)}`}
testId="usage-estimated-cost"
/>
</>
) : null}
</div>
</div>
);
}
function formatPeriod(start: string | undefined, end: string | undefined): string {
if (!start || !end) return "—";
const fmt = (s: string) =>
new Date(s).toLocaleDateString(undefined, {
month: "short",
day: "numeric",
});
return `${fmt(start)} ${fmt(end)}`;
}
function SkeletonRow() {
return (
<div
className="flex justify-between items-center animate-pulse"
data-testid="usage-skeleton-row"
>
<div className="h-3 w-20 rounded bg-surface-card" />
<div className="h-3 w-16 rounded bg-surface-card" />
</div>
);
}
function StatRow({
label,
value,
testId,
}: {
label: string;
value: string;
testId?: string;
}) {
return (
<div className="flex justify-between items-center" data-testid={testId}>
<span className="text-xs text-ink-mid">{label}</span>
<span className="text-xs text-ink-mid font-mono">{value}</span>
</div>
);
}