Add light mode support with theme toggle

Introduce semantic CSS custom properties (page, panel, fg, line, overlay,
etc.) that swap values via .light/.dark class on <html>. An inline script
reads localStorage / prefers-color-scheme before first paint to prevent
flash-of-wrong-theme.

- ThemeProvider + useTheme hook in app/lib/theme.tsx
- Sun/Moon toggle in desktop nav and mobile menu
- .light overrides for all semantic tokens, accent colors (WCAG AA on
  white), atmosphere gradient, @pierre/diffs surfaces, and chart variables
- Migrated ~30 files from hardcoded color classes to semantic tokens
- GraphViz diagrams use getGraphTheme() for light/dark hex maps
- Chart gridlines and axis labels use CSS custom properties
- Workflow card colors reference CSS vars for automatic theme switching
- Pierre diffs switch between pierre-dark and pierre-light themes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-01 18:06:45 -05:00
parent cab25a3e7e
commit 2ed164d190
32 changed files with 792 additions and 545 deletions

View file

@ -24,17 +24,103 @@
--color-mint: #5AC8A8;
--color-amber: #F0A45B;
--color-coral: #E86B6B;
/* ── Semantic tokens (dark defaults) ── */
--color-page: #0F1729;
--color-panel: #252C3D;
--color-panel-alt: #1a2133;
--color-overlay: rgba(255, 255, 255, 0.04);
--color-overlay-strong: rgba(255, 255, 255, 0.10);
--color-fg: #ffffff;
--color-fg-2: #E8EDF3;
--color-fg-3: #A8B5C5;
--color-fg-muted: #4B5768;
--color-line: rgba(255, 255, 255, 0.06);
--color-line-strong: rgba(255, 255, 255, 0.12);
--color-focus: rgba(103, 178, 215, 0.4);
}
/* ── Light theme overrides ── */
.light {
/* Semantic tokens */
--color-page: #ffffff;
--color-panel: #f8fafc;
--color-panel-alt: #f1f5f9;
--color-overlay: rgba(0, 0, 0, 0.04);
--color-overlay-strong: rgba(0, 0, 0, 0.10);
--color-fg: #0f172a;
--color-fg-2: #1e293b;
--color-fg-3: #475569;
--color-fg-muted: #94a3b8;
--color-line: rgba(0, 0, 0, 0.08);
--color-line-strong: rgba(0, 0, 0, 0.15);
--color-focus: rgba(14, 116, 144, 0.5);
/* Accent — darkened for WCAG AA on white */
--color-mint: #0d9488;
--color-amber: #d97706;
--color-coral: #dc2626;
--color-teal-300: #0891b2;
--color-teal-500: #0e7490;
/* @pierre/diffs — light surfaces */
--diffs-bg-buffer-override: #ffffff;
--diffs-bg-context-override: #f8fafc;
--diffs-bg-hover-override: #f1f5f9;
--diffs-bg-separator-override: #e2e8f0;
--diffs-fg-number-override: #94a3b8;
--diffs-addition-color-override: #0d9488;
--diffs-fg-number-addition-override: rgba(13, 148, 136, 0.5);
--diffs-deletion-color-override: #dc2626;
--diffs-fg-number-deletion-override: rgba(220, 38, 38, 0.5);
/* Chart variables */
--chart-gridline: rgba(0, 0, 0, 0.06);
--chart-axis-label: rgba(71, 85, 105, 1);
}
/* ── Chart variables (dark defaults) ── */
:root {
--chart-gridline: rgba(255, 255, 255, 0.04);
--chart-axis-label: rgba(75, 87, 104, 1);
}
/* ── Background atmosphere ── */
.bg-atmosphere {
background-color: var(--color-navy-950);
background-color: var(--color-page);
background-image:
radial-gradient(ellipse 120% 60% at 15% 5%, rgba(53, 127, 158, 0.12) 0%, transparent 50%),
radial-gradient(ellipse 80% 50% at 85% 90%, rgba(90, 200, 168, 0.07) 0%, transparent 45%);
background-attachment: fixed;
}
.light .bg-atmosphere,
.light.bg-atmosphere {
background-image:
radial-gradient(ellipse 120% 60% at 15% 5%, rgba(14, 116, 144, 0.06) 0%, transparent 50%),
radial-gradient(ellipse 80% 50% at 85% 90%, rgba(13, 148, 136, 0.04) 0%, transparent 45%);
}
.bg-atmosphere::before {
content: "";
position: fixed;
inset: 0;
z-index: 1;
pointer-events: none;
opacity: 0.07;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
background-repeat: repeat;
background-size: 200px 200px;
}
.light .bg-atmosphere::before,
.light.bg-atmosphere::before {
opacity: 0.03;
}
/* ── @pierre/diffs brand overrides ── */
:root {
@ -60,15 +146,3 @@
--diffs-deletion-color-override: #E86B6B;
--diffs-fg-number-deletion-override: rgba(232, 107, 107, 0.5);
}
.bg-atmosphere::before {
content: "";
position: fixed;
inset: 0;
z-index: 1;
pointer-events: none;
opacity: 0.07;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
background-repeat: repeat;
background-size: 200px 200px;
}

View file

