feat(ui): light/dark theme toggle and light mode color support

Add ThemeContext with localStorage persistence and FOUC-preventing
inline script. Add theme toggle button in sidebar. Update status
badges, toast notifications, live indicators, and approval cards
with dark: prefixed classes for proper light mode rendering.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Forgotten
2026-02-26 16:33:29 -06:00
parent e2c5b6698c
commit 5cd12dec89
13 changed files with 245 additions and 92 deletions

View File

@@ -14,6 +14,27 @@
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
<script>
(() => {
const key = "paperclip.theme";
const darkThemeColor = "#18181b";
const lightThemeColor = "#ffffff";
try {
const stored = window.localStorage.getItem(key);
const theme = stored === "light" || stored === "dark" ? stored : "dark";
const isDark = theme === "dark";
document.documentElement.classList.toggle("dark", isDark);
document.documentElement.style.colorScheme = isDark ? "dark" : "light";
const themeColorMeta = document.querySelector('meta[name="theme-color"]');
if (themeColorMeta) {
themeColorMeta.setAttribute("content", isDark ? darkThemeColor : lightThemeColor);
}
} catch {
document.documentElement.classList.add("dark");
document.documentElement.style.colorScheme = "dark";
}
})();
</script>
</head>
<body>
<div id="root"></div>

View File

@@ -7,10 +7,10 @@ import { timeAgo } from "../lib/timeAgo";
import type { Approval, Agent } from "@paperclip/shared";
function statusIcon(status: string) {
if (status === "approved") return <CheckCircle2 className="h-3.5 w-3.5 text-green-400" />;
if (status === "rejected") return <XCircle className="h-3.5 w-3.5 text-red-400" />;
if (status === "revision_requested") return <Clock className="h-3.5 w-3.5 text-amber-400" />;
if (status === "pending") return <Clock className="h-3.5 w-3.5 text-yellow-400" />;
if (status === "approved") return <CheckCircle2 className="h-3.5 w-3.5 text-green-600 dark:text-green-400" />;
if (status === "rejected") return <XCircle className="h-3.5 w-3.5 text-red-600 dark:text-red-400" />;
if (status === "revision_requested") return <Clock className="h-3.5 w-3.5 text-amber-600 dark:text-amber-400" />;
if (status === "pending") return <Clock className="h-3.5 w-3.5 text-yellow-600 dark:text-yellow-400" />;
return null;
}

View File

