feat(11-02): replace 1113 old CSS variable references with Paperclip design tokens
- Replace all bg-[color:var(--old)] with bg-background/bg-card/bg-muted/bg-primary etc. - Replace all text-[color:var(--old)] with text-foreground/text-muted-foreground/text-primary etc. - Replace all border-[color:var(--line)] with border-border - Replace all inline var(--old) with var(--new) equivalents - Replace all rounded-[6px]/rounded-[8px] with rounded-lg - Replace .surface class with bg-card border border-border shadow-sm - Replace .focus-ring class with focus-visible Tailwind utilities - Replace .mono class with font-mono - Replace data-theme attribute with class-based dark mode - 99 files updated, ~1265 lines changed, zero old references remaining
This commit is contained in:
1 parent
2ed5dd3388
commit
06ea4c6aec
99 files changed
+1265
-1265
No files matched your search
@@ -32,7 +32,7 @@ export function ActivityFeed({ activity, agents, limit = 15 }: { activity: Activ
|
||||
if (recentActivity.length === 0) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center py-8 text-center">
|
||||
<div className="text-[13px] text-[color:var(--muted)]">No activity yet</div>
|
||||
<div className="text-[13px] text-muted-foreground">No activity yet</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -49,23 +49,23 @@ export function ActivityFeed({ activity, agents, limit = 15 }: { activity: Activ
|
||||
key={item.id}
|
||||
initial={index === 0 ? { opacity: 0, y: -8 } : false}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="flex items-start gap-3 rounded-[6px] px-3 py-2 transition hover:bg-[color:var(--panel-soft)]"
|
||||
className="flex items-start gap-3 rounded-lg px-3 py-2 transition hover:bg-muted"
|
||||
>
|
||||
<div className="mt-0.5 shrink-0">
|
||||
<Icon size={14} className="text-[color:var(--muted)]" />
|
||||
<Icon size={14} className="text-muted-foreground" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-[12px] leading-relaxed text-[color:var(--text)]">
|
||||
<div className="text-[12px] leading-relaxed text-foreground">
|
||||
<span className="font-medium">{actorName}</span>
|
||||
{" "}
|
||||
<span className="text-[color:var(--muted)]">{item.action}</span>
|
||||
<span className="text-muted-foreground">{item.action}</span>
|
||||
{" "}
|
||||
<span className="text-[color:var(--muted-strong)]">{item.entityType}</span>
|
||||
<span className="text-muted-foreground">{item.entityType}</span>
|
||||
{item.detail ? (
|
||||
<span className="text-[color:var(--muted)]"> — {item.detail.slice(0, 60)}{item.detail.length > 60 ? "..." : ""}</span>
|
||||
<span className="text-muted-foreground"> — {item.detail.slice(0, 60)}{item.detail.length > 60 ? "..." : ""}</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-0.5 whitespace-nowrap text-[10px] text-[color:var(--muted)]">{timeAgo(item.createdAt)}</div>
|
||||
<div className="mt-0.5 whitespace-nowrap text-[10px] text-muted-foreground">{timeAgo(item.createdAt)}</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
@@ -78,9 +78,9 @@ export function BudgetAlertBanner({ agents, onNavigate, companyId }: BudgetAlert
|
||||
<div className="space-y-2">
|
||||
{incidents.map((incident) => {
|
||||
const isHardStop = incident.thresholdType === "hard_stop";
|
||||
const borderColor = isHardStop ? "border-[color:var(--danger)]" : "border-[color:var(--warn)]";
|
||||
const bgColor = isHardStop ? "bg-[color:var(--danger-soft)]" : "bg-[color:var(--warn-soft)]";
|
||||
const textColor = isHardStop ? "text-[color:var(--danger)]" : "text-[color:var(--warn)]";
|
||||
const borderColor = isHardStop ? "border-[color:var(--destructive)]" : "border-[color:var(--destructive)]";
|
||||
const bgColor = isHardStop ? "bg-red-100 dark:bg-red-900/50" : "bg-yellow-100 dark:bg-yellow-900/50";
|
||||
const textColor = isHardStop ? "text-destructive" : "text-yellow-600 dark:text-yellow-400";
|
||||
const pct = incident.amountLimit > 0 ? Math.round((incident.amountObserved / incident.amountLimit) * 100) : 0;
|
||||
|
||||
return (
|
||||
@@ -88,7 +88,7 @@ export function BudgetAlertBanner({ agents, onNavigate, companyId }: BudgetAlert
|
||||
key={incident.id}
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
className={`rounded-[8px] border ${borderColor} border-opacity-30 ${bgColor} px-4 py-3`}
|
||||
className={`rounded-lg border ${borderColor} border-opacity-30 ${bgColor} px-4 py-3`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="mt-0.5 shrink-0">
|
||||
@@ -98,13 +98,13 @@ export function BudgetAlertBanner({ agents, onNavigate, companyId }: BudgetAlert
|
||||
<div className={`text-[13px] font-semibold ${textColor}`}>
|
||||
{isHardStop ? "Budget Hard Stop" : "Budget Warning"}
|
||||
</div>
|
||||
<div className="mt-0.5 text-[13px] text-[color:var(--muted-strong)]">
|
||||
<div className="mt-0.5 text-[13px] text-muted-foreground">
|
||||
{getScopeName(incident.scopeType, incident.scopeId)} — ${(incident.amountObserved / 100).toFixed(2)} / ${(incident.amountLimit / 100).toFixed(2)} ({pct}%)
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleViewCosts}
|
||||
className="mt-1.5 text-[11px] font-medium text-[color:var(--accent)] hover:underline cursor-pointer"
|
||||
className="mt-1.5 text-[11px] font-medium text-primary hover:underline cursor-pointer"
|
||||
>
|
||||
View Costs
|
||||
</button>
|
||||
@@ -119,7 +119,7 @@ export function BudgetAlertBanner({ agents, onNavigate, companyId }: BudgetAlert
|
||||
onClick={handleDismiss}
|
||||
title="Dismiss budget alerts"
|
||||
aria-label="Dismiss budget alerts"
|
||||
className="shrink-0 rounded p-0.5 text-[color:var(--muted-strong)] transition hover:text-[color:var(--text)]"
|
||||
className="shrink-0 rounded p-0.5 text-muted-foreground transition hover:text-foreground"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
@@ -132,17 +132,17 @@ export function BudgetAlertBanner({ agents, onNavigate, companyId }: BudgetAlert
|
||||
<div
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
className="rounded-[8px] border border-[color:var(--warn)] border-opacity-30 bg-[color:var(--warn-soft)] px-4 py-3"
|
||||
className="rounded-lg border border-[color:var(--destructive)] border-opacity-30 bg-yellow-100 dark:bg-yellow-900/50 px-4 py-3"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="mt-0.5 shrink-0">
|
||||
<Warning size={18} weight="bold" className="text-[color:var(--warn)]" />
|
||||
<Warning size={18} weight="bold" className="text-yellow-600 dark:text-yellow-400" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-[13px] font-semibold text-[color:var(--warn)]">
|
||||
<div className="text-[13px] font-semibold text-yellow-600 dark:text-yellow-400">
|
||||
Budget Alert
|
||||
</div>
|
||||
<div className="mt-0.5 text-[13px] text-[color:var(--muted-strong)]">
|
||||
<div className="mt-0.5 text-[13px] text-muted-foreground">
|
||||
{agentsOverThreshold.length} agent(s) approaching budget limit
|
||||
</div>
|
||||
<div className="mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1">
|
||||
@@ -151,8 +151,8 @@ export function BudgetAlertBanner({ agents, onNavigate, companyId }: BudgetAlert
|
||||
key={agent.id}
|
||||
className={`whitespace-nowrap text-[11px] font-[family-name:var(--font-mono)] ${
|
||||
agent.pct >= 100
|
||||
? "text-[color:var(--danger)]"
|
||||
: "text-[color:var(--warn)]"
|
||||
? "text-destructive"
|
||||
: "text-yellow-600 dark:text-yellow-400"
|
||||
}`}
|
||||
style={{ fontVariantNumeric: "tabular-nums" }}
|
||||
>
|
||||
@@ -163,7 +163,7 @@ export function BudgetAlertBanner({ agents, onNavigate, companyId }: BudgetAlert
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleViewCosts}
|
||||
className="mt-1.5 text-[11px] font-medium text-[color:var(--accent)] hover:underline cursor-pointer"
|
||||
className="mt-1.5 text-[11px] font-medium text-primary hover:underline cursor-pointer"
|
||||
>
|
||||
View Costs
|
||||
</button>
|
||||
@@ -173,7 +173,7 @@ export function BudgetAlertBanner({ agents, onNavigate, companyId }: BudgetAlert
|
||||
onClick={handleDismiss}
|
||||
title="Dismiss budget alert"
|
||||
aria-label="Dismiss budget alert"
|
||||
className="shrink-0 rounded p-0.5 text-[color:var(--muted-strong)] transition hover:text-[color:var(--text)]"
|
||||
className="shrink-0 rounded p-0.5 text-muted-foreground transition hover:text-foreground"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
|
||||
@@ -37,7 +37,7 @@ export function ChartSection({ runs, costs, agents, companyId }: ChartSectionPro
|
||||
const content = (
|
||||
<div className="space-y-6">
|
||||
{/* Section header */}
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">
|
||||
Performance
|
||||
</div>
|
||||
|
||||
@@ -74,7 +74,7 @@ export function ChartSection({ runs, costs, agents, companyId }: ChartSectionPro
|
||||
</div>
|
||||
|
||||
{/* Budget Utilization section */}
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">
|
||||
Budget Utilization
|
||||
</div>
|
||||
<BudgetUtilization companyId={companyId} agents={agents} />
|
||||
|
||||
@@ -78,18 +78,18 @@ export function Dashboard(props: DashboardProps) {
|
||||
{/* Company status line */}
|
||||
{companyName ? (
|
||||
<div>
|
||||
<h2 className="text-[28px] font-bold tracking-[-0.02em] text-[color:var(--text)]">{companyName}</h2>
|
||||
<div className="mt-2 flex items-center gap-3 text-[13px] tracking-[0.005em] text-[color:var(--muted)]">
|
||||
<h2 className="text-[28px] font-bold tracking-[-0.02em] text-foreground">{companyName}</h2>
|
||||
<div className="mt-2 flex items-center gap-3 text-[13px] tracking-[0.005em] text-muted-foreground">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-[color:var(--success)]" />
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-green-600 dark:bg-green-400" />
|
||||
{activeAgents.length} {activeAgents.length === 1 ? "agent" : "agents"} active
|
||||
</span>
|
||||
<span className="h-3 w-px bg-[color:var(--line-strong)]" />
|
||||
<span className="h-3 w-px bg-[color:var(--border)]" />
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-[color:var(--accent)]" />
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-primary" />
|
||||
{activeTasks.length} {activeTasks.length === 1 ? "task" : "tasks"} in progress
|
||||
</span>
|
||||
<span className="h-3 w-px bg-[color:var(--line-strong)]" />
|
||||
<span className="h-3 w-px bg-[color:var(--border)]" />
|
||||
<span>{tasks.filter((tk) => tk.status === "done").length} completed</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -139,8 +139,8 @@ export function Dashboard(props: DashboardProps) {
|
||||
{/* Needs Attention — moved to top, only shows when items exist */}
|
||||
{hasAttentionItems ? (
|
||||
<section>
|
||||
<div className="mb-3 flex items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--warn)]">
|
||||
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-[color:var(--warn)]" />
|
||||
<div className="mb-3 flex items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-yellow-600 dark:text-yellow-400">
|
||||
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-yellow-600 dark:bg-yellow-400" />
|
||||
{t("dash.needsAttentionSection")}
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
@@ -154,17 +154,17 @@ export function Dashboard(props: DashboardProps) {
|
||||
transition={{ duration: 0.22, delay: i * 0.04, ease: [0.16, 1, 0.3, 1] }}
|
||||
type="button"
|
||||
onClick={() => onNavigate("approvals", approval.id)}
|
||||
className="focus-ring flex w-full items-center gap-3 rounded-[8px] border border-[color:var(--warn)] border-opacity-20 bg-[color:var(--warn-soft)] px-4 py-3 text-left transition hover:border-opacity-40"
|
||||
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 flex w-full items-center gap-3 rounded-lg border border-[color:var(--destructive)] border-opacity-20 bg-yellow-100 dark:bg-yellow-900/50 px-4 py-3 text-left transition hover:border-opacity-40"
|
||||
>
|
||||
<ShieldCheck size={18} className="shrink-0 text-[color:var(--warn)]" />
|
||||
<ShieldCheck size={18} className="shrink-0 text-yellow-600 dark:text-yellow-400" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-[13px] font-medium text-[color:var(--text)]">{approval.payloadSummary}</div>
|
||||
<div className="mt-0.5 flex items-center gap-2 text-[11px] text-[color:var(--muted)]">
|
||||
<div className="truncate text-[13px] font-medium text-foreground">{approval.payloadSummary}</div>
|
||||
<div className="mt-0.5 flex items-center gap-2 text-[11px] text-muted-foreground">
|
||||
<span className="uppercase tracking-[0.1em]">{approval.type.replaceAll("_", " ")}</span>
|
||||
{requester ? <span>by {requester.name}</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
<ArrowRight size={14} className="shrink-0 text-[color:var(--muted)]" />
|
||||
<ArrowRight size={14} className="shrink-0 text-muted-foreground" />
|
||||
</motion.button>
|
||||
);
|
||||
})}
|
||||
@@ -175,16 +175,16 @@ export function Dashboard(props: DashboardProps) {
|
||||
key={run.id}
|
||||
type="button"
|
||||
onClick={() => onNavigate("runs", run.id)}
|
||||
className="focus-ring flex w-full items-center gap-3 rounded-[8px] border border-[color:var(--danger)] border-opacity-20 bg-[color:var(--danger-soft)] px-4 py-3 text-left transition hover:border-opacity-40"
|
||||
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 flex w-full items-center gap-3 rounded-lg border border-[color:var(--destructive)] border-opacity-20 bg-red-100 dark:bg-red-900/50 px-4 py-3 text-left transition hover:border-opacity-40"
|
||||
>
|
||||
<XCircle size={18} className="shrink-0 text-[color:var(--danger)]" />
|
||||
<XCircle size={18} className="shrink-0 text-destructive" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-[13px] font-medium text-[color:var(--text)]">{run.summary || "Run failed"}</div>
|
||||
<div className="mt-0.5 text-[11px] text-[color:var(--muted)]">
|
||||
<div className="truncate text-[13px] font-medium text-foreground">{run.summary || "Run failed"}</div>
|
||||
<div className="mt-0.5 text-[11px] text-muted-foreground">
|
||||
{agent?.name} — {run.errorMessage?.slice(0, 80) ?? "Unknown error"}
|
||||
</div>
|
||||
</div>
|
||||
<ArrowRight size={14} className="shrink-0 text-[color:var(--muted)]" />
|
||||
<ArrowRight size={14} className="shrink-0 text-muted-foreground" />
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
@@ -195,16 +195,16 @@ export function Dashboard(props: DashboardProps) {
|
||||
key={msg.id}
|
||||
type="button"
|
||||
onClick={() => onNavigate("communication", msg.id)}
|
||||
className="focus-ring flex w-full items-center gap-3 rounded-[8px] border border-[color:var(--warn)] border-opacity-20 bg-[color:var(--warn-soft)] px-4 py-3 text-left transition hover:border-opacity-40"
|
||||
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 flex w-full items-center gap-3 rounded-lg border border-[color:var(--destructive)] border-opacity-20 bg-yellow-100 dark:bg-yellow-900/50 px-4 py-3 text-left transition hover:border-opacity-40"
|
||||
>
|
||||
<Warning size={18} className="shrink-0 text-[color:var(--warn)]" />
|
||||
<Warning size={18} className="shrink-0 text-yellow-600 dark:text-yellow-400" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-[13px] font-medium text-[color:var(--text)]">{msg.subject}</div>
|
||||
<div className="mt-0.5 text-[11px] text-[color:var(--muted)]">
|
||||
<div className="truncate text-[13px] font-medium text-foreground">{msg.subject}</div>
|
||||
<div className="mt-0.5 text-[11px] text-muted-foreground">
|
||||
{sender?.name ?? "Unknown"} — {msg.channel}{msg.priority === "urgent" ? " · urgent" : ""}
|
||||
</div>
|
||||
</div>
|
||||
<ArrowRight size={14} className="shrink-0 text-[color:var(--muted)]" />
|
||||
<ArrowRight size={14} className="shrink-0 text-muted-foreground" />
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
@@ -215,36 +215,36 @@ export function Dashboard(props: DashboardProps) {
|
||||
{/* Active Work — what's running right now */}
|
||||
<section>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">
|
||||
Active Work
|
||||
</div>
|
||||
<button type="button" onClick={() => onNavigate("runs")} className="flex items-center gap-1 text-[11px] font-medium text-[color:var(--accent)] transition hover:underline">
|
||||
<button type="button" onClick={() => onNavigate("runs")} className="flex items-center gap-1 text-[11px] font-medium text-primary transition hover:underline">
|
||||
All runs <ArrowRight size={12} />
|
||||
</button>
|
||||
</div>
|
||||
{activeWork.length > 0 ? (
|
||||
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] divide-y divide-[color:var(--line)]">
|
||||
<div className="rounded-lg border border-border bg-card divide-y divide-[color:var(--border)]">
|
||||
{activeWork.map(({ run, agent, task }) => (
|
||||
<button
|
||||
key={run.id}
|
||||
type="button"
|
||||
onClick={() => onNavigate("runs", run.id)}
|
||||
className="focus-ring flex w-full items-center gap-3 px-4 py-3 text-left transition hover:bg-[color:var(--panel-soft)]"
|
||||
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 flex w-full items-center gap-3 px-4 py-3 text-left transition hover:bg-muted"
|
||||
>
|
||||
<div className="relative shrink-0">
|
||||
<Robot size={16} className="text-[color:var(--success)]" />
|
||||
<Robot size={16} className="text-green-600 dark:text-green-400" />
|
||||
{run.status === "running" ? (
|
||||
<span className="absolute -right-0.5 -top-0.5 h-2 w-2">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[color:var(--success)] opacity-40" style={{ willChange: "transform" }} />
|
||||
<span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-[color:var(--success)]" />
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-600 dark:bg-green-400 opacity-40" style={{ willChange: "transform" }} />
|
||||
<span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-green-600 dark:bg-green-400" />
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-[13px] font-medium text-[color:var(--text)]">
|
||||
<div className="truncate text-[13px] font-medium text-foreground">
|
||||
{task?.title ?? run.summary ?? `Run ${run.id.slice(0, 8)}`}
|
||||
</div>
|
||||
<div className="mt-0.5 text-[11px] text-[color:var(--muted)]">
|
||||
<div className="mt-0.5 text-[11px] text-muted-foreground">
|
||||
{agent?.name ?? "Unknown agent"} · {run.status}
|
||||
</div>
|
||||
</div>
|
||||
@@ -252,9 +252,9 @@ export function Dashboard(props: DashboardProps) {
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-[8px] border border-dashed border-[color:var(--line)] py-8 text-center">
|
||||
<div className="text-[13px] text-[color:var(--muted)]">{t("dash.allQuiet")}</div>
|
||||
<div className="mt-1 text-[11px] text-[color:var(--muted)]">No tasks currently running</div>
|
||||
<div className="rounded-lg border border-dashed border-border py-8 text-center">
|
||||
<div className="text-[13px] text-muted-foreground">{t("dash.allQuiet")}</div>
|
||||
<div className="mt-1 text-[11px] text-muted-foreground">No tasks currently running</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
@@ -262,11 +262,11 @@ export function Dashboard(props: DashboardProps) {
|
||||
{/* Recent Activity */}
|
||||
<section>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">
|
||||
{t("dash.recentActivity")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-h-[360px] overflow-y-auto rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] py-2">
|
||||
<div className="max-h-[360px] overflow-y-auto rounded-lg border border-border bg-card py-2">
|
||||
<ActivityFeed activity={activity} agents={agents} limit={10} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
import type { AgentRecord, RunRecord, SectionId, TaskRecord } from "@shared/types";
|
||||
|
||||
const DEPARTMENT_INFO: Record<string, { label: string; icon: React.ElementType; color: string }> = {
|
||||
executive: { label: "Executive", icon: Buildings, color: "var(--accent)" },
|
||||
executive: { label: "Executive", icon: Buildings, color: "var(--primary)" },
|
||||
engineering: { label: "Engineering", icon: Cpu, color: "#6366f1" },
|
||||
product: { label: "Product", icon: Rocket, color: "#8b5cf6" },
|
||||
design: { label: "Design", icon: Target, color: "#ec4899" },
|
||||
@@ -76,7 +76,7 @@ export function DepartmentOverview({
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6">
|
||||
{departments.map(([deptId, data]) => {
|
||||
const info = DEPARTMENT_INFO[deptId] ?? { label: deptId, icon: Buildings, color: "var(--muted)" };
|
||||
const info = DEPARTMENT_INFO[deptId] ?? { label: deptId, icon: Buildings, color: "var(--muted-foreground)" };
|
||||
const Icon = info.icon;
|
||||
const activeTasks = data.tasks.filter((t) => t.status === "in_progress" || t.status === "in_review").length;
|
||||
const doneTasks = data.tasks.filter((t) => t.status === "done").length;
|
||||
@@ -88,33 +88,33 @@ export function DepartmentOverview({
|
||||
key={deptId}
|
||||
type="button"
|
||||
onClick={() => onNavigate("agents")}
|
||||
className="focus-ring group relative overflow-hidden rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] p-3.5 text-left transition duration-150 hover:border-[color:var(--line-strong)] hover:shadow-sm"
|
||||
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 group relative overflow-hidden rounded-lg border border-border bg-card p-3.5 text-left transition duration-150 hover:border-border hover:shadow-sm"
|
||||
>
|
||||
<div className="absolute left-0 top-0 h-full w-[3px]" style={{ backgroundColor: info.color }} />
|
||||
<div className="flex items-center gap-2 pl-1">
|
||||
<Icon size={15} weight="fill" style={{ color: info.color }} />
|
||||
<span className="truncate text-[11px] font-medium text-[color:var(--text)]">{info.label}</span>
|
||||
<span className="truncate text-[11px] font-medium text-foreground">{info.label}</span>
|
||||
{runningAgents > 0 && (
|
||||
<span className="ml-auto flex items-center gap-1">
|
||||
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-[color:var(--success)]" />
|
||||
<span className="text-[9px] text-[color:var(--success)]">{runningAgents}</span>
|
||||
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-green-600 dark:bg-green-400" />
|
||||
<span className="text-[9px] text-green-600 dark:text-green-400">{runningAgents}</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-2 grid grid-cols-3 gap-1 pl-1">
|
||||
<div>
|
||||
<div className="text-[16px] font-bold tabular-nums text-[color:var(--text)]">{data.agents.length}</div>
|
||||
<div className="text-[9px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">People</div>
|
||||
<div className="text-[16px] font-bold tabular-nums text-foreground">{data.agents.length}</div>
|
||||
<div className="text-[9px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">People</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-[16px] font-bold tabular-nums text-[color:var(--accent)]">{activeTasks}</div>
|
||||
<div className="text-[9px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Active</div>
|
||||
<div className="text-[16px] font-bold tabular-nums text-primary">{activeTasks}</div>
|
||||
<div className="text-[9px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">Active</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className={`text-[16px] font-bold tabular-nums ${blockedTasks > 0 ? "text-[color:var(--danger)]" : "text-[color:var(--success)]"}`}>
|
||||
<div className={`text-[16px] font-bold tabular-nums ${blockedTasks > 0 ? "text-destructive" : "text-green-600 dark:text-green-400"}`}>
|
||||
{blockedTasks > 0 ? blockedTasks : doneTasks}
|
||||
</div>
|
||||
<div className="text-[9px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
|
||||
<div className="text-[9px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">
|
||||
{blockedTasks > 0 ? "Blocked" : "Done"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -20,22 +20,22 @@ export function GoalProgress({ goals, tasks, onNavigate }: { goals: GoalRecord[]
|
||||
key={goal.id}
|
||||
type="button"
|
||||
onClick={() => onNavigate("goals")}
|
||||
className="focus-ring w-full rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3.5 text-left transition hover:border-[color:var(--line-strong)] hover:shadow-sm"
|
||||
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 w-full rounded-lg border border-border bg-card px-4 py-3.5 text-left transition hover:border-border hover:shadow-sm"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<Target size={15} weight="fill" className="shrink-0 text-[color:var(--accent)]" />
|
||||
<span className="truncate text-[13px] font-medium text-[color:var(--text)]">{goal.title}</span>
|
||||
<Target size={15} weight="fill" className="shrink-0 text-primary" />
|
||||
<span className="truncate text-[13px] font-medium text-foreground">{goal.title}</span>
|
||||
</div>
|
||||
<span className="shrink-0 whitespace-nowrap text-[13px] font-bold tabular-nums text-[color:var(--accent)]">{pct}%</span>
|
||||
<span className="shrink-0 whitespace-nowrap text-[13px] font-bold tabular-nums text-primary">{pct}%</span>
|
||||
</div>
|
||||
<div className="mt-2.5 h-2 w-full overflow-hidden rounded-full bg-[color:var(--panel-soft)]">
|
||||
<div className="mt-2.5 h-2 w-full overflow-hidden rounded-full bg-muted">
|
||||
<div
|
||||
className="h-full rounded-full bg-[color:var(--accent)] transition-all duration-700 ease-out"
|
||||
className="h-full rounded-full bg-primary transition-all duration-700 ease-out"
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-1.5 text-[11px] text-[color:var(--muted)]" style={{ fontVariantNumeric: "tabular-nums" }}>{done}/{total} tasks complete</div>
|
||||
<div className="mt-1.5 text-[11px] text-muted-foreground" style={{ fontVariantNumeric: "tabular-nums" }}>{done}/{total} tasks complete</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -58,11 +58,11 @@ export function computeHealthScore(props: HealthInputProps): number {
|
||||
}
|
||||
|
||||
function healthLabel(score: number): { label: string; tone: string } {
|
||||
if (score >= 90) return { label: "Excellent", tone: "text-[color:var(--success)]" };
|
||||
if (score >= 70) return { label: "Good", tone: "text-[color:var(--success)]" };
|
||||
if (score >= 50) return { label: "Fair", tone: "text-[color:var(--warn)]" };
|
||||
if (score >= 30) return { label: "Poor", tone: "text-[color:var(--warn)]" };
|
||||
return { label: "Critical", tone: "text-[color:var(--danger)]" };
|
||||
if (score >= 90) return { label: "Excellent", tone: "text-green-600 dark:text-green-400" };
|
||||
if (score >= 70) return { label: "Good", tone: "text-green-600 dark:text-green-400" };
|
||||
if (score >= 50) return { label: "Fair", tone: "text-yellow-600 dark:text-yellow-400" };
|
||||
if (score >= 30) return { label: "Poor", tone: "text-yellow-600 dark:text-yellow-400" };
|
||||
return { label: "Critical", tone: "text-destructive" };
|
||||
}
|
||||
|
||||
export function HealthRing({ score }: { score: number }) {
|
||||
@@ -72,16 +72,16 @@ export function HealthRing({ score }: { score: number }) {
|
||||
const { label, tone } = healthLabel(score);
|
||||
|
||||
const strokeColor = score >= 70
|
||||
? "var(--success)"
|
||||
? "var(--primary)"
|
||||
: score >= 50
|
||||
? "var(--warn)"
|
||||
: "var(--danger)";
|
||||
? "var(--destructive)"
|
||||
: "var(--destructive)";
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-5">
|
||||
<div className="relative h-[90px] w-[90px] shrink-0">
|
||||
<svg viewBox="0 0 88 88" className="h-full w-full -rotate-90">
|
||||
<circle cx="44" cy="44" r={radius} fill="none" stroke="var(--line)" strokeWidth="6" />
|
||||
<circle cx="44" cy="44" r={radius} fill="none" stroke="var(--border)" strokeWidth="6" />
|
||||
<motion.circle
|
||||
cx="44"
|
||||
cy="44"
|
||||
@@ -101,7 +101,7 @@ export function HealthRing({ score }: { score: number }) {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Company Health</div>
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">Company Health</div>
|
||||
<div className={`text-[16px] font-semibold ${tone}`}>{label}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,19 +10,19 @@ interface KanbanProps {
|
||||
}
|
||||
|
||||
const KANBAN_COLUMNS: Array<{ status: TaskRecord["status"]; label: string; tone: string; dotColor: string }> = [
|
||||
{ status: "backlog", label: "Backlog", tone: "text-[color:var(--muted)]", dotColor: "bg-[color:var(--muted)]" },
|
||||
{ status: "todo", label: "Todo", tone: "text-[color:var(--warn)]", dotColor: "bg-[color:var(--warn)]" },
|
||||
{ status: "in_progress", label: "In Progress", tone: "text-[color:var(--accent)]", dotColor: "bg-[color:var(--accent)]" },
|
||||
{ status: "in_review", label: "In Review", tone: "text-[color:var(--warn)]", dotColor: "bg-[color:var(--warn)]" },
|
||||
{ status: "done", label: "Done", tone: "text-[color:var(--success)]", dotColor: "bg-[color:var(--success)]" },
|
||||
{ status: "blocked", label: "Blocked", tone: "text-[color:var(--danger)]", dotColor: "bg-[color:var(--danger)]" },
|
||||
{ status: "backlog", label: "Backlog", tone: "text-muted-foreground", dotColor: "bg-[color:var(--muted-foreground)]" },
|
||||
{ status: "todo", label: "Todo", tone: "text-yellow-600 dark:text-yellow-400", dotColor: "bg-yellow-600 dark:bg-yellow-400" },
|
||||
{ status: "in_progress", label: "In Progress", tone: "text-primary", dotColor: "bg-primary" },
|
||||
{ status: "in_review", label: "In Review", tone: "text-yellow-600 dark:text-yellow-400", dotColor: "bg-yellow-600 dark:bg-yellow-400" },
|
||||
{ status: "done", label: "Done", tone: "text-green-600 dark:text-green-400", dotColor: "bg-green-600 dark:bg-green-400" },
|
||||
{ status: "blocked", label: "Blocked", tone: "text-destructive", dotColor: "bg-destructive" },
|
||||
];
|
||||
|
||||
const priorityColors: Record<string, string> = {
|
||||
critical: "border-l-[color:var(--danger)]",
|
||||
high: "border-l-[color:var(--warn)]",
|
||||
medium: "border-l-[color:var(--accent)]",
|
||||
low: "border-l-[color:var(--muted)]",
|
||||
critical: "border-l-[color:var(--destructive)]",
|
||||
high: "border-l-[color:var(--destructive)]",
|
||||
medium: "border-l-[color:var(--primary)]",
|
||||
low: "border-l-[color:var(--muted-foreground)]",
|
||||
};
|
||||
|
||||
export function KanbanBoard({ tasks, agents, runs, onSelectTask }: KanbanProps) {
|
||||
@@ -44,10 +44,10 @@ export function KanbanBoard({ tasks, agents, runs, onSelectTask }: KanbanProps)
|
||||
|
||||
if (tasks.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center rounded-[8px] border border-dashed border-[color:var(--line)] py-16 text-center">
|
||||
<Briefcase size={32} className="mb-3 text-[color:var(--muted)]" />
|
||||
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">No tasks yet</div>
|
||||
<div className="mt-1 text-[12px] text-[color:var(--muted)]">Tasks created by agents will appear here on the board</div>
|
||||
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed border-border py-16 text-center">
|
||||
<Briefcase size={32} className="mb-3 text-muted-foreground" />
|
||||
<div className="text-[14px] font-medium text-muted-foreground">No tasks yet</div>
|
||||
<div className="mt-1 text-[12px] text-muted-foreground">Tasks created by agents will appear here on the board</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -61,11 +61,11 @@ export function KanbanBoard({ tasks, agents, runs, onSelectTask }: KanbanProps)
|
||||
<div className="mb-2 flex items-center gap-2 px-1">
|
||||
<span className={`h-2.5 w-2.5 rounded-full ${col.dotColor}`} aria-hidden="true" />
|
||||
<span className={`text-[11px] font-semibold uppercase tracking-[0.08em] ${col.tone}`}>{col.label}</span>
|
||||
<span className="ml-auto rounded-full bg-[color:var(--panel-soft)] px-2 py-0.5 text-[10px] font-bold tabular-nums text-[color:var(--muted-strong)]">
|
||||
<span className="ml-auto rounded-full bg-muted px-2 py-0.5 text-[10px] font-bold tabular-nums text-muted-foreground">
|
||||
{colTasks.length}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col gap-1.5 rounded-[8px] bg-[color:var(--panel-soft)] p-1.5" style={{ minHeight: 120 }}>
|
||||
<div className="flex flex-1 flex-col gap-1.5 rounded-lg bg-muted p-1.5" style={{ minHeight: 120 }}>
|
||||
{colTasks.map((task) => {
|
||||
const assignee = agents.find((a) => a.id === task.assigneeAgentId);
|
||||
const activeRun = runs.find((r) => r.id === task.activeRunId);
|
||||
@@ -75,27 +75,27 @@ export function KanbanBoard({ tasks, agents, runs, onSelectTask }: KanbanProps)
|
||||
key={task.id}
|
||||
type="button"
|
||||
onClick={() => onSelectTask(task.id)}
|
||||
className={`focus-ring rounded-[6px] border border-l-[3px] border-[color:var(--line)] bg-[color:var(--panel)] px-3 py-2.5 text-left transition duration-150 hover:shadow-sm ${priorityColors[task.priority] ?? ""}`}
|
||||
className={`focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-lg border border-l-[3px] border-border bg-card px-3 py-2.5 text-left transition duration-150 hover:shadow-sm ${priorityColors[task.priority] ?? ""}`}
|
||||
>
|
||||
<div className="truncate text-[12px] font-medium leading-snug text-[color:var(--text)]">{task.title}</div>
|
||||
<div className="truncate text-[12px] font-medium leading-snug text-foreground">{task.title}</div>
|
||||
<div className="mt-1.5 flex items-center gap-2">
|
||||
{assignee ? (
|
||||
<span className="flex items-center gap-1 text-[10px] text-[color:var(--muted)]">
|
||||
<span className="flex items-center gap-1 text-[10px] text-muted-foreground">
|
||||
<Robot size={10} />
|
||||
{assignee.name}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-[10px] italic text-[color:var(--muted)]">Unassigned</span>
|
||||
<span className="text-[10px] italic text-muted-foreground">Unassigned</span>
|
||||
)}
|
||||
{isRunning ? (
|
||||
<span className="flex items-center gap-1 whitespace-nowrap text-[10px] text-[color:var(--success)]">
|
||||
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-[color:var(--success)]" aria-hidden="true" />
|
||||
<span className="flex items-center gap-1 whitespace-nowrap text-[10px] text-green-600 dark:text-green-400">
|
||||
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-green-600 dark:bg-green-400" aria-hidden="true" />
|
||||
Running
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{task.priority === "critical" || task.priority === "high" ? (
|
||||
<div className={`mt-1 text-[9px] font-bold uppercase tracking-[0.12em] ${task.priority === "critical" ? "text-[color:var(--danger)]" : "text-[color:var(--warn)]"}`}>
|
||||
<div className={`mt-1 text-[9px] font-bold uppercase tracking-[0.12em] ${task.priority === "critical" ? "text-destructive" : "text-yellow-600 dark:text-yellow-400"}`}>
|
||||
{task.priority}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -16,41 +16,41 @@ export function MetricCard({
|
||||
subtitle?: string;
|
||||
}) {
|
||||
const toneMap = {
|
||||
default: "text-[color:var(--text)]",
|
||||
success: "text-[color:var(--success)]",
|
||||
warn: "text-[color:var(--warn)]",
|
||||
danger: "text-[color:var(--danger)]",
|
||||
accent: "text-[color:var(--accent)]",
|
||||
default: "text-foreground",
|
||||
success: "text-green-600 dark:text-green-400",
|
||||
warn: "text-yellow-600 dark:text-yellow-400",
|
||||
danger: "text-destructive",
|
||||
accent: "text-primary",
|
||||
};
|
||||
const bgMap = {
|
||||
default: "bg-[color:var(--panel-soft)]",
|
||||
success: "bg-[color:var(--success-soft)]",
|
||||
warn: "bg-[color:var(--warn-soft)]",
|
||||
danger: "bg-[color:var(--danger-soft)]",
|
||||
accent: "bg-[color:var(--accent-soft)]",
|
||||
default: "bg-muted",
|
||||
success: "bg-green-100 dark:bg-green-900/50",
|
||||
warn: "bg-yellow-100 dark:bg-yellow-900/50",
|
||||
danger: "bg-red-100 dark:bg-red-900/50",
|
||||
accent: "bg-accent",
|
||||
};
|
||||
|
||||
const borderMap = {
|
||||
default: "border-l-transparent",
|
||||
success: "border-l-[color:var(--success)]",
|
||||
warn: "border-l-[color:var(--warn)]",
|
||||
danger: "border-l-[color:var(--danger)]",
|
||||
accent: "border-l-[color:var(--accent)]",
|
||||
success: "border-l-[color:var(--primary)]",
|
||||
warn: "border-l-[color:var(--destructive)]",
|
||||
danger: "border-l-[color:var(--destructive)]",
|
||||
accent: "border-l-[color:var(--primary)]",
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={`focus-ring flex items-center gap-4 rounded-[8px] border border-l-[3px] border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-4 text-left transition duration-150 hover:border-[color:var(--line-strong)] hover:shadow-sm ${borderMap[tone]}`}
|
||||
className={`focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 flex items-center gap-4 rounded-lg border border-l-[3px] border-border bg-card px-4 py-4 text-left transition duration-150 hover:border-border hover:shadow-sm ${borderMap[tone]}`}
|
||||
>
|
||||
<div className={`flex h-12 w-12 shrink-0 items-center justify-center rounded-[8px] ${bgMap[tone]}`}>
|
||||
<div className={`flex h-12 w-12 shrink-0 items-center justify-center rounded-lg ${bgMap[tone]}`}>
|
||||
<Icon size={24} weight="fill" className={toneMap[tone]} />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{label}</div>
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{label}</div>
|
||||
<div className={`text-[28px] font-bold tracking-[-0.02em] leading-none ${toneMap[tone]}`} style={{ fontVariantNumeric: "tabular-nums" }}>{value}</div>
|
||||
{subtitle ? <div className="mt-1.5 truncate text-[11px] font-medium text-[color:var(--muted)]">{subtitle}</div> : null}
|
||||
{subtitle ? <div className="mt-1.5 truncate text-[11px] font-medium text-muted-foreground">{subtitle}</div> : null}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -48,7 +48,7 @@ export function NeedsAttention({
|
||||
|
||||
return (
|
||||
<section>
|
||||
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--warn)]">{t("dash.needsAttentionSection")}</div>
|
||||
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-yellow-600 dark:text-yellow-400">{t("dash.needsAttentionSection")}</div>
|
||||
<div className="space-y-1.5">
|
||||
{pendingApprovals.slice(0, 5).map((approval) => {
|
||||
const requester = agents.find((a) => a.id === approval.requestedByAgentId);
|
||||
@@ -57,17 +57,17 @@ export function NeedsAttention({
|
||||
key={approval.id}
|
||||
type="button"
|
||||
onClick={() => onNavigate("approvals", approval.id)}
|
||||
className="focus-ring flex w-full items-center gap-3 rounded-[8px] border border-[color:var(--warn)] border-opacity-20 bg-[color:var(--warn-soft)] px-4 py-3 text-left transition hover:border-opacity-40"
|
||||
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 flex w-full items-center gap-3 rounded-lg border border-[color:var(--destructive)] border-opacity-20 bg-yellow-100 dark:bg-yellow-900/50 px-4 py-3 text-left transition hover:border-opacity-40"
|
||||
>
|
||||
<ShieldCheck size={18} className="shrink-0 text-[color:var(--warn)]" />
|
||||
<ShieldCheck size={18} className="shrink-0 text-yellow-600 dark:text-yellow-400" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-[13px] font-medium text-[color:var(--text)]">{approval.payloadSummary}</div>
|
||||
<div className="mt-0.5 flex items-center gap-2 text-[11px] text-[color:var(--muted)]">
|
||||
<div className="truncate text-[13px] font-medium text-foreground">{approval.payloadSummary}</div>
|
||||
<div className="mt-0.5 flex items-center gap-2 text-[11px] text-muted-foreground">
|
||||
<span className="uppercase tracking-[0.1em]">{approval.type.replaceAll("_", " ")}</span>
|
||||
{requester ? <span>by {requester.name}</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
<ArrowRight size={14} className="shrink-0 text-[color:var(--muted)]" />
|
||||
<ArrowRight size={14} className="shrink-0 text-muted-foreground" />
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
@@ -78,16 +78,16 @@ export function NeedsAttention({
|
||||
key={run.id}
|
||||
type="button"
|
||||
onClick={() => onNavigate("runs", run.id)}
|
||||
className="focus-ring flex w-full items-center gap-3 rounded-[8px] border border-[color:var(--danger)] border-opacity-20 bg-[color:var(--danger-soft)] px-4 py-3 text-left transition hover:border-opacity-40"
|
||||
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 flex w-full items-center gap-3 rounded-lg border border-[color:var(--destructive)] border-opacity-20 bg-red-100 dark:bg-red-900/50 px-4 py-3 text-left transition hover:border-opacity-40"
|
||||
>
|
||||
<XCircle size={18} className="shrink-0 text-[color:var(--danger)]" />
|
||||
<XCircle size={18} className="shrink-0 text-destructive" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-[13px] font-medium text-[color:var(--text)]">{run.summary || "Run failed"}</div>
|
||||
<div className="mt-0.5 text-[11px] text-[color:var(--muted)]">
|
||||
<div className="truncate text-[13px] font-medium text-foreground">{run.summary || "Run failed"}</div>
|
||||
<div className="mt-0.5 text-[11px] text-muted-foreground">
|
||||
{agent?.name} — {run.errorMessage?.slice(0, 80) ?? "Unknown error"}
|
||||
</div>
|
||||
</div>
|
||||
<ArrowRight size={14} className="shrink-0 text-[color:var(--muted)]" />
|
||||
<ArrowRight size={14} className="shrink-0 text-muted-foreground" />
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
@@ -98,16 +98,16 @@ export function NeedsAttention({
|
||||
key={message.id}
|
||||
type="button"
|
||||
onClick={() => onNavigate("communication", message.id)}
|
||||
className="focus-ring flex w-full items-center gap-3 rounded-[8px] border border-[color:var(--warn)] border-opacity-20 bg-[color:var(--warn-soft)] px-4 py-3 text-left transition hover:border-opacity-40"
|
||||
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 flex w-full items-center gap-3 rounded-lg border border-[color:var(--destructive)] border-opacity-20 bg-yellow-100 dark:bg-yellow-900/50 px-4 py-3 text-left transition hover:border-opacity-40"
|
||||
>
|
||||
<Users size={18} className="shrink-0 text-[color:var(--warn)]" />
|
||||
<Users size={18} className="shrink-0 text-yellow-600 dark:text-yellow-400" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-[13px] font-medium text-[color:var(--text)]">{message.subject}</div>
|
||||
<div className="mt-0.5 text-[11px] text-[color:var(--muted)]">
|
||||
<div className="truncate text-[13px] font-medium text-foreground">{message.subject}</div>
|
||||
<div className="mt-0.5 text-[11px] text-muted-foreground">
|
||||
{sender?.name ?? "Unknown"} — {message.channel} {message.priority === "urgent" ? "· urgent" : ""}
|
||||
</div>
|
||||
</div>
|
||||
<ArrowRight size={14} className="shrink-0 text-[color:var(--muted)]" />
|
||||
<ArrowRight size={14} className="shrink-0 text-muted-foreground" />
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
@@ -116,14 +116,14 @@ export function NeedsAttention({
|
||||
key={action.id}
|
||||
type="button"
|
||||
onClick={() => onNavigate("social")}
|
||||
className="focus-ring flex w-full items-center gap-3 rounded-[8px] border border-[color:var(--warn)] border-opacity-20 bg-[color:var(--warn-soft)] px-4 py-3 text-left transition hover:border-opacity-40"
|
||||
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 flex w-full items-center gap-3 rounded-lg border border-[color:var(--destructive)] border-opacity-20 bg-yellow-100 dark:bg-yellow-900/50 px-4 py-3 text-left transition hover:border-opacity-40"
|
||||
>
|
||||
<Cpu size={18} className="shrink-0 text-[color:var(--warn)]" />
|
||||
<Cpu size={18} className="shrink-0 text-yellow-600 dark:text-yellow-400" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-[13px] font-medium text-[color:var(--text)]">{action.resultSummary || action.actionType}</div>
|
||||
<div className="mt-0.5 whitespace-nowrap text-[11px] text-[color:var(--muted)]">Browser action {action.status.replaceAll("_", " ")}</div>
|
||||
<div className="truncate text-[13px] font-medium text-foreground">{action.resultSummary || action.actionType}</div>
|
||||
<div className="mt-0.5 whitespace-nowrap text-[11px] text-muted-foreground">Browser action {action.status.replaceAll("_", " ")}</div>
|
||||
</div>
|
||||
<ArrowRight size={14} className="shrink-0 text-[color:var(--muted)]" />
|
||||
<ArrowRight size={14} className="shrink-0 text-muted-foreground" />
|
||||
</button>
|
||||
))}
|
||||
{stalledWorkflows.slice(0, 3).map((workflow) => (
|
||||
@@ -131,14 +131,14 @@ export function NeedsAttention({
|
||||
key={workflow.id}
|
||||
type="button"
|
||||
onClick={() => onNavigate("automation")}
|
||||
className="focus-ring flex w-full items-center gap-3 rounded-[8px] border border-[color:var(--warn)] border-opacity-20 bg-[color:var(--warn-soft)] px-4 py-3 text-left transition hover:border-opacity-40"
|
||||
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 flex w-full items-center gap-3 rounded-lg border border-[color:var(--destructive)] border-opacity-20 bg-yellow-100 dark:bg-yellow-900/50 px-4 py-3 text-left transition hover:border-opacity-40"
|
||||
>
|
||||
<Lightning size={18} className="shrink-0 text-[color:var(--warn)]" />
|
||||
<Lightning size={18} className="shrink-0 text-yellow-600 dark:text-yellow-400" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-[13px] font-medium text-[color:var(--text)]">{workflow.name}</div>
|
||||
<div className="mt-0.5 whitespace-nowrap text-[11px] text-[color:var(--muted)]">Workflow {workflow.status}</div>
|
||||
<div className="truncate text-[13px] font-medium text-foreground">{workflow.name}</div>
|
||||
<div className="mt-0.5 whitespace-nowrap text-[11px] text-muted-foreground">Workflow {workflow.status}</div>
|
||||
</div>
|
||||
<ArrowRight size={14} className="shrink-0 text-[color:var(--muted)]" />
|
||||
<ArrowRight size={14} className="shrink-0 text-muted-foreground" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -3,12 +3,12 @@ import type { AgentRecord } from "@shared/types";
|
||||
import { getAgentInitials } from "../../lib/formatters";
|
||||
|
||||
const statusGlow: Record<string, string> = {
|
||||
running: "ring-2 ring-[color:var(--success)] ring-opacity-50",
|
||||
active: "ring-1 ring-[color:var(--success)] ring-opacity-30",
|
||||
running: "ring-2 ring-[color:var(--primary)] ring-opacity-50",
|
||||
active: "ring-1 ring-[color:var(--primary)] ring-opacity-30",
|
||||
idle: "",
|
||||
paused: "opacity-60",
|
||||
error: "ring-2 ring-[color:var(--danger)] ring-opacity-50",
|
||||
pending_approval: "ring-1 ring-[color:var(--warn)] ring-opacity-40",
|
||||
error: "ring-2 ring-[color:var(--destructive)] ring-opacity-50",
|
||||
pending_approval: "ring-1 ring-[color:var(--destructive)] ring-opacity-40",
|
||||
terminated: "opacity-40",
|
||||
};
|
||||
|
||||
@@ -20,39 +20,39 @@ function AgentCard({ agent, onClick }: { agent: AgentRecord; onClick: () => void
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={`focus-ring group relative flex flex-col items-center gap-2 rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-3 py-3.5 text-center transition duration-150 hover:border-[color:var(--line-strong)] hover:shadow-sm ${statusGlow[agent.status] ?? ""}`}
|
||||
className={`focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 group relative flex flex-col items-center gap-2 rounded-lg border border-border bg-card px-3 py-3.5 text-center transition duration-150 hover:border-border hover:shadow-sm ${statusGlow[agent.status] ?? ""}`}
|
||||
>
|
||||
<div className="relative">
|
||||
<div className={`flex h-12 w-12 items-center justify-center rounded-full text-[15px] font-bold ${
|
||||
isActive ? "bg-[color:var(--success-soft)] text-[color:var(--success)]" : "bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)]"
|
||||
isActive ? "bg-green-100 dark:bg-green-900/50 text-green-600 dark:text-green-400" : "bg-muted text-muted-foreground"
|
||||
}`}>
|
||||
{initials}
|
||||
</div>
|
||||
{agent.status === "running" ? (
|
||||
<span className="absolute -bottom-0.5 -right-0.5 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[color:var(--success)] opacity-40" style={{ willChange: "transform" }} />
|
||||
<span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-[color:var(--success)]" />
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-600 dark:bg-green-400 opacity-40" style={{ willChange: "transform" }} />
|
||||
<span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-green-600 dark:bg-green-400" />
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
className="absolute -bottom-0.5 -right-0.5 h-3 w-3 rounded-full border-2 border-[color:var(--panel)]"
|
||||
className="absolute -bottom-0.5 -right-0.5 h-3 w-3 rounded-full border-2 border-[color:var(--card)]"
|
||||
style={{
|
||||
backgroundColor: agent.status === "error"
|
||||
? "var(--danger)"
|
||||
? "var(--destructive)"
|
||||
: agent.status === "active"
|
||||
? "var(--success)"
|
||||
? "var(--primary)"
|
||||
: agent.status === "pending_approval"
|
||||
? "var(--warn)"
|
||||
? "var(--destructive)"
|
||||
: agent.status === "paused" || agent.status === "terminated"
|
||||
? "var(--muted)"
|
||||
: "var(--muted)",
|
||||
? "var(--muted-foreground)"
|
||||
: "var(--muted-foreground)",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-full min-w-0">
|
||||
<div className="truncate text-[12px] font-semibold text-[color:var(--text)]">{agent.name}</div>
|
||||
<div className="truncate text-[10px] text-[color:var(--muted)]">{agent.title || agent.role}</div>
|
||||
<div className="truncate text-[12px] font-semibold text-foreground">{agent.name}</div>
|
||||
<div className="truncate text-[10px] text-muted-foreground">{agent.title || agent.role}</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
@@ -61,9 +61,9 @@ function AgentCard({ agent, onClick }: { agent: AgentRecord; onClick: () => void
|
||||
export function WorkforceGrid({ agents, onSelectAgent }: { agents: AgentRecord[]; onSelectAgent: (id: string) => void }) {
|
||||
if (agents.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center rounded-[8px] border border-dashed border-[color:var(--line)] py-10 text-center">
|
||||
<Robot size={32} className="mb-3 text-[color:var(--muted)]" />
|
||||
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">No agents hired yet</div>
|
||||
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed border-border py-10 text-center">
|
||||
<Robot size={32} className="mb-3 text-muted-foreground" />
|
||||
<div className="text-[14px] font-medium text-muted-foreground">No agents hired yet</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,33 +41,33 @@ export function RecoveryBanner() {
|
||||
const handleDismissAll = () => setDismissed(true);
|
||||
|
||||
return (
|
||||
<div className="bg-[color:var(--warn-soft)] border border-[color:var(--warn)]/30 rounded-[8px] px-4 py-4 mb-4">
|
||||
<div className="bg-yellow-100 dark:bg-yellow-900/50 border border-[color:var(--destructive)]/30 rounded-lg px-4 py-4 mb-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2 text-[color:var(--warn)] font-medium">
|
||||
<div className="flex items-center gap-2 text-yellow-600 dark:text-yellow-400 font-medium">
|
||||
<Warning size={18} weight="bold" />
|
||||
<span>{runs.length} interrupted task(s) detected from previous session</span>
|
||||
</div>
|
||||
<button onClick={handleDismissAll} className="text-[color:var(--muted-strong)] hover:text-[color:var(--text)] transition-colors" aria-label="Dismiss recovery banner">
|
||||
<button onClick={handleDismissAll} className="text-muted-foreground hover:text-foreground transition-colors" aria-label="Dismiss recovery banner">
|
||||
<X size={16} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{runs.map(run => (
|
||||
<div key={run.runId} className="flex items-center justify-between bg-[color:var(--panel-soft)] rounded-[4px] px-3 py-2 text-[13px]">
|
||||
<div key={run.runId} className="flex items-center justify-between bg-muted rounded-[4px] px-3 py-2 text-[13px]">
|
||||
<div className="min-w-0 flex-1">
|
||||
<span className="truncate text-[color:var(--text)]">{run.taskTitle}</span>
|
||||
<span className="whitespace-nowrap text-[color:var(--muted)] ml-2">({run.agentName})</span>
|
||||
<span className="truncate text-foreground">{run.taskTitle}</span>
|
||||
<span className="whitespace-nowrap text-muted-foreground ml-2">({run.agentName})</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleRetry(run)}
|
||||
className="flex items-center gap-1 text-[color:var(--success)] hover:text-[color:var(--success)] text-xs transition-colors"
|
||||
className="flex items-center gap-1 text-green-600 dark:text-green-400 hover:text-green-600 dark:text-green-400 text-xs transition-colors"
|
||||
>
|
||||
<ArrowClockwise size={14} /> Retry
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleFail(run)}
|
||||
className="flex items-center gap-1 text-[color:var(--danger)] hover:text-[color:var(--danger)] text-xs transition-colors"
|
||||
className="flex items-center gap-1 text-destructive hover:text-destructive text-xs transition-colors"
|
||||
>
|
||||
<Trash size={14} /> Cancel
|
||||
</button>
|
||||
|
||||
@@ -30,39 +30,39 @@ export function SprintProgress({ sprint, tasks }: { sprint: SprintRecord; tasks:
|
||||
}, [tasks]);
|
||||
|
||||
return (
|
||||
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-4">
|
||||
<div className="rounded-lg border border-border bg-card px-4 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<CalendarCheck size={16} className="text-[color:var(--accent)]" />
|
||||
<span className="truncate text-[12px] font-semibold text-[color:var(--text)]">{sprint.name}</span>
|
||||
<CalendarCheck size={16} className="text-primary" />
|
||||
<span className="truncate text-[12px] font-semibold text-foreground">{sprint.name}</span>
|
||||
<StatusPill status={sprint.status} />
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-[11px] text-[color:var(--muted)]">
|
||||
<div className="flex items-center gap-1 text-[11px] text-muted-foreground">
|
||||
<Clock size={12} />
|
||||
{daysLeft} days left
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 h-2 w-full overflow-hidden rounded-full bg-[color:var(--panel-soft)]">
|
||||
<div className="mt-3 h-2 w-full overflow-hidden rounded-full bg-muted">
|
||||
<motion.div
|
||||
className="h-full rounded-full bg-[color:var(--accent)]"
|
||||
className="h-full rounded-full bg-primary"
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: `${progress}%` }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 flex items-center justify-between text-[10px] text-[color:var(--muted)]">
|
||||
<div className="mt-2 flex items-center justify-between text-[10px] text-muted-foreground">
|
||||
<span style={{ fontVariantNumeric: "tabular-nums" }}>{sprint.completedPoints}/{sprint.velocityPoints} points ({progress}%)</span>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="whitespace-nowrap text-[color:var(--success)]">{sprintTasks.done} done</span>
|
||||
<span className="whitespace-nowrap text-[color:var(--accent)]">{sprintTasks.active} active</span>
|
||||
<span className="whitespace-nowrap text-green-600 dark:text-green-400">{sprintTasks.done} done</span>
|
||||
<span className="whitespace-nowrap text-primary">{sprintTasks.active} active</span>
|
||||
<span className="whitespace-nowrap">{sprintTasks.total} total</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{sprint.goal && (
|
||||
<div className="mt-2 text-[11px] italic text-[color:var(--muted)]">"{sprint.goal}"</div>
|
||||
<div className="mt-2 text-[11px] italic text-muted-foreground">"{sprint.goal}"</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -9,12 +9,12 @@ export function TaskPipeline({ tasks }: { tasks: TaskRecord[] }) {
|
||||
if (t.status in counts) counts[t.status as keyof typeof counts]++;
|
||||
}
|
||||
return [
|
||||
{ label: "Backlog", count: counts.backlog, tone: "text-[color:var(--muted)]", bg: "bg-[color:var(--panel-soft)]" },
|
||||
{ label: "Todo", count: counts.todo, tone: "text-[color:var(--warn)]", bg: "bg-[color:var(--warn-soft)]" },
|
||||
{ label: "In Progress", count: counts.in_progress, tone: "text-[color:var(--accent)]", bg: "bg-[color:var(--accent-soft)]" },
|
||||
{ label: "In Review", count: counts.in_review, tone: "text-[color:var(--warn)]", bg: "bg-[color:var(--warn-soft)]" },
|
||||
{ label: "Done", count: counts.done, tone: "text-[color:var(--success)]", bg: "bg-[color:var(--success-soft)]" },
|
||||
{ label: "Blocked", count: counts.blocked, tone: "text-[color:var(--danger)]", bg: "bg-[color:var(--danger-soft)]" },
|
||||
{ label: "Backlog", count: counts.backlog, tone: "text-muted-foreground", bg: "bg-muted" },
|
||||
{ label: "Todo", count: counts.todo, tone: "text-yellow-600 dark:text-yellow-400", bg: "bg-yellow-100 dark:bg-yellow-900/50" },
|
||||
{ label: "In Progress", count: counts.in_progress, tone: "text-primary", bg: "bg-accent" },
|
||||
{ label: "In Review", count: counts.in_review, tone: "text-yellow-600 dark:text-yellow-400", bg: "bg-yellow-100 dark:bg-yellow-900/50" },
|
||||
{ label: "Done", count: counts.done, tone: "text-green-600 dark:text-green-400", bg: "bg-green-100 dark:bg-green-900/50" },
|
||||
{ label: "Blocked", count: counts.blocked, tone: "text-destructive", bg: "bg-red-100 dark:bg-red-900/50" },
|
||||
];
|
||||
}, [tasks]);
|
||||
|
||||
@@ -22,12 +22,12 @@ export function TaskPipeline({ tasks }: { tasks: TaskRecord[] }) {
|
||||
<div className="flex items-center gap-1.5">
|
||||
{stages.map((stage, i) => (
|
||||
<div key={stage.label} className="flex items-center gap-1.5">
|
||||
<div className={`flex items-center gap-2 rounded-[8px] px-3 py-2 ${stage.bg}`}>
|
||||
<div className={`flex items-center gap-2 rounded-lg px-3 py-2 ${stage.bg}`}>
|
||||
<span className={`text-[18px] font-bold ${stage.tone}`} style={{ fontVariantNumeric: "tabular-nums" }}>{stage.count}</span>
|
||||
<span className="text-[10px] font-medium uppercase tracking-[0.06em] text-[color:var(--muted)]">{stage.label}</span>
|
||||
<span className="text-[10px] font-medium uppercase tracking-[0.06em] text-muted-foreground">{stage.label}</span>
|
||||
</div>
|
||||
{i < stages.length - 1 ? (
|
||||
<ArrowRight size={12} className="text-[color:var(--muted)]" />
|
||||
<ArrowRight size={12} className="text-muted-foreground" />
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -21,26 +21,26 @@ export function AgentWorkloadChart({ data, reducedMotion }: AgentWorkloadChartPr
|
||||
<BarChart data={data}>
|
||||
<CartesianGrid
|
||||
strokeDasharray="3 3"
|
||||
stroke="var(--line)"
|
||||
stroke="var(--border)"
|
||||
horizontal={true}
|
||||
vertical={false}
|
||||
/>
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
tick={{ fontSize: 11, fill: "var(--muted)" }}
|
||||
axisLine={{ stroke: "var(--line)" }}
|
||||
tick={{ fontSize: 11, fill: "var(--muted-foreground)" }}
|
||||
axisLine={{ stroke: "var(--border)" }}
|
||||
tickLine={false}
|
||||
/>
|
||||
<YAxis
|
||||
allowDecimals={false}
|
||||
tick={{ fontSize: 11, fill: "var(--muted)" }}
|
||||
tick={{ fontSize: 11, fill: "var(--muted-foreground)" }}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
/>
|
||||
<Tooltip content={<ChartTooltip valueSuffix=" runs" />} />
|
||||
<Bar
|
||||
dataKey="runs"
|
||||
fill="var(--accent)"
|
||||
fill="var(--primary)"
|
||||
fillOpacity={0.85}
|
||||
radius={[4, 4, 0, 0]}
|
||||
maxBarSize={40}
|
||||
|
||||
@@ -16,9 +16,9 @@ interface BudgetRow {
|
||||
}
|
||||
|
||||
function getBarColor(pct: number): string {
|
||||
if (pct >= 100) return "bg-[color:var(--danger)]";
|
||||
if (pct >= 80) return "bg-[color:var(--warn)]";
|
||||
return "bg-[color:var(--success)]";
|
||||
if (pct >= 100) return "bg-destructive";
|
||||
if (pct >= 80) return "bg-yellow-600 dark:bg-yellow-400";
|
||||
return "bg-green-600 dark:bg-green-400";
|
||||
}
|
||||
|
||||
function scopeLabel(scopeType: string): string {
|
||||
@@ -77,12 +77,12 @@ export function BudgetUtilization({ companyId, agents }: BudgetUtilizationProps)
|
||||
|
||||
if (rows.length === 0) {
|
||||
return (
|
||||
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-4">
|
||||
<div className="rounded-lg border border-border bg-card px-4 py-4">
|
||||
<div className="flex h-[120px] flex-col items-center justify-center gap-2">
|
||||
<span className="text-[13px] font-semibold text-[color:var(--muted)]">
|
||||
<span className="text-[13px] font-semibold text-muted-foreground">
|
||||
No budget policies configured
|
||||
</span>
|
||||
<span className="max-w-[200px] text-center text-[11px] text-[color:var(--muted)]">
|
||||
<span className="max-w-[200px] text-center text-[11px] text-muted-foreground">
|
||||
Set budget policies in Settings to track utilization here.
|
||||
</span>
|
||||
</div>
|
||||
@@ -91,19 +91,19 @@ export function BudgetUtilization({ companyId, agents }: BudgetUtilizationProps)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-4">
|
||||
<div className="rounded-lg border border-border bg-card px-4 py-4">
|
||||
<div className="space-y-3">
|
||||
{rows.map((row) => (
|
||||
<div key={row.id}>
|
||||
<div className="mb-1 flex items-center justify-between">
|
||||
<span className="flex items-center gap-2 text-[13px] font-semibold text-[color:var(--text)]">
|
||||
<span className="flex items-center gap-2 text-[13px] font-semibold text-foreground">
|
||||
{row.name}
|
||||
<span className="text-[11px] font-normal text-[color:var(--muted)]">
|
||||
<span className="text-[11px] font-normal text-muted-foreground">
|
||||
{scopeLabel(row.scopeType)}
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
className="text-[13px] text-[color:var(--muted)]"
|
||||
className="text-[13px] text-muted-foreground"
|
||||
style={{
|
||||
fontFamily: "var(--font-mono)",
|
||||
fontVariantNumeric: "tabular-nums",
|
||||
@@ -112,14 +112,14 @@ export function BudgetUtilization({ companyId, agents }: BudgetUtilizationProps)
|
||||
{row.pct}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="relative h-1.5 rounded-full bg-[color:var(--panel-soft)]">
|
||||
<div className="relative h-1.5 rounded-full bg-muted">
|
||||
<div
|
||||
className={`h-1.5 rounded-full transition-all duration-300 ${getBarColor(row.pct)}`}
|
||||
style={{ width: `${Math.min(row.pct, 100)}%` }}
|
||||
/>
|
||||
{/* 80% warning marker */}
|
||||
<div
|
||||
className="absolute top-0 h-full w-px bg-[color:var(--warn)] opacity-50"
|
||||
className="absolute top-0 h-full w-px bg-yellow-600 dark:bg-yellow-400 opacity-50"
|
||||
style={{ left: "80%" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -21,15 +21,15 @@ export function ChartCard({
|
||||
}: ChartCardProps) {
|
||||
return (
|
||||
<div
|
||||
className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-4"
|
||||
className="rounded-lg border border-border bg-card px-4 py-4"
|
||||
role="img"
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<span className="text-[13px] font-semibold text-[color:var(--text)]">
|
||||
<span className="text-[13px] font-semibold text-foreground">
|
||||
{title}
|
||||
</span>
|
||||
<span className="text-[11px] text-[color:var(--muted)]">
|
||||
<span className="text-[11px] text-muted-foreground">
|
||||
{period}
|
||||
</span>
|
||||
</div>
|
||||
@@ -37,12 +37,12 @@ export function ChartCard({
|
||||
<div className="flex h-[220px] flex-col items-center justify-center gap-2">
|
||||
<ChartLine
|
||||
size={32}
|
||||
className="text-[color:var(--muted)] opacity-50"
|
||||
className="text-muted-foreground opacity-50"
|
||||
/>
|
||||
<span className="text-[13px] font-semibold text-[color:var(--muted)]">
|
||||
<span className="text-[13px] font-semibold text-muted-foreground">
|
||||
{emptyHeading}
|
||||
</span>
|
||||
<span className="max-w-[200px] text-center text-[11px] text-[color:var(--muted)]">
|
||||
<span className="max-w-[200px] text-center text-[11px] text-muted-foreground">
|
||||
{emptyBody}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -27,10 +27,10 @@ export function ChartTooltip({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="rounded-[6px] border border-[color:var(--line-strong)] bg-[color:var(--panel)] px-3 py-2 shadow-md"
|
||||
className="rounded-lg border border-border bg-card px-3 py-2 shadow-md"
|
||||
style={{ maxWidth: 200, zIndex: 50 }}
|
||||
>
|
||||
<div className="text-[11px] text-[color:var(--muted)]">{dateStr}</div>
|
||||
<div className="text-[11px] text-muted-foreground">{dateStr}</div>
|
||||
{payload.map((entry, i) => (
|
||||
<div key={i} className="mt-1 flex items-center gap-2">
|
||||
<span
|
||||
@@ -38,7 +38,7 @@ export function ChartTooltip({
|
||||
style={{ backgroundColor: entry.color }}
|
||||
/>
|
||||
<span
|
||||
className="text-[13px] font-semibold text-[color:var(--text)]"
|
||||
className="text-[13px] font-semibold text-foreground"
|
||||
style={{
|
||||
fontFamily: "var(--font-mono)",
|
||||
fontVariantNumeric: "tabular-nums",
|
||||
|
||||
@@ -22,20 +22,20 @@ export function CostTrendChart({ data, reducedMotion }: CostTrendChartProps) {
|
||||
<AreaChart data={data}>
|
||||
<CartesianGrid
|
||||
strokeDasharray="3 3"
|
||||
stroke="var(--line)"
|
||||
stroke="var(--border)"
|
||||
horizontal={true}
|
||||
vertical={false}
|
||||
/>
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tickFormatter={formatShortDate}
|
||||
tick={{ fontSize: 11, fill: "var(--muted)" }}
|
||||
axisLine={{ stroke: "var(--line)" }}
|
||||
tick={{ fontSize: 11, fill: "var(--muted-foreground)" }}
|
||||
axisLine={{ stroke: "var(--border)" }}
|
||||
tickLine={false}
|
||||
/>
|
||||
<YAxis
|
||||
tickFormatter={(val: number) => "$" + val.toFixed(0)}
|
||||
tick={{ fontSize: 11, fill: "var(--muted)" }}
|
||||
tick={{ fontSize: 11, fill: "var(--muted-foreground)" }}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
/>
|
||||
@@ -43,12 +43,12 @@ export function CostTrendChart({ data, reducedMotion }: CostTrendChartProps) {
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="total"
|
||||
stroke="var(--accent)"
|
||||
fill="var(--accent-soft)"
|
||||
stroke="var(--primary)"
|
||||
fill="var(--primary)"
|
||||
fillOpacity={0.15}
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
activeDot={{ r: 4, fill: "var(--accent)" }}
|
||||
activeDot={{ r: 4, fill: "var(--primary)" }}
|
||||
isAnimationActive={!reducedMotion}
|
||||
/>
|
||||
</AreaChart>
|
||||
|
||||
@@ -22,20 +22,20 @@ export function RunActivityChart({ data, reducedMotion }: RunActivityChartProps)
|
||||
<AreaChart data={data}>
|
||||
<CartesianGrid
|
||||
strokeDasharray="3 3"
|
||||
stroke="var(--line)"
|
||||
stroke="var(--border)"
|
||||
horizontal={true}
|
||||
vertical={false}
|
||||
/>
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tickFormatter={formatShortDate}
|
||||
tick={{ fontSize: 11, fill: "var(--muted)" }}
|
||||
axisLine={{ stroke: "var(--line)" }}
|
||||
tick={{ fontSize: 11, fill: "var(--muted-foreground)" }}
|
||||
axisLine={{ stroke: "var(--border)" }}
|
||||
tickLine={false}
|
||||
/>
|
||||
<YAxis
|
||||
allowDecimals={false}
|
||||
tick={{ fontSize: 11, fill: "var(--muted)" }}
|
||||
tick={{ fontSize: 11, fill: "var(--muted-foreground)" }}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
/>
|
||||
@@ -43,12 +43,12 @@ export function RunActivityChart({ data, reducedMotion }: RunActivityChartProps)
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="total"
|
||||
stroke="var(--accent)"
|
||||
fill="var(--accent-soft)"
|
||||
stroke="var(--primary)"
|
||||
fill="var(--primary)"
|
||||
fillOpacity={0.3}
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
activeDot={{ r: 4, fill: "var(--accent)" }}
|
||||
activeDot={{ r: 4, fill: "var(--primary)" }}
|
||||
isAnimationActive={!reducedMotion}
|
||||
/>
|
||||
</AreaChart>
|
||||
|
||||
@@ -32,21 +32,21 @@ export function SuccessRateChart({ data, reducedMotion }: SuccessRateChartProps)
|
||||
<LineChart data={rateData}>
|
||||
<CartesianGrid
|
||||
strokeDasharray="3 3"
|
||||
stroke="var(--line)"
|
||||
stroke="var(--border)"
|
||||
horizontal={true}
|
||||
vertical={false}
|
||||
/>
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tickFormatter={formatShortDate}
|
||||
tick={{ fontSize: 11, fill: "var(--muted)" }}
|
||||
axisLine={{ stroke: "var(--line)" }}
|
||||
tick={{ fontSize: 11, fill: "var(--muted-foreground)" }}
|
||||
axisLine={{ stroke: "var(--border)" }}
|
||||
tickLine={false}
|
||||
/>
|
||||
<YAxis
|
||||
domain={[0, 100]}
|
||||
tickFormatter={(val: number) => `${val}%`}
|
||||
tick={{ fontSize: 11, fill: "var(--muted)" }}
|
||||
tick={{ fontSize: 11, fill: "var(--muted-foreground)" }}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
/>
|
||||
@@ -54,10 +54,10 @@ export function SuccessRateChart({ data, reducedMotion }: SuccessRateChartProps)
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="rate"
|
||||
stroke="var(--success)"
|
||||
stroke="var(--primary)"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
activeDot={{ r: 4, fill: "var(--success)" }}
|
||||
activeDot={{ r: 4, fill: "var(--primary)" }}
|
||||
isAnimationActive={!reducedMotion}
|
||||
/>
|
||||
</LineChart>
|
||||
|
||||
Reference in new issue
Block a user