@ -2,6 +2,7 @@ import { useState } from "react";
import { ChevronRightIcon } from "@heroicons/react/20/solid";
import type { FileContents } from "@pierre/diffs";
import { File } from "@pierre/diffs/react";
import { useTheme } from "../lib/theme";
export function CollapsibleFile({
file,
@ -11,32 +12,33 @@ export function CollapsibleFile({
defaultOpen?: boolean;
}) {
const [open, setOpen] = useState(defaultOpen);
const { theme } = useTheme();
const lines = file.contents.split("\n");
const lineCount = lines.length;
const loc = lines.filter((l) => l.trim().length > 0).length;
return (
<div className="rounded-md border border-white/[0.06] bg-navy-800/50 overflow-hidden">
<div className="rounded-md border border-line bg-panel/50 overflow-hidden">
<button
type="button"
onClick={() => setOpen((v) => !v)}
className="flex w-full items-center gap-2 px-4 py-2.5 text-left hover:bg-white/[0.02] transition-colors"
className="flex w-full items-center gap-2 px-4 py-2.5 text-left hover:bg-overlay transition-colors"
>
<ChevronRightIcon
className={`size-4 text-navy-600 transition-transform duration-150 ${open ? "rotate-90" : ""}`}
className={`size-4 text-fg-muted transition-transform duration-150 ${open ? "rotate-90" : ""}`}
/>
<span className="font-mono text-xs text-navy-600">{file.name}</span>
<span className="ml-auto font-mono text-xs text-navy-600/60">
<span className="font-mono text-xs text-fg-muted">{file.name}</span>
<span className="ml-auto font-mono text-xs text-fg-muted/60">
{lineCount} lines ({loc} loc)
</span>
</button>
<div className={open ? "" : "hidden"}>
<div className="border-t border-white/[0.06]" />
<div className="border-t border-line" />
<File
file={file}
options={{ theme: "pierre-dark", disableFileHeader: true }}
options={{ theme: theme === "dark" ? "pierre-dark" : "pierre-light", disableFileHeader: true }}
/>
</div>
</div>

View file

@ -7,7 +7,7 @@ import { dotLanguage } from "./dot-grammar";
export async function registerDotLanguage(): Promise<void> {
const highlighter = await getSharedHighlighter({
themes: ["pierre-dark"],
themes: ["pierre-dark", "pierre-light"],
langs: [],
});
attachResolvedLanguages(

View file

@ -33,10 +33,10 @@ export const statusConfig = {
},
na: {
label: "N/A",
color: "text-navy-600",
bg: "bg-white/[0.04]",
dot: "bg-navy-600",
border: "border-l-navy-600/50",
color: "text-fg-muted",
bg: "bg-overlay",
dot: "bg-fg-muted",
border: "border-l-fg-muted/50",
},
} as const satisfies Record<
VerificationStatus,
@ -157,7 +157,7 @@ export interface CriterionPerformance {
export const modeConfig = {
active: { label: "Active", color: "text-mint", bg: "bg-mint/10" },
evaluate: { label: "Evaluate", color: "text-amber", bg: "bg-amber/10" },
disabled: { label: "Disabled", color: "text-navy-600", bg: "bg-white/[0.04]" },
disabled: { label: "Disabled", color: "text-fg-muted", bg: "bg-overlay" },
} as const satisfies Record<
VerificationMode,
{ label: string; color: string; bg: string }

View file

@ -13,12 +13,15 @@ import {
CheckBadgeIcon,
Cog6ToothIcon,
LightBulbIcon,
MoonIcon,
PlayIcon,
RectangleStackIcon,
SparklesIcon,
SunIcon,
XMarkIcon,
} from "@heroicons/react/24/outline";
import { Link, Outlet, useLocation, useMatches } from "react-router";
import { useTheme } from "../lib/theme";
const user = {
name: "Tom Cook",
@ -46,6 +49,7 @@ function classNames(...classes: Array<string | false | null | undefined>) {
export default function AppShell() {
const { pathname } = useLocation();
const matches = useMatches();
const { theme, toggle } = useTheme();
const currentNav = navigation.find((item) => pathname.startsWith(item.href));
const title = currentNav?.name ?? "";
const lastMatch = matches[matches.length - 1];
@ -55,9 +59,11 @@ export default function AppShell() {
const wide = matches.some((m) => (m.handle as { wide?: boolean } | undefined)?.wide);
const maxWidth = wide ? "" : "max-w-5xl";
const ThemeIcon = theme === "dark" ? SunIcon : MoonIcon;
return (
<div className="min-h-full">
<Disclosure as="nav" className="bg-navy-800/50">
<Disclosure as="nav" className="bg-panel/50">
<div className="px-4 sm:px-6 lg:px-8">
<div className="flex h-16 items-center justify-between">
<div className="flex items-center">
@ -77,8 +83,8 @@ export default function AppShell() {
aria-current={current ? "page" : undefined}
className={classNames(
current
? "bg-navy-950/50 text-white"
: "text-ice-300 hover:bg-white/5 hover:text-white",
? "bg-page/50 text-fg"
: "text-fg-3 hover:bg-overlay hover:text-fg",
"inline-flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium",
)}
>
@ -91,7 +97,16 @@ export default function AppShell() {
</div>
</div>
<div className="hidden md:block">
<div className="ml-4 flex items-center md:ml-6">
<div className="ml-4 flex items-center gap-3 md:ml-6">
<button
type="button"
onClick={toggle}
className="rounded-full p-1.5 text-fg-muted transition-colors hover:bg-overlay hover:text-fg"
title={theme === "dark" ? "Switch to light mode" : "Switch to dark mode"}
>
<ThemeIcon className="size-5" aria-hidden="true" />
<span className="sr-only">Toggle theme</span>
</button>
<Menu as="div" className="relative">
<MenuButton className="relative flex max-w-xs items-center rounded-full focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-500">
<span className="absolute -inset-1.5" />
@ -99,19 +114,19 @@ export default function AppShell() {
<img
alt=""
src={user.imageUrl}
className="size-8 rounded-full outline -outline-offset-1 outline-white/10"
className="size-8 rounded-full outline -outline-offset-1 outline-line-strong"
/>
</MenuButton>
<MenuItems
transition
className="absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-navy-800 py-1 outline-1 -outline-offset-1 outline-white/10 transition data-closed:scale-95 data-closed:transform data-closed:opacity-0 data-enter:duration-100 data-enter:ease-out data-leave:duration-75 data-leave:ease-in"
className="absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-panel py-1 outline-1 -outline-offset-1 outline-line-strong transition data-closed:scale-95 data-closed:transform data-closed:opacity-0 data-enter:duration-100 data-enter:ease-out data-leave:duration-75 data-leave:ease-in"
>
{userNavigation.map((item) => (
<MenuItem key={item.name}>
<a
href={item.href}
className="block px-4 py-2 text-sm text-ice-300 data-focus:bg-white/5 data-focus:outline-hidden"
className="block px-4 py-2 text-sm text-fg-3 data-focus:bg-overlay data-focus:outline-hidden"
>
{item.name}
</a>
@ -122,7 +137,7 @@ export default function AppShell() {
</div>
</div>
<div className="-mr-2 flex md:hidden">
<DisclosureButton className="group relative inline-flex items-center justify-center rounded-md p-2 text-navy-600 hover:bg-white/5 hover:text-white focus:outline-2 focus:outline-offset-2 focus:outline-teal-500">
<DisclosureButton className="group relative inline-flex items-center justify-center rounded-md p-2 text-fg-muted hover:bg-overlay hover:text-fg focus:outline-2 focus:outline-offset-2 focus:outline-teal-500">
<span className="absolute -inset-0.5" />
<span className="sr-only">Open main menu</span>
<Bars3Icon
@ -150,8 +165,8 @@ export default function AppShell() {
aria-current={current ? "page" : undefined}
className={classNames(
current
? "bg-navy-950 text-white"
: "text-ice-300 hover:bg-white/5 hover:text-white",
? "bg-page text-fg"
: "text-fg-3 hover:bg-overlay hover:text-fg",
"flex items-center gap-2 rounded-md px-3 py-2 text-base font-medium",
)}
>
@ -161,23 +176,32 @@ export default function AppShell() {
);
})}
</div>
<div className="border-t border-white/10 pt-4 pb-3">
<div className="border-t border-line-strong pt-4 pb-3">
<div className="flex items-center px-5">
<div className="shrink-0">
<img
alt=""
src={user.imageUrl}
className="size-10 rounded-full outline -outline-offset-1 outline-white/10"
className="size-10 rounded-full outline -outline-offset-1 outline-line-strong"
/>
</div>
<div className="ml-3">
<div className="text-base font-medium text-white">
<div className="text-base font-medium text-fg">
{user.name}
</div>
<div className="text-sm font-medium text-navy-600">
<div className="text-sm font-medium text-fg-muted">
{user.email}
</div>
</div>
<button
type="button"
onClick={toggle}
className="ml-auto rounded-full p-1.5 text-fg-muted transition-colors hover:bg-overlay hover:text-fg"
title={theme === "dark" ? "Switch to light mode" : "Switch to dark mode"}
>
<ThemeIcon className="size-5" aria-hidden="true" />
<span className="sr-only">Toggle theme</span>
</button>
</div>
<div className="mt-3 space-y-1 px-2">
{userNavigation.map((item) => (
@ -185,7 +209,7 @@ export default function AppShell() {
key={item.name}
as="a"
href={item.href}
className="block rounded-md px-3 py-2 text-base font-medium text-navy-600 hover:bg-white/5 hover:text-white"
className="block rounded-md px-3 py-2 text-base font-medium text-fg-muted hover:bg-overlay hover:text-fg"
>
{item.name}
</DisclosureButton>
@ -196,10 +220,10 @@ export default function AppShell() {
</Disclosure>
{!hideHeader && (
<header className="relative bg-navy-800 after:pointer-events-none after:absolute after:inset-x-0 after:inset-y-0 after:bottom-0 after:border-y after:border-white/10">
<header className="relative bg-panel after:pointer-events-none after:absolute after:inset-x-0 after:inset-y-0 after:bottom-0 after:border-y after:border-line-strong">
<div className={`mx-auto ${maxWidth} px-4 py-4 sm:px-6 lg:px-8`}>
<div className="flex items-center">
<h1 className="text-lg/6 font-semibold text-white">{title}</h1>
<h1 className="text-lg/6 font-semibold text-fg">{title}</h1>
{headerExtra && <div className="ml-auto">{headerExtra}</div>}
</div>
</div>

View file

@ -0,0 +1,70 @@
import type { Theme } from "./theme";
interface GraphTheme {
fontcolor: string;
edgeColor: string;
nodeFill: string;
nodeText: string;
startFill: string;
startBorder: string;
startText: string;
gateFill: string;
gateBorder: string;
gateText: string;
completedFill: string;
completedBorder: string;
completedText: string;
runningFill: string;
runningBorder: string;
runningText: string;
runningPulseFill: string;
runningPulseStroke: string;
}
const dark: GraphTheme = {
fontcolor: "#5a7a94",
edgeColor: "#2a3f52",
nodeFill: "#1a2b3c",
nodeText: "#c6d4e0",
startFill: "#0d4f4f",
startBorder: "#14b8a6",
startText: "#5eead4",
gateFill: "#1a2030",
gateBorder: "#f59e0b",
gateText: "#fbbf24",
completedFill: "#0a2a20",
completedBorder: "#34d399",
completedText: "#6ee7b7",
runningFill: "#0d3a3a",
runningBorder: "#14b8a6",
runningText: "#5eead4",
runningPulseFill: "#134e4a",
runningPulseStroke: "#5eead4",
};
const light: GraphTheme = {
fontcolor: "#475569",
edgeColor: "#cbd5e1",
nodeFill: "#f1f5f9",
nodeText: "#1e293b",
startFill: "#ccfbf1",
startBorder: "#0d9488",
startText: "#0d9488",
gateFill: "#fef3c7",
gateBorder: "#d97706",
gateText: "#d97706",
completedFill: "#d1fae5",
completedBorder: "#059669",
completedText: "#059669",
runningFill: "#ccfbf1",
runningBorder: "#0d9488",
runningText: "#0d9488",
runningPulseFill: "#99f6e4",
runningPulseStroke: "#0d9488",
};
export function getGraphTheme(theme: Theme): GraphTheme {
return theme === "dark" ? dark : light;
}
export type { GraphTheme };

View file

@ -0,0 +1,53 @@
import { createContext, useCallback, useContext, useEffect, useState } from "react";
type Theme = "light" | "dark";
const STORAGE_KEY = "arc-theme";
function getInitialTheme(): Theme {
if (typeof window === "undefined") return "dark";
const stored = localStorage.getItem(STORAGE_KEY);
if (stored === "light" || stored === "dark") return stored;
return window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
}
function applyThemeClass(theme: Theme) {
const root = document.documentElement;
root.classList.remove("light", "dark");
root.classList.add(theme);
}
const ThemeContext = createContext<{
theme: Theme;
toggle: () => void;
}>({ theme: "dark", toggle: () => {} });
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>(getInitialTheme);
useEffect(() => {
applyThemeClass(theme);
}, [theme]);
const toggle = useCallback(() => {
setTheme((prev) => {
const next = prev === "dark" ? "light" : "dark";
localStorage.setItem(STORAGE_KEY, next);
return next;
});
}, []);
return (
<ThemeContext value={{ theme, toggle }}>
{children}
</ThemeContext>
);
}
export function useTheme() {
return useContext(ThemeContext);
}
export type { Theme };

View file

@ -8,8 +8,11 @@ import {
} from "react-router";
import type { Route } from "./+types/root";
import { ThemeProvider } from "./lib/theme";
import "./app.css";
const themeScript = `(function(){try{var t=localStorage.getItem("arc-theme");if(t!=="light"&&t!=="dark")t=window.matchMedia("(prefers-color-scheme:dark)").matches?"dark":"light";document.documentElement.classList.add(t)}catch(e){document.documentElement.classList.add("dark")}})()`;
export const links: Route.LinksFunction = () => [
{ rel: "icon", href: "/favicon.svg", type: "image/svg+xml" },
{ rel: "icon", href: "/favicon.ico", sizes: "32x32" },
@ -30,6 +33,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className="h-full bg-atmosphere">
<head>
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
@ -45,7 +49,11 @@ export function Layout({ children }: { children: React.ReactNode }) {
}
export default function App() {
return <Outlet />;
return (
<ThemeProvider>
<Outlet />
</ThemeProvider>
);
}
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {

View file

@ -141,7 +141,7 @@ function BarChart({ result }: { result: QueryResult }) {
if (valueCols.length === 0 || result.rows.length === 0) {
return (
<div className="flex h-64 items-center justify-center text-sm text-navy-600">
<div className="flex h-64 items-center justify-center text-sm text-fg-muted">
No numeric columns to chart
</div>
);
@ -185,14 +185,14 @@ function BarChart({ result }: { result: QueryResult }) {
y1={y}
x2={containerWidth - padding.right}
y2={y}
stroke="rgba(255,255,255,0.04)"
stroke="var(--chart-gridline)"
strokeDasharray="4,4"
/>
<text
x={yAxisWidth - 8}
y={y + 4}
textAnchor="end"
fill="rgba(75, 87, 104, 1)"
fill="var(--chart-axis-label)"
fontSize="11"
fontFamily="JetBrains Mono, monospace"
>
@ -232,7 +232,7 @@ function BarChart({ result }: { result: QueryResult }) {
x={x + barWidth / 2}
y={chartHeight - padding.bottom + 16}
textAnchor="middle"
fill="rgba(75, 87, 104, 1)"
fill="var(--chart-axis-label)"
fontSize="10"
fontFamily="JetBrains Mono, monospace"
>
@ -246,7 +246,7 @@ function BarChart({ result }: { result: QueryResult }) {
</svg>
)}
<div className="mt-1 flex items-center gap-4 px-2">
<span className="font-mono text-[10px] uppercase tracking-wider text-navy-600">
<span className="font-mono text-[10px] uppercase tracking-wider text-fg-muted">
{valueCol.replace(/_/g, " ")}
</span>
</div>
@ -261,11 +261,11 @@ function ResultTable({ result }: { result: QueryResult }) {
<div className="overflow-x-auto">
<table className="w-full text-left">
<thead>
<tr className="border-b border-white/[0.06]">
<tr className="border-b border-line">
{result.columns.map((col) => (
<th
key={col}
className="whitespace-nowrap px-3 py-2.5 font-mono text-[11px] font-semibold uppercase tracking-wider text-navy-600"
className="whitespace-nowrap px-3 py-2.5 font-mono text-[11px] font-semibold uppercase tracking-wider text-fg-muted"
>
{col}
</th>
@ -276,7 +276,7 @@ function ResultTable({ result }: { result: QueryResult }) {
{result.rows.map((row, i) => (
<tr
key={i}
className="border-b border-white/[0.03] transition-colors hover:bg-white/[0.02]"
className="border-b border-line transition-colors hover:bg-overlay"
>
{result.columns.map((col) => {
const val = row[col];
@ -285,7 +285,7 @@ function ResultTable({ result }: { result: QueryResult }) {
<td
key={col}
className={`whitespace-nowrap px-3 py-2 font-mono text-xs ${
isNum ? "tabular-nums text-ice-100" : "text-ice-300"
isNum ? "tabular-nums text-fg-2" : "text-fg-3"
}`}
>
{isNum ? formatNumber(val) : String(val)}
@ -344,11 +344,11 @@ function SqlEditor({
};
return (
<div className="relative flex overflow-hidden rounded-md border border-white/[0.06] bg-navy-950/80 font-mono text-sm">
<div className="relative flex overflow-hidden rounded-md border border-line bg-page/80 font-mono text-sm">
{/* Line numbers */}
<div
ref={lineNumbersRef}
className="pointer-events-none flex shrink-0 flex-col overflow-hidden border-r border-white/[0.04] bg-navy-950/60 px-3 py-3 text-right leading-[1.625rem] text-navy-600 select-none"
className="pointer-events-none flex shrink-0 flex-col overflow-hidden border-r border-line bg-page/60 px-3 py-3 text-right leading-[1.625rem] text-fg-muted select-none"
aria-hidden="true"
>
{Array.from({ length: lineCount }, (_, i) => (
@ -367,7 +367,7 @@ function SqlEditor({
spellCheck={false}
autoCapitalize="off"
autoCorrect="off"
className="min-h-[7lh] w-full resize-y bg-transparent px-4 py-3 leading-[1.625rem] text-ice-100 placeholder-navy-600 outline-none"
className="min-h-[7lh] w-full resize-y bg-transparent px-4 py-3 leading-[1.625rem] text-fg-2 placeholder-fg-muted outline-none"
placeholder="SELECT * FROM runs LIMIT 10"
/>
</div>
@ -441,11 +441,11 @@ export default function InsightsEditor() {
}
}}
placeholder="Untitled query"
className="min-w-0 max-w-xs rounded border border-teal-500/40 bg-navy-950/60 px-2 py-0.5 text-sm font-medium text-ice-100 placeholder-navy-600 outline-none"
className="min-w-0 max-w-xs rounded border border-focus bg-page/60 px-2 py-0.5 text-sm font-medium text-fg-2 placeholder-fg-muted outline-none"
/>
) : (
<div className="flex items-center gap-1.5">
<span className="text-sm font-medium text-ice-100">
<span className="text-sm font-medium text-fg-2">
{queryName || "Untitled query"}
</span>
<button
@ -454,7 +454,7 @@ export default function InsightsEditor() {
setIsEditingName(true);
requestAnimationFrame(() => nameInputRef.current?.select());
}}
className="rounded p-0.5 text-navy-600 transition-colors hover:bg-white/[0.05] hover:text-ice-300"
className="rounded p-0.5 text-fg-muted transition-colors hover:bg-overlay hover:text-fg-3"
>
<PencilIcon className="size-3.5" />
</button>
@ -468,7 +468,7 @@ export default function InsightsEditor() {
<button
type="button"
onClick={() => setShowAiDialog(true)}
className="inline-flex items-center gap-1.5 rounded-md border border-white/[0.06] px-3 py-1.5 text-sm font-medium text-ice-300 transition-colors hover:border-teal-500/30 hover:bg-white/[0.03] hover:text-white"
className="inline-flex items-center gap-1.5 rounded-md border border-line px-3 py-1.5 text-sm font-medium text-fg-3 transition-colors hover:border-teal-500/30 hover:bg-overlay hover:text-fg"
>
<SparklesIcon className="size-3.5 text-teal-500" />
SQL AI
@ -477,7 +477,7 @@ export default function InsightsEditor() {
{/* Save */}
<button
type="button"
className="inline-flex items-center gap-1.5 rounded-md border border-white/[0.06] px-3 py-1.5 text-sm font-medium text-ice-300 transition-colors hover:border-white/[0.12] hover:bg-white/[0.03] hover:text-white"
className="inline-flex items-center gap-1.5 rounded-md border border-line px-3 py-1.5 text-sm font-medium text-fg-3 transition-colors hover:border-line-strong hover:bg-overlay hover:text-fg"
>
<BookmarkIcon className="size-3.5" />
Save
@ -488,7 +488,7 @@ export default function InsightsEditor() {
type="button"
onClick={runQuery}
disabled={isRunning || sql.trim().length === 0}
className="inline-flex items-center gap-1.5 rounded-md border border-mint/20 bg-mint/5 px-3.5 py-1.5 text-sm font-medium text-mint transition-all hover:border-mint/50 hover:bg-mint/10 hover:text-white disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:border-mint/20 disabled:hover:bg-mint/5 disabled:hover:text-mint"
className="inline-flex items-center gap-1.5 rounded-md border border-mint/20 bg-mint/5 px-3.5 py-1.5 text-sm font-medium text-mint transition-all hover:border-mint/50 hover:bg-mint/10 hover:text-fg disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:border-mint/20 disabled:hover:bg-mint/5 disabled:hover:text-mint"
>
{isRunning ? (
<ArrowPathIcon className="size-3.5 animate-spin" />
@ -496,7 +496,7 @@ export default function InsightsEditor() {
<PlayIcon className="size-3.5" />
)}
{isRunning ? "Running\u2026" : "Run"}
<kbd className="ml-1 hidden rounded border border-white/[0.08] bg-white/[0.04] px-1 py-0.5 font-sans text-[10px] leading-none text-navy-600 sm:inline">
<kbd className="ml-1 hidden rounded border border-line-strong bg-overlay px-1 py-0.5 font-sans text-[10px] leading-none text-fg-muted sm:inline">
{"\u2318\u21B5"}
</kbd>
</button>
@ -511,37 +511,37 @@ export default function InsightsEditor() {
{/* Results bar */}
<div className="flex items-center justify-between">
{/* Query stats */}
<div className="flex items-center gap-5 font-mono text-[11px] tabular-nums text-navy-600">
<div className="flex items-center gap-5 font-mono text-[11px] tabular-nums text-fg-muted">
<span>
Elapsed:{" "}
<span className="text-ice-300">
<span className="text-fg-3">
{result.elapsed.toFixed(3)}s
</span>
</span>
<span>
Read:{" "}
<span className="text-ice-300">
<span className="text-fg-3">
{formatNumber(result.rowsRead)} rows
</span>{" "}
({formatBytes(result.bytesRead)})
</span>
<span>
Returned:{" "}
<span className="text-ice-300">
<span className="text-fg-3">
{formatNumber(result.rowsReturned)} rows
</span>
</span>
</div>
{/* View toggle */}
<div className="flex items-center gap-1 rounded-md border border-white/[0.06] bg-navy-800/80 p-0.5">
<div className="flex items-center gap-1 rounded-md border border-line bg-panel/80 p-0.5">
<button
type="button"
onClick={() => setResultView("chart")}
className={`inline-flex items-center gap-1.5 rounded px-2.5 py-1 text-xs font-medium transition-colors ${
resultView === "chart"
? "bg-white/[0.06] text-teal-500"
: "text-navy-600 hover:text-ice-300"
? "bg-overlay text-teal-500"
: "text-fg-muted hover:text-fg-3"
}`}
>
<ChartBarIcon className="size-3.5" />
@ -552,8 +552,8 @@ export default function InsightsEditor() {
onClick={() => setResultView("table")}
className={`inline-flex items-center gap-1.5 rounded px-2.5 py-1 text-xs font-medium transition-colors ${
resultView === "table"
? "bg-white/[0.06] text-teal-500"
: "text-navy-600 hover:text-ice-300"
? "bg-overlay text-teal-500"
: "text-fg-muted hover:text-fg-3"
}`}
>
<TableCellsIcon className="size-3.5" />
@ -563,7 +563,7 @@ export default function InsightsEditor() {
</div>
{/* Results content */}
<div className="rounded-md border border-white/[0.06] bg-navy-800/60 p-4">
<div className="rounded-md border border-line bg-panel/60 p-4">
{resultView === "chart" ? (
<BarChart result={result} />
) : (
@ -575,8 +575,8 @@ export default function InsightsEditor() {
{/* ── Running overlay ── */}
{isRunning && !result && (
<div className="flex h-48 items-center justify-center rounded-md border border-white/[0.06] bg-navy-800/60">
<div className="flex items-center gap-3 text-sm text-navy-600">
<div className="flex h-48 items-center justify-center rounded-md border border-line bg-panel/60">
<div className="flex items-center gap-3 text-sm text-fg-muted">
<ArrowPathIcon className="size-5 animate-spin text-teal-500" />
Executing query&hellip;
</div>
@ -594,36 +594,36 @@ export default function InsightsEditor() {
aria-hidden="true"
/>
<div className="fixed inset-0 flex items-start justify-center pt-[15vh]">
<DialogPanel className="w-full max-w-lg rounded-lg border border-white/[0.08] bg-navy-800 shadow-2xl shadow-black/40">
<div className="flex items-center justify-between border-b border-white/[0.06] px-5 py-3.5">
<DialogTitle className="flex items-center gap-2 text-sm font-semibold text-white">
<DialogPanel className="w-full max-w-lg rounded-lg border border-line-strong bg-panel shadow-2xl shadow-black/40">
<div className="flex items-center justify-between border-b border-line px-5 py-3.5">
<DialogTitle className="flex items-center gap-2 text-sm font-semibold text-fg">
<SparklesIcon className="size-4 text-teal-500" />
SQL AI
</DialogTitle>
<button
type="button"
onClick={() => setShowAiDialog(false)}
className="text-navy-600 transition-colors hover:text-ice-300"
className="text-fg-muted transition-colors hover:text-fg-3"
>
<XMarkIcon className="size-4" />
</button>
</div>
<div className="p-5">
<label className="mb-2 block text-xs font-medium text-ice-300">
<label className="mb-2 block text-xs font-medium text-fg-3">
Describe what you want to query
</label>
<textarea
value={aiPrompt}
onChange={(e) => setAiPrompt(e.target.value)}
placeholder="e.g. Show me the average build time per workflow over the last 30 days"
className="w-full rounded-md border border-white/[0.06] bg-navy-950/60 px-3 py-2.5 text-sm text-ice-100 placeholder-navy-600 outline-none transition-colors focus:border-teal-500/40"
className="w-full rounded-md border border-line bg-page/60 px-3 py-2.5 text-sm text-fg-2 placeholder-fg-muted outline-none transition-colors focus:border-focus"
rows={3}
/>
<div className="mt-4 flex justify-end gap-2">
<button
type="button"
onClick={() => setShowAiDialog(false)}
className="rounded-md border border-white/[0.06] px-3 py-1.5 text-sm text-ice-300 transition-colors hover:bg-white/[0.03]"
className="rounded-md border border-line px-3 py-1.5 text-sm text-fg-3 transition-colors hover:bg-overlay"
>
Cancel
</button>
@ -639,7 +639,7 @@ export default function InsightsEditor() {
setShowAiDialog(false);
}}
disabled={aiPrompt.trim().length === 0}
className="inline-flex items-center gap-1.5 rounded-md border border-teal-500/30 bg-teal-500/10 px-3 py-1.5 text-sm font-medium text-teal-300 transition-all hover:border-teal-500/50 hover:bg-teal-500/20 hover:text-white disabled:cursor-not-allowed disabled:opacity-40"
className="inline-flex items-center gap-1.5 rounded-md border border-teal-500/30 bg-teal-500/10 px-3 py-1.5 text-sm font-medium text-teal-300 transition-all hover:border-teal-500/50 hover:bg-teal-500/20 hover:text-fg disabled:cursor-not-allowed disabled:opacity-40"
>
<SparklesIcon className="size-3.5" />
Generate SQL

View file

@ -37,17 +37,17 @@ export default function InsightsNew() {
<div className="space-y-3">
<div className="flex items-center gap-2">
<SparklesIcon className="size-5 text-teal-500" />
<h2 className="text-lg font-semibold text-white">New Query</h2>
<h2 className="text-lg font-semibold text-fg">New Query</h2>
</div>
<textarea
placeholder="Ask assistant to generate a report"
className="w-full rounded-lg border border-white/[0.06] bg-navy-950/80 px-4 py-3.5 text-sm text-ice-100 placeholder-navy-600 outline-none transition-colors focus:border-teal-500/40"
className="w-full rounded-lg border border-line bg-page/80 px-4 py-3.5 text-sm text-fg-2 placeholder-fg-muted outline-none transition-colors focus:border-focus"
rows={4}
/>
<div className="flex justify-end">
<button
type="button"
className="inline-flex items-center gap-1.5 rounded-md border border-teal-500/30 bg-teal-500/10 px-4 py-2 text-sm font-medium text-teal-300 transition-all hover:border-teal-500/50 hover:bg-teal-500/20 hover:text-white"
className="inline-flex items-center gap-1.5 rounded-md border border-teal-500/30 bg-teal-500/10 px-4 py-2 text-sm font-medium text-teal-300 transition-all hover:border-teal-500/50 hover:bg-teal-500/20 hover:text-fg"
>
<SparklesIcon className="size-3.5" />
Generate
@ -57,7 +57,7 @@ export default function InsightsNew() {
{/* Template cards */}
<div className="mt-10 space-y-3">
<h3 className="text-[11px] font-semibold uppercase tracking-wider text-navy-600">
<h3 className="text-[11px] font-semibold uppercase tracking-wider text-fg-muted">
Start from a template
</h3>
<div className="grid gap-3">
@ -68,14 +68,14 @@ export default function InsightsNew() {
onClick={() => {
navigate("/insights", { state: { sql: tpl.sql, name: tpl.title } });
}}
className="flex items-start gap-3.5 rounded-lg border border-white/[0.06] bg-navy-800/60 px-4 py-3.5 text-left transition-colors hover:border-white/[0.12] hover:bg-navy-800/80"
className="flex items-start gap-3.5 rounded-lg border border-line bg-panel/60 px-4 py-3.5 text-left transition-colors hover:border-line-strong hover:bg-panel/80"
>
<tpl.icon className="mt-0.5 size-5 shrink-0 text-navy-600" />
<tpl.icon className="mt-0.5 size-5 shrink-0 text-fg-muted" />
<div>
<span className="text-sm font-medium text-ice-100">
<span className="text-sm font-medium text-fg-2">
{tpl.title}
</span>
<p className="mt-0.5 text-xs text-navy-600">
<p className="mt-0.5 text-xs text-fg-muted">
{tpl.description}
</p>
</div>
@ -88,7 +88,7 @@ export default function InsightsNew() {
<div className="mt-10 text-center">
<Link
to="/insights"
className="inline-flex items-center gap-1.5 text-sm text-navy-600 transition-colors hover:text-ice-300"
className="inline-flex items-center gap-1.5 text-sm text-fg-muted transition-colors hover:text-fg-3"
>
<CommandLineIcon className="size-4" />
<span className="font-semibold">Write my own report with SQL</span>

View file

@ -64,14 +64,14 @@ export default function InsightsLayout() {
<div className="sticky top-6 space-y-4">
<Link
to="/insights/new"
className="inline-flex w-full items-center justify-center gap-1.5 rounded-md border border-white/[0.06] bg-navy-800/80 px-3 py-2 text-sm font-medium text-ice-300 transition-colors hover:border-white/[0.12] hover:bg-navy-800 hover:text-white"
className="inline-flex w-full items-center justify-center gap-1.5 rounded-md border border-line bg-panel/80 px-3 py-2 text-sm font-medium text-fg-3 transition-colors hover:border-line-strong hover:bg-panel hover:text-fg"
>
<PlusIcon className="size-3.5" />
New Query
</Link>
<div>
<h3 className="mb-2 text-[11px] font-semibold uppercase tracking-wider text-navy-600">
<h3 className="mb-2 text-[11px] font-semibold uppercase tracking-wider text-fg-muted">
Saved Queries
</h3>
<div className="space-y-0.5">
@ -82,12 +82,12 @@ export default function InsightsLayout() {
onClick={() => {
navigate("/insights", { state: { sql: q.sql, name: q.name } });
}}
className="flex w-full flex-col gap-0.5 rounded-md px-2.5 py-2 text-left transition-colors hover:bg-white/[0.05]"
className="flex w-full flex-col gap-0.5 rounded-md px-2.5 py-2 text-left transition-colors hover:bg-overlay"
>
<span className="text-sm font-medium text-ice-100">
<span className="text-sm font-medium text-fg-2">
{q.name}
</span>
<span className="truncate font-mono text-[10px] text-navy-600">
<span className="truncate font-mono text-[10px] text-fg-muted">
{q.sql.split("\n")[0]}
</span>
</button>
@ -96,7 +96,7 @@ export default function InsightsLayout() {
</div>
<div>
<h3 className="mb-2 text-[11px] font-semibold uppercase tracking-wider text-navy-600">
<h3 className="mb-2 text-[11px] font-semibold uppercase tracking-wider text-fg-muted">
History
</h3>
<div className="space-y-0.5">
@ -107,12 +107,12 @@ export default function InsightsLayout() {
onClick={() => {
navigate("/insights", { state: { sql: entry.sql } });
}}
className="flex w-full flex-col gap-0.5 rounded-md px-2.5 py-2 text-left transition-colors hover:bg-white/[0.05]"
className="flex w-full flex-col gap-0.5 rounded-md px-2.5 py-2 text-left transition-colors hover:bg-overlay"
>
<span className="truncate font-mono text-[10px] text-ice-300">
<span className="truncate font-mono text-[10px] text-fg-3">
{entry.sql}
</span>
<span className="font-mono text-[10px] text-navy-600">
<span className="font-mono text-[10px] text-fg-muted">
{entry.timestamp} · {entry.rowsReturned} rows
</span>
</button>

View file

@ -81,12 +81,12 @@ function CheckStatusIcon({ status }: { status: CheckStatus }) {
case "queued":
return (
<span className="flex size-3 shrink-0 items-center justify-center">
<span className="size-2 rounded-full border border-navy-600" />
<span className="size-2 rounded-full border border-fg-muted" />
</span>
);
case "skipped":
return (
<svg viewBox="0 0 16 16" fill="currentColor" className="size-3 shrink-0 text-navy-600" aria-hidden="true">
<svg viewBox="0 0 16 16" fill="currentColor" className="size-3 shrink-0 text-fg-muted" aria-hidden="true">
<path d="M2 7.75A.75.75 0 0 1 2.75 7h10a.75.75 0 0 1 0 1.5h-10A.75.75 0 0 1 2 7.75Z" />
</svg>
);
@ -155,26 +155,26 @@ function ChecksStatus({ checks }: { checks: CheckRun[] }) {
return (
<div
className="-mx-4 mt-3 overflow-hidden border-y border-white/[0.04]"
className="-mx-4 mt-3 overflow-hidden border-y border-line"
onClick={(e) => { e.preventDefault(); e.stopPropagation(); }}
>
<button
type="button"
onClick={() => setExpanded(!expanded)}
className="flex w-full items-center gap-2 px-4 py-2 text-left transition-colors hover:bg-white/[0.03]"
className="flex w-full items-center gap-2 px-4 py-2 text-left transition-colors hover:bg-overlay"
>
<SummaryStatusIcon status={overallStatus} />
<span className={`min-w-0 flex-1 truncate font-mono text-xs font-medium ${config.text}`}>{summary}</span>
<ChevronDownIcon className={`size-3 shrink-0 text-navy-600 transition-transform duration-200 ${expanded ? "rotate-180" : ""}`} />
<ChevronDownIcon className={`size-3 shrink-0 text-fg-muted transition-transform duration-200 ${expanded ? "rotate-180" : ""}`} />
</button>
<div className={`grid transition-[grid-template-rows] duration-200 ease-out ${expanded ? "grid-rows-[1fr]" : "grid-rows-[0fr]"}`}>
<div className="overflow-hidden">
<div className="border-t border-white/[0.04] px-4 pb-2 pt-1.5">
<div className="border-t border-line px-4 pb-2 pt-1.5">
{checks.map((check) => (
<div key={check.name} className="flex items-center gap-2 py-1 font-mono text-[11px]">
<CheckStatusIcon status={check.status} />
<span className={check.status === "skipped" || check.status === "queued" ? "text-navy-600" : "text-ice-300"}>{check.name}</span>
<span className="ml-auto text-navy-600">
<span className={check.status === "skipped" || check.status === "queued" ? "text-fg-muted" : "text-fg-3"}>{check.name}</span>
<span className="ml-auto text-fg-muted">
{check.duration ?? (check.status === "skipped" ? "skipped" : check.status === "queued" ? "queued" : "")}
</span>
</div>
@ -195,12 +195,12 @@ const totalPrs = staticColumns.reduce(
export const handle = {
wide: true,
headerExtra: (
<div className="flex items-center gap-4 font-mono text-xs text-ice-300">
<div className="flex items-center gap-4 font-mono text-xs text-fg-3">
<span>
<span className="text-white">{totalCards}</span> runs
<span className="text-fg">{totalCards}</span> runs
</span>
<span>
<span className="text-white">{totalPrs}</span> PRs
<span className="text-fg">{totalPrs}</span> PRs
</span>
</div>
),
@ -218,25 +218,25 @@ function PrCard({
actions?: string[];
}) {
return (
<Link to={`/runs/${pr.id}`} className="group block rounded-md border border-white/[0.06] bg-navy-800/80 p-4 transition-all duration-200 hover:border-white/[0.12] hover:bg-navy-800 hover:shadow-lg hover:shadow-black/20">
<Link to={`/runs/${pr.id}`} className="group block rounded-md border border-line bg-panel/80 p-4 transition-all duration-200 hover:border-line-strong hover:bg-panel hover:shadow-lg hover:shadow-black/20">
<div className="mb-2 flex items-center gap-1.5">
<Icon className={`size-3.5 shrink-0 ${iconColor}`} />
<span className="font-mono text-xs font-medium text-teal-500">
{pr.repo}
</span>
{pr.number != null && (
<span className="font-mono text-xs text-navy-600">
<span className="font-mono text-xs text-fg-muted">
#{pr.number}
</span>
)}
</div>
<p className="text-sm leading-snug text-ice-100">{pr.title}</p>
<p className="text-sm leading-snug text-fg-2">{pr.title}</p>
{(pr.additions != null || pr.resources != null || pr.elapsed != null) && (
<div className="mt-3 flex items-center gap-3 font-mono text-xs">
{pr.resources != null && (
<span className="text-ice-300">{pr.resources}</span>
<span className="text-fg-3">{pr.resources}</span>
)}
{pr.additions != null && pr.deletions != null && (
<>
@ -249,7 +249,7 @@ function PrCard({
</>
)}
{pr.comments != null && (
<span className="inline-flex items-center gap-1 text-navy-600">
<span className="inline-flex items-center gap-1 text-fg-muted">
<svg viewBox="0 0 16 16" fill="currentColor" className="size-3" aria-hidden="true">
<path d="M1 2.75C1 1.784 1.784 1 2.75 1h10.5c.966 0 1.75.784 1.75 1.75v7.5A1.75 1.75 0 0 1 13.25 12H9.06l-2.573 2.573A1.458 1.458 0 0 1 4 13.543V12H2.75A1.75 1.75 0 0 1 1 10.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h4.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z" />
</svg>
@ -257,7 +257,7 @@ function PrCard({
</span>
)}
{pr.elapsed != null && (
<span className={`ml-auto font-mono ${pr.elapsedWarning ? "text-amber" : "text-navy-600"}`}>{pr.elapsed}</span>
<span className={`ml-auto font-mono ${pr.elapsedWarning ? "text-amber" : "text-fg-muted"}`}>{pr.elapsed}</span>
)}
</div>
)}
@ -275,14 +275,14 @@ function PrCard({
key={label}
type="button"
disabled={pr.actionDisabled}
className={`inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1 text-[11px] font-medium transition-colors disabled:cursor-not-allowed disabled:text-navy-600 disabled:border-white/[0.04] ${
className={`inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1 text-[11px] font-medium transition-colors disabled:cursor-not-allowed disabled:text-fg-muted disabled:border-line ${
label === "Merge"
? "border-mint/20 text-mint hover:border-mint/50 hover:text-white"
? "border-mint/20 text-mint hover:border-mint/50 hover:text-fg"
: label === "Answer Question"
? "border-amber/20 text-amber hover:border-amber/50 hover:text-white"
? "border-amber/20 text-amber hover:border-amber/50 hover:text-fg"
: label === "Resolve"
? "border-teal-500/20 text-teal-500 hover:border-teal-500/50 hover:text-white"
: "border-white/[0.08] text-ice-300 hover:border-teal-500/40 hover:text-white"
? "border-teal-500/20 text-teal-500 hover:border-teal-500/50 hover:text-fg"
: "border-line-strong text-fg-3 hover:border-teal-500/40 hover:text-fg"
}`}
>
{label === "Watch" && (
@ -365,10 +365,10 @@ function BoardColumn({ column }: { column: (typeof staticColumns)[number] }) {
<div className="flex min-w-[280px] flex-1 flex-col">
<div className="mb-4 flex items-center gap-3">
<div className={`h-2.5 w-2.5 rounded-full ${column.accent}`} />
<h3 className="text-sm font-semibold tracking-wide text-ice-100">
<h3 className="text-sm font-semibold tracking-wide text-fg-2">
{column.name}
</h3>
<span className="rounded-full bg-white/[0.06] px-2 py-0.5 font-mono text-xs text-navy-600">
<span className="rounded-full bg-overlay px-2 py-0.5 font-mono text-xs text-fg-muted">
{column.items.length}
</span>
</div>
@ -394,16 +394,16 @@ type ViewMode = "columns" | "list";
function RunRow({ run }: { run: RunWithStatus }) {
return (
<Link to={`/runs/${run.id}`} className="grid items-center rounded-md border border-white/[0.06] bg-navy-800/80 px-4 py-3 transition-all duration-200 hover:border-white/[0.12] hover:bg-navy-800" style={{ gridColumn: "1 / -1", gridTemplateColumns: "subgrid" }}>
<span className={`font-mono text-xs pr-2 ${run.elapsedWarning ? "text-amber" : "text-navy-600"}`}>
<Link to={`/runs/${run.id}`} className="grid items-center rounded-md border border-line bg-panel/80 px-4 py-3 transition-all duration-200 hover:border-line-strong hover:bg-panel" style={{ gridColumn: "1 / -1", gridTemplateColumns: "subgrid" }}>
<span className={`font-mono text-xs pr-2 ${run.elapsedWarning ? "text-amber" : "text-fg-muted"}`}>
{run.elapsed}
</span>
<span className="flex items-center gap-2 min-w-0">
<span className="font-mono text-xs font-medium text-teal-500">{run.repo}</span>
<span className="truncate text-sm text-ice-100">{run.title}</span>
<span className="truncate text-sm text-fg-2">{run.title}</span>
{run.comments != null && run.comments > 0 && (
<span className="inline-flex shrink-0 items-center gap-1 font-mono text-xs text-navy-600">
<span className="inline-flex shrink-0 items-center gap-1 font-mono text-xs text-fg-muted">
<svg viewBox="0 0 16 16" fill="currentColor" className="size-3" aria-hidden="true">
<path d="M1 2.75C1 1.784 1.784 1 2.75 1h10.5c.966 0 1.75.784 1.75 1.75v7.5A1.75 1.75 0 0 1 13.25 12H9.06l-2.573 2.573A1.458 1.458 0 0 1 4 13.543V12H2.75A1.75 1.75 0 0 1 1 10.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h4.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z" />
</svg>
@ -417,7 +417,7 @@ function RunRow({ run }: { run: RunWithStatus }) {
{run.deletions != null && <span className="text-coral">-{run.deletions.toLocaleString()}</span>}
</span>
<span className="inline-flex items-center justify-end gap-1.5 font-mono text-xs text-navy-600">
<span className="inline-flex items-center justify-end gap-1.5 font-mono text-xs text-fg-muted">
{run.number != null && (
<>
<GitPullRequestIcon className="size-3" />
@ -509,33 +509,33 @@ export default function Pipelines() {
<div className="space-y-4">
<div className="flex gap-3">
<div className="relative flex-1">
<MagnifyingGlassIcon className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-navy-600" />
<MagnifyingGlassIcon className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-fg-muted" />
<input
type="text"
placeholder="Search runs..."
value={query}
onChange={(e) => setQuery(e.target.value)}
className="w-full rounded-md border border-white/[0.06] bg-navy-800/80 py-2 pl-9 pr-3 text-sm text-ice-100 placeholder-navy-600 outline-none transition-colors focus:border-teal-500/40 focus:ring-0"
className="w-full rounded-md border border-line bg-panel/80 py-2 pl-9 pr-3 text-sm text-fg-2 placeholder-fg-muted outline-none transition-colors focus:border-focus focus:ring-0"
/>
</div>
<div className="relative">
<select
value={repoFilter}
onChange={(e) => setRepoFilter(e.target.value)}
className="appearance-none rounded-md border border-white/[0.06] bg-navy-800/80 py-2 pl-3 pr-8 text-sm text-ice-100 outline-none transition-colors focus:border-teal-500/40 focus:ring-0"
className="appearance-none rounded-md border border-line bg-panel/80 py-2 pl-3 pr-8 text-sm text-fg-2 outline-none transition-colors focus:border-focus focus:ring-0"
>
<option value="all">All repos</option>
{allRepos.map((repo) => (
<option key={repo} value={repo}>{repo}</option>
))}
</select>
<ChevronDownIcon className="pointer-events-none absolute right-2 top-1/2 size-4 -translate-y-1/2 text-navy-600" />
<ChevronDownIcon className="pointer-events-none absolute right-2 top-1/2 size-4 -translate-y-1/2 text-fg-muted" />
</div>
<div className="flex rounded-md border border-white/[0.06] bg-navy-800/80">
<div className="flex rounded-md border border-line bg-panel/80">
<button
type="button"
onClick={() => setView("columns")}
className={`inline-flex items-center gap-1.5 px-3 py-2 text-xs font-medium transition-colors ${view === "columns" ? "text-teal-500" : "text-navy-600 hover:text-ice-300"}`}
className={`inline-flex items-center gap-1.5 px-3 py-2 text-xs font-medium transition-colors ${view === "columns" ? "text-teal-500" : "text-fg-muted hover:text-fg-3"}`}
aria-label="Columns view"
>
<svg viewBox="0 0 20 20" fill="currentColor" className="size-4" aria-hidden="true">
@ -545,7 +545,7 @@ export default function Pipelines() {
<button
type="button"
onClick={() => setView("list")}
className={`inline-flex items-center gap-1.5 px-3 py-2 text-xs font-medium transition-colors ${view === "list" ? "text-teal-500" : "text-navy-600 hover:text-ice-300"}`}
className={`inline-flex items-center gap-1.5 px-3 py-2 text-xs font-medium transition-colors ${view === "list" ? "text-teal-500" : "text-fg-muted hover:text-fg-3"}`}
aria-label="List view"
>
<svg viewBox="0 0 20 20" fill="currentColor" className="size-4" aria-hidden="true">
@ -578,11 +578,11 @@ export default function Pipelines() {
className="mb-3 flex w-full items-center gap-2 text-left"
>
{isCollapsed
? <ChevronRightIcon className="size-3.5 text-navy-600" />
: <ChevronDownIcon className="size-3.5 text-navy-600" />}
? <ChevronRightIcon className="size-3.5 text-fg-muted" />
: <ChevronDownIcon className="size-3.5 text-fg-muted" />}
<div className={`h-2.5 w-2.5 rounded-full ${col.accent}`} />
<h3 className="text-sm font-semibold tracking-wide text-ice-100">{col.name}</h3>
<span className="rounded-full bg-white/[0.06] px-2 py-0.5 font-mono text-xs text-navy-600">
<h3 className="text-sm font-semibold tracking-wide text-fg-2">{col.name}</h3>
<span className="rounded-full bg-overlay px-2 py-0.5 font-mono text-xs text-fg-muted">
{col.items.length}
</span>
</button>
@ -595,7 +595,7 @@ export default function Pipelines() {
</div>
</SortableContext>
) : (
<p className="py-4 text-center text-sm text-navy-600">No runs</p>
<p className="py-4 text-center text-sm text-fg-muted">No runs</p>
))}
</div>
);

View file

@ -19,7 +19,7 @@ const smoothnessOptions: Array<{ value: SmoothnessRating; label: string }> = [
function SmoothnesssBadge({ smoothness }: { smoothness: Retro["smoothness"] }) {
if (!smoothness) {
return <span className="text-xs text-navy-600">--</span>;
return <span className="text-xs text-fg-muted">--</span>;
}
const config = smoothnessConfig[smoothness];
return (
@ -52,7 +52,7 @@ export default function Retros() {
const [smoothnessFilter, setSmoothnessFilter] = useState<SmoothnessRating | "all">("all");
if (retros.length === 0) {
return <p className="py-8 text-center text-sm text-navy-600">No retrospectives yet.</p>;
return <p className="py-8 text-center text-sm text-fg-muted">No retrospectives yet.</p>;
}
const lowerQuery = query.toLowerCase();
@ -67,34 +67,34 @@ export default function Retros() {
<div className="space-y-4">
<div className="flex gap-3">
<div className="relative flex-1">
<MagnifyingGlassIcon className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-navy-600" />
<MagnifyingGlassIcon className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-fg-muted" />
<input
type="text"
placeholder="Search retros…"
value={query}
onChange={(e) => setQuery(e.target.value)}
className="w-full rounded-md border border-white/[0.06] bg-navy-800/80 py-2 pl-9 pr-3 text-sm text-ice-100 placeholder-navy-600 outline-none transition-colors focus:border-teal-500/40 focus:ring-0"
className="w-full rounded-md border border-line bg-panel/80 py-2 pl-9 pr-3 text-sm text-fg-2 placeholder-fg-muted outline-none transition-colors focus:border-focus focus:ring-0"
/>
</div>
<div className="relative">
<select
value={smoothnessFilter}
onChange={(e) => setSmoothnessFilter(e.target.value as SmoothnessRating | "all")}
className="appearance-none rounded-md border border-white/[0.06] bg-navy-800/80 py-2 pl-3 pr-8 text-sm text-ice-100 outline-none transition-colors focus:border-teal-500/40 focus:ring-0"
className="appearance-none rounded-md border border-line bg-panel/80 py-2 pl-3 pr-8 text-sm text-fg-2 outline-none transition-colors focus:border-focus focus:ring-0"
>
<option value="all">All smoothness</option>
{smoothnessOptions.map((opt) => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
<ChevronDownIcon className="pointer-events-none absolute right-2 top-1/2 size-4 -translate-y-1/2 text-navy-600" />
<ChevronDownIcon className="pointer-events-none absolute right-2 top-1/2 size-4 -translate-y-1/2 text-fg-muted" />
</div>
</div>
<div className="rounded-md border border-white/[0.06] overflow-hidden">
<div className="rounded-md border border-line overflow-hidden">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-white/[0.06] bg-navy-800/60 text-left text-xs text-navy-600">
<tr className="border-b border-line bg-panel/60 text-left text-xs text-fg-muted">
<th className="px-4 py-2.5 font-medium">Pipeline</th>
<th className="px-4 py-2.5 font-medium">Goal</th>
<th className="px-4 py-2.5 font-medium">Smoothness</th>
@ -105,23 +105,23 @@ export default function Retros() {
</thead>
<tbody>
{filtered.map((retro) => (
<tr key={retro.run_id} className="border-b border-white/[0.06] last:border-b-0 transition-colors hover:bg-white/[0.02] cursor-pointer" onClick={() => navigate(`/runs/${retro.run_id}/retro`)}>
<tr key={retro.run_id} className="border-b border-line last:border-b-0 transition-colors hover:bg-overlay cursor-pointer" onClick={() => navigate(`/runs/${retro.run_id}/retro`)}>
<td className="px-4 py-3 font-mono text-xs font-medium text-teal-500">
{retro.pipeline_name}
</td>
<td className="px-4 py-3 text-ice-100">
<td className="px-4 py-3 text-fg-2">
{truncate(retro.goal, 60)}
</td>
<td className="px-4 py-3">
<SmoothnesssBadge smoothness={retro.smoothness} />
</td>
<td className="px-4 py-3 text-right font-mono text-xs tabular-nums text-ice-300">
<td className="px-4 py-3 text-right font-mono text-xs tabular-nums text-fg-3">
{formatDuration(retro.stats.total_duration_ms)}
</td>
<td className="px-4 py-3 text-right font-mono text-xs tabular-nums text-ice-300">
<td className="px-4 py-3 text-right font-mono text-xs tabular-nums text-fg-3">
{retro.friction_points?.length ?? 0}
</td>
<td className="px-4 py-3 text-right font-mono text-xs text-navy-600">
<td className="px-4 py-3 text-right font-mono text-xs text-fg-muted">
{formatTimestamp(retro.timestamp)}
</td>
</tr>

View file

@ -26,7 +26,7 @@ const stages: Stage[] = [
const statusConfig: Record<StageStatus, { icon: typeof CheckCircleIcon; color: string }> = {
completed: { icon: CheckCircleIcon, color: "text-mint" },
running: { icon: ArrowPathIcon, color: "text-teal-500" },
pending: { icon: PauseCircleIcon, color: "text-navy-600" },
pending: { icon: PauseCircleIcon, color: "text-fg-muted" },
failed: { icon: XCircleIcon, color: "text-coral" },
};
@ -39,7 +39,7 @@ export default function RunConfiguration() {
<div className="flex gap-6">
<nav className="w-56 shrink-0 space-y-6">
<div>
<h3 className="px-2 text-xs font-medium uppercase tracking-wider text-navy-600">Stages</h3>
<h3 className="px-2 text-xs font-medium uppercase tracking-wider text-fg-muted">Stages</h3>
<ul className="mt-2 space-y-0.5">
{stages.map((stage) => {
const config = statusConfig[stage.status];
@ -48,11 +48,11 @@ export default function RunConfiguration() {
<li key={stage.id}>
<Link
to={`/runs/${id}/stages/${stage.id}`}
className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-ice-300 transition-colors hover:bg-white/[0.04] hover:text-white"
className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-fg-3 transition-colors hover:bg-overlay hover:text-fg"
>
<Icon className={`size-4 shrink-0 ${config.color} ${stage.status === "running" ? "animate-spin" : ""}`} />
<span className="flex-1 truncate">{stage.name}</span>
<span className="font-mono text-xs tabular-nums text-navy-600">{stage.duration}</span>
<span className="font-mono text-xs tabular-nums text-fg-muted">{stage.duration}</span>
</Link>
</li>
);
@ -62,23 +62,23 @@ export default function RunConfiguration() {
{workflow && (
<div>
<h3 className="px-2 text-xs font-medium uppercase tracking-wider text-navy-600">Workflow</h3>
<h3 className="px-2 text-xs font-medium uppercase tracking-wider text-fg-muted">Workflow</h3>
<ul className="mt-2 space-y-0.5">
<li>
<Link
to={`/runs/${id}/configuration`}
className="flex items-center gap-2 rounded-md bg-white/[0.06] px-2 py-1.5 text-sm text-white transition-colors"
className="flex items-center gap-2 rounded-md bg-overlay px-2 py-1.5 text-sm text-fg transition-colors"
>
<DocumentTextIcon className="size-4 shrink-0 text-navy-600" />
<DocumentTextIcon className="size-4 shrink-0 text-fg-muted" />
Run Configuration
</Link>
</li>
<li>
<Link
to={`/runs/${id}/graph`}
className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-ice-300 transition-colors hover:bg-white/[0.04] hover:text-white"
className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-fg-3 transition-colors hover:bg-overlay hover:text-fg"
>
<MapIcon className="size-4 shrink-0 text-navy-600" />
<MapIcon className="size-4 shrink-0 text-fg-muted" />
Workflow Graph
</Link>
</li>
@ -93,7 +93,7 @@ export default function RunConfiguration() {
file={{ name: "task.toml", contents: workflow.config, lang: "toml" }}
/>
) : (
<p className="text-sm text-navy-600">No configuration found.</p>
<p className="text-sm text-fg-muted">No configuration found.</p>
)}
</div>
</div>

View file

@ -26,17 +26,17 @@ export default function RunDetail({ params }: Route.ComponentProps) {
const basePath = `/runs/${params.id}`;
if (!run) {
return <p className="py-8 text-center text-sm text-navy-600">Run not found.</p>;
return <p className="py-8 text-center text-sm text-fg-muted">Run not found.</p>;
}
const colors = statusColors[run.status];
return (
<div>
<nav className="mb-4 flex items-center gap-1 text-sm text-navy-600">
<Link to="/runs" className="text-ice-300 hover:text-white">Runs</Link>
<nav className="mb-4 flex items-center gap-1 text-sm text-fg-muted">
<Link to="/runs" className="text-fg-3 hover:text-fg">Runs</Link>
<ChevronRightIcon className="size-3" />
<Link to={`/workflows/${run.workflow}`} className="text-ice-300 hover:text-white">
<Link to={`/workflows/${run.workflow}`} className="text-fg-3 hover:text-fg">
{workflowData[run.workflow]?.title ?? run.workflow}
</Link>
<ChevronRightIcon className="size-3" />
@ -45,22 +45,22 @@ export default function RunDetail({ params }: Route.ComponentProps) {
<div className="mb-6 flex items-center gap-4">
<div className="min-w-0 flex-1">
<h2 className="text-xl font-semibold text-white">{run.title}</h2>
<h2 className="text-xl font-semibold text-fg">{run.title}</h2>
<div className="mt-2 flex items-center gap-3 text-sm">
<span className="flex items-center gap-1.5">
<span className={`size-2 rounded-full ${colors.dot}`} />
<span className={`font-medium ${colors.text}`}>{run.statusLabel}</span>
</span>
<span className="font-mono text-xs text-navy-600">{run.repo}</span>
<span className="font-mono text-xs text-fg-muted">{run.repo}</span>
{run.elapsed && (
<span className={`font-mono text-xs ${run.elapsedWarning ? "text-amber" : "text-navy-600"}`}>{run.elapsed}</span>
<span className={`font-mono text-xs ${run.elapsedWarning ? "text-amber" : "text-fg-muted"}`}>{run.elapsed}</span>
)}
</div>
</div>
<button
type="button"
title="Open pull request"
className="flex shrink-0 items-center gap-1.5 rounded-md border border-mint/20 px-3 py-1.5 text-sm font-medium text-mint transition-colors hover:border-mint/50 hover:bg-mint/10 hover:text-white"
className="flex shrink-0 items-center gap-1.5 rounded-md border border-mint/20 px-3 py-1.5 text-sm font-medium text-mint transition-colors hover:border-mint/50 hover:bg-mint/10 hover:text-fg"
>
<svg viewBox="0 0 16 16" fill="currentColor" className="size-3.5" aria-hidden="true">
<path d="M1.5 3.25a2.25 2.25 0 1 1 3 2.122v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.25 2.25 0 0 1 1.5 3.25Zm5.677-.177L9.573.677A.25.25 0 0 1 10 .854V2.5h1A2.5 2.5 0 0 1 13.5 5v5.628a2.251 2.251 0 1 1-1.5 0V5a1 1 0 0 0-1-1h-1v1.646a.25.25 0 0 1-.427.177L7.177 3.427a.25.25 0 0 1 0-.354ZM3.75 2.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm0 9.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm8.25.75a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Z" />
@ -69,7 +69,7 @@ export default function RunDetail({ params }: Route.ComponentProps) {
</button>
</div>
<div className="border-b border-white/[0.06]">
<div className="border-b border-line">
<nav className="-mb-px flex gap-6">
{tabs.map((tab) => {
const tabPath = `${basePath}${tab.path}`;
@ -82,14 +82,14 @@ export default function RunDetail({ params }: Route.ComponentProps) {
to={tabPath}
className={`border-b-2 pb-3 text-sm font-medium transition-colors ${
isActive
? "border-teal-500 text-white"
: "border-transparent text-navy-600 hover:border-white/10 hover:text-ice-300"
? "border-teal-500 text-fg"
: "border-transparent text-fg-muted hover:border-line-strong hover:text-fg-3"
}`}
>
{tab.name}
{tab.count != null && (
<span className={`ml-1.5 rounded-full px-1.5 py-0.5 text-xs font-normal tabular-nums ${
isActive ? "bg-white/10 text-ice-300" : "bg-white/[0.04] text-navy-600"
isActive ? "bg-overlay-strong text-fg-3" : "bg-overlay text-fg-muted"
}`}>
{tab.count}
</span>

View file

@ -5,6 +5,7 @@ import {
type AnnotationSide,
type DiffLineAnnotation,
} from "@pierre/diffs/react";
import { useTheme } from "../lib/theme";
export const handle = { wide: true };
@ -309,6 +310,7 @@ function DiffWithSteer({
}) {
const [diffStyle, setDiffStyle] = useState<"split" | "unified">("split");
const [disableBackground, setDisableBackground] = useState(false);
const { theme } = useTheme();
const containerRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLDivElement>(null);
@ -341,14 +343,14 @@ function DiffWithSteer({
}, []);
return (
<div ref={containerRef} className="relative rounded-md overflow-hidden border border-white/[0.06]">
<div ref={containerRef} className="relative rounded-md overflow-hidden border border-line">
<MultiFileDiff<SteerAnnotation & { text?: string }>
oldFile={oldFile}
newFile={newFile}
options={{
diffStyle,
disableBackground,
theme: "pierre-dark",
theme: theme === "dark" ? "pierre-dark" : "pierre-light",
lineDiffType: "word",
onLineEnter({ lineNumber, annotationSide, lineElement }) {
showButton(lineElement, lineNumber, annotationSide);
@ -437,14 +439,14 @@ function SteerCommentForm({
}, []);
return (
<div className="flex flex-col gap-2 rounded-md border border-teal-500/30 bg-navy-900/90 p-3">
<div className="flex flex-col gap-2 rounded-md border border-teal-500/30 bg-panel-alt/90 p-3">
<textarea
ref={textareaRef}
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Add steering guidance..."
rows={3}
className="w-full resize-none rounded border border-white/[0.06] bg-navy-800/80 px-2 py-1.5 text-sm text-ice-100 placeholder:text-navy-600 outline-none focus:border-teal-500/40"
className="w-full resize-none rounded border border-line bg-panel/80 px-2 py-1.5 text-sm text-fg-2 placeholder:text-fg-muted outline-none focus:border-focus"
/>
<div className="flex gap-2">
<button
@ -458,7 +460,7 @@ function SteerCommentForm({
<button
type="button"
onClick={onCancel}
className="rounded border border-white/[0.06] bg-navy-800/80 px-3 py-1 text-xs font-medium text-ice-300 transition-colors hover:bg-white/[0.04]"
className="rounded border border-line bg-panel/80 px-3 py-1 text-xs font-medium text-fg-3 transition-colors hover:bg-overlay"
>
Cancel
</button>
@ -469,7 +471,7 @@ function SteerCommentForm({
function SubmittedSteerComment({ text }: { text: string }) {
return (
<div className="rounded-md border border-teal-500/20 bg-teal-950/30 px-3 py-2 text-sm text-ice-200">
<div className="rounded-md border border-teal-500/20 bg-teal-950/30 px-3 py-2 text-sm text-fg-2">
{text}
</div>
);
@ -549,20 +551,20 @@ export default function RunFilesChanged() {
<select
value={checkpoint}
onChange={(e) => setCheckpoint(e.target.value)}
className="appearance-none rounded-md border border-white/[0.06] bg-navy-800/80 py-2 pl-3 pr-8 text-sm text-ice-100 outline-none transition-colors focus:border-teal-500/40 focus:ring-0"
className="appearance-none rounded-md border border-line bg-panel/80 py-2 pl-3 pr-8 text-sm text-fg-2 outline-none transition-colors focus:border-focus focus:ring-0"
>
{checkpoints.map((cp) => (
<option key={cp.id} value={cp.id}>{cp.label}</option>
))}
</select>
<ChevronDownIcon className="pointer-events-none absolute right-2 top-1/2 size-4 -translate-y-1/2 text-navy-600" />
<ChevronDownIcon className="pointer-events-none absolute right-2 top-1/2 size-4 -translate-y-1/2 text-fg-muted" />
</div>
<div className="ml-auto flex items-center gap-3">
<DiffStat additions={567} deletions={234} />
<button
type="button"
title="Settings"
className="flex size-8 items-center justify-center rounded-md border border-white/[0.06] bg-navy-800/80 text-ice-300 transition-colors hover:bg-white/[0.04] hover:text-white"
className="flex size-8 items-center justify-center rounded-md border border-line bg-panel/80 text-fg-3 transition-colors hover:bg-overlay hover:text-fg"
>
<Cog6ToothIcon className="size-4" />
</button>

View file

@ -5,6 +5,8 @@ import { CheckCircleIcon, ArrowPathIcon, PauseCircleIcon, XCircleIcon } from "@h
import { DocumentTextIcon, MapIcon } from "@heroicons/react/24/outline";
import { findRun } from "../data/runs";
import { workflowData } from "./workflow-detail";
import { useTheme } from "../lib/theme";
import { getGraphTheme } from "../lib/graph-theme";
export const handle = { wide: true };
@ -28,13 +30,13 @@ const stages: Stage[] = [
const statusConfig: Record<StageStatus, { icon: typeof CheckCircleIcon; color: string }> = {
completed: { icon: CheckCircleIcon, color: "text-mint" },
running: { icon: ArrowPathIcon, color: "text-teal-500" },
pending: { icon: PauseCircleIcon, color: "text-navy-600" },
pending: { icon: PauseCircleIcon, color: "text-fg-muted" },
failed: { icon: XCircleIcon, color: "text-coral" },
};
type Direction = "LR" | "TB";
function buildDot(direction: Direction) {
function buildDot(direction: Direction, gt: ReturnType<typeof getGraphTheme>) {
return `digraph sync {
graph [label="Sync"]
rankdir=${direction}
@ -44,27 +46,27 @@ function buildDot(direction: Direction) {
node [
fontname="ui-monospace, monospace"
fontsize=11
fontcolor="#c6d4e0"
color="#2a3f52"
fillcolor="#1a2b3c"
fontcolor="${gt.nodeText}"
color="${gt.edgeColor}"
fillcolor="${gt.nodeFill}"
style=filled
penwidth=1.2
]
edge [
fontname="ui-monospace, monospace"
fontsize=9
fontcolor="#5a7a94"
color="#2a3f52"
fontcolor="${gt.fontcolor}"
color="${gt.edgeColor}"
arrowsize=0.7
penwidth=1.2
]
start [shape=Mdiamond, label="Start", fillcolor="#0d4f4f", color="#14b8a6", fontcolor="#5eead4"]
exit [shape=Msquare, label="Exit", fillcolor="#0d4f4f", color="#14b8a6", fontcolor="#5eead4"]
start [shape=Mdiamond, label="Start", fillcolor="${gt.startFill}", color="${gt.startBorder}", fontcolor="${gt.startText}"]
exit [shape=Msquare, label="Exit", fillcolor="${gt.startFill}", color="${gt.startBorder}", fontcolor="${gt.startText}"]
detect [label="Detect\\nDrift"]
propose [label="Propose\\nChanges"]
review [shape=hexagon, label="Review\\nChanges", fillcolor="#1a2030", color="#f59e0b", fontcolor="#fbbf24"]
review [shape=hexagon, label="Review\\nChanges", fillcolor="${gt.gateFill}", color="${gt.gateBorder}", fontcolor="${gt.gateText}"]
apply [label="Apply\\nChanges"]
start -> detect
@ -89,7 +91,7 @@ function stripGraphTitle(svg: SVGSVGElement) {
title.remove();
}
function annotateRunningNodes(svg: SVGSVGElement) {
function annotateRunningNodes(svg: SVGSVGElement, gt: ReturnType<typeof getGraphTheme>) {
const runningDotIds = new Set(
stages.filter((s) => s.status === "running").map((s) => s.dotId),
);
@ -109,20 +111,20 @@ function annotateRunningNodes(svg: SVGSVGElement) {
const ns = "http://www.w3.org/2000/svg";
const shapes = group.querySelectorAll("ellipse, polygon, path");
for (const shape of shapes) {
shape.setAttribute("fill", "#0d3a3a");
shape.setAttribute("stroke", "#14b8a6");
shape.setAttribute("fill", gt.runningFill);
shape.setAttribute("stroke", gt.runningBorder);
shape.setAttribute("stroke-width", "2");
const animFill = document.createElementNS(ns, "animate");
animFill.setAttribute("attributeName", "fill");
animFill.setAttribute("values", "#0d3a3a;#134e4a;#0d3a3a");
animFill.setAttribute("values", `${gt.runningFill};${gt.runningPulseFill};${gt.runningFill}`);
animFill.setAttribute("dur", "1.5s");
animFill.setAttribute("repeatCount", "indefinite");
shape.appendChild(animFill);
const animStroke = document.createElementNS(ns, "animate");
animStroke.setAttribute("attributeName", "stroke");
animStroke.setAttribute("values", "#14b8a6;#5eead4;#14b8a6");
animStroke.setAttribute("values", `${gt.runningBorder};${gt.runningPulseStroke};${gt.runningBorder}`);
animStroke.setAttribute("dur", "1.5s");
animStroke.setAttribute("repeatCount", "indefinite");
shape.appendChild(animStroke);
@ -136,18 +138,18 @@ function annotateRunningNodes(svg: SVGSVGElement) {
}
const texts = group.querySelectorAll("text");
for (const text of texts) {
text.setAttribute("fill", "#5eead4");
text.setAttribute("fill", gt.runningText);
}
} else if (completedDotIds.has(nodeId)) {
// Tint completed nodes green
const shapes = group.querySelectorAll("ellipse, polygon, path");
for (const shape of shapes) {
shape.setAttribute("fill", "#0a2a20");
shape.setAttribute("stroke", "#34d399");
shape.setAttribute("fill", gt.completedFill);
shape.setAttribute("stroke", gt.completedBorder);
}
const texts = group.querySelectorAll("text");
for (const text of texts) {
text.setAttribute("fill", "#6ee7b7");
text.setAttribute("fill", gt.completedText);
}
}
}
@ -165,8 +167,8 @@ function annotateRunningNodes(svg: SVGSVGElement) {
if (completedDotIds.has(targetId)) {
const paths = group.querySelectorAll("path, polygon");
for (const p of paths) {
p.setAttribute("stroke", "#34d399");
if (p.tagName === "polygon") p.setAttribute("fill", "#34d399");
p.setAttribute("stroke", gt.completedBorder);
if (p.tagName === "polygon") p.setAttribute("fill", gt.completedBorder);
}
}
}
@ -189,6 +191,8 @@ export default function RunGraph() {
const [pan, setPan] = useState({ x: 0, y: 0 });
const dragState = useRef<{ startX: number; startY: number; startPanX: number; startPanY: number } | null>(null);
const zoom = ZOOM_STEPS[zoomIndex];
const { theme } = useTheme();
const graphTheme = getGraphTheme(theme);
useEffect(() => {
let cancelled = false;
@ -199,9 +203,9 @@ export default function RunGraph() {
if (cancelled) return;
try {
const svg = viz.renderSVGElement(buildDot(direction));
const svg = viz.renderSVGElement(buildDot(direction, graphTheme));
stripGraphTitle(svg);
annotateRunningNodes(svg);
annotateRunningNodes(svg, graphTheme);
svgRef.current = svg;
if (innerRef.current) {
@ -215,7 +219,7 @@ export default function RunGraph() {
setPan({ x: 0, y: 0 });
render();
return () => { cancelled = true; };
}, [direction]);
}, [direction, graphTheme]);
const onPointerDown = useCallback((e: React.PointerEvent) => {
if ((e.target as HTMLElement).closest("button")) return;
@ -264,7 +268,7 @@ export default function RunGraph() {
<div className="flex gap-6">
<nav className="w-56 shrink-0 space-y-6">
<div>
<h3 className="px-2 text-xs font-medium uppercase tracking-wider text-navy-600">Stages</h3>
<h3 className="px-2 text-xs font-medium uppercase tracking-wider text-fg-muted">Stages</h3>
<ul className="mt-2 space-y-0.5">
{stages.map((stage) => {
const config = statusConfig[stage.status];
@ -273,11 +277,11 @@ export default function RunGraph() {
<li key={stage.id}>
<Link
to={`/runs/${id}/stages/${stage.id}`}
className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-ice-300 transition-colors hover:bg-white/[0.04] hover:text-white"
className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-fg-3 transition-colors hover:bg-overlay hover:text-fg"
>
<Icon className={`size-4 shrink-0 ${config.color} ${stage.status === "running" ? "animate-spin" : ""}`} />
<span className="flex-1 truncate">{stage.name}</span>
<span className="font-mono text-xs tabular-nums text-navy-600">{stage.duration}</span>
<span className="font-mono text-xs tabular-nums text-fg-muted">{stage.duration}</span>
</Link>
</li>
);
@ -287,23 +291,23 @@ export default function RunGraph() {
{workflow && (
<div>
<h3 className="px-2 text-xs font-medium uppercase tracking-wider text-navy-600">Workflow</h3>
<h3 className="px-2 text-xs font-medium uppercase tracking-wider text-fg-muted">Workflow</h3>
<ul className="mt-2 space-y-0.5">
<li>
<Link
to={`/runs/${id}/configuration`}
className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-ice-300 transition-colors hover:bg-white/[0.04] hover:text-white"
className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-fg-3 transition-colors hover:bg-overlay hover:text-fg"
>
<DocumentTextIcon className="size-4 shrink-0 text-navy-600" />
<DocumentTextIcon className="size-4 shrink-0 text-fg-muted" />
Run Configuration
</Link>
</li>
<li>
<Link
to={`/runs/${id}/graph`}
className="flex items-center gap-2 rounded-md bg-white/[0.06] px-2 py-1.5 text-sm text-white transition-colors"
className="flex items-center gap-2 rounded-md bg-overlay px-2 py-1.5 text-sm text-fg transition-colors"
>
<MapIcon className="size-4 shrink-0 text-navy-600" />
<MapIcon className="size-4 shrink-0 text-fg-muted" />
Workflow Graph
</Link>
</li>
@ -313,14 +317,14 @@ export default function RunGraph() {
</nav>
<div className="min-w-0 flex-1">
<div className="relative rounded-md border border-white/[0.06] bg-navy-900/40">
<div className="relative rounded-md border border-line bg-panel-alt/40">
<div className="absolute right-3 top-3 z-10 flex items-center gap-2">
<div className="flex items-center gap-0.5 rounded-md border border-white/[0.06] bg-navy-800/90 p-0.5">
<div className="flex items-center gap-0.5 rounded-md border border-line bg-panel/90 p-0.5">
<button
type="button"
title="Left to right"
onClick={() => setDirection("LR")}
className={`flex size-7 items-center justify-center rounded transition-colors ${direction === "LR" ? "bg-white/10 text-ice-300" : "text-navy-400 hover:bg-white/5 hover:text-ice-300"}`}
className={`flex size-7 items-center justify-center rounded transition-colors ${direction === "LR" ? "bg-overlay-strong text-fg-3" : "text-fg-muted hover:bg-overlay hover:text-fg-3"}`}
>
<ArrowRightIcon className="size-3.5" />
</button>
@ -328,18 +332,18 @@ export default function RunGraph() {
type="button"
title="Top to bottom"
onClick={() => setDirection("TB")}
className={`flex size-7 items-center justify-center rounded transition-colors ${direction === "TB" ? "bg-white/10 text-ice-300" : "text-navy-400 hover:bg-white/5 hover:text-ice-300"}`}
className={`flex size-7 items-center justify-center rounded transition-colors ${direction === "TB" ? "bg-overlay-strong text-fg-3" : "text-fg-muted hover:bg-overlay hover:text-fg-3"}`}
>
<ArrowDownIcon className="size-3.5" />
</button>
</div>
<div className="flex items-center rounded-md border border-white/[0.06] bg-navy-800/90 p-0.5">
<div className="flex items-center rounded-md border border-line bg-panel/90 p-0.5">
<button
type="button"
title="Fit to window"
onClick={fitToWindow}
className="flex size-7 items-center justify-center rounded text-navy-400 transition-colors hover:bg-white/5 hover:text-ice-300"
className="flex size-7 items-center justify-center rounded text-fg-muted transition-colors hover:bg-overlay hover:text-fg-3"
>
<svg viewBox="0 0 14 14" fill="none" stroke="currentColor" className="size-3.5" aria-hidden="true">
<rect x="1" y="1" width="12" height="12" rx="1.5" strokeWidth="1.5" strokeDasharray="3 2" />
@ -347,13 +351,13 @@ export default function RunGraph() {
</button>
</div>
<div className="flex items-center gap-0.5 rounded-md border border-white/[0.06] bg-navy-800/90 p-0.5">
<div className="flex items-center gap-0.5 rounded-md border border-line bg-panel/90 p-0.5">
<button
type="button"
title="Zoom out"
onClick={() => setZoomIndex((i) => Math.max(0, i - 1))}
disabled={zoomIndex === 0}
className="flex size-7 items-center justify-center rounded text-navy-400 transition-colors hover:bg-white/5 hover:text-ice-300 disabled:opacity-30 disabled:hover:bg-transparent disabled:hover:text-navy-400"
className="flex size-7 items-center justify-center rounded text-fg-muted transition-colors hover:bg-overlay hover:text-fg-3 disabled:opacity-30 disabled:hover:bg-transparent disabled:hover:text-fg-muted"
>
<MinusIcon className="size-4" />
</button>
@ -362,7 +366,7 @@ export default function RunGraph() {
title="Zoom in"
onClick={() => setZoomIndex((i) => Math.min(ZOOM_STEPS.length - 1, i + 1))}
disabled={zoomIndex === ZOOM_STEPS.length - 1}
className="flex size-7 items-center justify-center rounded text-navy-400 transition-colors hover:bg-white/5 hover:text-ice-300 disabled:opacity-30 disabled:hover:bg-transparent disabled:hover:text-navy-400"
className="flex size-7 items-center justify-center rounded text-fg-muted transition-colors hover:bg-overlay hover:text-fg-3 disabled:opacity-30 disabled:hover:bg-transparent disabled:hover:text-fg-muted"
>
<PlusIcon className="size-4" />
</button>
@ -383,7 +387,7 @@ export default function RunGraph() {
className="flex items-center justify-center"
style={{ transform: `translate(${pan.x}px, ${pan.y}px) scale(${zoom / 100})`, transformOrigin: "center center" }}
>
<p className="text-sm text-navy-600">Loading diagram...</p>
<p className="text-sm text-fg-muted">Loading diagram...</p>
</div>
</div>
</div>

View file

@ -1,3 +1,3 @@
export default function RunLogs() {
return <p className="text-sm text-navy-600">Logs will appear here.</p>;
return <p className="text-sm text-fg-muted">Logs will appear here.</p>;
}

View file

@ -5,6 +5,8 @@ import { CheckCircleIcon, ArrowPathIcon, PauseCircleIcon, XCircleIcon } from "@h
import { DocumentTextIcon, MapIcon } from "@heroicons/react/24/outline";
import { findRun } from "../data/runs";
import { workflowData } from "./workflow-detail";
import { useTheme } from "../lib/theme";
import { getGraphTheme } from "../lib/graph-theme";
export const handle = { wide: true };
@ -27,61 +29,63 @@ const stages: Stage[] = [
const statusConfig: Record<StageStatus, { icon: typeof CheckCircleIcon; color: string }> = {
completed: { icon: CheckCircleIcon, color: "text-mint" },
running: { icon: ArrowPathIcon, color: "text-teal-500" },
pending: { icon: PauseCircleIcon, color: "text-navy-600" },
pending: { icon: PauseCircleIcon, color: "text-fg-muted" },
failed: { icon: XCircleIcon, color: "text-coral" },
};
const THEME_ATTRS = `
function buildThemeAttrs(gt: ReturnType<typeof getGraphTheme>) {
return `
bgcolor="transparent"
pad=0.5
fontname="ui-monospace, monospace"
fontsize=10
fontcolor="#5a7a94"
color="#2a3f52"
fontcolor="${gt.fontcolor}"
color="${gt.edgeColor}"
graph [
fontname="ui-monospace, monospace"
fontsize=10
fontcolor="#5a7a94"
color="#2a3f52"
fontcolor="${gt.fontcolor}"
color="${gt.edgeColor}"
style="dashed"
penwidth=1
]
node [
fontname="ui-monospace, monospace"
fontsize=11
fontcolor="#c6d4e0"
color="#2a3f52"
fillcolor="#1a2b3c"
fontcolor="${gt.nodeText}"
color="${gt.edgeColor}"
fillcolor="${gt.nodeFill}"
style=filled
penwidth=1.2
]
edge [
fontname="ui-monospace, monospace"
fontsize=9
fontcolor="#5a7a94"
color="#2a3f52"
fontcolor="${gt.fontcolor}"
color="${gt.edgeColor}"
arrowsize=0.7
penwidth=1.2
]
`;
}
function colorSpecialNodes(dot: string): string {
function colorSpecialNodes(dot: string, gt: ReturnType<typeof getGraphTheme>): string {
// Mdiamond / Msquare → teal start/exit nodes
dot = dot.replace(
/(\bshape\s*=\s*M(?:diamond|square)\b[^\]]*)\]/g,
'$1, fillcolor="#0d4f4f", color="#14b8a6", fontcolor="#5eead4"]',
`$1, fillcolor="${gt.startFill}", color="${gt.startBorder}", fontcolor="${gt.startText}"]`,
);
// hexagon / diamond (but not Mdiamond) → amber gate nodes
dot = dot.replace(
/(\bshape\s*=\s*(?:hexagon|(?<!M)diamond)\b[^\]]*)\]/g,
'$1, fillcolor="#1a2030", color="#f59e0b", fontcolor="#fbbf24"]',
`$1, fillcolor="${gt.gateFill}", color="${gt.gateBorder}", fontcolor="${gt.gateText}"]`,
);
return dot;
}
function applyTheme(dot: string): string {
return colorSpecialNodes(dot.replace(/\{/, `{\n${THEME_ATTRS}\n`));
function applyTheme(dot: string, gt: ReturnType<typeof getGraphTheme>): string {
return colorSpecialNodes(dot.replace(/\{/, `{\n${buildThemeAttrs(gt)}\n`), gt);
}
function stripGraphTitle(svg: SVGSVGElement) {
@ -111,6 +115,8 @@ function DotDiagram({ dot }: { dot: string }) {
const [pan, setPan] = useState({ x: 0, y: 0 });
const dragState = useRef<{ startX: number; startY: number; startPanX: number; startPanY: number } | null>(null);
const zoom = ZOOM_STEPS[zoomIndex];
const { theme } = useTheme();
const graphTheme = getGraphTheme(theme);
useEffect(() => {
let cancelled = false;
@ -121,7 +127,7 @@ function DotDiagram({ dot }: { dot: string }) {
if (cancelled) return;
try {
const themed = applyTheme(dot).replace(/rankdir\s*=\s*\w+/, `rankdir=${direction}`);
const themed = applyTheme(dot, graphTheme).replace(/rankdir\s*=\s*\w+/, `rankdir=${direction}`);
const svg = viz.renderSVGElement(themed);
stripGraphTitle(svg);
svgRef.current = svg;
@ -136,7 +142,7 @@ function DotDiagram({ dot }: { dot: string }) {
setPan({ x: 0, y: 0 });
render();
return () => { cancelled = true; };
}, [dot, direction]);
}, [dot, direction, graphTheme]);
const onPointerDown = useCallback((e: React.PointerEvent) => {
if ((e.target as HTMLElement).closest("button")) return;
@ -184,12 +190,12 @@ function DotDiagram({ dot }: { dot: string }) {
return (
<div className="relative">
<div className="absolute right-3 top-3 z-10 flex items-center gap-2">
<div className="flex items-center gap-0.5 rounded-md border border-white/[0.06] bg-navy-800/90 p-0.5">
<div className="flex items-center gap-0.5 rounded-md border border-line bg-panel/90 p-0.5">
<button
type="button"
title="Left to right"
onClick={() => setDirection("LR")}
className={`flex size-7 items-center justify-center rounded transition-colors ${direction === "LR" ? "bg-white/10 text-ice-300" : "text-navy-400 hover:bg-white/5 hover:text-ice-300"}`}
className={`flex size-7 items-center justify-center rounded transition-colors ${direction === "LR" ? "bg-overlay-strong text-fg-3" : "text-fg-muted hover:bg-overlay hover:text-fg-3"}`}
>
<ArrowRightIcon className="size-3.5" />
</button>
@ -197,18 +203,18 @@ function DotDiagram({ dot }: { dot: string }) {
type="button"
title="Top to bottom"
onClick={() => setDirection("TB")}
className={`flex size-7 items-center justify-center rounded transition-colors ${direction === "TB" ? "bg-white/10 text-ice-300" : "text-navy-400 hover:bg-white/5 hover:text-ice-300"}`}
className={`flex size-7 items-center justify-center rounded transition-colors ${direction === "TB" ? "bg-overlay-strong text-fg-3" : "text-fg-muted hover:bg-overlay hover:text-fg-3"}`}
>
<ArrowDownIcon className="size-3.5" />
</button>
</div>
<div className="flex items-center rounded-md border border-white/[0.06] bg-navy-800/90 p-0.5">
<div className="flex items-center rounded-md border border-line bg-panel/90 p-0.5">
<button
type="button"
title="Fit to window"
onClick={fitToWindow}
className="flex size-7 items-center justify-center rounded text-navy-400 transition-colors hover:bg-white/5 hover:text-ice-300"
className="flex size-7 items-center justify-center rounded text-fg-muted transition-colors hover:bg-overlay hover:text-fg-3"
>
<svg viewBox="0 0 14 14" fill="none" stroke="currentColor" className="size-3.5" aria-hidden="true">
<rect x="1" y="1" width="12" height="12" rx="1.5" strokeWidth="1.5" strokeDasharray="3 2" />
@ -216,13 +222,13 @@ function DotDiagram({ dot }: { dot: string }) {
</button>
</div>
<div className="flex items-center gap-0.5 rounded-md border border-white/[0.06] bg-navy-800/90 p-0.5">
<div className="flex items-center gap-0.5 rounded-md border border-line bg-panel/90 p-0.5">
<button
type="button"
title="Zoom out"
onClick={() => setZoomIndex((i) => Math.max(0, i - 1))}
disabled={zoomIndex === 0}
className="flex size-7 items-center justify-center rounded text-navy-400 transition-colors hover:bg-white/5 hover:text-ice-300 disabled:opacity-30 disabled:hover:bg-transparent disabled:hover:text-navy-400"
className="flex size-7 items-center justify-center rounded text-fg-muted transition-colors hover:bg-overlay hover:text-fg-3 disabled:opacity-30 disabled:hover:bg-transparent disabled:hover:text-fg-muted"
>
<MinusIcon className="size-4" />
</button>
@ -231,7 +237,7 @@ function DotDiagram({ dot }: { dot: string }) {
title="Zoom in"
onClick={() => setZoomIndex((i) => Math.min(ZOOM_STEPS.length - 1, i + 1))}
disabled={zoomIndex === ZOOM_STEPS.length - 1}
className="flex size-7 items-center justify-center rounded text-navy-400 transition-colors hover:bg-white/5 hover:text-ice-300 disabled:opacity-30 disabled:hover:bg-transparent disabled:hover:text-navy-400"
className="flex size-7 items-center justify-center rounded text-fg-muted transition-colors hover:bg-overlay hover:text-fg-3 disabled:opacity-30 disabled:hover:bg-transparent disabled:hover:text-fg-muted"
>
<PlusIcon className="size-4" />
</button>
@ -252,7 +258,7 @@ function DotDiagram({ dot }: { dot: string }) {
className="flex items-center justify-center"
style={{ transform: `translate(${pan.x}px, ${pan.y}px) scale(${zoom / 100})`, transformOrigin: "center center" }}
>
<p className="text-sm text-navy-600">Loading diagram...</p>
<p className="text-sm text-fg-muted">Loading diagram...</p>
</div>
</div>
</div>
@ -268,7 +274,7 @@ export default function RunOverview() {
<div className="flex gap-6">
<nav className="w-56 shrink-0 space-y-6">
<div>
<h3 className="px-2 text-xs font-medium uppercase tracking-wider text-navy-600">Stages</h3>
<h3 className="px-2 text-xs font-medium uppercase tracking-wider text-fg-muted">Stages</h3>
<ul className="mt-2 space-y-0.5">
{stages.map((stage) => {
const config = statusConfig[stage.status];
@ -277,11 +283,11 @@ export default function RunOverview() {
<li key={stage.id}>
<Link
to={`/runs/${id}/stages/${stage.id}`}
className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-ice-300 transition-colors hover:bg-white/[0.04] hover:text-white"
className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-fg-3 transition-colors hover:bg-overlay hover:text-fg"
>
<Icon className={`size-4 shrink-0 ${config.color} ${stage.status === "running" ? "animate-spin" : ""}`} />
<span className="flex-1 truncate">{stage.name}</span>
<span className="font-mono text-xs tabular-nums text-navy-600">{stage.duration}</span>
<span className="font-mono text-xs tabular-nums text-fg-muted">{stage.duration}</span>
</Link>
</li>
);
@ -291,23 +297,23 @@ export default function RunOverview() {
{workflow && (
<div>
<h3 className="px-2 text-xs font-medium uppercase tracking-wider text-navy-600">Workflow</h3>
<h3 className="px-2 text-xs font-medium uppercase tracking-wider text-fg-muted">Workflow</h3>
<ul className="mt-2 space-y-0.5">
<li>
<Link
to={`/runs/${id}/configuration`}
className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-ice-300 transition-colors hover:bg-white/[0.04] hover:text-white"
className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-fg-3 transition-colors hover:bg-overlay hover:text-fg"
>
<DocumentTextIcon className="size-4 shrink-0 text-navy-600" />
<DocumentTextIcon className="size-4 shrink-0 text-fg-muted" />
Run Configuration
</Link>
</li>
<li>
<Link
to={`/runs/${id}/graph`}
className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-ice-300 transition-colors hover:bg-white/[0.04] hover:text-white"
className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-fg-3 transition-colors hover:bg-overlay hover:text-fg"
>
<MapIcon className="size-4 shrink-0 text-navy-600" />
<MapIcon className="size-4 shrink-0 text-fg-muted" />
Workflow Graph
</Link>
</li>
@ -318,11 +324,11 @@ export default function RunOverview() {
<div className="min-w-0 flex-1">
{workflow ? (
<div className="rounded-md border border-white/[0.06] bg-navy-900/40 overflow-hidden">
<div className="rounded-md border border-line bg-panel-alt/40 overflow-hidden">
<DotDiagram dot={workflow.graph} />
</div>
) : (
<p className="text-sm text-navy-600">No workflow graph available.</p>
<p className="text-sm text-fg-muted">No workflow graph available.</p>
)}
</div>
</div>

View file

@ -23,7 +23,7 @@ export default function RunRetro({ params }: Route.ComponentProps) {
const retro = findRetro(params.id);
if (!retro) {
return <p className="py-8 text-center text-sm text-navy-600">No retrospective found for this run.</p>;
return <p className="py-8 text-center text-sm text-fg-muted">No retrospective found for this run.</p>;
}
const smoothness = retro.smoothness ? smoothnessConfig[retro.smoothness] : null;
@ -39,8 +39,8 @@ export default function RunRetro({ params }: Route.ComponentProps) {
</span>
)}
<div className="min-w-0 flex-1">
<p className="text-sm text-ice-300">{retro.goal}</p>
<p className="mt-1 font-mono text-xs text-navy-600">
<p className="text-sm text-fg-3">{retro.goal}</p>
<p className="mt-1 font-mono text-xs text-fg-muted">
{retro.pipeline_name} &middot; {new Date(retro.timestamp).toLocaleString()}
</p>
</div>
@ -58,15 +58,15 @@ export default function RunRetro({ params }: Route.ComponentProps) {
{(retro.intent ?? retro.outcome) && (
<div className="grid gap-3 sm:grid-cols-2">
{retro.intent && (
<div className="rounded-md border border-white/[0.06] bg-navy-800/60 p-4">
<h3 className="text-xs font-medium uppercase tracking-wider text-navy-600">Intent</h3>
<p className="mt-2 text-sm leading-relaxed text-ice-300">{retro.intent}</p>
<div className="rounded-md border border-line bg-panel/60 p-4">
<h3 className="text-xs font-medium uppercase tracking-wider text-fg-muted">Intent</h3>
<p className="mt-2 text-sm leading-relaxed text-fg-3">{retro.intent}</p>
</div>
)}
{retro.outcome && (
<div className="rounded-md border border-white/[0.06] bg-navy-800/60 p-4">
<h3 className="text-xs font-medium uppercase tracking-wider text-navy-600">Outcome</h3>
<p className="mt-2 text-sm leading-relaxed text-ice-300">{retro.outcome}</p>
<div className="rounded-md border border-line bg-panel/60 p-4">
<h3 className="text-xs font-medium uppercase tracking-wider text-fg-muted">Outcome</h3>
<p className="mt-2 text-sm leading-relaxed text-fg-3">{retro.outcome}</p>
</div>
)}
</div>
@ -75,16 +75,16 @@ export default function RunRetro({ params }: Route.ComponentProps) {
{/* Learnings */}
{retro.learnings && retro.learnings.length > 0 && (
<div>
<h3 className="mb-3 text-xs font-medium uppercase tracking-wider text-navy-600">Learnings</h3>
<h3 className="mb-3 text-xs font-medium uppercase tracking-wider text-fg-muted">Learnings</h3>
<div className="space-y-2">
{retro.learnings.map((learning, i) => {
const config = learningCategoryConfig[learning.category];
return (
<div key={i} className="flex items-start gap-3 rounded-md border border-white/[0.06] bg-navy-800/60 px-4 py-3">
<span className={`mt-0.5 shrink-0 rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider ${config.text} bg-white/[0.06]`}>
<div key={i} className="flex items-start gap-3 rounded-md border border-line bg-panel/60 px-4 py-3">
<span className={`mt-0.5 shrink-0 rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider ${config.text} bg-overlay`}>
{config.label}
</span>
<p className="text-sm leading-relaxed text-ice-300">{learning.text}</p>
<p className="text-sm leading-relaxed text-fg-3">{learning.text}</p>
</div>
);
})}
@ -95,19 +95,19 @@ export default function RunRetro({ params }: Route.ComponentProps) {
{/* Friction Points */}
{retro.friction_points && retro.friction_points.length > 0 && (
<div>
<h3 className="mb-3 text-xs font-medium uppercase tracking-wider text-navy-600">Friction Points</h3>
<h3 className="mb-3 text-xs font-medium uppercase tracking-wider text-fg-muted">Friction Points</h3>
<div className="space-y-2">
{retro.friction_points.map((fp, i) => {
const config = frictionKindConfig[fp.kind];
return (
<div key={i} className="flex items-start gap-3 rounded-md border border-white/[0.06] bg-navy-800/60 px-4 py-3">
<span className={`mt-0.5 shrink-0 rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider ${config.text} bg-white/[0.06]`}>
<div key={i} className="flex items-start gap-3 rounded-md border border-line bg-panel/60 px-4 py-3">
<span className={`mt-0.5 shrink-0 rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider ${config.text} bg-overlay`}>
{config.label}
</span>
<div className="min-w-0 flex-1">
<p className="text-sm leading-relaxed text-ice-300">{fp.description}</p>
<p className="text-sm leading-relaxed text-fg-3">{fp.description}</p>
{fp.stage_id && (
<p className="mt-1 font-mono text-xs text-navy-600">
<p className="mt-1 font-mono text-xs text-fg-muted">
Stage: {retro.stages.find((s) => s.stage_id === fp.stage_id)?.stage_label ?? fp.stage_id}
</p>
)}
@ -122,16 +122,16 @@ export default function RunRetro({ params }: Route.ComponentProps) {
{/* Open Items */}
{retro.open_items && retro.open_items.length > 0 && (
<div>
<h3 className="mb-3 text-xs font-medium uppercase tracking-wider text-navy-600">Open Items</h3>
<h3 className="mb-3 text-xs font-medium uppercase tracking-wider text-fg-muted">Open Items</h3>
<div className="space-y-2">
{retro.open_items.map((item, i) => {
const config = openItemKindConfig[item.kind];
return (
<div key={i} className="flex items-start gap-3 rounded-md border border-white/[0.06] bg-navy-800/60 px-4 py-3">
<span className={`mt-0.5 shrink-0 rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider ${config.text} bg-white/[0.06]`}>
<div key={i} className="flex items-start gap-3 rounded-md border border-line bg-panel/60 px-4 py-3">
<span className={`mt-0.5 shrink-0 rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider ${config.text} bg-overlay`}>
{config.label}
</span>
<p className="text-sm leading-relaxed text-ice-300">{item.description}</p>
<p className="text-sm leading-relaxed text-fg-3">{item.description}</p>
</div>
);
})}
@ -141,11 +141,11 @@ export default function RunRetro({ params }: Route.ComponentProps) {
{/* Stage Breakdown */}
<div>
<h3 className="mb-3 text-xs font-medium uppercase tracking-wider text-navy-600">Stage Breakdown</h3>
<div className="rounded-md border border-white/[0.06] overflow-hidden">
<h3 className="mb-3 text-xs font-medium uppercase tracking-wider text-fg-muted">Stage Breakdown</h3>
<div className="rounded-md border border-line overflow-hidden">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-white/[0.06] bg-navy-800/60 text-left text-xs text-navy-600">
<tr className="border-b border-line bg-panel/60 text-left text-xs text-fg-muted">
<th className="px-4 py-2.5 font-medium">Stage</th>
<th className="px-4 py-2.5 font-medium">Status</th>
<th className="px-4 py-2.5 font-medium text-right">Duration</th>
@ -156,16 +156,16 @@ export default function RunRetro({ params }: Route.ComponentProps) {
</thead>
<tbody>
{retro.stages.map((stage) => (
<tr key={stage.stage_id} className="border-b border-white/[0.06] last:border-b-0">
<tr key={stage.stage_id} className="border-b border-line last:border-b-0">
<td className="px-4 py-3">
<Link
to={`/runs/${retro.run_id}/stages/${stage.stage_id}`}
className="text-ice-100 hover:text-white"
className="text-fg-2 hover:text-fg"
>
{stage.stage_label}
</Link>
{stage.notes && (
<p className="mt-1 text-xs text-navy-600">{stage.notes}</p>
<p className="mt-1 text-xs text-fg-muted">{stage.notes}</p>
)}
{stage.failure_reason && (
<p className="mt-1 text-xs text-coral/80">{stage.failure_reason}</p>
@ -174,18 +174,18 @@ export default function RunRetro({ params }: Route.ComponentProps) {
<td className="px-4 py-3">
<StageStatusBadge status={stage.status} />
</td>
<td className="px-4 py-3 text-right font-mono text-xs tabular-nums text-ice-300">
<td className="px-4 py-3 text-right font-mono text-xs tabular-nums text-fg-3">
{formatDuration(stage.duration_ms)}
</td>
<td className="px-4 py-3 text-right font-mono text-xs tabular-nums">
<span className={stage.retries > 0 ? "text-amber" : "text-ice-300"}>
<span className={stage.retries > 0 ? "text-amber" : "text-fg-3"}>
{stage.retries}
</span>
</td>
<td className="px-4 py-3 text-right font-mono text-xs tabular-nums text-ice-300">
<td className="px-4 py-3 text-right font-mono text-xs tabular-nums text-fg-3">
{formatCost(stage.cost)}
</td>
<td className="px-4 py-3 text-right font-mono text-xs tabular-nums text-ice-300">
<td className="px-4 py-3 text-right font-mono text-xs tabular-nums text-fg-3">
{stage.files_touched.length}
</td>
</tr>
@ -200,9 +200,9 @@ export default function RunRetro({ params }: Route.ComponentProps) {
function StatCard({ label, value, warn }: { label: string; value: string; warn?: boolean }) {
return (
<div className="rounded-md border border-white/[0.06] bg-navy-800/60 px-4 py-3">
<p className="text-xs font-medium uppercase tracking-wider text-navy-600">{label}</p>
<p className={`mt-1 font-mono text-lg font-semibold tabular-nums ${warn ? "text-amber" : "text-white"}`}>
<div className="rounded-md border border-line bg-panel/60 px-4 py-3">
<p className="text-xs font-medium uppercase tracking-wider text-fg-muted">{label}</p>
<p className={`mt-1 font-mono text-lg font-semibold tabular-nums ${warn ? "text-amber" : "text-fg"}`}>
{value}
</p>
</div>
@ -213,10 +213,10 @@ function StageStatusBadge({ status }: { status: string }) {
const styles: Record<string, string> = {
completed: "text-mint",
running: "text-teal-500",
pending: "text-navy-600",
pending: "text-fg-muted",
failed: "text-coral",
};
const colorClass = styles[status] ?? "text-ice-300";
const colorClass = styles[status] ?? "text-fg-3";
return (
<span className={`text-xs font-medium capitalize ${colorClass}`}>
{status}

View file

@ -27,7 +27,7 @@ const stages: Stage[] = [
const statusConfig: Record<StageStatus, { icon: typeof CheckCircleIcon; color: string }> = {
completed: { icon: CheckCircleIcon, color: "text-mint" },
running: { icon: ArrowPathIcon, color: "text-teal-500" },
pending: { icon: PauseCircleIcon, color: "text-navy-600" },
pending: { icon: PauseCircleIcon, color: "text-fg-muted" },
failed: { icon: XCircleIcon, color: "text-coral" },
};
@ -83,26 +83,26 @@ function ToolRow({ tool }: { tool: ToolUse }) {
const [open, setOpen] = useState(false);
return (
<div className="border-b border-white/[0.04] last:border-b-0">
<div className="border-b border-line last:border-b-0">
<button
type="button"
onClick={() => setOpen((v) => !v)}
className="flex w-full items-center gap-1.5 px-2.5 py-1.5 text-left transition-colors hover:bg-white/[0.02]"
className="flex w-full items-center gap-1.5 px-2.5 py-1.5 text-left transition-colors hover:bg-overlay"
>
<ChevronRightIcon className={`size-3 shrink-0 text-navy-600 transition-transform duration-150 ${open ? "rotate-90" : ""}`} />
<WrenchScrewdriverIcon className="size-3.5 shrink-0 text-navy-600" />
<span className="font-mono text-xs text-ice-300">{tool.toolName}</span>
<span className="truncate font-mono text-xs text-navy-600">{tool.args}</span>
<ChevronRightIcon className={`size-3 shrink-0 text-fg-muted transition-transform duration-150 ${open ? "rotate-90" : ""}`} />
<WrenchScrewdriverIcon className="size-3.5 shrink-0 text-fg-muted" />
<span className="font-mono text-xs text-fg-3">{tool.toolName}</span>
<span className="truncate font-mono text-xs text-fg-muted">{tool.args}</span>
</button>
{open && (
<div className="space-y-px bg-white/[0.01] px-2.5 pb-2 pt-1">
<div className="rounded bg-white/[0.02] px-2.5 py-2">
<div className="mb-1 text-[10px] font-medium uppercase tracking-wider text-navy-600">Args</div>
<pre className="whitespace-pre-wrap font-mono text-xs leading-relaxed text-ice-300">{tool.args}</pre>
<div className="space-y-px bg-overlay px-2.5 pb-2 pt-1">
<div className="rounded bg-overlay px-2.5 py-2">
<div className="mb-1 text-[10px] font-medium uppercase tracking-wider text-fg-muted">Args</div>
<pre className="whitespace-pre-wrap font-mono text-xs leading-relaxed text-fg-3">{tool.args}</pre>
</div>
<div className="rounded bg-white/[0.02] px-2.5 py-2">
<div className="mb-1 text-[10px] font-medium uppercase tracking-wider text-navy-600">Result</div>
<pre className="whitespace-pre-wrap font-mono text-xs leading-relaxed text-ice-300">{tool.result}</pre>
<div className="rounded bg-overlay px-2.5 py-2">
<div className="mb-1 text-[10px] font-medium uppercase tracking-wider text-fg-muted">Result</div>
<pre className="whitespace-pre-wrap font-mono text-xs leading-relaxed text-fg-3">{tool.result}</pre>
</div>
</div>
)}
@ -112,7 +112,7 @@ function ToolRow({ tool }: { tool: ToolUse }) {
function ToolBlock({ tools }: { tools: ToolUse[] }) {
return (
<div className="rounded-md border border-white/[0.06] bg-white/[0.01] overflow-hidden">
<div className="rounded-md border border-line bg-overlay overflow-hidden">
{tools.map((tool, i) => (
<ToolRow key={i} tool={tool} />
))}
@ -125,10 +125,10 @@ function SystemBlock({ content }: { content: string }) {
<div className="rounded-md border border-amber/10 bg-amber/5 overflow-hidden">
<div className="flex items-center gap-2 px-3 py-2">
<CommandLineIcon className="size-4 shrink-0 text-amber" />
<span className="text-xs font-medium text-ice-300">System Prompt</span>
<span className="text-xs font-medium text-fg-3">System Prompt</span>
</div>
<div className="border-t border-white/[0.04] px-3 py-2.5">
<pre className="whitespace-pre-wrap font-mono text-xs leading-relaxed text-ice-300">{content}</pre>
<div className="border-t border-line px-3 py-2.5">
<pre className="whitespace-pre-wrap font-mono text-xs leading-relaxed text-fg-3">{content}</pre>
</div>
</div>
);
@ -139,10 +139,10 @@ function AssistantBlock({ content }: { content: string }) {
<div className="rounded-md border border-teal-500/10 bg-teal-500/5 overflow-hidden">
<div className="flex items-center gap-2 px-3 py-2">
<ChatBubbleLeftIcon className="size-4 shrink-0 text-teal-500" />
<span className="text-xs font-medium text-ice-300">Assistant</span>
<span className="text-xs font-medium text-fg-3">Assistant</span>
</div>
<div className="border-t border-white/[0.04] px-3 py-2.5">
<pre className="whitespace-pre-wrap font-mono text-xs leading-relaxed text-ice-300">{content}</pre>
<div className="border-t border-line px-3 py-2.5">
<pre className="whitespace-pre-wrap font-mono text-xs leading-relaxed text-fg-3">{content}</pre>
</div>
</div>
);
@ -160,7 +160,7 @@ export default function RunStages() {
<div className="flex gap-6">
<nav className="w-56 shrink-0 space-y-6">
<div>
<h3 className="px-2 text-xs font-medium uppercase tracking-wider text-navy-600">Stages</h3>
<h3 className="px-2 text-xs font-medium uppercase tracking-wider text-fg-muted">Stages</h3>
<ul className="mt-2 space-y-0.5">
{stages.map((stage) => {
const config = statusConfig[stage.status];
@ -172,13 +172,13 @@ export default function RunStages() {
to={`/runs/${id}/stages/${stage.id}`}
className={`flex items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors ${
isSelected
? "bg-white/[0.06] text-white"
: "text-ice-300 hover:bg-white/[0.04] hover:text-white"
? "bg-overlay text-fg"
: "text-fg-3 hover:bg-overlay hover:text-fg"
}`}
>
<Icon className={`size-4 shrink-0 ${config.color} ${stage.status === "running" ? "animate-spin" : ""}`} />
<span className="flex-1 truncate">{stage.name}</span>
<span className="font-mono text-xs tabular-nums text-navy-600">{stage.duration}</span>
<span className="font-mono text-xs tabular-nums text-fg-muted">{stage.duration}</span>
</Link>
</li>
);
@ -188,23 +188,23 @@ export default function RunStages() {
{workflow && (
<div>
<h3 className="px-2 text-xs font-medium uppercase tracking-wider text-navy-600">Workflow</h3>
<h3 className="px-2 text-xs font-medium uppercase tracking-wider text-fg-muted">Workflow</h3>
<ul className="mt-2 space-y-0.5">
<li>
<Link
to={`/runs/${id}/configuration`}
className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-ice-300 transition-colors hover:bg-white/[0.04] hover:text-white"
className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-fg-3 transition-colors hover:bg-overlay hover:text-fg"
>
<DocumentTextIcon className="size-4 shrink-0 text-navy-600" />
<DocumentTextIcon className="size-4 shrink-0 text-fg-muted" />
Run Configuration
</Link>
</li>
<li>
<Link
to={`/runs/${id}/graph`}
className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-ice-300 transition-colors hover:bg-white/[0.04] hover:text-white"
className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-fg-3 transition-colors hover:bg-overlay hover:text-fg"
>
<MapIcon className="size-4 shrink-0 text-navy-600" />
<MapIcon className="size-4 shrink-0 text-fg-muted" />
Workflow Graph
</Link>
</li>
@ -216,8 +216,8 @@ export default function RunStages() {
<div className="min-w-0 flex-1 space-y-3">
<div className="flex items-center gap-2">
<SelectedIcon className={`size-5 ${selectedConfig.color}`} />
<h3 className="text-sm font-medium text-white">{selectedStage.name}</h3>
<span className="font-mono text-xs text-navy-600">{selectedStage.duration}</span>
<h3 className="text-sm font-medium text-fg">{selectedStage.name}</h3>
<span className="font-mono text-xs text-fg-muted">{selectedStage.duration}</span>
</div>
{turns.map((turn, i) => {

View file

@ -32,10 +32,10 @@ function formatTokens(n: number) {
export default function RunUsage() {
return (
<div className="space-y-6">
<div className="rounded-md border border-white/[0.06] overflow-hidden">
<div className="rounded-md border border-line overflow-hidden">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-white/[0.06] bg-navy-800/60 text-left text-xs text-navy-600">
<tr className="border-b border-line bg-panel/60 text-left text-xs text-fg-muted">
<th className="px-4 py-2.5 font-medium">Stage</th>
<th className="px-4 py-2.5 font-medium">Model</th>
<th className="px-4 py-2.5 font-medium text-right">Tokens</th>
@ -45,37 +45,37 @@ export default function RunUsage() {
</thead>
<tbody>
{stages.map((row) => (
<tr key={row.stage} className="border-b border-white/[0.06] last:border-b-0">
<td className="px-4 py-3 text-ice-100">{row.stage}</td>
<td className="px-4 py-3 font-mono text-xs text-ice-300">{row.model}</td>
<td className="px-4 py-3 text-right font-mono text-xs tabular-nums text-ice-300">
{formatTokens(row.inputTokens)} <span className="text-navy-600">/</span> {formatTokens(row.outputTokens)}
<tr key={row.stage} className="border-b border-line last:border-b-0">
<td className="px-4 py-3 text-fg-2">{row.stage}</td>
<td className="px-4 py-3 font-mono text-xs text-fg-3">{row.model}</td>
<td className="px-4 py-3 text-right font-mono text-xs tabular-nums text-fg-3">
{formatTokens(row.inputTokens)} <span className="text-fg-muted">/</span> {formatTokens(row.outputTokens)}
</td>
<td className="px-4 py-3 text-right font-mono text-xs text-ice-300">{row.runtime}</td>
<td className="px-4 py-3 text-right font-mono text-xs text-ice-300">${row.cost.toFixed(2)}</td>
<td className="px-4 py-3 text-right font-mono text-xs text-fg-3">{row.runtime}</td>
<td className="px-4 py-3 text-right font-mono text-xs text-fg-3">${row.cost.toFixed(2)}</td>
</tr>
))}
</tbody>
<tfoot>
<tr className="border-t border-white/[0.08] bg-navy-800/40">
<td className="px-4 py-3 font-medium text-white">Total</td>
<tr className="border-t border-line-strong bg-panel/40">
<td className="px-4 py-3 font-medium text-fg">Total</td>
<td />
<td className="px-4 py-3 text-right font-mono text-xs tabular-nums font-medium text-white">
{formatTokens(totalInput)} <span className="text-navy-600">/</span> {formatTokens(totalOutput)}
<td className="px-4 py-3 text-right font-mono text-xs tabular-nums font-medium text-fg">
{formatTokens(totalInput)} <span className="text-fg-muted">/</span> {formatTokens(totalOutput)}
</td>
<td className="px-4 py-3 text-right font-mono text-xs font-medium text-white">{totalRuntime}</td>
<td className="px-4 py-3 text-right font-mono text-xs font-medium text-white">${totalCost.toFixed(2)}</td>
<td className="px-4 py-3 text-right font-mono text-xs font-medium text-fg">{totalRuntime}</td>
<td className="px-4 py-3 text-right font-mono text-xs font-medium text-fg">${totalCost.toFixed(2)}</td>
</tr>
</tfoot>
</table>
</div>
<div>
<h3 className="mb-3 text-xs font-medium uppercase tracking-wider text-navy-600">By Model</h3>
<div className="rounded-md border border-white/[0.06] overflow-hidden">
<h3 className="mb-3 text-xs font-medium uppercase tracking-wider text-fg-muted">By Model</h3>
<div className="rounded-md border border-line overflow-hidden">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-white/[0.06] bg-navy-800/60 text-left text-xs text-navy-600">
<tr className="border-b border-line bg-panel/60 text-left text-xs text-fg-muted">
<th className="px-4 py-2.5 font-medium">Model</th>
<th className="px-4 py-2.5 font-medium text-right">Stages</th>
<th className="px-4 py-2.5 font-medium text-right">Tokens</th>
@ -84,24 +84,24 @@ export default function RunUsage() {
</thead>
<tbody>
{modelBreakdown.map((row) => (
<tr key={row.model} className="border-b border-white/[0.06] last:border-b-0">
<td className="px-4 py-3 font-mono text-xs text-ice-100">{row.model}</td>
<td className="px-4 py-3 text-right font-mono text-xs tabular-nums text-ice-300">{row.stages}</td>
<td className="px-4 py-3 text-right font-mono text-xs tabular-nums text-ice-300">
{formatTokens(row.inputTokens)} <span className="text-navy-600">/</span> {formatTokens(row.outputTokens)}
<tr key={row.model} className="border-b border-line last:border-b-0">
<td className="px-4 py-3 font-mono text-xs text-fg-2">{row.model}</td>
<td className="px-4 py-3 text-right font-mono text-xs tabular-nums text-fg-3">{row.stages}</td>
<td className="px-4 py-3 text-right font-mono text-xs tabular-nums text-fg-3">
{formatTokens(row.inputTokens)} <span className="text-fg-muted">/</span> {formatTokens(row.outputTokens)}
</td>
<td className="px-4 py-3 text-right font-mono text-xs text-ice-300">${row.cost.toFixed(2)}</td>
<td className="px-4 py-3 text-right font-mono text-xs text-fg-3">${row.cost.toFixed(2)}</td>
</tr>
))}
</tbody>
<tfoot>
<tr className="border-t border-white/[0.08] bg-navy-800/40">
<td className="px-4 py-3 font-medium text-white">Total</td>
<td className="px-4 py-3 text-right font-mono text-xs tabular-nums font-medium text-white">{stages.length}</td>
<td className="px-4 py-3 text-right font-mono text-xs tabular-nums font-medium text-white">
{formatTokens(totalInput)} <span className="text-navy-600">/</span> {formatTokens(totalOutput)}
<tr className="border-t border-line-strong bg-panel/40">
<td className="px-4 py-3 font-medium text-fg">Total</td>
<td className="px-4 py-3 text-right font-mono text-xs tabular-nums font-medium text-fg">{stages.length}</td>
<td className="px-4 py-3 text-right font-mono text-xs tabular-nums font-medium text-fg">
{formatTokens(totalInput)} <span className="text-fg-muted">/</span> {formatTokens(totalOutput)}
</td>
<td className="px-4 py-3 text-right font-mono text-xs font-medium text-white">${totalCost.toFixed(2)}</td>
<td className="px-4 py-3 text-right font-mono text-xs font-medium text-fg">${totalCost.toFixed(2)}</td>
</tr>
</tfoot>
</table>

View file

@ -34,7 +34,7 @@ function StatusIcon({
case "fail":
return <XCircleIcon className={`${className} text-coral`} />;
case "na":
return <MinusCircleIcon className={`${className} text-navy-600`} />;
return <MinusCircleIcon className={`${className} text-fg-muted`} />;
}
}
@ -59,47 +59,47 @@ function CategoryCard({ category }: { category: VerificationCategory }) {
<Disclosure
as="div"
defaultOpen={category.status === "fail"}
className={`rounded-md border border-white/[0.06] overflow-hidden border-l-2 ${config.border}`}
className={`rounded-md border border-line overflow-hidden border-l-2 ${config.border}`}
>
<DisclosureButton className="group flex w-full items-center gap-3 px-4 py-3.5 text-left transition-colors hover:bg-white/[0.02]">
<DisclosureButton className="group flex w-full items-center gap-3 px-4 py-3.5 text-left transition-colors hover:bg-overlay">
<StatusIcon status={category.status} />
<div className="min-w-0 flex-1">
<div className="flex items-baseline gap-3">
<span className="shrink-0 text-sm font-semibold text-white">
<span className="shrink-0 text-sm font-semibold text-fg">
{category.name}
</span>
<span className="truncate text-xs text-navy-600">
<span className="truncate text-xs text-fg-muted">
{category.question}
</span>
</div>
</div>
<span className="shrink-0 font-mono text-xs tabular-nums text-navy-600">
<span className="shrink-0 font-mono text-xs tabular-nums text-fg-muted">
{criteriaStats.passing}/{applicable}
</span>
<ChevronRightIcon className="size-4 shrink-0 text-navy-600 transition-transform duration-200 group-data-open:rotate-90" />
<ChevronRightIcon className="size-4 shrink-0 text-fg-muted transition-transform duration-200 group-data-open:rotate-90" />
</DisclosureButton>
<DisclosurePanel
transition
className="origin-top transition duration-200 ease-out data-closed:-translate-y-1 data-closed:opacity-0"
>
<div className="border-t border-white/[0.06]">
<div className="border-t border-line">
<table className="w-full text-sm">
<tbody>
{category.criteria.map((criterion) => (
<tr
key={criterion.name}
className="border-b border-white/[0.04] last:border-b-0 transition-colors hover:bg-white/[0.015]"
className="border-b border-line last:border-b-0 transition-colors hover:bg-overlay"
>
<td className="w-8 py-2.5 pl-5 pr-0">
<span
className={`inline-block size-2 rounded-full ${statusConfig[criterion.status].dot}`}
/>
</td>
<td className="whitespace-nowrap py-2.5 pl-2 pr-3 font-medium text-ice-100">
<td className="whitespace-nowrap py-2.5 pl-2 pr-3 font-medium text-fg-2">
{criterion.name}
</td>
<td className="py-2.5 px-3 text-navy-600">
<td className="py-2.5 px-3 text-fg-muted">
{criterion.description || (
<span className="italic">Not configured</span>
)}

View file

@ -226,26 +226,26 @@ function ToolRow({ tool }: { tool: ToolUse }) {
const [open, setOpen] = useState(false);
return (
<div className="border-b border-white/[0.04] last:border-b-0">
<div className="border-b border-line last:border-b-0">
<button
type="button"
onClick={() => setOpen((v) => !v)}
className="flex w-full items-center gap-1.5 px-2.5 py-1.5 text-left transition-colors hover:bg-white/[0.02] cursor-pointer"
className="flex w-full items-center gap-1.5 px-2.5 py-1.5 text-left transition-colors hover:bg-overlay cursor-pointer"
>
<ChevronRightIcon className={`size-3 shrink-0 text-navy-600 transition-transform duration-150 ${open ? "rotate-90" : ""}`} />
<WrenchScrewdriverIcon className="size-3.5 shrink-0 text-navy-600" />
<span className="font-mono text-xs text-ice-300">{tool.toolName}</span>
<span className="truncate font-mono text-xs text-navy-600">{tool.args}</span>
<ChevronRightIcon className={`size-3 shrink-0 text-fg-muted transition-transform duration-150 ${open ? "rotate-90" : ""}`} />
<WrenchScrewdriverIcon className="size-3.5 shrink-0 text-fg-muted" />
<span className="font-mono text-xs text-fg-3">{tool.toolName}</span>
<span className="truncate font-mono text-xs text-fg-muted">{tool.args}</span>
</button>
{open && (
<div className="space-y-px bg-white/[0.01] px-2.5 pb-2 pt-1">
<div className="rounded bg-white/[0.02] px-2.5 py-2">
<div className="mb-1 text-[10px] font-medium uppercase tracking-wider text-navy-600">Args</div>
<pre className="whitespace-pre-wrap font-mono text-xs leading-relaxed text-ice-300">{tool.args}</pre>
<div className="space-y-px bg-overlay px-2.5 pb-2 pt-1">
<div className="rounded bg-overlay px-2.5 py-2">
<div className="mb-1 text-[10px] font-medium uppercase tracking-wider text-fg-muted">Args</div>
<pre className="whitespace-pre-wrap font-mono text-xs leading-relaxed text-fg-3">{tool.args}</pre>
</div>
<div className="rounded bg-white/[0.02] px-2.5 py-2">
<div className="mb-1 text-[10px] font-medium uppercase tracking-wider text-navy-600">Result</div>
<pre className="whitespace-pre-wrap font-mono text-xs leading-relaxed text-ice-300">{tool.result}</pre>
<div className="rounded bg-overlay px-2.5 py-2">
<div className="mb-1 text-[10px] font-medium uppercase tracking-wider text-fg-muted">Result</div>
<pre className="whitespace-pre-wrap font-mono text-xs leading-relaxed text-fg-3">{tool.result}</pre>
</div>
</div>
)}
@ -255,7 +255,7 @@ function ToolRow({ tool }: { tool: ToolUse }) {
function ToolBlock({ tools }: { tools: ToolUse[] }) {
return (
<div className="rounded-md border border-white/[0.06] bg-white/[0.01] overflow-hidden">
<div className="rounded-md border border-line bg-overlay overflow-hidden">
{tools.map((tool, i) => (
<ToolRow key={i} tool={tool} />
))}
@ -277,7 +277,7 @@ function CopyButton({ text }: { text: string }) {
<button
type="button"
onClick={handleCopy}
className="inline-flex items-center justify-center rounded-md border border-white/[0.06] bg-navy-800/80 p-1.5 text-navy-600 transition-colors hover:border-white/[0.12] hover:text-ice-300"
className="inline-flex items-center justify-center rounded-md border border-line bg-panel/80 p-1.5 text-fg-muted transition-colors hover:border-line-strong hover:text-fg-3"
aria-label="Copy"
>
{copied
@ -291,16 +291,16 @@ function UserBlock({ content, date }: { content: string; date?: string }) {
return (
<div className="group">
<div className="flex gap-3">
<div className="flex size-7 shrink-0 items-center justify-center rounded-full bg-navy-800 border border-white/[0.08]">
<UserIcon className="size-3.5 text-ice-300" />
<div className="flex size-7 shrink-0 items-center justify-center rounded-full bg-panel border border-line-strong">
<UserIcon className="size-3.5 text-fg-3" />
</div>
<div className="min-w-0 flex-1 pt-0.5">
<pre className="whitespace-pre-wrap font-sans text-sm leading-relaxed text-ice-100">{content}</pre>
<pre className="whitespace-pre-wrap font-sans text-sm leading-relaxed text-fg-2">{content}</pre>
</div>
</div>
<div className="ml-10 mt-1 flex h-6 items-center gap-2 opacity-0 transition-opacity group-hover:opacity-100">
<CopyButton text={content} />
{date != null && <span className="text-[11px] text-navy-600">{date}</span>}
{date != null && <span className="text-[11px] text-fg-muted">{date}</span>}
</div>
</div>
);
@ -314,7 +314,7 @@ function AssistantBlock({ content, showCopy }: { content: string; showCopy: bool
<ChatBubbleLeftIcon className="size-3.5 text-teal-500" />
</div>
<div className="min-w-0 flex-1 pt-0.5">
<pre className="whitespace-pre-wrap font-sans text-sm leading-relaxed text-ice-300">{content}</pre>
<pre className="whitespace-pre-wrap font-sans text-sm leading-relaxed text-fg-3">{content}</pre>
</div>
</div>
{showCopy && (
@ -328,20 +328,20 @@ function AssistantBlock({ content, showCopy }: { content: string; showCopy: bool
function SessionSidebar({ activeId }: { activeId: string }) {
return (
<aside className="w-64 shrink-0 border-r border-white/[0.06] flex flex-col h-[calc(100vh-4rem)]">
<aside className="w-64 shrink-0 border-r border-line flex flex-col h-[calc(100vh-4rem)]">
<div className="p-3">
<Link
to="/start"
className="flex w-full items-center gap-2 rounded-lg border border-white/[0.06] bg-navy-800/60 px-3 py-2 text-sm text-ice-100 transition-colors hover:bg-navy-800 hover:border-white/[0.12]"
className="flex w-full items-center gap-2 rounded-lg border border-line bg-panel/60 px-3 py-2 text-sm text-fg-2 transition-colors hover:bg-panel hover:border-line-strong"
>
<PencilSquareIcon className="size-4 text-navy-600" />
<PencilSquareIcon className="size-4 text-fg-muted" />
New session
</Link>
</div>
<nav className="flex-1 overflow-y-auto px-3 pb-4">
{sessionGroups.map((group) => (
<div key={group.label} className="mt-4 first:mt-1">
<p className="px-2 mb-1.5 text-[11px] font-medium uppercase tracking-wider text-navy-600">
<p className="px-2 mb-1.5 text-[11px] font-medium uppercase tracking-wider text-fg-muted">
{group.label}
</p>
<ul className="space-y-0.5">
@ -351,14 +351,14 @@ function SessionSidebar({ activeId }: { activeId: string }) {
to={`/sessions/${session.id}`}
className={`flex w-full flex-col rounded-lg px-2.5 py-2 text-left transition-colors ${
activeId === session.id
? "bg-white/[0.06] text-ice-100"
: "text-ice-300 hover:bg-white/[0.04]"
? "bg-overlay text-fg-2"
: "text-fg-3 hover:bg-overlay"
}`}
>
<span className="truncate text-sm">{session.title}</span>
<span className="flex items-center gap-1.5 mt-0.5">
<span className="font-mono text-[11px] text-teal-500">{session.repo}</span>
<span className="text-[11px] text-navy-600">{session.time}</span>
<span className="text-[11px] text-fg-muted">{session.time}</span>
</span>
</Link>
</li>
@ -380,11 +380,11 @@ export default function SessionDetail() {
<SessionSidebar activeId={session.id} />
<div className="flex-1 flex flex-col min-h-[calc(100vh-4rem)]">
<div className="border-b border-white/[0.06] px-6 py-3 flex items-center gap-3">
<h1 className="text-sm font-medium text-ice-100">{session.title}</h1>
<div className="border-b border-line px-6 py-3 flex items-center gap-3">
<h1 className="text-sm font-medium text-fg-2">{session.title}</h1>
<span className="font-mono text-xs text-teal-500">{session.repo}</span>
<span className="text-xs text-navy-600">{session.time}</span>
<span className="ml-auto font-mono text-xs text-navy-600">{session.model}</span>
<span className="text-xs text-fg-muted">{session.time}</span>
<span className="ml-auto font-mono text-xs text-fg-muted">{session.model}</span>
</div>
<div className="flex-1 overflow-y-auto px-6 py-6">
@ -405,13 +405,13 @@ export default function SessionDetail() {
</div>
</div>
<div className="border-t border-white/[0.06] px-6 py-4">
<div className="border-t border-line px-6 py-4">
<div className="mx-auto max-w-3xl">
<div className="flex items-start gap-3 rounded-lg border border-white/[0.06] bg-navy-800/80 px-4 py-3 focus-within:border-teal-500/40">
<div className="flex items-start gap-3 rounded-lg border border-line bg-panel/80 px-4 py-3 focus-within:border-focus">
<textarea
placeholder="Send a message..."
rows={1}
className="flex-1 resize-none bg-transparent text-sm text-ice-100 placeholder-navy-600 outline-none"
className="flex-1 resize-none bg-transparent text-sm text-fg-2 placeholder-fg-muted outline-none"
/>
<button
type="button"

View file

@ -136,7 +136,7 @@ function ToggleSwitch({ enabled, accent }: { enabled: boolean; accent: string })
return (
<button
type="button"
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ${enabled ? colors.toggle : "bg-white/[0.08]"}`}
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ${enabled ? colors.toggle : "bg-overlay-strong"}`}
role="switch"
aria-checked={enabled}
>
@ -149,11 +149,11 @@ function ToggleSwitch({ enabled, accent }: { enabled: boolean; accent: string })
function SettingRow({ field, accent, isLast }: { field: SettingField; accent: string; isLast: boolean }) {
return (
<div className={`flex items-center justify-between gap-8 px-5 py-3.5 ${isLast ? "" : "border-b border-white/[0.04]"}`}>
<div className={`flex items-center justify-between gap-8 px-5 py-3.5 ${isLast ? "" : "border-b border-line"}`}>
<div className="min-w-0">
<p className="text-sm text-ice-100">{field.label}</p>
<p className="text-sm text-fg-2">{field.label}</p>
{field.description != null && (
<p className="mt-0.5 text-xs text-navy-600">{field.description}</p>
<p className="mt-0.5 text-xs text-fg-muted">{field.description}</p>
)}
</div>
@ -163,7 +163,7 @@ function SettingRow({ field, accent, isLast }: { field: SettingField; accent: st
) : field.type === "select" ? (
<select
defaultValue={field.value}
className="appearance-none rounded-md border border-white/[0.06] bg-navy-950/60 py-1.5 pl-3 pr-8 text-sm text-ice-100 outline-none transition-colors focus:border-teal-500/40 focus:ring-0"
className="appearance-none rounded-md border border-line bg-page/60 py-1.5 pl-3 pr-8 text-sm text-fg-2 outline-none transition-colors focus:border-focus focus:ring-0"
>
{field.options?.map((opt) => (
<option key={opt} value={opt}>{opt}</option>
@ -173,7 +173,7 @@ function SettingRow({ field, accent, isLast }: { field: SettingField; accent: st
<input
type="text"
defaultValue={field.value}
className="w-64 rounded-md border border-white/[0.06] bg-navy-950/60 px-3 py-1.5 text-sm text-ice-100 placeholder-navy-600 outline-none transition-colors focus:border-teal-500/40 focus:ring-0"
className="w-64 rounded-md border border-line bg-page/60 px-3 py-1.5 text-sm text-fg-2 placeholder-fg-muted outline-none transition-colors focus:border-focus focus:ring-0"
/>
)}
</div>
@ -199,8 +199,8 @@ function SettingsSection({ group, isActive, onVisible }: { group: SettingGroup;
}}
className={`rounded-xl border transition-colors duration-300 ${
isActive
? `${colors.border} bg-navy-800/60`
: "border-white/[0.06] bg-navy-800/40"
? `${colors.border} bg-panel/60`
: "border-line bg-panel/40"
}`}
>
<div className="flex items-center gap-3 px-5 py-4">
@ -208,11 +208,11 @@ function SettingsSection({ group, isActive, onVisible }: { group: SettingGroup;
<Icon className={`size-4.5 ${colors.icon}`} />
</div>
<div>
<h2 className="text-sm font-semibold text-ice-100">{group.name}</h2>
<p className="text-xs text-navy-600">{group.description}</p>
<h2 className="text-sm font-semibold text-fg-2">{group.name}</h2>
<p className="text-xs text-fg-muted">{group.description}</p>
</div>
</div>
<div className="border-t border-white/[0.04]">
<div className="border-t border-line">
{group.fields.map((field, i) => (
<SettingRow
key={field.key}
@ -229,7 +229,7 @@ function SettingsSection({ group, isActive, onVisible }: { group: SettingGroup;
function SidebarNav({ activeId }: { activeId: string }) {
return (
<nav className="sticky top-8 w-44 shrink-0 hidden lg:block">
<p className="px-3 mb-3 text-[11px] font-medium uppercase tracking-wider text-navy-600">
<p className="px-3 mb-3 text-[11px] font-medium uppercase tracking-wider text-fg-muted">
Settings
</p>
<ul className="space-y-0.5">
@ -248,10 +248,10 @@ function SidebarNav({ activeId }: { activeId: string }) {
className={`flex items-center gap-2.5 rounded-lg px-3 py-2 text-sm transition-all duration-200 ${
active
? `${colors.glow} ${colors.icon} font-medium`
: "text-ice-300 hover:text-ice-100 hover:bg-white/[0.04]"
: "text-fg-3 hover:text-fg-2 hover:bg-overlay"
}`}
>
<Icon className={`size-4 ${active ? colors.icon : "text-navy-600"}`} />
<Icon className={`size-4 ${active ? colors.icon : "text-fg-muted"}`} />
{group.name}
</a>
</li>

View file

@ -76,9 +76,9 @@ function BranchIcon({ className }: { className?: string }) {
function SessionSidebar() {
return (
<aside className="w-64 shrink-0 border-r border-white/[0.06] flex flex-col h-[calc(100vh-4rem)]">
<aside className="w-64 shrink-0 border-r border-line flex flex-col h-[calc(100vh-4rem)]">
<div className="p-3">
<div className="flex w-full items-center gap-2 rounded-lg border border-teal-500/20 bg-navy-800/60 px-3 py-2 text-sm text-ice-100">
<div className="flex w-full items-center gap-2 rounded-lg border border-teal-500/20 bg-panel/60 px-3 py-2 text-sm text-fg-2">
<PencilSquareIcon className="size-4 text-teal-500" />
New session
</div>
@ -86,7 +86,7 @@ function SessionSidebar() {
<nav className="flex-1 overflow-y-auto px-3 pb-4">
{sessionGroups.map((group) => (
<div key={group.label} className="mt-4 first:mt-1">
<p className="px-2 mb-1.5 text-[11px] font-medium uppercase tracking-wider text-navy-600">
<p className="px-2 mb-1.5 text-[11px] font-medium uppercase tracking-wider text-fg-muted">
{group.label}
</p>
<ul className="space-y-0.5">
@ -94,12 +94,12 @@ function SessionSidebar() {
<li key={session.id}>
<Link
to={`/sessions/${session.id}`}
className="flex w-full flex-col rounded-lg px-2.5 py-2 text-left transition-colors text-ice-300 hover:bg-white/[0.04]"
className="flex w-full flex-col rounded-lg px-2.5 py-2 text-left transition-colors text-fg-3 hover:bg-overlay"
>
<span className="truncate text-sm">{session.title}</span>
<span className="flex items-center gap-1.5 mt-0.5">
<span className="font-mono text-[11px] text-teal-500">{session.repo}</span>
<span className="text-[11px] text-navy-600">{session.time}</span>
<span className="text-[11px] text-fg-muted">{session.time}</span>
</span>
</Link>
</li>
@ -148,7 +148,7 @@ export default function Start() {
<div className="flex-1 flex flex-col items-center pt-[12vh] px-4">
<div className="w-full max-w-2xl">
<h1 className="flex items-center justify-center gap-3 text-[2rem] font-medium tracking-tight text-ice-100 text-center mb-8">
<h1 className="flex items-center justify-center gap-3 text-[2rem] font-medium tracking-tight text-fg-2 text-center mb-8">
<img src="/logo.svg" alt="" className="size-9" />
What do you want to build?
</h1>
@ -156,7 +156,7 @@ export default function Start() {
<div className="relative group">
<div className="absolute -inset-px rounded-xl bg-gradient-to-b from-teal-500/30 to-mint/20 opacity-0 blur-sm transition-opacity duration-300 group-focus-within:opacity-100" />
<div className="relative rounded-xl bg-navy-800 border border-white/[0.08] group-focus-within:border-teal-500/40 transition-colors duration-300">
<div className="relative rounded-xl bg-panel border border-line-strong group-focus-within:border-focus transition-colors duration-300">
<textarea
ref={textareaRef}
value={prompt}
@ -167,7 +167,7 @@ export default function Start() {
onKeyDown={handleKeyDown}
placeholder="Describe a workflow, pipeline, or automation..."
rows={3}
className="w-full resize-none bg-transparent px-5 pt-4 pb-14 text-[15px] leading-relaxed text-ice-100 placeholder:text-navy-600 focus:outline-none"
className="w-full resize-none bg-transparent px-5 pt-4 pb-14 text-[15px] leading-relaxed text-fg-2 placeholder:text-fg-muted focus:outline-none"
/>
<div className="absolute bottom-3 inset-x-3 flex items-center justify-between">
@ -176,18 +176,18 @@ export default function Start() {
value={project}
onChange={setProject}
options={projects}
icon={<FolderIcon className="size-3.5 text-navy-600" />}
icon={<FolderIcon className="size-3.5 text-fg-muted" />}
/>
<Picker
value={branch}
onChange={setBranch}
options={branches}
icon={<BranchIcon className="size-3.5 text-navy-600" />}
icon={<BranchIcon className="size-3.5 text-fg-muted" />}
/>
</div>
<div className="flex items-center gap-3">
<span className="text-xs text-navy-600 select-none">
<span className="text-xs text-fg-muted select-none">
<kbd className="font-mono">Enter</kbd> to submit
</span>
<button
@ -211,7 +211,7 @@ export default function Start() {
className={`inline-flex items-center gap-2 rounded-full border px-4 py-2 text-sm transition-colors cursor-pointer ${
openCategory === cat.label
? "border-teal-500/30 bg-teal-500/10 text-teal-300"
: "border-white/[0.06] bg-navy-800/50 text-ice-300 hover:bg-navy-800 hover:border-white/[0.12]"
: "border-line bg-panel/50 text-fg-3 hover:bg-panel hover:border-line-strong"
}`}
>
<cat.icon className="size-4" />
@ -295,23 +295,23 @@ function CategoryPanel({
onSelect: (prompt: string) => void;
}) {
return (
<div className="rounded-xl border border-white/[0.08] bg-navy-800 overflow-hidden">
<div className="flex items-center gap-2 px-4 py-3 border-b border-white/[0.06]">
<div className="rounded-xl border border-line-strong bg-panel overflow-hidden">
<div className="flex items-center gap-2 px-4 py-3 border-b border-line">
<category.icon className="size-4 text-teal-500" />
<span className="text-sm font-medium text-ice-100">{category.label}</span>
<span className="text-sm font-medium text-fg-2">{category.label}</span>
<button
onClick={onClose}
className="ml-auto flex items-center justify-center size-6 rounded-md text-navy-600 hover:text-ice-300 hover:bg-white/[0.06] transition-colors cursor-pointer"
className="ml-auto flex items-center justify-center size-6 rounded-md text-fg-muted hover:text-fg-3 hover:bg-overlay transition-colors cursor-pointer"
>
<XMarkIcon className="size-4" />
</button>
</div>
<ul>
{category.items.map((item, i) => (
<li key={item.title} className={i > 0 ? "border-t border-white/[0.04]" : ""}>
<li key={item.title} className={i > 0 ? "border-t border-line" : ""}>
<button
onClick={() => onSelect(item.prompt)}
className="w-full px-4 py-3 text-left text-sm text-ice-300 transition-colors hover:bg-white/[0.03] hover:text-ice-100 cursor-pointer"
className="w-full px-4 py-3 text-left text-sm text-fg-3 transition-colors hover:bg-overlay hover:text-fg-2 cursor-pointer"
>
{item.title}
</button>
@ -336,18 +336,18 @@ function Picker<T extends { id: string; name: string }>({
return (
<Listbox value={value} onChange={onChange}>
<div className="relative">
<ListboxButton className="flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-xs text-ice-300 bg-navy-950/60 border border-white/[0.06] hover:border-white/[0.12] hover:bg-navy-950/80 transition-colors cursor-pointer">
<ListboxButton className="flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-xs text-fg-3 bg-page/60 border border-line hover:border-line-strong hover:bg-page/80 transition-colors cursor-pointer">
{icon}
<span className="max-w-[120px] truncate">{value.name}</span>
<ChevronUpDownIcon className="size-3.5 text-navy-600" />
<ChevronUpDownIcon className="size-3.5 text-fg-muted" />
</ListboxButton>
<ListboxOptions anchor="top start" className="z-20 w-56 rounded-lg bg-navy-800 border border-white/[0.08] py-1 shadow-xl shadow-black/30 focus:outline-none [--anchor-gap:4px]">
<ListboxOptions anchor="top start" className="z-20 w-56 rounded-lg bg-panel border border-line-strong py-1 shadow-xl shadow-black/30 focus:outline-none [--anchor-gap:4px]">
{options.map((option) => (
<ListboxOption
key={option.id}
value={option}
className="flex items-center gap-2 px-3 py-1.5 text-xs text-ice-300 cursor-pointer data-focus:bg-white/[0.06] data-selected:text-teal-300"
className="flex items-center gap-2 px-3 py-1.5 text-xs text-fg-3 cursor-pointer data-focus:bg-overlay data-selected:text-teal-300"
>
{option.name}
</ListboxOption>

View file

@ -146,34 +146,34 @@ function CategoryCard({ category }: { category: VerificationCategory }) {
return (
<Disclosure
as="div"
className="rounded-md border border-white/[0.06] overflow-hidden"
className="rounded-md border border-line overflow-hidden"
>
<DisclosureButton className="group flex w-full items-center gap-3 px-4 py-3.5 text-left transition-colors hover:bg-white/[0.02]">
<DisclosureButton className="group flex w-full items-center gap-3 px-4 py-3.5 text-left transition-colors hover:bg-overlay">
{(() => {
const CatIcon = categoryIcons[category.name];
return CatIcon ? <CatIcon className="size-5 shrink-0 text-ice-300" /> : null;
return CatIcon ? <CatIcon className="size-5 shrink-0 text-fg-3" /> : null;
})()}
<div className="min-w-0 flex-1">
<div className="flex items-baseline gap-3">
<span className="shrink-0 text-sm font-semibold text-white">
<span className="shrink-0 text-sm font-semibold text-fg">
{category.name}
</span>
<span className="truncate text-xs text-navy-600">
<span className="truncate text-xs text-fg-muted">
{category.question}
</span>
</div>
</div>
<span className="shrink-0 text-xs text-navy-600">
<span className="shrink-0 text-xs text-fg-muted">
<span className="font-mono tabular-nums">{category.criteria.length}</span> controls
</span>
<ChevronRightIcon className="size-4 shrink-0 text-navy-600 transition-transform duration-200 group-data-open:rotate-90" />
<ChevronRightIcon className="size-4 shrink-0 text-fg-muted transition-transform duration-200 group-data-open:rotate-90" />
</DisclosureButton>
<DisclosurePanel
transition
className="origin-top transition duration-200 ease-out data-closed:-translate-y-1 data-closed:opacity-0"
>
<div className="border-t border-white/[0.06]">
<div className="border-t border-line">
<table className="w-full text-sm">
<tbody>
{category.criteria.map((criterion) => {
@ -182,15 +182,15 @@ function CategoryCard({ category }: { category: VerificationCategory }) {
return (
<tr
key={criterion.name}
className="border-b border-white/[0.04] last:border-b-0 transition-colors hover:bg-white/[0.015]"
className="border-b border-line last:border-b-0 transition-colors hover:bg-overlay"
>
<td className="w-8 py-2.5 pl-5 pr-0">
{Icon && <Icon className="size-4 text-ice-300" />}
{Icon && <Icon className="size-4 text-fg-3" />}
</td>
<td className="whitespace-nowrap py-2.5 pl-2 pr-3 font-medium text-ice-100">
<td className="whitespace-nowrap py-2.5 pl-2 pr-3 font-medium text-fg-2">
{criterion.name}
</td>
<td className="py-2.5 px-3 text-navy-600">
<td className="py-2.5 px-3 text-fg-muted">
{criterion.description || (
<span className="italic">Not configured</span>
)}
@ -238,7 +238,7 @@ function ModeBadge({ mode }: { mode: VerificationMode }) {
function EvaluationDots({ evaluations }: { evaluations: readonly EvaluationResult[] }) {
if (evaluations.length === 0) {
return <span className="text-xs italic text-navy-600">—</span>;
return <span className="text-xs italic text-fg-muted">—</span>;
}
return (
<div className="flex items-center gap-0.5">
@ -260,10 +260,10 @@ function EvaluationDots({ evaluations }: { evaluations: readonly EvaluationResul
function UngroupedView({ categories }: { categories: readonly VerificationCategory[] }) {
return (
<div className="rounded-md border border-white/[0.06] overflow-hidden">
<div className="rounded-md border border-line overflow-hidden">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-white/[0.06] bg-navy-800/60 text-left text-xs text-navy-600">
<tr className="border-b border-line bg-panel/60 text-left text-xs text-fg-muted">
<th className="w-8 py-2.5 pl-4 pr-0 font-medium" />
<th className="py-2.5 pl-2 pr-3 font-medium">Verification</th>
<th className="py-2.5 px-3 font-medium">Description</th>
@ -283,30 +283,30 @@ function UngroupedView({ categories }: { categories: readonly VerificationCatego
return (
<tr
key={`${category.name}-${criterion.name}`}
className="border-b border-white/[0.04] last:border-b-0 transition-colors hover:bg-white/[0.015]"
className="border-b border-line last:border-b-0 transition-colors hover:bg-overlay"
>
<td className="w-8 py-2.5 pl-4 pr-0">
{Icon && <Icon className="size-4 text-ice-300" />}
{Icon && <Icon className="size-4 text-fg-3" />}
</td>
<td className="whitespace-nowrap py-2.5 pl-2 pr-3 font-medium text-ice-100">
<td className="whitespace-nowrap py-2.5 pl-2 pr-3 font-medium text-fg-2">
{criterion.name}
</td>
<td className="py-2.5 px-3 text-navy-600">
<td className="py-2.5 px-3 text-fg-muted">
{criterion.description || (
<span className="italic">Not configured</span>
)}
</td>
<td className="whitespace-nowrap py-2.5 px-3 text-xs text-navy-600">
<td className="whitespace-nowrap py-2.5 px-3 text-xs text-fg-muted">
{category.name}
</td>
<td className="whitespace-nowrap py-2.5 px-3 text-right">
<TypeBadge type={criterion.type} />
</td>
<td className="whitespace-nowrap py-2.5 px-3 text-right font-mono text-xs tabular-nums text-ice-100">
{perf?.f1 != null ? perf.f1.toFixed(2) : <span className="text-navy-600">—</span>}
<td className="whitespace-nowrap py-2.5 px-3 text-right font-mono text-xs tabular-nums text-fg-2">
{perf?.f1 != null ? perf.f1.toFixed(2) : <span className="text-fg-muted">—</span>}
</td>
<td className="whitespace-nowrap py-2.5 px-3 text-right font-mono text-xs tabular-nums text-ice-100">
{perf?.passAt1 != null ? perf.passAt1.toFixed(2) : <span className="text-navy-600">—</span>}
<td className="whitespace-nowrap py-2.5 px-3 text-right font-mono text-xs tabular-nums text-fg-2">
{perf?.passAt1 != null ? perf.passAt1.toFixed(2) : <span className="text-fg-muted">—</span>}
</td>
<td className="whitespace-nowrap py-2.5 px-3">
{perf && <ModeBadge mode={perf.mode} />}
@ -359,33 +359,33 @@ export default function Verifications() {
{/* Toolbar */}
<div className="flex gap-3">
<div className="relative flex-1">
<MagnifyingGlassIcon className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-navy-600" />
<MagnifyingGlassIcon className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-fg-muted" />
<input
type="text"
placeholder="Search verifications…"
value={query}
onChange={(e) => setQuery(e.target.value)}
className="w-full rounded-md border border-white/[0.06] bg-navy-800/80 py-2 pl-9 pr-3 text-sm text-ice-100 placeholder-navy-600 outline-none transition-colors focus:border-teal-500/40 focus:ring-0"
className="w-full rounded-md border border-line bg-panel/80 py-2 pl-9 pr-3 text-sm text-fg-2 placeholder-fg-muted outline-none transition-colors focus:border-focus focus:ring-0"
/>
</div>
<div className="relative">
<select
value={modeFilter}
onChange={(e) => setModeFilter(e.target.value as VerificationMode | "all")}
className="appearance-none rounded-md border border-white/[0.06] bg-navy-800/80 py-2 pl-3 pr-8 text-sm text-ice-100 outline-none transition-colors focus:border-teal-500/40 focus:ring-0"
className="appearance-none rounded-md border border-line bg-panel/80 py-2 pl-3 pr-8 text-sm text-fg-2 outline-none transition-colors focus:border-focus focus:ring-0"
>
<option value="all">All modes</option>
<option value="active">Active</option>
<option value="evaluate">Evaluate</option>
<option value="disabled">Disabled</option>
</select>
<ChevronDownIcon className="pointer-events-none absolute right-2 top-1/2 size-4 -translate-y-1/2 text-navy-600" />
<ChevronDownIcon className="pointer-events-none absolute right-2 top-1/2 size-4 -translate-y-1/2 text-fg-muted" />
</div>
<div className="flex items-center gap-1 rounded-md border border-white/[0.06] bg-navy-800/80 p-0.5">
<div className="flex items-center gap-1 rounded-md border border-line bg-panel/80 p-0.5">
<button
type="button"
onClick={() => setView("grouped")}
className={`inline-flex items-center gap-1.5 rounded px-2.5 py-1 text-xs font-medium transition-colors ${view === "grouped" ? "bg-white/[0.06] text-teal-500" : "text-navy-600 hover:text-ice-300"}`}
className={`inline-flex items-center gap-1.5 rounded px-2.5 py-1 text-xs font-medium transition-colors ${view === "grouped" ? "bg-overlay text-teal-500" : "text-fg-muted hover:text-fg-3"}`}
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="size-3.5" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 7.125C2.25 6.504 2.754 6 3.375 6h6c.621 0 1.125.504 1.125 1.125v3.75c0 .621-.504 1.125-1.125 1.125h-6a1.125 1.125 0 0 1-1.125-1.125v-3.75ZM14.25 8.625c0-.621.504-1.125 1.125-1.125h5.25c.621 0 1.125.504 1.125 1.125v8.25c0 .621-.504 1.125-1.125 1.125h-5.25a1.125 1.125 0 0 1-1.125-1.125v-8.25ZM3.75 16.125c0-.621.504-1.125 1.125-1.125h5.25c.621 0 1.125.504 1.125 1.125v1.5c0 .621-.504 1.125-1.125 1.125h-5.25a1.125 1.125 0 0 1-1.125-1.125v-1.5Z" />
@ -395,7 +395,7 @@ export default function Verifications() {
<button
type="button"
onClick={() => setView("ungrouped")}
className={`inline-flex items-center gap-1.5 rounded px-2.5 py-1 text-xs font-medium transition-colors ${view === "ungrouped" ? "bg-white/[0.06] text-teal-500" : "text-navy-600 hover:text-ice-300"}`}
className={`inline-flex items-center gap-1.5 rounded px-2.5 py-1 text-xs font-medium transition-colors ${view === "ungrouped" ? "bg-overlay text-teal-500" : "text-fg-muted hover:text-fg-3"}`}
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="size-3.5" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 12h16.5m-16.5 3.75h16.5M3.75 19.5h16.5M5.625 4.5h12.75a1.875 1.875 0 0 1 0 3.75H5.625a1.875 1.875 0 0 1 0-3.75Z" />

View file

@ -21,7 +21,7 @@ export default function WorkflowDefinition() {
}, []);
if (workflow == null) {
return <p className="text-sm text-navy-600">No configuration found.</p>;
return <p className="text-sm text-fg-muted">No configuration found.</p>;
}
return (

View file

@ -273,13 +273,13 @@ export default function WorkflowDetail() {
const basePath = `/workflows/${name}`;
if (workflow == null) {
return <p className="text-sm text-ice-300">Workflow not found.</p>;
return <p className="text-sm text-fg-3">Workflow not found.</p>;
}
return (
<div>
<nav className="mb-4 flex items-center gap-1 text-sm text-navy-600">
<Link to="/workflows" className="text-ice-300 hover:text-white">Workflows</Link>
<nav className="mb-4 flex items-center gap-1 text-sm text-fg-muted">
<Link to="/workflows" className="text-fg-3 hover:text-fg">Workflows</Link>
<ChevronRightIcon className="size-3" />
<span>{workflow.title}</span>
</nav>
@ -287,15 +287,15 @@ export default function WorkflowDetail() {
<div className="mb-6 flex items-center gap-4">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-3">
<h2 className="text-xl font-semibold text-white">{workflow.title}</h2>
<span className="font-mono text-xs text-navy-600">{workflow.filename}</span>
<h2 className="text-xl font-semibold text-fg">{workflow.title}</h2>
<span className="font-mono text-xs text-fg-muted">{workflow.filename}</span>
</div>
<p className="mt-2 max-w-prose text-sm leading-relaxed text-ice-300">{workflow.description}</p>
<p className="mt-2 max-w-prose text-sm leading-relaxed text-fg-3">{workflow.description}</p>
</div>
<button
type="button"
title="Run workflow"
className="flex shrink-0 items-center gap-1.5 rounded-md border border-mint/20 px-3 py-1.5 text-sm font-medium text-mint transition-colors hover:border-mint/50 hover:bg-mint/10 hover:text-white"
className="flex shrink-0 items-center gap-1.5 rounded-md border border-mint/20 px-3 py-1.5 text-sm font-medium text-mint transition-colors hover:border-mint/50 hover:bg-mint/10 hover:text-fg"
>
<svg viewBox="0 0 24 24" fill="currentColor" className="size-3.5" aria-hidden="true">
<path fillRule="evenodd" d="M4.5 5.653c0-1.427 1.529-2.33 2.779-1.643l11.54 6.347c1.295.712 1.295 2.573 0 3.286L7.28 19.99c-1.25.687-2.779-.217-2.779-1.643V5.653Z" clipRule="evenodd" />
@ -304,7 +304,7 @@ export default function WorkflowDetail() {
</button>
</div>
<div className="border-b border-white/[0.06]">
<div className="border-b border-line">
<nav className="-mb-px flex gap-6">
{tabs.map((tab) => {
const tabPath = `${basePath}${tab.path}`;
@ -315,8 +315,8 @@ export default function WorkflowDetail() {
to={tabPath}
className={`border-b-2 pb-3 text-sm font-medium transition-colors ${
isActive
? "border-teal-500 text-white"
: "border-transparent text-navy-600 hover:border-white/10 hover:text-ice-300"
? "border-teal-500 text-fg"
: "border-transparent text-fg-muted hover:border-line-strong hover:text-fg-3"
}`}
>
{tab.name}

View file

@ -1,9 +1,11 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { ArrowDownIcon, ArrowRightIcon, MinusIcon, PlusIcon } from "@heroicons/react/20/solid";
import { useTheme } from "../lib/theme";
import { getGraphTheme } from "../lib/graph-theme";
type Direction = "LR" | "TB";
function buildDot(direction: Direction) {
function buildDot(direction: Direction, theme: ReturnType<typeof getGraphTheme>) {
return `digraph fix_build {
graph [
label="Fix Build"
@ -15,29 +17,29 @@ function buildDot(direction: Direction) {
node [
fontname="ui-monospace, monospace"
fontsize=11
fontcolor="#c6d4e0"
color="#2a3f52"
fillcolor="#1a2b3c"
fontcolor="${theme.nodeText}"
color="${theme.edgeColor}"
fillcolor="${theme.nodeFill}"
style=filled
penwidth=1.2
]
edge [
fontname="ui-monospace, monospace"
fontsize=9
fontcolor="#5a7a94"
color="#2a3f52"
fontcolor="${theme.fontcolor}"
color="${theme.edgeColor}"
arrowsize=0.7
penwidth=1.2
]
start [shape=Mdiamond, label="Start", fillcolor="#0d4f4f", color="#14b8a6", fontcolor="#5eead4"]
exit [shape=Msquare, label="Exit", fillcolor="#0d4f4f", color="#14b8a6", fontcolor="#5eead4"]
start [shape=Mdiamond, label="Start", fillcolor="${theme.startFill}", color="${theme.startBorder}", fontcolor="${theme.startText}"]
exit [shape=Msquare, label="Exit", fillcolor="${theme.startFill}", color="${theme.startBorder}", fontcolor="${theme.startText}"]
analyze [label="Analyze\\nBuild Errors"]
diagnose [label="Diagnose\\nRoot Cause"]
fix [label="Apply\\nFix"]
validate [label="Validate\\nBuild"]
approve [shape=hexagon, label="Review\\nChanges", fillcolor="#1a2030", color="#f59e0b", fontcolor="#fbbf24"]
approve [shape=hexagon, label="Review\\nChanges", fillcolor="${theme.gateFill}", color="${theme.gateBorder}", fontcolor="${theme.gateText}"]
start -> analyze
analyze -> diagnose
@ -45,7 +47,7 @@ function buildDot(direction: Direction) {
fix -> validate
validate -> exit [label="Pass"]
validate -> diagnose [label="Fail", style=dashed, color="#f87171"]
validate -> approve [label="Needs review", color="#f59e0b"]
validate -> approve [label="Needs review", color="${theme.gateBorder}"]
approve -> exit [label="Accept"]
approve -> fix [label="Revise", style=dashed]
}`;
@ -76,6 +78,8 @@ export default function WorkflowDiagram() {
const [pan, setPan] = useState({ x: 0, y: 0 });
const dragState = useRef<{ startX: number; startY: number; startPanX: number; startPanY: number } | null>(null);
const zoom = ZOOM_STEPS[zoomIndex];
const { theme } = useTheme();
const graphTheme = getGraphTheme(theme);
useEffect(() => {
let cancelled = false;
@ -86,7 +90,7 @@ export default function WorkflowDiagram() {
if (cancelled) return;
try {
const svg = viz.renderSVGElement(buildDot(direction));
const svg = viz.renderSVGElement(buildDot(direction, graphTheme));
stripGraphTitle(svg);
svgRef.current = svg;
@ -101,7 +105,7 @@ export default function WorkflowDiagram() {
setPan({ x: 0, y: 0 });
render();
return () => { cancelled = true; };
}, [direction]);
}, [direction, graphTheme]);
const onPointerDown = useCallback((e: React.PointerEvent) => {
if ((e.target as HTMLElement).closest("button")) return;
@ -148,14 +152,14 @@ export default function WorkflowDiagram() {
}
return (
<div className="relative rounded-md border border-white/[0.06] bg-navy-900/40">
<div className="relative rounded-md border border-line bg-panel-alt/40">
<div className="absolute right-3 top-3 z-10 flex items-center gap-2">
<div className="flex items-center gap-0.5 rounded-md border border-white/[0.06] bg-navy-800/90 p-0.5">
<div className="flex items-center gap-0.5 rounded-md border border-line bg-panel/90 p-0.5">
<button
type="button"
title="Left to right"
onClick={() => setDirection("LR")}
className={`flex size-7 items-center justify-center rounded transition-colors ${direction === "LR" ? "bg-white/10 text-ice-300" : "text-navy-400 hover:bg-white/5 hover:text-ice-300"}`}
className={`flex size-7 items-center justify-center rounded transition-colors ${direction === "LR" ? "bg-overlay-strong text-fg-3" : "text-fg-muted hover:bg-overlay hover:text-fg-3"}`}
>
<ArrowRightIcon className="size-3.5" />
</button>
@ -163,18 +167,18 @@ export default function WorkflowDiagram() {
type="button"
title="Top to bottom"
onClick={() => setDirection("TB")}
className={`flex size-7 items-center justify-center rounded transition-colors ${direction === "TB" ? "bg-white/10 text-ice-300" : "text-navy-400 hover:bg-white/5 hover:text-ice-300"}`}
className={`flex size-7 items-center justify-center rounded transition-colors ${direction === "TB" ? "bg-overlay-strong text-fg-3" : "text-fg-muted hover:bg-overlay hover:text-fg-3"}`}
>
<ArrowDownIcon className="size-3.5" />
</button>
</div>
<div className="flex items-center rounded-md border border-white/[0.06] bg-navy-800/90 p-0.5">
<div className="flex items-center rounded-md border border-line bg-panel/90 p-0.5">
<button
type="button"
title="Fit to window"
onClick={fitToWindow}
className="flex size-7 items-center justify-center rounded text-navy-400 transition-colors hover:bg-white/5 hover:text-ice-300"
className="flex size-7 items-center justify-center rounded text-fg-muted transition-colors hover:bg-overlay hover:text-fg-3"
>
<svg viewBox="0 0 14 14" fill="none" stroke="currentColor" className="size-3.5" aria-hidden="true">
<rect x="1" y="1" width="12" height="12" rx="1.5" strokeWidth="1.5" strokeDasharray="3 2" />
@ -182,13 +186,13 @@ export default function WorkflowDiagram() {
</button>
</div>
<div className="flex items-center gap-0.5 rounded-md border border-white/[0.06] bg-navy-800/90 p-0.5">
<div className="flex items-center gap-0.5 rounded-md border border-line bg-panel/90 p-0.5">
<button
type="button"
title="Zoom out"
onClick={() => setZoomIndex((i) => Math.max(0, i - 1))}
disabled={zoomIndex === 0}
className="flex size-7 items-center justify-center rounded text-navy-400 transition-colors hover:bg-white/5 hover:text-ice-300 disabled:opacity-30 disabled:hover:bg-transparent disabled:hover:text-navy-400"
className="flex size-7 items-center justify-center rounded text-fg-muted transition-colors hover:bg-overlay hover:text-fg-3 disabled:opacity-30 disabled:hover:bg-transparent disabled:hover:text-fg-muted"
>
<MinusIcon className="size-4" />
</button>
@ -197,7 +201,7 @@ export default function WorkflowDiagram() {
title="Zoom in"
onClick={() => setZoomIndex((i) => Math.min(ZOOM_STEPS.length - 1, i + 1))}
disabled={zoomIndex === ZOOM_STEPS.length - 1}
className="flex size-7 items-center justify-center rounded text-navy-400 transition-colors hover:bg-white/5 hover:text-ice-300 disabled:opacity-30 disabled:hover:bg-transparent disabled:hover:text-navy-400"
className="flex size-7 items-center justify-center rounded text-fg-muted transition-colors hover:bg-overlay hover:text-fg-3 disabled:opacity-30 disabled:hover:bg-transparent disabled:hover:text-fg-muted"
>
<PlusIcon className="size-4" />
</button>
@ -218,7 +222,7 @@ export default function WorkflowDiagram() {
className="flex items-center justify-center"
style={{ transform: `translate(${pan.x}px, ${pan.y}px) scale(${zoom / 100})`, transformOrigin: "center center" }}
>
<p className="text-sm text-navy-600">Loading diagram...</p>
<p className="text-sm text-fg-muted">Loading diagram...</p>
</div>
</div>
</div>

View file

@ -17,20 +17,20 @@ function GitPullRequestIcon({ className }: { className?: string }) {
function RunRow({ run }: { run: RunWithStatus }) {
const colors = statusColors[run.status];
return (
<Link to={`/runs/${run.id}`} className="grid items-center rounded-md border border-white/[0.06] bg-navy-800/80 px-4 py-3 transition-all duration-200 hover:border-white/[0.12] hover:bg-navy-800" style={{ gridColumn: "1 / -1", gridTemplateColumns: "subgrid" }}>
<Link to={`/runs/${run.id}`} className="grid items-center rounded-md border border-line bg-panel/80 px-4 py-3 transition-all duration-200 hover:border-line-strong hover:bg-panel" style={{ gridColumn: "1 / -1", gridTemplateColumns: "subgrid" }}>
<span className="flex items-center gap-2 pr-2">
<span className={`size-2 shrink-0 rounded-full ${colors.dot}`} />
<span className={`text-xs font-medium ${colors.text}`}>{run.statusLabel}</span>
</span>
<span className={`font-mono text-xs pr-2 ${run.elapsedWarning ? "text-amber" : "text-navy-600"}`}>
<span className={`font-mono text-xs pr-2 ${run.elapsedWarning ? "text-amber" : "text-fg-muted"}`}>
{run.elapsed}
</span>
<span className="flex items-center gap-2 min-w-0">
<span className="truncate text-sm text-ice-100">{run.title}</span>
<span className="truncate text-sm text-fg-2">{run.title}</span>
{run.comments != null && run.comments > 0 && (
<span className="inline-flex shrink-0 items-center gap-1 font-mono text-xs text-navy-600">
<span className="inline-flex shrink-0 items-center gap-1 font-mono text-xs text-fg-muted">
<svg viewBox="0 0 16 16" fill="currentColor" className="size-3" aria-hidden="true">
<path d="M1 2.75C1 1.784 1.784 1 2.75 1h10.5c.966 0 1.75.784 1.75 1.75v7.5A1.75 1.75 0 0 1 13.25 12H9.06l-2.573 2.573A1.458 1.458 0 0 1 4 13.543V12H2.75A1.75 1.75 0 0 1 1 10.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h4.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z" />
</svg>
@ -44,7 +44,7 @@ function RunRow({ run }: { run: RunWithStatus }) {
{run.deletions != null && <span className="text-coral">-{run.deletions.toLocaleString()}</span>}
</span>
<span className="inline-flex items-center justify-end gap-1.5 font-mono text-xs text-navy-600">
<span className="inline-flex items-center justify-end gap-1.5 font-mono text-xs text-fg-muted">
{run.number != null && (
<>
<GitPullRequestIcon className="size-3" />
@ -72,27 +72,27 @@ export default function WorkflowRuns() {
<div className="space-y-4">
<div className="flex gap-3">
<div className="relative flex-1">
<MagnifyingGlassIcon className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-navy-600" />
<MagnifyingGlassIcon className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-fg-muted" />
<input
type="text"
placeholder="Search runs..."
value={query}
onChange={(e) => setQuery(e.target.value)}
className="w-full rounded-md border border-white/[0.06] bg-navy-800/80 py-2 pl-9 pr-3 text-sm text-ice-100 placeholder-navy-600 outline-none transition-colors focus:border-teal-500/40 focus:ring-0"
className="w-full rounded-md border border-line bg-panel/80 py-2 pl-9 pr-3 text-sm text-fg-2 placeholder-fg-muted outline-none transition-colors focus:border-focus focus:ring-0"
/>
</div>
<div className="relative">
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value as ColumnStatus | "all")}
className="appearance-none rounded-md border border-white/[0.06] bg-navy-800/80 py-2 pl-3 pr-8 text-sm text-ice-100 outline-none transition-colors focus:border-teal-500/40 focus:ring-0"
className="appearance-none rounded-md border border-line bg-panel/80 py-2 pl-3 pr-8 text-sm text-fg-2 outline-none transition-colors focus:border-focus focus:ring-0"
>
<option value="all">All statuses</option>
{columns.map((col) => (
<option key={col.id} value={col.id}>{col.name}</option>
))}
</select>
<ChevronDownIcon className="pointer-events-none absolute right-2 top-1/2 size-4 -translate-y-1/2 text-navy-600" />
<ChevronDownIcon className="pointer-events-none absolute right-2 top-1/2 size-4 -translate-y-1/2 text-fg-muted" />
</div>
</div>
<div className="grid gap-2" style={{ gridTemplateColumns: "auto 5rem 1fr 8rem auto" }}>
@ -100,7 +100,7 @@ export default function WorkflowRuns() {
<RunRow key={run.id} run={run} />
))}
{filtered.length === 0 && (
<p className="py-8 text-center text-sm text-navy-600">No runs match "{query}"</p>
<p className="py-8 text-center text-sm text-fg-muted">No runs match "{query}"</p>
)}
</div>
</div>

View file

@ -24,23 +24,23 @@ export const handle = {
<div className="relative inline-flex rounded-md">
<button
type="button"
className="inline-flex items-center gap-1.5 rounded-l-md border border-r-0 border-mint/20 px-3 py-1.5 text-sm font-medium text-mint transition-colors hover:border-mint/50 hover:bg-mint/10 hover:text-white"
className="inline-flex items-center gap-1.5 rounded-l-md border border-r-0 border-mint/20 px-3 py-1.5 text-sm font-medium text-mint transition-colors hover:border-mint/50 hover:bg-mint/10 hover:text-fg"
>
<PlusIcon className="size-3.5" aria-hidden="true" />
Create Workflow
</button>
<Menu as="div" className="relative -ml-px flex">
<MenuButton className="inline-flex items-center rounded-r-md border border-mint/20 px-1.5 text-mint transition-colors hover:border-mint/50 hover:bg-mint/10 hover:text-white">
<MenuButton className="inline-flex items-center rounded-r-md border border-mint/20 px-1.5 text-mint transition-colors hover:border-mint/50 hover:bg-mint/10 hover:text-fg">
<ChevronDownIcon className="size-3.5" aria-hidden="true" />
</MenuButton>
<MenuItems
transition
className="absolute right-0 top-full z-10 mt-2 w-48 origin-top-right rounded-md bg-navy-800 py-1 outline-1 -outline-offset-1 outline-white/10 transition data-closed:scale-95 data-closed:transform data-closed:opacity-0 data-enter:duration-100 data-enter:ease-out data-leave:duration-75 data-leave:ease-in"
className="absolute right-0 top-full z-10 mt-2 w-48 origin-top-right rounded-md bg-panel py-1 outline-1 -outline-offset-1 outline-line-strong transition data-closed:scale-95 data-closed:transform data-closed:opacity-0 data-enter:duration-100 data-enter:ease-out data-leave:duration-75 data-leave:ease-in"
>
<MenuItem>
<button
type="button"
className="block w-full px-4 py-2 text-left text-sm text-ice-300 data-focus:bg-white/5 data-focus:outline-hidden"
className="block w-full px-4 py-2 text-left text-sm text-fg-3 data-focus:bg-overlay data-focus:outline-hidden"
>
Import from file
</button>
@ -48,7 +48,7 @@ export const handle = {
<MenuItem>
<button
type="button"
className="block w-full px-4 py-2 text-left text-sm text-ice-300 data-focus:bg-white/5 data-focus:outline-hidden"
className="block w-full px-4 py-2 text-left text-sm text-fg-3 data-focus:bg-overlay data-focus:outline-hidden"
>
Duplicate existing
</button>
@ -71,12 +71,12 @@ interface Workflow {
}
const workflows: Workflow[] = [
{ name: "Fix Build", slug: "fix_build", filename: "fix_build.dot", lastRun: "2 hours ago", icon: WrenchIcon, color: "#F0A45B" },
{ name: "Implement Feature", slug: "implement", filename: "implement.dot", lastRun: "4 days ago", icon: CodeBracketIcon, color: "#67B2D7" },
{ name: "Sync Drift", slug: "sync_drift", filename: "sync_drift.dot", lastRun: "1 day ago", icon: ArrowsRightLeftIcon, color: "#5AC8A8" },
{ name: "Expand Product", slug: "expand", filename: "expand.dot", lastRun: "2 weeks ago", icon: RocketLaunchIcon, color: "#E86B6B" },
{ name: "Security Scan", slug: "security_scan", filename: "security_scan.dot", lastRun: "9 hours ago", icon: ShieldCheckIcon, color: "#67B2D7", schedule: "Daily at 09:00", nextRun: "Starts in 3 hours" },
{ name: "Dependency Audit", slug: "dep_audit", filename: "dep_audit.dot", lastRun: "1 day ago", icon: ClockIcon, color: "#F0A45B", schedule: "Weekly on Mon 08:00", nextRun: "Starts in 2 days" },
{ name: "Fix Build", slug: "fix_build", filename: "fix_build.dot", lastRun: "2 hours ago", icon: WrenchIcon, color: "var(--color-amber)" },
{ name: "Implement Feature", slug: "implement", filename: "implement.dot", lastRun: "4 days ago", icon: CodeBracketIcon, color: "var(--color-teal-500)" },
{ name: "Sync Drift", slug: "sync_drift", filename: "sync_drift.dot", lastRun: "1 day ago", icon: ArrowsRightLeftIcon, color: "var(--color-mint)" },
{ name: "Expand Product", slug: "expand", filename: "expand.dot", lastRun: "2 weeks ago", icon: RocketLaunchIcon, color: "var(--color-coral)" },
{ name: "Security Scan", slug: "security_scan", filename: "security_scan.dot", lastRun: "9 hours ago", icon: ShieldCheckIcon, color: "var(--color-teal-500)", schedule: "Daily at 09:00", nextRun: "Starts in 3 hours" },
{ name: "Dependency Audit", slug: "dep_audit", filename: "dep_audit.dot", lastRun: "1 day ago", icon: ClockIcon, color: "var(--color-amber)", schedule: "Weekly on Mon 08:00", nextRun: "Starts in 2 days" },
];
function PlayIcon({ className }: { className?: string }) {
@ -98,19 +98,19 @@ function EllipsisIcon({ className }: { className?: string }) {
function WorkflowCard({ workflow }: { workflow: Workflow }) {
const Icon = workflow.icon;
return (
<div className="group flex items-center gap-4 rounded-md border border-white/[0.06] bg-navy-800/80 p-4 transition-all duration-200 hover:border-white/[0.12] hover:bg-navy-800 hover:shadow-lg hover:shadow-black/20">
<div className="group flex items-center gap-4 rounded-md border border-line bg-panel/80 p-4 transition-all duration-200 hover:border-line-strong hover:bg-panel hover:shadow-lg hover:shadow-black/20">
<Link to={`/workflows/${workflow.slug}`} className="flex min-w-0 flex-1 items-center gap-4">
<div
className="flex size-9 shrink-0 items-center justify-center rounded-md border bg-navy-900/60"
style={{ borderColor: `${workflow.color}33`, color: workflow.color }}
className="flex size-9 shrink-0 items-center justify-center rounded-md border bg-panel-alt/60"
style={{ borderColor: `color-mix(in srgb, ${workflow.color} 20%, transparent)`, color: workflow.color }}
>
<Icon className="size-4" />
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-ice-100 group-hover:text-white">{workflow.name}</span>
<span className="font-mono text-xs text-navy-600">{workflow.filename}</span>
<span className="text-sm font-medium text-fg-2 group-hover:text-fg">{workflow.name}</span>
<span className="font-mono text-xs text-fg-muted">{workflow.filename}</span>
{workflow.schedule && (
<span className="inline-flex items-center gap-1 rounded-full bg-teal-500/10 border border-teal-500/20 px-2 py-0.5 text-[11px] font-medium text-teal-300">
<ClockIcon className="size-3" />
@ -118,7 +118,7 @@ function WorkflowCard({ workflow }: { workflow: Workflow }) {
</span>
)}
</div>
<p className="mt-1 text-xs text-navy-600">
<p className="mt-1 text-xs text-fg-muted">
{workflow.nextRun ?? `Last run ${workflow.lastRun}`}
</p>
</div>
@ -128,7 +128,7 @@ function WorkflowCard({ workflow }: { workflow: Workflow }) {
<button
type="button"
title="Pause schedule"
className="flex size-8 shrink-0 items-center justify-center rounded-full border border-amber/20 text-amber transition-colors hover:border-amber/50 hover:bg-amber/10 hover:text-white"
className="flex size-8 shrink-0 items-center justify-center rounded-full border border-amber/20 text-amber transition-colors hover:border-amber/50 hover:bg-amber/10 hover:text-fg"
>
<PauseIcon className="size-3.5" />
</button>
@ -136,7 +136,7 @@ function WorkflowCard({ workflow }: { workflow: Workflow }) {
<button
type="button"
title="Run workflow"
className="flex size-8 shrink-0 items-center justify-center rounded-full border border-mint/20 text-mint transition-colors hover:border-mint/50 hover:bg-mint/10 hover:text-white"
className="flex size-8 shrink-0 items-center justify-center rounded-full border border-mint/20 text-mint transition-colors hover:border-mint/50 hover:bg-mint/10 hover:text-fg"
>
<PlayIcon className="size-3.5" />
</button>
@ -145,7 +145,7 @@ function WorkflowCard({ workflow }: { workflow: Workflow }) {
<button
type="button"
title="Actions"
className="flex size-8 shrink-0 items-center justify-center rounded-md text-navy-600 transition-colors hover:bg-white/5 hover:text-ice-300"
className="flex size-8 shrink-0 items-center justify-center rounded-md text-fg-muted transition-colors hover:bg-overlay hover:text-fg-3"
>
<EllipsisIcon className="size-5" />
</button>
@ -171,26 +171,26 @@ export default function Workflows() {
<div className="space-y-4">
<div className="flex gap-3">
<div className="relative flex-1">
<MagnifyingGlassIcon className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-navy-600" />
<MagnifyingGlassIcon className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-fg-muted" />
<input
type="text"
placeholder="Search workflows..."
value={query}
onChange={(e) => setQuery(e.target.value)}
className="w-full rounded-md border border-white/[0.06] bg-navy-800/80 py-2 pl-9 pr-3 text-sm text-ice-100 placeholder-navy-600 outline-none transition-colors focus:border-teal-500/40 focus:ring-0"
className="w-full rounded-md border border-line bg-panel/80 py-2 pl-9 pr-3 text-sm text-fg-2 placeholder-fg-muted outline-none transition-colors focus:border-focus focus:ring-0"
/>
</div>
<div className="relative">
<select
value={triggerFilter}
onChange={(e) => setTriggerFilter(e.target.value as TriggerFilter)}
className="appearance-none rounded-md border border-white/[0.06] bg-navy-800/80 py-2 pl-3 pr-8 text-sm text-ice-100 outline-none transition-colors focus:border-teal-500/40 focus:ring-0"
className="appearance-none rounded-md border border-line bg-panel/80 py-2 pl-3 pr-8 text-sm text-fg-2 outline-none transition-colors focus:border-focus focus:ring-0"
>
<option value="all">All triggers</option>
<option value="scheduled">Scheduled</option>
<option value="manual">Manual</option>
</select>
<ChevronDownOutline className="pointer-events-none absolute right-2 top-1/2 size-4 -translate-y-1/2 text-navy-600" />
<ChevronDownOutline className="pointer-events-none absolute right-2 top-1/2 size-4 -translate-y-1/2 text-fg-muted" />
</div>
</div>
<div className="space-y-3">
@ -198,7 +198,7 @@ export default function Workflows() {
<WorkflowCard key={workflow.filename} workflow={workflow} />
))}
{filtered.length === 0 && (
<p className="py-8 text-center text-sm text-navy-600">No workflows match "{query}"</p>
<p className="py-8 text-center text-sm text-fg-muted">No workflows match "{query}"</p>
)}
</div>
</div>