feat(ui): terminal, charts and low-contrast text follow the selected theme

- The lane terminal (xterm) takes its palette from the selected DARK theme —
  it stays dark in light mode, as a terminal should — and repaints live when
  the theme changes (`client/src/lib/terminalTheme.ts`).
- Charts on the Dashboard and Workflows pages draw their grid, axes, labels
  and tooltips from theme tokens instead of hardcoded near-black/white, and
  the activity heatmap ramps from the page surface to the accent, so it reads
  on every theme instead of going near-black on light ones.
- Status/brand text that only worked on dark backgrounds
  (`text-*-300/400`) gets a darker light-mode shade (`text-*-700
  dark:text-*-300`) across sessions, conversations, workflows, config and
  settings. Categorical chart colours are kept.
This commit is contained in:
2026-09-24 09:50:44 +07:00
parent 6353e88c9e
commit 8ea40260ad
36 changed files with 919 additions and 635 deletions
+54 -42
View File
@@ -212,27 +212,16 @@ function useTooltip() {
return { show, move, hide, node };
}
function cellColor(count: number, max: number) {
if (count === 0) return "#161625";
// Log scale + RGB interpolation across a wide color ramp for maximum perceptual range
/** Heatmap cell fill: the page surface mixed toward the accent on a log
* scale. Built from theme tokens so the ramp reads surface → accent in every
* theme; the old fixed indigo ramp went near-black on light themes. Exported
* for its test. */
export function cellColor(count: number, max: number) {
if (count === 0) return "rgb(var(--surface-0))";
const t = Math.log(count + 1) / Math.log(Math.max(max, 1) + 1);
// Ramp: near-black indigo → deep indigo → bright indigo → lavender
type RGB = [number, number, number];
const stops: RGB[] = [
[22, 20, 60], // near-black indigo
[55, 48, 163], // deep indigo
[99, 102, 241], // bright indigo
[199, 210, 254], // lavender
];
const scaled = t * (stops.length - 1);
const lo = Math.min(Math.floor(scaled), stops.length - 2);
const frac = scaled - lo;
const [r1, g1, b1]: RGB = stops[lo] as RGB;
const [r2, g2, b2]: RGB = stops[lo + 1] as RGB;
const r = Math.round(r1 + (r2 - r1) * frac);
const g = Math.round(g1 + (g2 - g1) * frac);
const b = Math.round(b1 + (b2 - b1) * frac);
return `rgb(${r},${g},${b})`;
// Floor at 20% so a single event still reads as distinct from an empty day.
const pct = Math.round(20 + 80 * t);
return `color-mix(in srgb, rgb(var(--accent)) ${pct}%, rgb(var(--surface-0)))`;
}
function Heatmap({ weeks }: { weeks: Array<Array<{ date: string; count: number }>> }) {
@@ -352,7 +341,7 @@ function Heatmap({ weeks }: { weeks: Array<Array<{ date: string; count: number }
height: 13,
borderRadius: 2,
backgroundColor: cellColor(cell.count, maxCount),
border: "1px solid rgba(255,255,255,0.04)",
border: "1px solid rgb(var(--border) / 0.4)",
flexShrink: 0,
cursor: "default",
}}
@@ -374,7 +363,7 @@ function Heatmap({ weeks }: { weeks: Array<Array<{ date: string; count: number }
height: 13,
borderRadius: 2,
backgroundColor: cellColor(v, maxCount),
border: "1px solid rgba(255,255,255,0.06)",
border: "1px solid rgb(var(--border) / 0.5)",
}}
/>
);
@@ -593,7 +582,14 @@ function DonutChart({
<div className="flex items-center justify-center gap-6 w-full">
{node}
<svg width={128} height={128} viewBox="0 0 128 128" className="flex-shrink-0">
<circle cx={cx} cy={cy} r={r} fill="none" stroke="#1e1e2e" strokeWidth={stroke} />
<circle
cx={cx}
cy={cy}
r={r}
fill="none"
stroke="rgb(var(--border))"
strokeWidth={stroke}
/>
{segments.map(({ label, value, color }, i) => {
const dash = (value / total) * circumference;
const gap = circumference - dash;
@@ -1010,7 +1006,7 @@ function AnalyticsTab() {
raw={data ? totalTokens.toLocaleString() : undefined}
sub={data ? `${cacheHitPct}${t("cacheHitRate")}` : undefined}
icon={Cpu}
color="text-violet-400"
color="text-violet-700 dark:text-violet-400"
loading={!data}
/>
<StatPill
@@ -1032,7 +1028,7 @@ function AnalyticsTab() {
raw={data ? data.overview.total_events.toLocaleString() : undefined}
sub={data ? `~${data.avg_events_per_session}${t("perSession")}` : undefined}
icon={Zap}
color="text-yellow-400"
color="text-yellow-700 dark:text-yellow-400"
loading={!data}
/>
</div>
@@ -1155,7 +1151,9 @@ function AnalyticsTab() {
</div>
<div className="flex justify-between text-xs text-fg-muted">
<span>{t("cacheEfficiency")}</span>
<span className="text-violet-400 font-mono">{cacheHitPct}%</span>
<span className="text-violet-700 dark:text-violet-400 font-mono">
{cacheHitPct}%
</span>
</div>
</div>
</div>
@@ -1180,12 +1178,12 @@ function AnalyticsTab() {
{
label: t("common:token.cacheRead"),
value: data?.tokens.total_cache_read ?? 0,
color: "text-violet-400",
color: "text-violet-700 dark:text-violet-400",
},
{
label: t("common:token.cacheWrite"),
value: data?.tokens.total_cache_write ?? 0,
color: "text-yellow-400",
color: "text-yellow-700 dark:text-yellow-400",
},
{ label: t("common:total"), value: totalTokens, color: "text-fg-primary" },
].map(({ label, value, color }) => (
@@ -1331,7 +1329,7 @@ function AnalyticsTab() {
</div>
<div className="mt-4 pt-4 border-t border-border text-xs flex justify-between">
<span className="text-fg-muted">{t("common:total")}</span>
<span className="text-cyan-400 font-mono">
<span className="text-cyan-700 dark:text-cyan-400 font-mono">
<Tip raw={fmtCostFull(totalCost30d)}>{fmtCost(totalCost30d)}</Tip>
</span>
</div>
@@ -1761,7 +1759,14 @@ function SystemHealthTab() {
viewBox="0 0 96 96"
className="flex-shrink-0 cursor-default"
>
<circle cx="48" cy="48" r="38" fill="none" stroke="#1e1e2e" strokeWidth="14" />
<circle
cx="48"
cy="48"
r="38"
fill="none"
stroke="rgb(var(--border))"
strokeWidth="14"
/>
{(() => {
const r = 38,
cx = 48,
@@ -1882,7 +1887,14 @@ function SystemHealthTab() {
raw={`Score: ${healthScore.toFixed(1)} / 100\n\n• Success Rate (40%): ${successRate.toFixed(1)}%\n• Cache Hit (25%): ${cacheHitRate.toFixed(1)}%\n• Error Avoidance (25%): ${(100 - errorRate).toFixed(1)}%\n• Memory Health (10%): ${(100 - heapUsedPct).toFixed(1)}%`}
>
<svg width="120" height="120" viewBox="0 0 120 120" className="cursor-default">
<circle cx="60" cy="60" r="48" fill="none" stroke="#1e1e2e" strokeWidth="10" />
<circle
cx="60"
cy="60"
r="48"
fill="none"
stroke="rgb(var(--border))"
strokeWidth="10"
/>
<circle
cx="60"
cy="60"
@@ -1954,7 +1966,7 @@ function SystemHealthTab() {
>
<div className="text-center cursor-default">
<p className="text-[9px] text-fg-muted uppercase">Compact</p>
<p className="text-xs font-mono font-bold text-violet-400">
<p className="text-xs font-mono font-bold text-violet-700 dark:text-violet-400">
{workflow.compaction?.totalCompactions ?? 0}
</p>
</div>
@@ -2036,7 +2048,7 @@ function SystemHealthTab() {
<div className="card p-5 flex flex-col gap-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<BarChart3 className="w-4 h-4 text-violet-400" />
<BarChart3 className="w-4 h-4 text-violet-700 dark:text-violet-400" />
<span className="text-xs text-fg-muted uppercase tracking-wider">Concurrency</span>
</div>
<span className="text-[10px] font-mono text-fg-muted">{lanes.length} intervals</span>
@@ -2058,7 +2070,7 @@ function SystemHealthTab() {
? "#fbbf24"
: lane.count > 0
? "#34d399"
: "#1e1e2e";
: "rgb(var(--border))";
return (
<div
key={i}
@@ -2172,7 +2184,7 @@ function SystemHealthTab() {
<div className="card p-5 flex flex-col gap-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<GitBranch className="w-4 h-4 text-violet-400" />
<GitBranch className="w-4 h-4 text-violet-700 dark:text-violet-400" />
<span className="text-xs text-fg-muted uppercase tracking-wider">
Subagent Effectiveness
</span>
@@ -2282,7 +2294,7 @@ function SystemHealthTab() {
<div className="card p-5 flex flex-col gap-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Cpu className="w-4 h-4 text-cyan-400" />
<Cpu className="w-4 h-4 text-cyan-700 dark:text-cyan-400" />
<span className="text-xs text-fg-muted uppercase tracking-wider">Platform</span>
</div>
<span className="text-[10px] font-mono text-fg-muted">{info.server.node_version}</span>
@@ -2620,7 +2632,7 @@ export function Dashboard() {
label={t("activeSubagents")}
value={stats ? allSubagents.filter((a) => a.status === "working").length : ""}
icon={GitBranch}
accentColor="text-violet-400"
accentColor="text-violet-700 dark:text-violet-400"
trend={stats ? `${allSubagents.length}${t("totalTrend")}` : undefined}
loading={!stats}
/>
@@ -2629,7 +2641,7 @@ export function Dashboard() {
value={stats ? fmt(stats.events_today) : ""}
raw={stats ? stats.events_today.toLocaleString() : undefined}
icon={Zap}
accentColor="text-yellow-400"
accentColor="text-yellow-700 dark:text-yellow-400"
loading={!stats}
/>
<StatCard
@@ -2637,7 +2649,7 @@ export function Dashboard() {
value={stats ? fmt(stats.total_events) : ""}
raw={stats ? stats.total_events.toLocaleString() : undefined}
icon={Activity}
accentColor="text-violet-400"
accentColor="text-violet-700 dark:text-violet-400"
loading={!stats}
/>
<StatCard
@@ -2721,7 +2733,7 @@ export function Dashboard() {
empty rather than as a misalignment. */}
{!hasChildren && !isSubagent && (
<span
className="w-6 h-6 flex-shrink-0 flex items-center justify-center text-violet-400/70"
className="w-6 h-6 flex-shrink-0 flex items-center justify-center text-violet-700/70 dark:text-violet-400/70"
aria-hidden="true"
title={t("common:noSubagents", "No subagents")}
>
@@ -2729,7 +2741,7 @@ export function Dashboard() {
</span>
)}
{isSubagent && (
<GitBranch className="w-3 h-3 text-violet-400 flex-shrink-0" />
<GitBranch className="w-3 h-3 text-violet-700 dark:text-violet-400 flex-shrink-0" />
)}
<div className="flex-1 min-w-0">
<AgentCard
@@ -2758,7 +2770,7 @@ export function Dashboard() {
onClick={() =>
setExpandedAgents((prev) => new Set([...prev, agent.id]))
}
className="ml-7 mt-1 text-[11px] text-violet-400 hover:text-violet-300 transition-colors"
className="ml-7 mt-1 text-[11px] text-violet-700 dark:text-violet-400 hover:text-violet-700 dark:hover:text-violet-300 transition-colors"
>
{t("common:subagent_label", { count: totalDesc })}
{activeDesc > 0 && (