chore(webapp): theme-ready charts, streamdown and dark variant

Run-status and chart colors in JS now reference var(--color-*) tokens
(run-status palette added to the theme). Streamdown container vars derive
from semantic tokens instead of hardcoded dark HSL values; removed dead
forced-dark code block overrides. dark: variant is now driven by data-theme
on <html> (set to dark) instead of the OS preference, with color-scheme
following the theme.
This commit is contained in:
Katia Bulatova
2026-07-03 12:45:52 +00:00
parent d70dfcbe51
commit 899dcbabb0
7 changed files with 109 additions and 78 deletions
@@ -12,7 +12,7 @@ import { ChartBlankState } from "../primitives/charts/ChartBlankState";
import { Callout } from "../primitives/Callout";
import type { AggregationType, ChartConfiguration } from "../metrics/QueryWidget";
import { aggregateValues } from "../primitives/charts/aggregation";
import { getRunStatusHexColor } from "~/components/runs/v3/TaskRunStatus";
import { getRunStatusChartColor } from "~/components/runs/v3/TaskRunStatus";
import { getSeriesColor } from "./chartColors";
const MAX_SERIES = 50;
@@ -936,7 +936,7 @@ export const QueryResultsChart = memo(function QueryResultsChart({
const chartConfig = useMemo(() => {
const cfg: ChartConfig = {};
sortedSeries.forEach((s, i) => {
const statusColor = groupByIsRunStatus ? getRunStatusHexColor(s) : undefined;
const statusColor = groupByIsRunStatus ? getRunStatusChartColor(s) : undefined;
const originalIndex = config.yAxisColumns.indexOf(s);
const colorIndex = originalIndex >= 0 ? originalIndex : i;
cfg[s] = {
@@ -5,7 +5,7 @@ import { AnimatedNumber } from "../AnimatedNumber";
import TooltipPortal from "../TooltipPortal";
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const;
const THEMES = { light: "", dark: '[data-theme="dark"]' } as const;
export type ChartState = "loading" | "noData" | "invalid" | "loaded" | undefined;
@@ -20,8 +20,8 @@ import { useXAxisTicks } from "./useXAxisTicks";
import { useChartSync } from "./ChartSyncContext";
import { ZoomTooltip, useZoomHandlers } from "./ChartZoom";
// charcoal-500: dashed line mirroring the hovered x across synced charts.
const SYNC_LINE_COLOR = "#5F6570";
// Dashed line mirroring the hovered x across synced charts.
const SYNC_LINE_COLOR = "var(--color-text-faint)";
// Shared with ChartLine so bar/line align when toggling. Right margin keeps the
// centered last x-axis label from clipping; bottom gives angled labels room.
@@ -228,7 +228,7 @@ export function ChartBarRenderer({
onClick={zoomHandlers.onClick}
onMouseLeave={handleMouseLeave}
>
<CartesianGrid vertical={false} stroke="#272A2E" />
<CartesianGrid vertical={false} stroke="var(--color-grid-bright)" />
<XAxis
dataKey={dataKey}
tickLine={false}
@@ -237,7 +237,7 @@ export function ChartBarRenderer({
ticks={baseXTicks}
interval={baseXInterval}
tick={{
fill: "#878C99",
fill: "var(--color-text-dimmed)",
fontSize: 11,
style: { fontVariantNumeric: "tabular-nums" },
}}
@@ -249,7 +249,7 @@ export function ChartBarRenderer({
tickMargin={8}
width={computedYAxisWidth}
tick={{
fill: "#878C99",
fill: "var(--color-text-dimmed)",
fontSize: 11,
style: { fontVariantNumeric: "tabular-nums" },
}}
@@ -288,7 +288,7 @@ export function ChartBarRenderer({
x1={zoom.refAreaLeft}
x2={zoom.refAreaRight}
strokeOpacity={0.4}
fill="#3B82F6"
fill="var(--color-pending)"
fillOpacity={0.3}
/>
)}
@@ -332,9 +332,9 @@ export function ChartBarRenderer({
x1={syncZoomSelection.start}
x2={syncZoomSelection.current}
isFront
stroke="#3B82F6"
stroke="var(--color-pending)"
strokeOpacity={0.3}
fill="#3B82F6"
fill="var(--color-pending)"
fillOpacity={0.15}
className="pointer-events-none"
/>
@@ -360,11 +360,11 @@ export function ChartBarRenderer({
label={{
position: "top",
value: referenceLine.label,
fill: "#878C99",
fill: "var(--color-text-dimmed)",
fontSize: 11,
}}
isFront={true}
stroke="#3B3E45"
stroke="var(--color-border-bright)"
strokeDasharray="4 4"
className="pointer-events-none"
/>
@@ -374,7 +374,7 @@ export function ChartBarRenderer({
{enableZoom && zoom?.inspectionLine && (
<ReferenceLine
x={zoom.inspectionLine}
stroke="#D7D9DD"
stroke="var(--color-text-bright)"
strokeWidth={2}
isFront={true}
onClick={(e: any) => {
@@ -1,17 +1,18 @@
/** Shared status → color map for the task/agent activity charts. */
/** Shared status → color map for the task/agent activity charts.
* Values are CSS variables so they follow the theme; CSS contexts only. */
export const STATUS_COLOR: Record<string, string> = {
// Run-status groups
COMPLETED: "#28BF5C",
RUNNING: "#3B82F6",
FAILED: "#E11D48",
CANCELED: "#878C99",
COMPLETED: "var(--color-success)",
RUNNING: "var(--color-pending)",
FAILED: "var(--color-error)",
CANCELED: "var(--color-text-dimmed)",
// Agent session statuses
ACTIVE: "#3B82F6",
CLOSED: "#28BF5C",
EXPIRED: "#878C99",
ACTIVE: "var(--color-pending)",
CLOSED: "var(--color-success)",
EXPIRED: "var(--color-text-dimmed)",
};
export const STATUS_COLOR_FALLBACK = "#9CA3AF";
export const STATUS_COLOR_FALLBACK = "var(--color-text-dimmed)";
export function statusColor(status: string): string {
return STATUS_COLOR[status] ?? STATUS_COLOR_FALLBACK;
@@ -290,40 +290,41 @@ export const runStatusTitleFromStatus: Record<TaskRunStatus, RunFriendlyStatus>
const titlesStatusesArray = Object.entries(runStatusTitleFromStatus);
/**
* Hex color for each TaskRunStatus, mirroring `runStatusClassNameColor` but as
* concrete hex values for non-CSS contexts (e.g. chart series colors).
* Chart series color for each TaskRunStatus, mirroring `runStatusClassNameColor`.
* Values are CSS variables (defined in tailwind.css) so they follow the theme;
* only usable in CSS contexts (SVG attributes, chart config styles).
*/
const RUN_STATUS_HEX_COLORS: Record<TaskRunStatus, string> = {
PENDING: "#5F6570", // charcoal-500
DELAYED: "#6B7580", // charcoal ~450
PENDING_VERSION: "#f59e0b", // amber-500
WAITING_FOR_DEPLOY: "#d97706", // amber-600
EXECUTING: "#3b82f6", // blue-500
RETRYING_AFTER_FAILURE: "#2f6fec", // blue ~550
DEQUEUED: "#4D8EF5", // blue ~475
WAITING_TO_RESUME: "#555D67", // charcoal ~550
PAUSED: "#fbbf24", // amber-400
CANCELED: "#78828C", // charcoal ~400
EXPIRED: "#848D96", // charcoal ~350
INTERRUPTED: "#D52C4D", // rose — evenly spaced (error)
COMPLETED_SUCCESSFULLY: "#28BF5C", // mint-500 (success)
COMPLETED_WITH_ERRORS: "#DE405C", // rose — evenly spaced (error)
SYSTEM_FAILURE: "#E7536C", // rose — evenly spaced (error)
CRASHED: "#cc193d", // rose — darkest (error)
TIMED_OUT: "#F0667B", // rose — lightest (error)
const RUN_STATUS_CHART_COLORS: Record<TaskRunStatus, string> = {
PENDING: "var(--color-run-pending)",
DELAYED: "var(--color-run-delayed)",
PENDING_VERSION: "var(--color-run-pending-version)",
WAITING_FOR_DEPLOY: "var(--color-run-waiting-for-deploy)",
EXECUTING: "var(--color-run-executing)",
RETRYING_AFTER_FAILURE: "var(--color-run-retrying-after-failure)",
DEQUEUED: "var(--color-run-dequeued)",
WAITING_TO_RESUME: "var(--color-run-waiting-to-resume)",
PAUSED: "var(--color-run-paused)",
CANCELED: "var(--color-run-canceled)",
EXPIRED: "var(--color-run-expired)",
INTERRUPTED: "var(--color-run-interrupted)",
COMPLETED_SUCCESSFULLY: "var(--color-run-completed-successfully)",
COMPLETED_WITH_ERRORS: "var(--color-run-completed-with-errors)",
SYSTEM_FAILURE: "var(--color-run-system-failure)",
CRASHED: "var(--color-run-crashed)",
TIMED_OUT: "var(--color-run-timed-out)",
};
/**
* Get the hex color for a run status value. Accepts either a raw TaskRunStatus
* Get the chart color for a run status value. Accepts either a raw TaskRunStatus
* (e.g. "COMPLETED_SUCCESSFULLY") or a friendly name (e.g. "Completed").
* Returns `undefined` when the value is not a recognised status.
*/
export function getRunStatusHexColor(value: string): string | undefined {
export function getRunStatusChartColor(value: string): string | undefined {
if (isTaskRunStatus(value)) {
return RUN_STATUS_HEX_COLORS[value];
return RUN_STATUS_CHART_COLORS[value];
}
if (isRunFriendlyStatus(value)) {
return RUN_STATUS_HEX_COLORS[runStatusFromFriendlyTitle(value)];
return RUN_STATUS_CHART_COLORS[runStatusFromFriendlyTitle(value)];
}
return undefined;
}
+2 -2
View File
@@ -93,7 +93,7 @@ export const shouldRevalidate: ShouldRevalidateFunction = (options) => {
export function ErrorBoundary() {
return (
<>
<html lang="en" className="h-full">
<html lang="en" className="h-full" data-theme="dark">
<head>
<meta charSet="utf-8" />
@@ -121,7 +121,7 @@ export default function App() {
return (
<>
<html lang="en" className="h-full">
<html lang="en" className="h-full" data-theme="dark">
<head>
<Meta />
<Links />
+58 -29
View File
@@ -212,22 +212,50 @@
}
/*
shadcn/ui tokens used by streamdown's internals. Inline so the HSL vars
resolve where the utility is used - .streamdown-container redefines them locally.
Run-status chart colors. In-between shades are intentional - statuses within
a family (blues, roses, charcoals) are evenly spaced so chart series stay
distinguishable. Referenced from JS via var(--color-run-*).
*/
@theme {
--color-run-pending: var(--color-charcoal-500);
--color-run-delayed: #6b7580;
--color-run-pending-version: var(--color-warning);
--color-run-waiting-for-deploy: #d97706;
--color-run-executing: var(--color-pending);
--color-run-retrying-after-failure: #2f6fec;
--color-run-dequeued: #4d8ef5;
--color-run-waiting-to-resume: #555d67;
--color-run-paused: #fbbf24;
--color-run-canceled: #78828c;
--color-run-expired: #848d96;
--color-run-interrupted: #d52c4d;
--color-run-completed-successfully: var(--color-success);
--color-run-completed-with-errors: #de405c;
--color-run-system-failure: #e7536c;
--color-run-crashed: #cc193d;
--color-run-timed-out: #f0667b;
}
/*
shadcn/ui tokens used by streamdown's internals. Inline so the vars resolve
where the utility is used - .streamdown-container redefines them locally.
*/
@theme inline {
--color-background: hsl(var(--background, 230 16% 9%));
--color-foreground: hsl(var(--foreground, 215 19% 87%));
--color-muted: hsl(var(--muted, 220 8% 17%));
--color-muted-foreground: hsl(var(--muted-foreground, 220 8% 57%));
--color-border: hsl(var(--border, 216 7% 27%));
--color-sidebar: hsl(var(--sidebar, 228 10% 11%));
--color-primary-foreground: hsl(var(--primary-foreground, 230 16% 9%));
--color-background: var(--background, #121317);
--color-foreground: var(--foreground, #d7d9dd);
--color-muted: var(--muted, #1c1e21);
--color-muted-foreground: var(--muted-foreground, #878c99);
--color-border: var(--border, #2c3034);
--color-sidebar: var(--sidebar, #15171a);
--color-primary-foreground: var(--primary-foreground, #121317);
}
@custom-variant lg-height (@media (max-height: 750px));
@custom-variant md-height (@media (max-height: 600px));
/* dark: follows the app theme (data-theme on <html>), not the OS preference */
@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
@utility focus-custom {
&:focus-visible {
outline: 1px solid var(--color-text-link);
@@ -328,6 +356,14 @@
@apply bg-text-bright/30 text-text-bright;
}
/* Native form controls, scrollbars etc. follow the app theme */
:root {
color-scheme: dark;
}
[data-theme="light"] {
color-scheme: light;
}
/* shadcn charts: https://ui.shadcn.com/docs/components/chart#add-a-grid */
:root {
--chart-1: 12 76% 61%;
@@ -337,7 +373,7 @@
--chart-5: 27 87% 67%;
}
.dark {
[data-theme="dark"] {
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
@@ -385,18 +421,18 @@
/* Streamdown markdown styling */
.streamdown-container {
/* Streamdown uses shadcn/ui CSS variables - define them for our theme.
These map Tailwind utility classes like bg-background, bg-primary, etc.
that streamdown uses internally for its link safety modal, code blocks,
and other interactive elements. */
--background: 230 16% 9%; /* charcoal-900 #121317 */
--foreground: 215 19% 87%; /* charcoal-200 #D7D9DD */
--muted: 220 8% 17%; /* charcoal-775 #1C1E21 */
--muted-foreground: 220 8% 57%; /* charcoal-400 #878C99 */
--border: 216 7% 27%; /* charcoal-650 #2C3034 */
--primary: 95 100% 66%; /* apple-500 #A8FF53 */
--primary-foreground: 230 16% 9%; /* charcoal-900 */
--sidebar: 228 10% 11%; /* charcoal-850 #15171A */
/* Streamdown uses shadcn/ui CSS variables - derive them from our semantic
tokens so they follow the theme. They map Tailwind utility classes like
bg-background, bg-primary, etc. that streamdown uses internally for its
link safety modal, code blocks, and other interactive elements. */
--background: var(--color-background-deep);
--foreground: var(--color-text-bright);
--muted: var(--color-charcoal-775);
--muted-foreground: var(--color-text-dimmed);
--border: var(--color-border-bright);
--primary: var(--color-primary);
--primary-foreground: var(--color-background-deep);
--sidebar: var(--color-background-dimmed);
/* Code block styling */
& [data-code-block-container] {
@@ -479,13 +515,6 @@
& [data-code-block-header] {
@apply bg-charcoal-800 text-text-dimmed border-b border-charcoal-700;
}
/* Hide light mode code block, show dark mode */
& [data-code-block].dark\:hidden {
display: none !important;
}
& [data-code-block].hidden.dark\:block {
display: block !important;
}
/* Override the bg-muted/40 class to let inline styles work */
& [data-code-block] pre {
background-color: inherit !important;