From 899dcbabb0abe851fd1b3f8fef5adfacde2abc67 Mon Sep 17 00:00:00 2001 From: Katia Bulatova Date: Fri, 3 Jul 2026 12:45:52 +0000 Subject: [PATCH] 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 (set to dark) instead of the OS preference, with color-scheme following the theme. --- .../app/components/code/QueryResultsChart.tsx | 4 +- .../components/primitives/charts/Chart.tsx | 2 +- .../components/primitives/charts/ChartBar.tsx | 22 ++--- .../primitives/charts/statusColors.ts | 19 ++-- .../app/components/runs/v3/TaskRunStatus.tsx | 49 ++++++----- apps/webapp/app/root.tsx | 4 +- apps/webapp/app/tailwind.css | 87 ++++++++++++------- 7 files changed, 109 insertions(+), 78 deletions(-) diff --git a/apps/webapp/app/components/code/QueryResultsChart.tsx b/apps/webapp/app/components/code/QueryResultsChart.tsx index fd7b3c32f..ef4030b10 100644 --- a/apps/webapp/app/components/code/QueryResultsChart.tsx +++ b/apps/webapp/app/components/code/QueryResultsChart.tsx @@ -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] = { diff --git a/apps/webapp/app/components/primitives/charts/Chart.tsx b/apps/webapp/app/components/primitives/charts/Chart.tsx index ed23d41cd..85d283368 100644 --- a/apps/webapp/app/components/primitives/charts/Chart.tsx +++ b/apps/webapp/app/components/primitives/charts/Chart.tsx @@ -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; diff --git a/apps/webapp/app/components/primitives/charts/ChartBar.tsx b/apps/webapp/app/components/primitives/charts/ChartBar.tsx index ef9daeb53..73ffb35c5 100644 --- a/apps/webapp/app/components/primitives/charts/ChartBar.tsx +++ b/apps/webapp/app/components/primitives/charts/ChartBar.tsx @@ -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} > - + )} @@ -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 && ( { diff --git a/apps/webapp/app/components/primitives/charts/statusColors.ts b/apps/webapp/app/components/primitives/charts/statusColors.ts index 2ae7470a2..8439b4d32 100644 --- a/apps/webapp/app/components/primitives/charts/statusColors.ts +++ b/apps/webapp/app/components/primitives/charts/statusColors.ts @@ -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 = { // 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; diff --git a/apps/webapp/app/components/runs/v3/TaskRunStatus.tsx b/apps/webapp/app/components/runs/v3/TaskRunStatus.tsx index bccb30a32..9d20f5a52 100644 --- a/apps/webapp/app/components/runs/v3/TaskRunStatus.tsx +++ b/apps/webapp/app/components/runs/v3/TaskRunStatus.tsx @@ -290,40 +290,41 @@ export const runStatusTitleFromStatus: Record 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 = { - 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 = { + 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; } diff --git a/apps/webapp/app/root.tsx b/apps/webapp/app/root.tsx index 66a33b5ce..b1027c498 100644 --- a/apps/webapp/app/root.tsx +++ b/apps/webapp/app/root.tsx @@ -93,7 +93,7 @@ export const shouldRevalidate: ShouldRevalidateFunction = (options) => { export function ErrorBoundary() { return ( <> - + @@ -121,7 +121,7 @@ export default function App() { return ( <> - + diff --git a/apps/webapp/app/tailwind.css b/apps/webapp/app/tailwind.css index 7d663fb54..d1bf953a3 100644 --- a/apps/webapp/app/tailwind.css +++ b/apps/webapp/app/tailwind.css @@ -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 ), 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;