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:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
83
ui/src/context/ThemeContext.tsx
Normal file
83
ui/src/context/ThemeContext.tsx
Normal 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;
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user