feat(theme): dark/light mode with a Radix Colors-based palette

Adds a working Dark/Light toggle (next to the language switcher, same row
as EN/VI) and re-themes the whole dashboard, not just the handful of
components that already used semantic tokens.

- Tailwind darkMode:"class" + CSS-variable color tokens (client/src/index.css,
  tailwind.config.js): surface.0-5, border/border-light, accent/accent-hover,
  fg.primary/secondary/muted, status.success/danger/warning. One class flip
  on <html> re-themes everything — no per-element dark: variant pairs.
- useTheme() hook: localStorage-persisted, defaults to dark, no
  prefers-color-scheme fallback (client/src/hooks/useTheme.ts).
- Mechanical, table-driven migration (scripts/migrate-color-tokens.mjs,
  scripts/tokenize-status-colors.mjs, scripts/darken-status-colors.mjs) of
  every raw neutral/gray/slate + emerald/red/amber Tailwind utility across
  client/src onto the new tokens, so every badge/button/component pulls the
  same shade per status/role instead of each picking its own.
- Palette values are the literal Radix Colors (radix-ui.com/colors) scale
  constants — slate/blue/green/red/amber steps 1-12 — adopted after three
  rounds of hand-picked values that kept overshooting (flat, then too dark,
  then glaring); see docs/superpowers/specs/2026-07-31-color-redesign-
  dark-light-mode-design.md for the full history and role mapping.
- PipelineMap: done/current/failed/passed-no-evidence/detected share one
  visual language (border + text + translucent wash of the same status
  color); `current` alone stays a solid accent fill, the one state that
  gets to look bolder ("you are here").
- LaneCard: removed the stage/kind/auto-stage chips that duplicated the
  Workspace lane-detail header already showing them.

