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:
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* @file terminalTheme.ts
|
||||
* @description The xterm colour theme for lane terminals. The terminal is
|
||||
* ALWAYS dark — Claude Code's TUI picks its own colours for a dark background,
|
||||
* so a light terminal would make its text unreadable — and is painted with the
|
||||
* palette of the DARK theme the user picked (`themeFor.dark`). Colours are read
|
||||
* from the CSS variables in `index.css` on a hidden probe carrying
|
||||
* `class="dark" data-theme=<id>`, so no hex is duplicated here.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
import type { ITheme } from "@xterm/xterm";
|
||||
|
||||
/** Reads one CSS variable (an RGB triplet like `17 17 19`) by name. */
|
||||
export type VarReader = (name: string) => string;
|
||||
|
||||
function toHex(triplet: string): string | undefined {
|
||||
const parts = triplet
|
||||
.trim()
|
||||
.split(/[\s,]+/)
|
||||
.map(Number);
|
||||
if (parts.length !== 3 || parts.some((n) => !Number.isFinite(n))) return undefined;
|
||||
return "#" + parts.map((n) => Math.round(n).toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
|
||||
/** Mix a hex colour toward white by `amount` (0..1) — the ANSI "bright" twin. */
|
||||
function lighten(hex: string | undefined, amount: number): string | undefined {
|
||||
if (!hex) return undefined;
|
||||
const rgb = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16));
|
||||
return (
|
||||
"#" +
|
||||
rgb
|
||||
.map((c) =>
|
||||
Math.round(c + (255 - c) * amount)
|
||||
.toString(16)
|
||||
.padStart(2, "0")
|
||||
)
|
||||
.join("")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an xterm theme from a theme's CSS variables. red/green/yellow come from
|
||||
* `--status-*`, blue from `--accent`, black/white from the surface/fg ramp.
|
||||
* Magenta and cyan stay the classic ANSI hues: no token keeps that hue in every
|
||||
* theme (darcula's keyword is orange), and a program printing "magenta" should
|
||||
* get magenta.
|
||||
*/
|
||||
export function buildTerminalTheme(read: VarReader): ITheme {
|
||||
const v = (name: string) => toHex(read(name));
|
||||
const red = v("--status-danger");
|
||||
const green = v("--status-success");
|
||||
const yellow = v("--status-warning");
|
||||
const blue = v("--accent");
|
||||
const magenta = "#c678dd";
|
||||
const cyan = "#56b6c2";
|
||||
return {
|
||||
background: v("--surface-0"),
|
||||
foreground: v("--fg-primary"),
|
||||
cursor: v("--fg-primary"),
|
||||
cursorAccent: v("--surface-0"),
|
||||
selectionBackground: v("--border-light"),
|
||||
black: v("--border-light"),
|
||||
red,
|
||||
green,
|
||||
yellow,
|
||||
blue,
|
||||
magenta,
|
||||
cyan,
|
||||
white: v("--fg-secondary"),
|
||||
brightBlack: v("--fg-muted"),
|
||||
brightRed: lighten(red, 0.25),
|
||||
brightGreen: lighten(green, 0.25),
|
||||
brightYellow: lighten(yellow, 0.25),
|
||||
brightBlue: lighten(blue, 0.25),
|
||||
brightMagenta: lighten(magenta, 0.25),
|
||||
brightCyan: lighten(cyan, 0.25),
|
||||
brightWhite: v("--fg-primary"),
|
||||
};
|
||||
}
|
||||
|
||||
/** Resolve the dark theme `id`'s variables on a hidden probe and build the theme. */
|
||||
export function terminalThemeFor(id: string): ITheme {
|
||||
const probe = document.createElement("div");
|
||||
probe.className = "dark";
|
||||
if (id !== "default") probe.dataset.theme = id;
|
||||
probe.style.display = "none";
|
||||
document.body.appendChild(probe);
|
||||
try {
|
||||
const style = getComputedStyle(probe);
|
||||
return buildTerminalTheme((name) => style.getPropertyValue(name));
|
||||
} finally {
|
||||
probe.remove();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user