mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-11 03:40:05 +00:00
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:
parent
cab25a3e7e
commit
2ed164d190
32 changed files with 792 additions and 545 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
70
apps/arc-web/app/lib/graph-theme.ts
Normal file
70
apps/arc-web/app/lib/graph-theme.ts
Normal 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 };
|
||||
53
apps/arc-web/app/lib/theme.tsx
Normal file
53
apps/arc-web/app/lib/theme.tsx
Normal 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 };
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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…
|
||||
</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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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} · {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}
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue