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:
@@ -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 && (
|
||||
|
||||
Reference in New Issue
Block a user