@@ -1,6 +1,6 @@
import { useCallback, useEffect, useRef, useState, type UIEvent } from "react";
import { useQuery } from "@tanstack/react-query";
import { BookOpen } from "lucide-react";
import { BookOpen, Moon, Sun } from "lucide-react";
import { Outlet } from "react-router-dom";
import { CompanyRail } from "./CompanyRail";
import { Sidebar } from "./Sidebar";
@@ -19,20 +19,24 @@ import { useDialog } from "../context/DialogContext";
import { usePanel } from "../context/PanelContext";
import { useCompany } from "../context/CompanyContext";
import { useSidebar } from "../context/SidebarContext";
import { useTheme } from "../context/ThemeContext";
import { useKeyboardShortcuts } from "../hooks/useKeyboardShortcuts";
import { useCompanyPageMemory } from "../hooks/useCompanyPageMemory";
import { healthApi } from "../api/health";
import { queryKeys } from "../lib/queryKeys";
import { cn } from "../lib/utils";
import { Button } from "@/components/ui/button";
export function Layout() {
const { sidebarOpen, setSidebarOpen, toggleSidebar, isMobile } = useSidebar();
const { openNewIssue, openOnboarding } = useDialog();
const { panelContent, closePanel } = usePanel();
const { companies, loading: companiesLoading, setSelectedCompanyId } = useCompany();
const { theme, toggleTheme } = useTheme();
const onboardingTriggered = useRef(false);
const lastMainScrollTop = useRef(0);
const [mobileNavVisible, setMobileNavVisible] = useState(true);
const nextTheme = theme === "dark" ? "light" : "dark";
const { data: health } = useQuery({
queryKey: queryKeys.health,
queryFn: () => healthApi.get(),
@@ -168,7 +172,25 @@ export function Layout() {
<Sidebar />
</div>
<div className="border-t border-r border-border px-3 py-2 bg-background">
<SidebarNavItem to="/docs" label="Documentation" icon={BookOpen} />
<div className="flex items-center gap-1">
<SidebarNavItem
to="/docs"
label="Documentation"
icon={BookOpen}
className="flex-1 min-w-0"
/>
<Button
type="button"
variant="ghost"
size="icon-sm"
className="text-muted-foreground shrink-0"
onClick={toggleTheme}
aria-label={`Switch to ${nextTheme} mode`}
title={`Switch to ${nextTheme} mode`}
>
{theme === "dark" ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
</Button>
</div>
</div>
</div>
) : (
@@ -185,7 +207,25 @@ export function Layout() {
</div>
</div>
<div className="border-t border-r border-border px-3 py-2">
<SidebarNavItem to="/docs" label="Documentation" icon={BookOpen} />
<div className="flex items-center gap-1">
<SidebarNavItem
to="/docs"
label="Documentation"
icon={BookOpen}
className="flex-1 min-w-0"
/>
<Button
type="button"
variant="ghost"
size="icon-sm"
className="text-muted-foreground shrink-0"
onClick={toggleTheme}
aria-label={`Switch to ${nextTheme} mode`}
title={`Switch to ${nextTheme} mode`}
>
{theme === "dark" ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
</Button>
</div>
</div>
</div>
)}

View File

@@ -344,7 +344,7 @@ export function LiveRunWidget({ issueId, companyId }: LiveRunWidgetProps) {
<button
onClick={() => handleCancelRun(headerRun.id)}
disabled={cancellingRunIds.has(headerRun.id)}
className="inline-flex items-center gap-1 text-[10px] text-red-400 hover:text-red-300 disabled:opacity-50"
className="inline-flex items-center gap-1 text-[10px] text-red-600 hover:text-red-500 dark:text-red-400 dark:hover:text-red-300 disabled:opacity-50"
>
<Square className="h-2 w-2" fill="currentColor" />
{cancellingRunIds.has(headerRun.id) ? "Stopping…" : "Stop"}
@@ -352,7 +352,7 @@ export function LiveRunWidget({ issueId, companyId }: LiveRunWidgetProps) {
)}
<Link
to={`/agents/${headerRun.agentId}/runs/${headerRun.id}`}
className="inline-flex items-center gap-1 text-[10px] text-cyan-300 hover:text-cyan-200"
className="inline-flex items-center gap-1 text-[10px] text-cyan-600 hover:text-cyan-500 dark:text-cyan-300 dark:hover:text-cyan-200"
>
Open run
<ExternalLink className="h-2.5 w-2.5" />
@@ -376,13 +376,13 @@ export function LiveRunWidget({ issueId, companyId }: LiveRunWidgetProps) {
<span className="text-[10px] text-muted-foreground">{relativeTime(item.ts)}</span>
<div className={cn(
"min-w-0",
item.tone === "error" && "text-red-300",
item.tone === "warn" && "text-amber-300",
item.tone === "assistant" && "text-emerald-200",
item.tone === "tool" && "text-cyan-300",
item.tone === "error" && "text-red-600 dark:text-red-300",
item.tone === "warn" && "text-amber-600 dark:text-amber-300",
item.tone === "assistant" && "text-emerald-700 dark:text-emerald-200",
item.tone === "tool" && "text-cyan-600 dark:text-cyan-300",
item.tone === "info" && "text-foreground/80",
)}>
<Identity name={item.agentName} size="sm" className="text-cyan-400" />
<Identity name={item.agentName} size="sm" className="text-cyan-600 dark:text-cyan-400" />
<span className="text-muted-foreground"> [{item.runId.slice(0, 8)}] </span>
<span className="break-words">{item.text}</span>
</div>
@@ -396,7 +396,7 @@ export function LiveRunWidget({ issueId, companyId }: LiveRunWidgetProps) {
<div key={run.id} className="inline-flex items-center gap-1.5">
<Link
to={`/agents/${run.agentId}/runs/${run.id}`}
className="inline-flex items-center gap-1 text-[10px] text-cyan-300 hover:text-cyan-200"
className="inline-flex items-center gap-1 text-[10px] text-cyan-600 hover:text-cyan-500 dark:text-cyan-300 dark:hover:text-cyan-200"
>
<Identity name={run.agentName} size="sm" /> {run.id.slice(0, 8)}
<ExternalLink className="h-2.5 w-2.5" />

View File

@@ -1,6 +1,7 @@
import Markdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { cn } from "../lib/utils";
import { useTheme } from "../context/ThemeContext";
interface MarkdownBodyProps {
children: string;
@@ -8,10 +9,12 @@ interface MarkdownBodyProps {
}
export function MarkdownBody({ children, className }: MarkdownBodyProps) {
const { theme } = useTheme();
return (
<div
className={cn(
"prose prose-sm prose-invert max-w-none prose-p:my-1 prose-ul:my-1 prose-ol:my-1 prose-li:my-0 prose-pre:my-2 prose-pre:whitespace-pre-wrap prose-pre:break-words prose-headings:my-2 prose-headings:text-sm prose-table:my-2 prose-th:px-3 prose-th:py-1.5 prose-td:px-3 prose-td:py-1.5 prose-code:break-all",
"prose prose-sm max-w-none prose-p:my-1 prose-ul:my-1 prose-ol:my-1 prose-li:my-0 prose-pre:my-2 prose-pre:whitespace-pre-wrap prose-pre:break-words prose-headings:my-2 prose-headings:text-sm prose-table:my-2 prose-th:px-3 prose-th:py-1.5 prose-td:px-3 prose-td:py-1.5 prose-code:break-all",
theme === "dark" && "prose-invert",
className,
)}
>

View File

@@ -118,7 +118,7 @@ export function SidebarAgents() {
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-blue-500" />
</span>
<span className="text-[11px] font-medium text-blue-400">
<span className="text-[11px] font-medium text-blue-600 dark:text-blue-400">
{runCount} live
</span>
</span>

View File

@@ -8,6 +8,7 @@ interface SidebarNavItemProps {
label: string;
icon: LucideIcon;
end?: boolean;
className?: string;
badge?: number;
badgeTone?: "default" | "danger";
alert?: boolean;
@@ -19,6 +20,7 @@ export function SidebarNavItem({
label,
icon: Icon,
end,
className,
badge,
badgeTone = "default",
alert = false,
@@ -36,7 +38,8 @@ export function SidebarNavItem({
"flex items-center gap-2.5 px-3 py-2 text-[13px] font-medium transition-colors",
isActive
? "bg-accent text-foreground"
: "text-foreground/80 hover:bg-accent/50 hover:text-foreground"
: "text-foreground/80 hover:bg-accent/50 hover:text-foreground",
className,
)
}
>
@@ -53,7 +56,7 @@ export function SidebarNavItem({
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-blue-500" />
</span>
<span className="text-[11px] font-medium text-blue-400">{liveCount} live</span>
<span className="text-[11px] font-medium text-blue-600 dark:text-blue-400">{liveCount} live</span>
</span>
)}
{badge != null && badge > 0 && (

View File

@@ -0,0 +1,83 @@
import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useState,
type ReactNode,
} from "react";
type Theme = "light" | "dark";
interface ThemeContextValue {
theme: Theme;
setTheme: (theme: Theme) => void;
toggleTheme: () => void;
}
const THEME_STORAGE_KEY = "paperclip.theme";
const DARK_THEME_COLOR = "#18181b";
const LIGHT_THEME_COLOR = "#ffffff";
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
function resolveThemeFromDocument(): Theme {
if (typeof document === "undefined") return "dark";
return document.documentElement.classList.contains("dark") ? "dark" : "light";
}
function applyTheme(theme: Theme) {
if (typeof document === "undefined") return;
const isDark = theme === "dark";
const root = document.documentElement;
root.classList.toggle("dark", isDark);
root.style.colorScheme = isDark ? "dark" : "light";
const themeColorMeta = document.querySelector('meta[name="theme-color"]');
if (themeColorMeta instanceof HTMLMetaElement) {
themeColorMeta.setAttribute("content", isDark ? DARK_THEME_COLOR : LIGHT_THEME_COLOR);
}
}
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setThemeState] = useState<Theme>(() => resolveThemeFromDocument());
const setTheme = useCallback((nextTheme: Theme) => {
setThemeState(nextTheme);
}, []);
const toggleTheme = useCallback(() => {
setThemeState((current) => (current === "dark" ? "light" : "dark"));
}, []);
useEffect(() => {
applyTheme(theme);
try {
localStorage.setItem(THEME_STORAGE_KEY, theme);
} catch {
// Ignore local storage write failures in restricted environments.
}
}, [theme]);
const value = useMemo(
() => ({
theme,
setTheme,
toggleTheme,
}),
[theme, setTheme, toggleTheme],
);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error("useTheme must be used within ThemeProvider");
}
return context;
}

View File

@@ -12,12 +12,12 @@
/** StatusIcon circle: text + border classes */
export const issueStatusIcon: Record<string, string> = {
backlog: "text-muted-foreground border-muted-foreground",
todo: "text-blue-400 border-blue-400",
in_progress: "text-yellow-400 border-yellow-400",
in_review: "text-violet-400 border-violet-400",
done: "text-green-400 border-green-400",
todo: "text-blue-600 border-blue-600 dark:text-blue-400 dark:border-blue-400",
in_progress: "text-yellow-600 border-yellow-600 dark:text-yellow-400 dark:border-yellow-400",
in_review: "text-violet-600 border-violet-600 dark:text-violet-400 dark:border-violet-400",
done: "text-green-600 border-green-600 dark:text-green-400 dark:border-green-400",
cancelled: "text-neutral-500 border-neutral-500",
blocked: "text-red-400 border-red-400",
blocked: "text-red-600 border-red-600 dark:text-red-400 dark:border-red-400",
};
export const issueStatusIconDefault = "text-muted-foreground border-muted-foreground";
@@ -25,12 +25,12 @@ export const issueStatusIconDefault = "text-muted-foreground border-muted-foregr
/** Text-only color for issue statuses (dropdowns, labels) */
export const issueStatusText: Record<string, string> = {
backlog: "text-muted-foreground",
todo: "text-blue-400",
in_progress: "text-yellow-400",
in_review: "text-violet-400",
done: "text-green-400",
todo: "text-blue-600 dark:text-blue-400",
in_progress: "text-yellow-600 dark:text-yellow-400",
in_review: "text-violet-600 dark:text-violet-400",
done: "text-green-600 dark:text-green-400",
cancelled: "text-neutral-500",
blocked: "text-red-400",
blocked: "text-red-600 dark:text-red-400",
};
export const issueStatusTextDefault = "text-muted-foreground";
@@ -41,42 +41,42 @@ export const issueStatusTextDefault = "text-muted-foreground";
export const statusBadge: Record<string, string> = {
// Agent statuses
active: "bg-green-900/50 text-green-300",
running: "bg-cyan-900/50 text-cyan-300",
paused: "bg-orange-900/50 text-orange-300",
idle: "bg-yellow-900/50 text-yellow-300",
archived: "bg-neutral-800 text-neutral-400",
active: "bg-green-100 text-green-700 dark:bg-green-900/50 dark:text-green-300",
running: "bg-cyan-100 text-cyan-700 dark:bg-cyan-900/50 dark:text-cyan-300",
paused: "bg-orange-100 text-orange-700 dark:bg-orange-900/50 dark:text-orange-300",
idle: "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/50 dark:text-yellow-300",
archived: "bg-muted text-muted-foreground",
// Goal statuses
planned: "bg-neutral-800 text-neutral-400",
achieved: "bg-green-900/50 text-green-300",
completed: "bg-green-900/50 text-green-300",
planned: "bg-muted text-muted-foreground",
achieved: "bg-green-100 text-green-700 dark:bg-green-900/50 dark:text-green-300",
completed: "bg-green-100 text-green-700 dark:bg-green-900/50 dark:text-green-300",
// Run statuses
failed: "bg-red-900/50 text-red-300",
timed_out: "bg-orange-900/50 text-orange-300",
succeeded: "bg-green-900/50 text-green-300",
error: "bg-red-900/50 text-red-300",
terminated: "bg-red-900/50 text-red-300",
pending: "bg-yellow-900/50 text-yellow-300",
failed: "bg-red-100 text-red-700 dark:bg-red-900/50 dark:text-red-300",
timed_out: "bg-orange-100 text-orange-700 dark:bg-orange-900/50 dark:text-orange-300",
succeeded: "bg-green-100 text-green-700 dark:bg-green-900/50 dark:text-green-300",
error: "bg-red-100 text-red-700 dark:bg-red-900/50 dark:text-red-300",
terminated: "bg-red-100 text-red-700 dark:bg-red-900/50 dark:text-red-300",
pending: "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/50 dark:text-yellow-300",
// Approval statuses
pending_approval: "bg-amber-900/50 text-amber-300",
revision_requested: "bg-amber-900/50 text-amber-300",
approved: "bg-green-900/50 text-green-300",
rejected: "bg-red-900/50 text-red-300",
pending_approval: "bg-amber-100 text-amber-700 dark:bg-amber-900/50 dark:text-amber-300",
revision_requested: "bg-amber-100 text-amber-700 dark:bg-amber-900/50 dark:text-amber-300",
approved: "bg-green-100 text-green-700 dark:bg-green-900/50 dark:text-green-300",
rejected: "bg-red-100 text-red-700 dark:bg-red-900/50 dark:text-red-300",
// Issue statuses — consistent hues with issueStatusIcon above
backlog: "bg-neutral-800 text-neutral-400",
todo: "bg-blue-900/50 text-blue-300",
in_progress: "bg-yellow-900/50 text-yellow-300",
in_review: "bg-violet-900/50 text-violet-300",
blocked: "bg-red-900/50 text-red-300",
done: "bg-green-900/50 text-green-300",
cancelled: "bg-neutral-800 text-neutral-500",
backlog: "bg-muted text-muted-foreground",
todo: "bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300",
in_progress: "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/50 dark:text-yellow-300",
in_review: "bg-violet-100 text-violet-700 dark:bg-violet-900/50 dark:text-violet-300",
blocked: "bg-red-100 text-red-700 dark:bg-red-900/50 dark:text-red-300",
done: "bg-green-100 text-green-700 dark:bg-green-900/50 dark:text-green-300",
cancelled: "bg-muted text-muted-foreground",
};
export const statusBadgeDefault = "bg-neutral-800 text-neutral-400";
export const statusBadgeDefault = "bg-muted text-muted-foreground";
// ---------------------------------------------------------------------------
// Agent status dot — solid background for small indicator dots
@@ -99,10 +99,10 @@ export const agentStatusDotDefault = "bg-neutral-400";
// ---------------------------------------------------------------------------
export const priorityColor: Record<string, string> = {
critical: "text-red-400",
high: "text-orange-400",
medium: "text-yellow-400",
low: "text-blue-400",
critical: "text-red-600 dark:text-red-400",
high: "text-orange-600 dark:text-orange-400",
medium: "text-yellow-600 dark:text-yellow-400",
low: "text-blue-600 dark:text-blue-400",
};
export const priorityColorDefault = "text-yellow-400";
export const priorityColorDefault = "text-yellow-600 dark:text-yellow-400";

View File

@@ -10,6 +10,7 @@ import { PanelProvider } from "./context/PanelContext";
import { SidebarProvider } from "./context/SidebarContext";
import { DialogProvider } from "./context/DialogContext";
import { ToastProvider } from "./context/ToastContext";
import { ThemeProvider } from "./context/ThemeContext";
import { TooltipProvider } from "@/components/ui/tooltip";
import "@mdxeditor/editor/style.css";
import "./index.css";
@@ -26,25 +27,27 @@ const queryClient = new QueryClient({
createRoot(document.getElementById("root")!).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<CompanyProvider>
<ToastProvider>
<LiveUpdatesProvider>
<BrowserRouter>
<TooltipProvider>
<BreadcrumbProvider>
<SidebarProvider>
<PanelProvider>
<DialogProvider>
<App />
</DialogProvider>
</PanelProvider>
</SidebarProvider>
</BreadcrumbProvider>
</TooltipProvider>
</BrowserRouter>
</LiveUpdatesProvider>
</ToastProvider>
</CompanyProvider>
<ThemeProvider>
<CompanyProvider>
<ToastProvider>
<LiveUpdatesProvider>
<BrowserRouter>
<TooltipProvider>
<BreadcrumbProvider>
<SidebarProvider>
<PanelProvider>
<DialogProvider>
<App />
</DialogProvider>
</PanelProvider>
</SidebarProvider>
</BreadcrumbProvider>
</TooltipProvider>
</BrowserRouter>
</LiveUpdatesProvider>
</ToastProvider>
</CompanyProvider>
</ThemeProvider>
</QueryClientProvider>
</StrictMode>
);

View File

@@ -165,16 +165,16 @@ export function ApprovalDetail() {
return (
<div className="space-y-6 max-w-3xl">
{showApprovedBanner && (
<div className="border border-green-700/40 bg-green-900/20 rounded-lg px-4 py-3 animate-in fade-in zoom-in-95 duration-300">
<div className="border border-green-300 dark:border-green-700/40 bg-green-50 dark:bg-green-900/20 rounded-lg px-4 py-3 animate-in fade-in zoom-in-95 duration-300">
<div className="flex items-start justify-between gap-3">
<div className="flex items-start gap-2">
<div className="relative mt-0.5">
<CheckCircle2 className="h-4 w-4 text-green-300" />
<Sparkles className="h-3 w-3 text-green-200 absolute -right-2 -top-1 animate-pulse" />
<CheckCircle2 className="h-4 w-4 text-green-600 dark:text-green-300" />
<Sparkles className="h-3 w-3 text-green-500 dark:text-green-200 absolute -right-2 -top-1 animate-pulse" />
</div>
<div>
<p className="text-sm text-green-100 font-medium">Approval confirmed</p>
<p className="text-xs text-green-200/90">
<p className="text-sm text-green-800 dark:text-green-100 font-medium">Approval confirmed</p>
<p className="text-xs text-green-700 dark:text-green-200/90">
Requesting agent was notified to review this approval and linked issues.
</p>
</div>
@@ -182,7 +182,7 @@ export function ApprovalDetail() {
<Button
size="sm"
variant="outline"
className="border-green-600/50 text-green-100 hover:bg-green-900/30"
className="border-green-400 dark:border-green-600/50 text-green-800 dark:text-green-100 hover:bg-green-100 dark:hover:bg-green-900/30"
onClick={() => navigate(resolvedCta.to)}
>
{resolvedCta.label}

View File

@@ -455,10 +455,10 @@ export function DesignGuide() {
<SubSection title="Run invocation badges">
<div className="flex items-center gap-2 flex-wrap">
{[
["timer", "bg-blue-900/50 text-blue-300"],
["assignment", "bg-violet-900/50 text-violet-300"],
["on_demand", "bg-cyan-900/50 text-cyan-300"],
["automation", "bg-neutral-800 text-neutral-400"],
["timer", "bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300"],
["assignment", "bg-violet-100 text-violet-700 dark:bg-violet-900/50 dark:text-violet-300"],
["on_demand", "bg-cyan-100 text-cyan-700 dark:bg-cyan-900/50 dark:text-cyan-300"],
["automation", "bg-muted text-muted-foreground"],
].map(([label, cls]) => (
<span key={label} className={`rounded-full px-1.5 py-0.5 text-[10px] font-medium ${cls}`}>
{label}

View File

@@ -185,7 +185,7 @@ function FailedRunCard({
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="rounded-md bg-red-500/20 p-1.5">
<XCircle className="h-4 w-4 text-red-400" />
<XCircle className="h-4 w-4 text-red-600 dark:text-red-400" />
</span>
{linkedAgentName ? (
<Identity name={linkedAgentName} size="sm" />
@@ -586,7 +586,7 @@ export function Inbox() {
to={`/issues/${issue.identifier ?? issue.id}`}
className="flex cursor-pointer items-center gap-3 px-4 py-3 transition-colors hover:bg-accent/50 no-underline text-inherit"
>
<UserCheck className="h-4 w-4 shrink-0 text-blue-400" />
<UserCheck className="h-4 w-4 shrink-0 text-blue-600 dark:text-blue-400" />
<PriorityIcon priority={issue.priority} />
<StatusIcon status={issue.status} />
<span className="text-xs font-mono text-muted-foreground">
@@ -719,7 +719,7 @@ export function Inbox() {
to="/agents"
className="flex cursor-pointer items-center gap-3 px-4 py-3 transition-colors hover:bg-accent/50 no-underline text-inherit"
>
<AlertTriangle className="h-4 w-4 shrink-0 text-red-400" />
<AlertTriangle className="h-4 w-4 shrink-0 text-red-600 dark:text-red-400" />
<span className="text-sm">
<span className="font-medium">{dashboard!.agents.error}</span>{" "}
{dashboard!.agents.error === 1 ? "agent has" : "agents have"} errors