Categorical/decorative hues (violet, indigo, cyan, teal, sky, rose, pink,
orange, yellow, and blue where it plays a role-coloring part e.g. message
bubbles) are deliberately out of scope — collapsing those onto shared
tokens would erase the distinction between different kinds of thing, not
a status.
This commit is contained in:
2026-07-31 10:54:31 +07:00
parent 4905d63b97
commit b673363351
82 changed files with 3776 additions and 2578 deletions
+107 -97
View File
@@ -85,7 +85,7 @@ function ChartTooltip({ x, y, children }: { x: number; y: number; children: Reac
const nearRight = x > window.innerWidth - 200;
return (
<div
className="fixed z-50 px-2 py-1.5 text-xs bg-[#12121f] border border-[#2a2a4a] rounded shadow-xl text-gray-200 pointer-events-none whitespace-nowrap"
className="fixed z-50 px-2 py-1.5 text-xs bg-[#12121f] border border-[#2a2a4a] rounded shadow-xl text-fg-secondary pointer-events-none whitespace-nowrap"
style={{
left: nearRight ? x - 14 : x + 14,
top: y - 10,
@@ -211,7 +211,7 @@ function Heatmap({ weeks }: { weeks: Array<Array<{ date: string; count: number }
{monthPositions.map((mp, i) => (
<div
key={i}
className="absolute text-[10px] text-gray-600 font-medium whitespace-nowrap"
className="absolute text-[10px] text-fg-muted font-medium whitespace-nowrap"
style={{ left: mp.col * 16 }}
>
{mp.label}
@@ -224,7 +224,7 @@ function Heatmap({ weeks }: { weeks: Array<Array<{ date: string; count: number }
{dayLabels.map((d, i) => (
<div
key={i}
className="text-[9px] text-gray-700 flex items-center justify-end pr-1.5"
className="text-[9px] text-fg-muted flex items-center justify-end pr-1.5"
style={{ height: 13 }}
>
{d}
@@ -247,7 +247,7 @@ function Heatmap({ weeks }: { weeks: Array<Array<{ date: string; count: number }
show(
e,
<>
<span className="text-gray-400">
<span className="text-fg-secondary">
{dayNames[dow] ?? ""}, {cell.date}
</span>
<span className="ml-2 font-medium">
@@ -273,7 +273,7 @@ function Heatmap({ weeks }: { weeks: Array<Array<{ date: string; count: number }
))}
</div>
{/* Legend */}
<div className="flex items-center gap-2 mt-3 text-[11px] text-gray-600">
<div className="flex items-center gap-2 mt-3 text-[11px] text-fg-muted">
<span>{t("less")}</span>
{[0, 0.25, 0.5, 0.75, 1].map((f) => {
const v = Math.round(f * maxCount);
@@ -324,7 +324,7 @@ function Sparkline({
show(
e,
<>
<span className="text-gray-400">{date}</span>
<span className="text-fg-secondary">{date}</span>
<span className="ml-2 font-medium">{t("eventCountLabel", { count })}</span>
</>
)
@@ -404,7 +404,7 @@ function CostTrendLine({
show(
e,
<>
<span className="text-gray-400">{point.date}</span>
<span className="text-fg-secondary">{point.date}</span>
<span className="ml-2 font-medium">{fmtCostFull(point.cost)}</span>
</>
)
@@ -437,7 +437,7 @@ function BarRow({
const width = pct !== undefined ? pct : max > 0 ? Math.round((count / max) * 100) : 0;
return (
<div className="flex items-center gap-3">
<span className="text-xs text-gray-400 w-28 truncate flex-shrink-0" title={label}>
<span className="text-xs text-fg-secondary w-28 truncate flex-shrink-0" title={label}>
{label}
</span>
<div className="flex-1 bg-surface-3 rounded-full h-2">
@@ -447,7 +447,7 @@ function BarRow({
/>
</div>
<Tip raw={count.toLocaleString()}>
<span className="text-xs text-gray-500 w-10 text-right flex-shrink-0">{fmt(count)}</span>
<span className="text-xs text-fg-muted w-10 text-right flex-shrink-0">{fmt(count)}</span>
</Tip>
</div>
);
@@ -457,7 +457,7 @@ function CostBarRow({
label,
cost,
max,
color = "bg-emerald-400",
color = "bg-status-success",
}: {
label: string;
cost: number;
@@ -467,7 +467,7 @@ function CostBarRow({
const width = max > 0 ? Math.max(2, Math.round((cost / max) * 100)) : 0;
return (
<div className="flex items-center gap-3">
<span className="text-xs text-gray-400 w-24 truncate flex-shrink-0" title={label}>
<span className="text-xs text-fg-secondary w-24 truncate flex-shrink-0" title={label}>
{label}
</span>
<div className="flex-1 bg-surface-3 rounded-full h-2">
@@ -476,7 +476,7 @@ function CostBarRow({
style={{ width: `${width}%` }}
/>
</div>
<span className="text-xs text-emerald-400 font-mono w-16 text-right flex-shrink-0">
<span className="text-xs text-status-success font-mono w-16 text-right flex-shrink-0">
<Tip raw={fmtCostFull(cost)}>{fmtCost(cost)}</Tip>
</span>
</div>
@@ -495,7 +495,7 @@ function DonutChart({
const { t } = useTranslation(["analytics", "common"]);
const { show, move, hide, node } = useTooltip();
const total = segments.reduce((s, g) => s + g.value, 0);
if (total === 0) return <div className="text-xs text-gray-500">{t("common:noData")}</div>;
if (total === 0) return <div className="text-xs text-fg-muted">{t("common:noData")}</div>;
const r = 52;
const cx = 64;
@@ -543,10 +543,10 @@ function DonutChart({
/>
);
})}
<text x={cx} y={cy - 6} textAnchor="middle" className="fill-gray-300" fontSize={11}>
<text x={cx} y={cy - 6} textAnchor="middle" className="fill-fg-secondary" fontSize={11}>
{(formatTotal ?? fmt)(total)}
</text>
<text x={cx} y={cy + 10} textAnchor="middle" className="fill-gray-600" fontSize={9}>
<text x={cx} y={cy + 10} textAnchor="middle" className="fill-fg-muted" fontSize={9}>
{t("common:total_lower")}
</text>
</svg>
@@ -557,8 +557,8 @@ function DonutChart({
className="w-2.5 h-2.5 rounded-sm flex-shrink-0"
style={{ backgroundColor: color }}
/>
<span className="text-gray-400">{label}</span>
<span className="text-gray-500 ml-auto pl-4">{Math.round((value / total) * 100)}%</span>
<span className="text-fg-secondary">{label}</span>
<span className="text-fg-muted ml-auto pl-4">{Math.round((value / total) * 100)}%</span>
</div>
))}
</div>
@@ -588,7 +588,7 @@ function StatPill({
return (
<div className="card p-5 flex flex-col gap-2">
<div className="flex items-center justify-between">
<span className="text-xs text-gray-500 uppercase tracking-wider">{label}</span>
<span className="text-xs text-fg-muted uppercase tracking-wider">{label}</span>
<Icon className={`w-4 h-4 ${color}`} />
</div>
{loading ? (
@@ -601,7 +601,7 @@ function StatPill({
{loading ? (
<TextSkeleton width="w-20" />
) : (
sub && <p className="text-[11px] text-gray-500">{sub}</p>
sub && <p className="text-[11px] text-fg-muted">{sub}</p>
)}
</div>
);
@@ -881,8 +881,8 @@ export function Analytics() {
].filter((s) => s.value > 0);
const EVENT_TYPE_COLORS: Record<string, string> = {
PreToolUse: "bg-emerald-400",
PostToolUse: "bg-blue-400",
PreToolUse: "bg-status-success",
PostToolUse: "bg-blue-500",
Stop: "bg-violet-400",
SubagentStop: "bg-yellow-400",
Notification: "bg-orange-400",
@@ -899,22 +899,22 @@ export function Analytics() {
</div>
<div>
<div className="flex items-center gap-2">
<h1 className="text-lg font-semibold text-gray-100">{t("title")}</h1>
<h1 className="text-lg font-semibold text-fg-primary">{t("title")}</h1>
{wsConnected ? (
<span className="flex items-center gap-1.5 text-[11px] text-emerald-400 bg-emerald-500/10 border border-emerald-500/20 px-2 py-0.5 rounded-full">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse-dot" />
<span className="flex items-center gap-1.5 text-[11px] text-status-success bg-status-success/10 border border-status-success/20 px-2 py-0.5 rounded-full">
<span className="w-1.5 h-1.5 rounded-full bg-status-success animate-pulse-dot" />
{t("common:live")}
</span>
) : (
<span className="flex items-center gap-1.5 text-[11px] text-gray-400 bg-gray-500/10 border border-gray-500/20 px-2 py-0.5 rounded-full">
<span className="w-1.5 h-1.5 rounded-full bg-gray-400" />
<span className="flex items-center gap-1.5 text-[11px] text-fg-secondary bg-surface-4/10 border border-border-light/20 px-2 py-0.5 rounded-full">
<span className="w-1.5 h-1.5 rounded-full bg-surface-4" />
{t("common:offline")}
</span>
)}
</div>
<p className="text-xs text-gray-500 flex items-center gap-2">
<p className="text-xs text-fg-muted flex items-center gap-2">
{t("subtitle")}
<span className="inline-flex items-center gap-1.5 text-[11px] text-gray-500 bg-surface-2 border border-border px-2 py-0.5 rounded-md font-mono ml-2">
<span className="inline-flex items-center gap-1.5 text-[11px] text-fg-muted bg-surface-2 border border-border px-2 py-0.5 rounded-md font-mono ml-2">
<Clock className="w-3 h-3" />
{lastUpdate.toLocaleTimeString()}
</span>
@@ -942,7 +942,7 @@ export function Analytics() {
raw={data ? data.overview.total_sessions.toLocaleString() : undefined}
sub={data ? `${data.overview.active_sessions} ${t("common:active")}` : undefined}
icon={FolderOpen}
color="text-blue-400"
color="text-blue-500"
loading={!data}
/>
<StatPill
@@ -951,7 +951,7 @@ export function Analytics() {
raw={data ? data.overview.total_agents.toLocaleString() : undefined}
sub={data ? `${data.overview.active_agents} ${t("common:active")}` : undefined}
icon={Bot}
color="text-emerald-400"
color="text-status-success"
loading={!data}
/>
<StatPill
@@ -973,7 +973,7 @@ export function Analytics() {
: undefined
}
icon={DollarSign}
color="text-emerald-400"
color="text-status-success"
loading={!costData}
/>
<StatPill
@@ -994,7 +994,7 @@ export function Analytics() {
{/* Activity heatmap + 30-day sparkline */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="card p-5 lg:col-span-2">
<h3 className="text-sm font-medium text-gray-300 mb-4">{t("eventActivity")}</h3>
<h3 className="text-sm font-medium text-fg-secondary mb-4">{t("eventActivity")}</h3>
<div className="overflow-x-auto">
<div className="w-fit min-w-max mx-auto">
<Heatmap weeks={weeks} />
@@ -1002,17 +1002,17 @@ export function Analytics() {
</div>
</div>
<div className="card p-5">
<h3 className="text-sm font-medium text-gray-300 mb-1">{t("last30Days")}</h3>
<p className="text-[11px] text-gray-600 mb-4">{t("dailyEventCount")}</p>
<h3 className="text-sm font-medium text-fg-secondary mb-1">{t("last30Days")}</h3>
<p className="text-[11px] text-fg-muted mb-4">{t("dailyEventCount")}</p>
<Sparkline data={last30} />
<div className="flex justify-between text-[11px] text-gray-600 mt-2">
<div className="flex justify-between text-[11px] text-fg-muted mt-2">
<span>{last30[0]?.date?.slice(5)}</span>
<span>{last30[last30.length - 1]?.date?.slice(5)}</span>
</div>
<div className="mt-4 pt-4 border-t border-border space-y-1">
<div className="flex justify-between text-xs">
<span className="text-gray-500">{t("peakDay")}</span>
<span className="text-gray-300 font-mono">
<span className="text-fg-muted">{t("peakDay")}</span>
<span className="text-fg-secondary font-mono">
<Tip raw={Math.max(...last30.map((d) => d.count)).toLocaleString()}>
{fmt(Math.max(...last30.map((d) => d.count)))}
</Tip>{" "}
@@ -1020,8 +1020,8 @@ export function Analytics() {
</span>
</div>
<div className="flex justify-between text-xs">
<span className="text-gray-500">{t("total30d")}</span>
<span className="text-gray-300 font-mono">
<span className="text-fg-muted">{t("total30d")}</span>
<span className="text-fg-secondary font-mono">
<Tip raw={last30.reduce((s, d) => s + d.count, 0).toLocaleString()}>
{fmt(last30.reduce((s, d) => s + d.count, 0))}
</Tip>{" "}
@@ -1048,8 +1048,8 @@ export function Analytics() {
onClick={() => setActiveTab(key)}
className={`px-4 py-1.5 text-xs font-medium rounded-md transition-colors ${
activeTab === key
? "bg-surface-4 text-gray-200"
: "text-gray-500 hover:text-gray-300"
? "bg-surface-4 text-fg-secondary"
: "text-fg-muted hover:text-fg-secondary"
}`}
>
{label}
@@ -1061,7 +1061,7 @@ export function Analytics() {
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{/* Token bars */}
<div className="card p-5">
<h3 className="text-sm font-medium text-gray-300 mb-5">
<h3 className="text-sm font-medium text-fg-secondary mb-5">
{t("tokenDistribution")}
</h3>
<div className="space-y-4">
@@ -1069,12 +1069,12 @@ export function Analytics() {
{
label: t("common:token.input"),
value: data?.tokens.total_input ?? 0,
color: "bg-blue-400",
color: "bg-blue-500",
},
{
label: t("common:token.output"),
value: data?.tokens.total_output ?? 0,
color: "bg-emerald-400",
color: "bg-status-success",
},
{
label: t("common:token.cacheRead"),
@@ -1097,13 +1097,13 @@ export function Analytics() {
))}
</div>
<div className="mt-6 pt-4 border-t border-border space-y-1.5">
<div className="flex justify-between text-xs text-gray-500">
<div className="flex justify-between text-xs text-fg-muted">
<span>{t("common:token.totalTokens")}</span>
<Tip raw={totalTokens.toLocaleString()}>
<span className="text-gray-300 font-mono">{fmt(totalTokens)}</span>
<span className="text-fg-secondary font-mono">{fmt(totalTokens)}</span>
</Tip>
</div>
<div className="flex justify-between text-xs text-gray-500">
<div className="flex justify-between text-xs text-fg-muted">
<span>{t("cacheEfficiency")}</span>
<span className="text-violet-400 font-mono">{cacheHitPct}%</span>
</div>
@@ -1112,18 +1112,20 @@ export function Analytics() {
{/* Token summary */}
<div className="card p-5">
<h3 className="text-sm font-medium text-gray-300 mb-5">{t("tokenBreakdown")}</h3>
<h3 className="text-sm font-medium text-fg-secondary mb-5">
{t("tokenBreakdown")}
</h3>
<div className="space-y-3">
{[
{
label: t("common:token.input"),
value: data?.tokens.total_input ?? 0,
color: "text-blue-400",
color: "text-blue-500",
},
{
label: t("common:token.output"),
value: data?.tokens.total_output ?? 0,
color: "text-emerald-400",
color: "text-status-success",
},
{
label: t("common:token.cacheRead"),
@@ -1135,13 +1137,13 @@ export function Analytics() {
value: data?.tokens.total_cache_write ?? 0,
color: "text-yellow-400",
},
{ label: t("common:total"), value: totalTokens, color: "text-gray-100" },
{ label: t("common:total"), value: totalTokens, color: "text-fg-primary" },
].map(({ label, value, color }) => (
<div
key={label}
className="flex justify-between items-center py-2 border-b border-border last:border-0"
>
<span className="text-xs text-gray-400">{label}</span>
<span className="text-xs text-fg-secondary">{label}</span>
<span className={`text-sm font-mono font-medium ${color}`}>
{value.toLocaleString()}
</span>
@@ -1149,23 +1151,23 @@ export function Analytics() {
))}
</div>
{totalTokens === 0 && (
<p className="text-[11px] text-gray-600 mt-4">{t("tokenInfo")}</p>
<p className="text-[11px] text-fg-muted mt-4">{t("tokenInfo")}</p>
)}
</div>
{/* Token mix donut */}
<div className="card p-5">
<h3 className="text-sm font-medium text-gray-300 mb-5">{t("tokenMix")}</h3>
<h3 className="text-sm font-medium text-fg-secondary mb-5">{t("tokenMix")}</h3>
{tokenMixSegments.length === 0 ? (
<p className="text-sm text-gray-500">{t("common:noData")}</p>
<p className="text-sm text-fg-muted">{t("common:noData")}</p>
) : (
<>
<DonutChart segments={tokenMixSegments} formatTotal={(total) => fmt(total)} />
<div className="mt-4 pt-4 border-t border-border space-y-2">
{tokenMixSegments.map((segment) => (
<div key={segment.label} className="flex justify-between text-xs">
<span className="text-gray-400">{segment.label}</span>
<span className="text-gray-300 font-mono">
<span className="text-fg-secondary">{segment.label}</span>
<span className="text-fg-secondary font-mono">
<Tip raw={segment.value.toLocaleString()}>{fmt(segment.value)}</Tip>
</span>
</div>
@@ -1181,29 +1183,31 @@ export function Analytics() {
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{/* Daily cost trends */}
<div className="card p-5">
<h3 className="text-sm font-medium text-gray-300 mb-1">{t("dailyCostTrends")}</h3>
<h3 className="text-sm font-medium text-fg-secondary mb-1">
{t("dailyCostTrends")}
</h3>
{dailyCostsLocal.length === 0 ? (
<p className="text-sm text-gray-500">{t("noDailyCostData")}</p>
<p className="text-sm text-fg-muted">{t("noDailyCostData")}</p>
) : (
<>
<p className="text-[11px] text-gray-600 mb-4">{t("costPerDay")}</p>
<p className="text-[11px] text-fg-muted mb-4">{t("costPerDay")}</p>
<CostTrendLine data={dailyCostLast30} />
<div className="flex justify-between text-[11px] text-gray-600 mt-2">
<div className="flex justify-between text-[11px] text-fg-muted mt-2">
<span>{dailyCostLast30[0]?.date?.slice(5)}</span>
<span>{dailyCostLast30[dailyCostLast30.length - 1]?.date?.slice(5)}</span>
</div>
<div className="mt-4 pt-4 border-t border-border space-y-1">
<div className="flex justify-between text-xs">
<span className="text-gray-500">{t("peakCostDay")}</span>
<span className="text-emerald-400 font-mono">
<span className="text-fg-muted">{t("peakCostDay")}</span>
<span className="text-status-success font-mono">
<Tip raw={`${peakCostDay.date} • ${fmtCostFull(peakCostDay.cost)}`}>
{fmtCost(peakCostDay.cost)}
</Tip>
</span>
</div>
<div className="flex justify-between text-xs">
<span className="text-gray-500">{t("totalCost30d")}</span>
<span className="text-emerald-400 font-mono">
<span className="text-fg-muted">{t("totalCost30d")}</span>
<span className="text-status-success font-mono">
<Tip raw={fmtCostFull(totalCost30d)}>{fmtCost(totalCost30d)}</Tip>
</span>
</div>
@@ -1214,7 +1218,7 @@ export function Analytics() {
{/* Cost by model */}
<div className="card p-5">
<h3 className="text-sm font-medium text-gray-300 mb-5">{t("costByModel")}</h3>
<h3 className="text-sm font-medium text-fg-secondary mb-5">{t("costByModel")}</h3>
{costBreakdown.length > 0 ? (
<>
<DonutChart
@@ -1231,17 +1235,17 @@ export function Analytics() {
<div className="mt-4 pt-4 border-t border-border space-y-2">
{costBreakdown.map((b) => (
<div key={b.model} className="flex justify-between text-xs">
<span className="text-gray-400 font-mono truncate">
<span className="text-fg-secondary font-mono truncate">
{formatModelName(b.model)}
</span>
<span className="text-emerald-400 font-mono font-medium ml-2">
<span className="text-status-success font-mono font-medium ml-2">
<Tip raw={fmtCostFull(b.cost)}>{fmtCost(b.cost)}</Tip>
</span>
</div>
))}
<div className="flex justify-between text-xs pt-2 border-t border-border">
<span className="text-gray-300 font-medium">{t("common:total")}</span>
<span className="text-emerald-400 font-mono font-semibold">
<span className="text-fg-secondary font-medium">{t("common:total")}</span>
<span className="text-status-success font-mono font-semibold">
<Tip raw={fmtCostFull(costData?.total_cost ?? 0)}>
{fmtCost(costData?.total_cost ?? 0)}
</Tip>
@@ -1250,18 +1254,20 @@ export function Analytics() {
</div>
</>
) : (
<p className="text-sm text-gray-500">{t("noCostData")}</p>
<p className="text-sm text-fg-muted">{t("noCostData")}</p>
)}
</div>
{/* Cost by weekday */}
<div className="card p-5">
<h3 className="text-sm font-medium text-gray-300 mb-1">{t("costByWeekday")}</h3>
<h3 className="text-sm font-medium text-fg-secondary mb-1">
{t("costByWeekday")}
</h3>
{dailyCostsLocal.length === 0 ? (
<p className="text-sm text-gray-500">{t("noDailyCostData")}</p>
<p className="text-sm text-fg-muted">{t("noDailyCostData")}</p>
) : (
<>
<p className="text-[11px] text-gray-600 mb-4">{t("last30Days")}</p>
<p className="text-[11px] text-fg-muted mb-4">{t("last30Days")}</p>
<div className="space-y-3">
{weekdayCosts.map(({ label, cost }) => (
<CostBarRow
@@ -1274,7 +1280,7 @@ export function Analytics() {
))}
</div>
<div className="mt-4 pt-4 border-t border-border text-xs flex justify-between">
<span className="text-gray-500">{t("common:total")}</span>
<span className="text-fg-muted">{t("common:total")}</span>
<span className="text-cyan-400 font-mono">
<Tip raw={fmtCostFull(totalCost30d)}>{fmtCost(totalCost30d)}</Tip>
</span>
@@ -1289,9 +1295,11 @@ export function Analytics() {
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{/* Agent type distribution */}
<div className="card p-5">
<h3 className="text-sm font-medium text-gray-300 mb-5">{t("subagentTypes")}</h3>
<h3 className="text-sm font-medium text-fg-secondary mb-5">
{t("subagentTypes")}
</h3>
{(data?.agent_types ?? []).length === 0 ? (
<p className="text-sm text-gray-500">{t("noSubagentData")}</p>
<p className="text-sm text-fg-muted">{t("noSubagentData")}</p>
) : (
<div className="space-y-3">
{(data?.agent_types ?? []).slice(0, 10).map(({ subagent_type, count }) => (
@@ -1309,13 +1317,13 @@ export function Analytics() {
{/* Agent status donut */}
<div className="card p-5">
<h3 className="text-sm font-medium text-gray-300 mb-5">{t("agentStatus")}</h3>
<h3 className="text-sm font-medium text-fg-secondary mb-5">{t("agentStatus")}</h3>
<DonutChart segments={agentStatusSegments} />
<div className="mt-4 pt-4 border-t border-border space-y-1.5">
<div className="flex justify-between text-xs text-gray-500">
<div className="flex justify-between text-xs text-fg-muted">
<span>{t("totalAgentsLabel")}</span>
<Tip raw={(data?.overview.total_agents ?? 0).toLocaleString()}>
<span className="text-gray-300 font-mono">
<span className="text-fg-secondary font-mono">
{fmt(data?.overview.total_agents ?? 0)}
</span>
</Tip>
@@ -1323,7 +1331,7 @@ export function Analytics() {
{agentStatusSegments.map((s) => (
<div
key={s.label}
className="flex items-center justify-between text-xs text-gray-500"
className="flex items-center justify-between text-xs text-fg-muted"
>
<span className="flex items-center gap-1.5">
<span
@@ -1333,7 +1341,7 @@ export function Analytics() {
{s.label}
</span>
<Tip raw={s.value.toLocaleString()}>
<span className="text-gray-400 font-mono">{fmt(s.value)}</span>
<span className="text-fg-secondary font-mono">{fmt(s.value)}</span>
</Tip>
</div>
))}
@@ -1342,9 +1350,9 @@ export function Analytics() {
{/* Event type breakdown */}
<div className="card p-5">
<h3 className="text-sm font-medium text-gray-300 mb-5">{t("eventTypes")}</h3>
<h3 className="text-sm font-medium text-fg-secondary mb-5">{t("eventTypes")}</h3>
{(data?.event_types ?? []).length === 0 ? (
<p className="text-sm text-gray-500">{t("noEventData")}</p>
<p className="text-sm text-fg-muted">{t("noEventData")}</p>
) : (
<div className="space-y-3">
{(data?.event_types ?? []).map(({ event_type, count }) => (
@@ -1353,7 +1361,7 @@ export function Analytics() {
label={event_type}
count={count}
max={maxEventTypeCount}
color={EVENT_TYPE_COLORS[event_type] ?? "bg-gray-400"}
color={EVENT_TYPE_COLORS[event_type] ?? "bg-surface-4"}
/>
))}
</div>
@@ -1366,9 +1374,9 @@ export function Analytics() {
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{/* Top tools */}
<div className="card p-5">
<h3 className="text-sm font-medium text-gray-300 mb-5">{t("toolUsage")}</h3>
<h3 className="text-sm font-medium text-fg-secondary mb-5">{t("toolUsage")}</h3>
{(data?.tool_usage ?? []).length === 0 ? (
<p className="text-sm text-gray-500">{t("noToolData")}</p>
<p className="text-sm text-fg-muted">{t("noToolData")}</p>
) : (
<div className="space-y-3">
{(data?.tool_usage ?? []).slice(0, 12).map(({ tool_name, count }) => (
@@ -1386,13 +1394,15 @@ export function Analytics() {
{/* Session outcomes donut */}
<div className="card p-5">
<h3 className="text-sm font-medium text-gray-300 mb-5">{t("sessionOutcomes")}</h3>
<h3 className="text-sm font-medium text-fg-secondary mb-5">
{t("sessionOutcomes")}
</h3>
<DonutChart segments={sessionOutcomeSegments} />
<div className="mt-4 pt-4 border-t border-border space-y-1.5">
<div className="flex justify-between text-xs text-gray-500">
<div className="flex justify-between text-xs text-fg-muted">
<span>{t("totalSessionsLabel")}</span>
<Tip raw={(data?.overview.total_sessions ?? 0).toLocaleString()}>
<span className="text-gray-300 font-mono">
<span className="text-fg-secondary font-mono">
{fmt(data?.overview.total_sessions ?? 0)}
</span>
</Tip>
@@ -1400,7 +1410,7 @@ export function Analytics() {
{sessionOutcomeSegments.map((s) => (
<div
key={s.label}
className="flex items-center justify-between text-xs text-gray-500"
className="flex items-center justify-between text-xs text-fg-muted"
>
<span className="flex items-center gap-1.5">
<span
@@ -1410,7 +1420,7 @@ export function Analytics() {
{s.label}
</span>
<Tip raw={s.value.toLocaleString()}>
<span className="text-gray-400 font-mono">{fmt(s.value)}</span>
<span className="text-fg-secondary font-mono">{fmt(s.value)}</span>
</Tip>
</div>
))}
@@ -1419,11 +1429,11 @@ export function Analytics() {
{/* Daily session trends */}
<div className="card p-5">
<h3 className="text-sm font-medium text-gray-300 mb-5">
<h3 className="text-sm font-medium text-fg-secondary mb-5">
{t("dailySessionTrends")}
</h3>
{dailySessionsLocal.length === 0 ? (
<p className="text-sm text-gray-500">{t("noSessionTrendData")}</p>
<p className="text-sm text-fg-muted">{t("noSessionTrendData")}</p>
) : (
<>
<Sparkline data={dailySessionsLocal.slice(-30)} color="#6366f1" />
@@ -1440,7 +1450,7 @@ export function Analytics() {
);
return (
<div key={date} className="flex items-center gap-3">
<span className="text-[11px] text-gray-500 font-mono w-20 flex-shrink-0">
<span className="text-[11px] text-fg-muted font-mono w-20 flex-shrink-0">
{date.slice(5)}
</span>
<div className="flex-1 bg-surface-3 rounded-full h-1.5">
@@ -1451,14 +1461,14 @@ export function Analytics() {
}}
/>
</div>
<span className="text-[11px] text-gray-500 w-4 text-right">
<span className="text-[11px] text-fg-muted w-4 text-right">
{count}
</span>
</div>
);
})}
</div>
<p className="text-[11px] text-gray-600 mt-3">{t("last7Days")}</p>
<p className="text-[11px] text-fg-muted mt-3">{t("last7Days")}</p>
</>
)}
</div>